fix(prompt-gate): harden internal prompt dispatch
This commit is contained in:
@@ -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
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user