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
+55
View File
@@ -6,6 +6,10 @@ import { createChatMessageHandler } from "./chat-message"
import { _resetForTesting, setMainSession } from "../features/claude-code-session-state"
import { createModelFallbackHook, clearPendingModelFallback } from "../hooks/model-fallback/hook"
import * as connectedProvidersCache from "../shared/connected-providers-cache"
import {
releaseAllPromptAsyncReservationsForTesting,
releasePromptAsyncReservation,
} from "../hooks/shared/prompt-async-gate"
import { unsafeTestValue } from "../../test-support/unsafe-test-value"
type EventInput = { event: { type: string; properties?: unknown } }
@@ -95,6 +99,7 @@ describe("createEventHandler - model fallback", () => {
readConnectedProvidersCacheSpy = undefined
readProviderModelsCacheSpy = undefined
_resetForTesting()
releaseAllPromptAsyncReservationsForTesting()
})
test("triggers retry prompt for assistant message.updated APIError payloads (headless resume)", async () => {
@@ -139,6 +144,56 @@ describe("createEventHandler - model fallback", () => {
expect(promptCalls).toEqual([sessionID])
})
test("#given model-fallback promptAsync may have been accepted before EOF #when the same assistant error repeats after the gate hold #then fallback continue is not duplicated", async () => {
//#given
const sessionID = "ses_message_updated_fallback_eof"
const modelFallback = createModelFallbackHook()
const { handler, abortCalls, promptAsyncCalls } = createHandler({
hooks: { modelFallback },
promptAsync: async () => {
throw new Error("JSON Parse error: Unexpected EOF")
},
})
const input: EventInput = {
event: {
type: "message.updated",
properties: {
info: {
id: "msg_err_eof",
sessionID,
role: "assistant",
time: { created: 1, completed: 2 },
error: {
name: "APIError",
data: {
message:
"Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}",
isRetryable: true,
},
},
parentID: "msg_user_eof",
modelID: "claude-opus-4-7-thinking",
providerID: "anthropic",
agent: "Sisyphus - Ultraworker",
path: { cwd: "/tmp", root: "/tmp" },
},
},
},
}
//#when
await handler(input)
const released = releasePromptAsyncReservation(sessionID, "test:simulate-expired-hold", {
reservedBy: "model-fallback:message.updated",
})
await handler(input)
//#then
expect(released).toBe(true)
expect(abortCalls).toEqual([sessionID])
expect(promptAsyncCalls).toEqual([sessionID])
})
test("triggers retry prompt for nested model error payloads", async () => {
//#given
const sessionID = "ses_main_fallback_nested"
+8 -4
View File
@@ -42,7 +42,11 @@ import { buildTeamIdleWakeHintClient } from "./build-team-idle-wake-hint-client"
import { createTeamLeadOrphanHandler } from "../hooks/team-session-events/team-lead-orphan-handler";
import { createTeamMemberErrorHandler } from "../hooks/team-session-events/team-member-error-handler";
import { createTeamMemberStatusHandler } from "../hooks/team-session-events/team-member-status-handler";
import { dispatchInternalPrompt, releasePromptAsyncReservation } from "../hooks/shared/prompt-async-gate";
import {
dispatchInternalPrompt,
isInternalPromptDispatchAccepted,
releasePromptAsyncReservation,
} from "../hooks/shared/prompt-async-gate";
import type { CreatedHooks } from "../create-hooks";
import type { Managers } from "../create-managers";
@@ -519,7 +523,7 @@ export function createEventHandler(args: {
source: `model-fallback:${source}`,
input: promptBody,
});
if (promptResult.status === "dispatched") {
if (isInternalPromptDispatchAccepted(promptResult)) {
dispatched = true;
} else if (promptResult.status === "failed") {
const error = promptResult.error;
@@ -537,7 +541,7 @@ export function createEventHandler(args: {
source: `model-fallback:${source}:sync`,
input: promptBody,
});
if (promptResult.status === "dispatched") {
if (isInternalPromptDispatchAccepted(promptResult)) {
dispatched = true;
} else if (promptResult.status === "failed") {
log("[event] model-fallback prompt failed", { sessionID, source, error: promptResult.error });
@@ -959,7 +963,7 @@ export function createEventHandler(args: {
});
if (promptResult.status === "failed") {
log("[event] recovery continue prompt failed", { sessionID, error: promptResult.error });
} else if (promptResult.status !== "dispatched") {
} else if (!isInternalPromptDispatchAccepted(promptResult)) {
log("[event] recovery continue prompt skipped by gate", { sessionID, status: promptResult.status });
}
}
-1
View File
@@ -25,7 +25,6 @@ export function createUnstableAgentBabysitter(args: {
return []
},
status: async () => ctx.client.session.status(),
prompt: async (promptArgs) => ctx.client.session.prompt(promptArgs),
promptAsync: async (promptArgs) => ctx.client.session.promptAsync(promptArgs),
},
},