fix(prompt): treat post-dispatch failures as accepted

This commit is contained in:
YeonGyu-Kim
2026-05-20 11:42:32 +09:00
parent f540249838
commit d3e218f912
31 changed files with 503 additions and 76 deletions
@@ -18,6 +18,7 @@ import {
findNearestMessageWithFieldsFromSDK,
} from "../../features/hook-message-injector"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
import { isAmbiguousPostDispatchPromptFailure } from "../../shared/prompt-failure-classifier"
export async function runAggressiveTruncationStrategy(params: {
sessionID: string
@@ -108,6 +109,13 @@ export async function runAggressiveTruncationStrategy(params: {
} as never,
})
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
log("[auto-compact] delayed auto prompt may have been accepted before ambiguous failure", {
sessionID: params.sessionID,
error: String(promptResult.error),
})
return
}
log("[auto-compact] delayed auto prompt skipped by promptAsync gate", {
sessionID: params.sessionID,
status: promptResult.status,
@@ -6,7 +6,7 @@ import {
import { stripAgentListSortPrefix } from "../../shared/agent-display-names"
import { log } from "../../shared/logger"
import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared"
import { isAmbiguousPromptDispatchFailure } from "../../shared/prompt-failure-classifier"
import { isAmbiguousPostDispatchPromptFailure } from "../../shared/prompt-failure-classifier"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
import { HOOK_NAME } from "./hook-name"
import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates"
@@ -114,6 +114,15 @@ export async function injectBoulderContinuation(input: {
},
})
if (promptResult.status === "failed") {
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
sessionState.promptFailureCount = 0
markContinuationInjectedAwaitingToolProgress(sessionState)
log(`[${HOOK_NAME}] Boulder continuation prompt failed after dispatch may have been accepted`, {
sessionID,
error: String(promptResult.error),
})
return "injected"
}
throw promptResult.error
}
if (!isInternalPromptDispatchAccepted(promptResult)) {
@@ -129,15 +138,6 @@ export async function injectBoulderContinuation(input: {
log(`[${HOOK_NAME}] Boulder continuation injected`, { sessionID })
return "injected"
} catch (err) {
if (isAmbiguousPromptDispatchFailure(err)) {
sessionState.promptFailureCount = 0
markContinuationInjectedAwaitingToolProgress(sessionState)
log(`[${HOOK_NAME}] Boulder continuation prompt failed after dispatch may have been accepted`, {
sessionID,
error: String(err),
})
return "injected"
}
sessionState.promptFailureCount += 1
sessionState.lastFailureAt = Date.now()
log(`[${HOOK_NAME}] Boulder continuation failed`, {
+2 -2
View File
@@ -19,7 +19,7 @@ import { isSessionInBoulderLineage } from "./boulder-session-lineage"
import { createInternalAgentContinuationTextPart } from "../../shared"
import { getAgentConfigKey } from "../../shared/agent-display-names"
import { log } from "../../shared/logger"
import { isAmbiguousPromptDispatchFailure } from "../../shared/prompt-failure-classifier"
import { isAmbiguousPostDispatchPromptFailure } from "../../shared/prompt-failure-classifier"
import { shouldPromptAfterSessionIdle } from "../shared/session-idle-settle"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
import { injectBoulderContinuation } from "./boulder-continuation-injector"
@@ -316,7 +316,7 @@ export async function handleAtlasSessionIdle(input: {
},
})
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
sessionState.boulderCompletionNudgedAt = {
...(sessionState.boulderCompletionNudgedAt ?? {}),
[work.work_id]: Date.now(),
@@ -8,6 +8,7 @@ import { clearToolInputCache, stopToolInputCacheCleanup } from "../tool-input-ca
import type { PluginConfig } from "../types"
import { createInternalAgentTextPart, isHookDisabled, log } from "../../../shared"
import { resolveSessionEventID } from "../../../shared/event-session-id"
import { isAmbiguousPostDispatchPromptFailure } from "../../../shared/prompt-failure-classifier"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../../../shared/prompt-async-gate"
import {
clearAllSessionHookState,
@@ -124,7 +125,14 @@ export function createSessionEventHandler(
},
})
if (promptResult.status === "failed") {
log("Failed to inject prompt from Stop hook", { error: String(promptResult.error) })
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
log("Prompt injected from Stop hook may have been accepted before ambiguous failure", {
sessionID,
error: String(promptResult.error),
})
} else {
log("Failed to inject prompt from Stop hook", { error: String(promptResult.error) })
}
} else if (!isInternalPromptDispatchAccepted(promptResult)) {
log("Skipped prompt injection from Stop hook", { sessionID, status: promptResult.status })
}
@@ -7,7 +7,7 @@ import {
} from "../../shared/compaction-agent-config-checkpoint"
import { createInternalAgentContinuationTextPart } from "../../shared/internal-initiator-marker"
import { log } from "../../shared/logger"
import { isAmbiguousPromptDispatchFailure } from "../../shared/prompt-failure-classifier"
import { isAmbiguousPostDispatchPromptFailure } from "../../shared/prompt-failure-classifier"
import { setSessionModel } from "../../shared/session-model-state"
import { setSessionTools } from "../../shared/session-tools-store"
import {
@@ -102,7 +102,7 @@ export function createRecoveryLogic(
},
})
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
tailState.lastRecoveryAt = now
}
log(`[compaction-context-injector] Recovery skipped by promptAsync gate`, {
@@ -5,7 +5,7 @@ import { getMessageDir } from "./message-storage-directory"
import { withTimeout } from "./with-timeout"
import {
createInternalAgentContinuationTextPart,
isAmbiguousPromptDispatchFailure,
isAmbiguousPostDispatchPromptFailure,
isRecord,
normalizeSDKResponse,
resolveInheritedPromptTools,
@@ -160,7 +160,7 @@ export async function injectContinuationPrompt(
},
})
if (promptResult.status === "failed") {
if (isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
return { status: "dispatched" }
}
throw promptResult.error
@@ -179,9 +179,6 @@ export async function injectContinuationPrompt(
}
response = promptResult.response
} catch (error) {
if (isAmbiguousPromptDispatchFailure(error)) {
return { status: "dispatched" }
}
const promptError = error instanceof Error
? error
: createPromptAsyncError("promptAsync rejected", error)
@@ -2,7 +2,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk"
import type { MessageData, ResumeConfig } from "./types"
import { readParts } from "./storage/parts-reader"
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
import { isAmbiguousPromptDispatchFailure, normalizeSDKResponse } from "../../shared"
import { isAmbiguousPostDispatchPromptFailure, normalizeSDKResponse } from "../../shared"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
type Client = ReturnType<typeof createOpencodeClient>
@@ -179,7 +179,7 @@ export async function recoverToolResultMissing(
queueBehavior: "defer",
})
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
return true
}
return isInternalPromptDispatchAccepted(promptResult)
@@ -2,7 +2,7 @@ import type { createOpencodeClient } from "@opencode-ai/sdk"
import { extractUnavailableToolName } from "./detect-error-type"
import { readParts } from "./storage"
import type { MessageData } from "./types"
import { isAmbiguousPromptDispatchFailure, normalizeSDKResponse } from "../../shared"
import { isAmbiguousPostDispatchPromptFailure, normalizeSDKResponse } from "../../shared"
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
@@ -130,7 +130,7 @@ export async function recoverUnavailableTool(
checkToolState: false,
input: promptInput,
})
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
return true
}
return isInternalPromptDispatchAccepted(promptResult)
+2 -2
View File
@@ -1,7 +1,7 @@
import type { createOpencodeClient } from "@opencode-ai/sdk"
import {
createInternalAgentContinuationTextPart,
isAmbiguousPromptDispatchFailure,
isAmbiguousPostDispatchPromptFailure,
isRealUserMessage,
resolveInheritedPromptTools,
} from "../../shared"
@@ -56,7 +56,7 @@ export async function resumeSession(client: Client, config: ResumeConfig): Promi
},
},
})
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
return true
}
return isInternalPromptDispatchAccepted(promptResult)
@@ -1126,7 +1126,9 @@ describe("dispatchInternalPrompt shared gate behavior", () => {
// then
expect(first.status).toBe("failed")
expect(first).toMatchObject({ dispatchAttempted: true })
expect(second.status).toBe("failed")
expect(second).toMatchObject({ dispatchAttempted: true })
expect(promptCalls).toBe(2)
})
@@ -1162,6 +1164,7 @@ describe("dispatchInternalPrompt shared gate behavior", () => {
// then
expect(first.status).toBe("failed")
expect(first).toMatchObject({ dispatchAttempted: true })
expect(second).toEqual({ status: "queued", queuedBy: "test:reject:first", position: 1 })
expect(promptCalls).toBe(1)
})
@@ -7,7 +7,7 @@ import {
} from "../../features/claude-code-session-state"
import {
createInternalAgentContinuationTextPart,
isAmbiguousPromptDispatchFailure,
isAmbiguousPostDispatchPromptFailure,
normalizeSDKResponse,
resolveInheritedPromptTools,
} from "../../shared"
@@ -208,6 +208,15 @@ ${todoList}`
},
})
if (promptResult.status === "failed") {
if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
if (injectionState) {
injectionState.inFlight = false
injectionState.lastInjectedAt = Date.now()
injectionState.awaitingPostInjectionProgressCheck = true
injectionState.consecutiveFailures = 0
}
return
}
throw promptResult.error
}
if (!isInternalPromptDispatchAccepted(promptResult)) {
@@ -230,11 +239,6 @@ ${todoList}`
if (injectionState) {
injectionState.inFlight = false
injectionState.lastInjectedAt = Date.now()
if (isAmbiguousPromptDispatchFailure(error)) {
injectionState.awaitingPostInjectionProgressCheck = true
injectionState.consecutiveFailures = 0
return
}
injectionState.consecutiveFailures = (injectionState.consecutiveFailures ?? 0) + 1
const errorObj = error instanceof Error
@@ -1,7 +1,7 @@
import type { BackgroundManager } from "../../features/background-agent"
import { getMainSessionID, getSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger"
import { createInternalAgentTextPart, isAmbiguousPromptDispatchFailure, resolveInheritedPromptTools } from "../../shared"
import { createInternalAgentTextPart, isAmbiguousPostDispatchPromptFailure, resolveInheritedPromptTools } from "../../shared"
import { resolveMessageEventSessionID, resolveSessionEventID } from "../../shared/event-session-id"
import { isAbortError } from "../../shared/is-abort-error"
import {
@@ -261,7 +261,7 @@ export function createUnstableAgentBabysitterHook(ctx: BabysitterContext, option
},
})
if (!isInternalPromptDispatchAccepted(promptResult)) {
if (promptResult.status === "failed" && isAmbiguousPromptDispatchFailure(promptResult.error)) {
if (promptResult.status === "failed" && isAmbiguousPostDispatchPromptFailure(promptResult)) {
reminderCooldowns.set(task.id, now)
}
log(`[${HOOK_NAME}] Reminder skipped by promptAsync gate`, {