fix(fallback): skip stopped duplicate re-arms

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-15 12:12:12 +09:00
parent 23dfe7eec2
commit c177e9fc0c
2 changed files with 113 additions and 3 deletions
+110
View File
@@ -618,6 +618,116 @@ describe("createEventHandler - model fallback", () => {
expect(staleOutput.message["model"]).toBeUndefined()
})
test("does not leave stale pending fallback when duplicate arrives after auto-continue stops", async () => {
//#given
const sessionID = "ses_model_fallback_stopped_duplicate"
setMainSession(sessionID)
const modelFallback = createModelFallbackHook()
clearPendingModelFallback(modelFallback, sessionID)
let stopped = false
const stopContinuationGuard = {
event: async () => {},
isStopped: () => stopped,
clear: () => {},
stop: () => {},
"chat.message": async () => {},
}
const { handler, abortCalls, promptCalls } = createHandler({ hooks: { modelFallback, stopContinuationGuard } })
const chatMessageHandler = createChatMessageHandler({
ctx: unsafeTestValue({
client: {
tui: {
showToast: async () => ({}),
},
},
}),
pluginConfig: unsafeTestValue({}),
firstMessageVariantGate: {
shouldOverride: () => false,
markApplied: () => {},
},
hooks: unsafeTestValue({
modelFallback,
stopContinuationGuard,
keywordDetector: null,
claudeCodeHooks: null,
autoSlashCommand: null,
startWork: null,
ralphLoop: null,
}),
})
await handler({
event: {
type: "message.updated",
properties: {
info: {
id: "msg_stopped_duplicate_error",
sessionID,
role: "assistant",
error: {
name: "APIError",
data: {
message:
"Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}",
isRetryable: true,
},
},
modelID: "claude-opus-4-7-thinking",
providerID: "anthropic",
agent: "Sisyphus - Ultraworker",
},
},
},
})
const output: ChatMessageOutput = { message: {}, parts: [] }
await chatMessageHandler(
{
sessionID,
agent: "sisyphus",
model: { providerID: "anthropic", modelID: "claude-opus-4-7-thinking" },
},
output,
)
//#when
stopped = true
await handler({
event: {
type: "session.error",
properties: {
sessionID,
error: {
name: "UnknownError",
data: {
error: {
message:
"Bad Gateway: {\"error\":{\"message\":\"unknown provider for model claude-opus-4-7-thinking\"}}",
},
},
},
},
},
})
const staleOutput: ChatMessageOutput = { message: {}, parts: [] }
await chatMessageHandler(
{
sessionID,
agent: "sisyphus",
model: { providerID: "opencode-go", modelID: "kimi-k2.6" },
},
staleOutput,
)
//#then
expect(abortCalls).toEqual([sessionID])
expect(promptCalls).toEqual([sessionID])
expect(modelFallback.hasPendingModelFallback(sessionID)).toBe(false)
expect(staleOutput.message["model"]).toBeUndefined()
})
test("does not trigger model-fallback from session.status when runtime_fallback is enabled", async () => {
//#given
const sessionID = "ses_status_retry_runtime_enabled"
+3 -3
View File
@@ -770,7 +770,7 @@ export function createEventHandler(args: {
const shouldAutoContinue = shouldAutoRetrySession(sessionID) &&
!hooks.stopContinuationGuard?.isStopped(sessionID);
if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "message.updated", fallbackContext)) {
if (!shouldSkipFallbackContinuation(sessionID, "message.updated", fallbackContext)) {
applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig);
const setFallback = modelFallback
@@ -843,7 +843,7 @@ export function createEventHandler(args: {
const shouldAutoContinue = shouldAutoRetrySession(sessionID) &&
!hooks.stopContinuationGuard?.isStopped(sessionID);
if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "session.status", fallbackContext)) {
if (!shouldSkipFallbackContinuation(sessionID, "session.status", fallbackContext)) {
applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig);
const setFallback = modelFallback
@@ -936,7 +936,7 @@ export function createEventHandler(args: {
const shouldAutoContinue = shouldAutoRetrySession(sessionID) &&
!hooks.stopContinuationGuard?.isStopped(sessionID);
if (!shouldAutoContinue || !shouldSkipFallbackContinuation(sessionID, "session.error", fallbackContext)) {
if (!shouldSkipFallbackContinuation(sessionID, "session.error", fallbackContext)) {
applyUserConfiguredFallbackChain(modelFallback, sessionID, agentName, currentProvider, args.pluginConfig);
const setFallback = modelFallback