fix(prompt-gate): harden internal prompt dispatch

This commit is contained in:
YeonGyu-Kim
2026-05-19 16:02:18 +09:00
parent 6c63372ef9
commit 1492bffd20
49 changed files with 1488 additions and 141 deletions
@@ -238,7 +238,7 @@ describe("injectContinuation", () => {
expect(capturedBody?.variant).toBe("max")
})
test("#given a peer-message hold survives an unrelated release #when todo continuation injects #then it skips and clears in-flight state", async () => {
test("#given a peer-message hold survives an unrelated release #when todo continuation injects #then it queues behind the peer message", async () => {
// given
const sessionID = "ses_todo_reserved_by_peer_message"
let promptCalls = 0
@@ -286,6 +286,50 @@ describe("injectContinuation", () => {
expect(peerMessageResult.status).toBe("dispatched")
expect(promptCalls).toBe(1)
expect(state.inFlight).toBe(false)
expect(state.lastInjectedAt).toBe(0)
expect(state.lastInjectedAt).toBeGreaterThan(0)
})
test("#given promptAsync may have accepted before EOF #when continuation injection observes the failure #then it records an optimistic injection", async () => {
// given
const state = {
inFlight: false,
lastInjectedAt: 0,
awaitingPostInjectionProgressCheck: false,
consecutiveFailures: 2,
}
let promptCalls = 0
const ctx = {
directory: "/tmp/test",
client: {
session: {
todo: async () => ({ data: [{ id: "1", content: "todo", status: "pending", priority: "high" }] }),
promptAsync: async () => {
promptCalls += 1
throw new Error("JSON Parse error: Unexpected EOF")
},
},
},
}
const sessionStateStore = {
getExistingState: () => state,
}
// when
await injectContinuation({
ctx: ctx as never,
sessionID: "ses_continuation_eof",
resolvedInfo: {
agent: "Sisyphus - Ultraworker",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
},
sessionStateStore: sessionStateStore as never,
})
// then
expect(promptCalls).toBe(1)
expect(state.inFlight).toBe(false)
expect(state.awaitingPostInjectionProgressCheck).toBe(true)
expect(state.consecutiveFailures).toBe(0)
expect(state.lastInjectedAt).toBeGreaterThan(0)
})
})
@@ -7,6 +7,7 @@ import {
} from "../../features/claude-code-session-state"
import {
createInternalAgentContinuationTextPart,
isAmbiguousPromptDispatchFailure,
normalizeSDKResponse,
resolveInheritedPromptTools,
} from "../../shared"
@@ -22,7 +23,7 @@ import {
normalizeAgentForPromptKey,
stripAgentListSortPrefix,
} from "../../shared/agent-display-names"
import { dispatchInternalPrompt } from "../shared/prompt-async-gate"
import { dispatchInternalPrompt, isInternalPromptDispatchAccepted } from "../shared/prompt-async-gate"
import {
CONTINUATION_PROMPT,
@@ -208,7 +209,7 @@ ${todoList}`
if (promptResult.status === "failed") {
throw promptResult.error
}
if (promptResult.status !== "dispatched") {
if (!isInternalPromptDispatchAccepted(promptResult)) {
log(`[${HOOK_NAME}] Injection skipped by promptAsync gate`, { sessionID, status: promptResult.status })
if (injectionState) {
injectionState.inFlight = false
@@ -216,7 +217,7 @@ ${todoList}`
return
}
log(`[${HOOK_NAME}] Injection successful`, { sessionID })
log(`[${HOOK_NAME}] Injection successful`, { sessionID, status: promptResult.status })
if (injectionState) {
injectionState.inFlight = false
injectionState.lastInjectedAt = Date.now()
@@ -228,6 +229,11 @@ ${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