Merge pull request #4072 from PeterPonyu/fix/3645-runtime-fallback-git-silent-fail
fix(runtime-fallback): fall back to synthetic continuation when session messages are empty (#3645)
This commit is contained in:
@@ -155,8 +155,20 @@ export function createAutoRetryHelpers(deps: HookDeps) {
|
|||||||
query: { directory: ctx.directory },
|
query: { directory: ctx.directory },
|
||||||
})
|
})
|
||||||
const retryPayload = getLastUserRetryPayload(messagesResp, sessionID)
|
const retryPayload = getLastUserRetryPayload(messagesResp, sessionID)
|
||||||
const retryParts = retryPayload.retryParts
|
const fetchedParts = retryPayload.retryParts
|
||||||
if (retryParts.length > 0) {
|
const retryParts =
|
||||||
|
fetchedParts.length > 0
|
||||||
|
? fetchedParts
|
||||||
|
: (() => {
|
||||||
|
log(
|
||||||
|
`[${HOOK_NAME}] No user message parts found for auto-retry (${source}); using synthetic continuation`,
|
||||||
|
{
|
||||||
|
sessionID,
|
||||||
|
hint: "This can occur when the working directory contains .git and messages are not yet persisted",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return [{ type: "text" as const, text: "continue" }]
|
||||||
|
})()
|
||||||
log(`[${HOOK_NAME}] Auto-retrying with fallback model (${source})`, {
|
log(`[${HOOK_NAME}] Auto-retrying with fallback model (${source})`, {
|
||||||
sessionID,
|
sessionID,
|
||||||
model: newModel,
|
model: newModel,
|
||||||
@@ -214,9 +226,6 @@ export function createAutoRetryHelpers(deps: HookDeps) {
|
|||||||
state.pendingFallbackPromptMayHaveBeenAccepted = false
|
state.pendingFallbackPromptMayHaveBeenAccepted = false
|
||||||
}
|
}
|
||||||
retryDispatched = true
|
retryDispatched = true
|
||||||
} else {
|
|
||||||
log(`[${HOOK_NAME}] No user message found for auto-retry (${source})`, { sessionID })
|
|
||||||
}
|
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
log(`[${HOOK_NAME}] Auto-retry failed (${source})`, { sessionID, error: String(retryError) })
|
log(`[${HOOK_NAME}] Auto-retry failed (${source})`, { sessionID, error: String(retryError) })
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -380,6 +380,9 @@ describe("runtime-fallback", () => {
|
|||||||
type: "session.error",
|
type: "session.error",
|
||||||
properties: {
|
properties: {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
// model at the top level so the awaiting-fallback gate recognises this
|
||||||
|
// as an error from the fallback model we just dispatched
|
||||||
|
model: "anthropic/claude-opus-4.7",
|
||||||
error: { name: "UnknownError", data: { message: "Model not found: anthropic/claude-opus-4.7." } },
|
error: { name: "UnknownError", data: { message: "Model not found: anthropic/claude-opus-4.7." } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -432,6 +435,9 @@ describe("runtime-fallback", () => {
|
|||||||
type: "session.error",
|
type: "session.error",
|
||||||
properties: {
|
properties: {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
// model at the top level so the awaiting-fallback gate recognises this
|
||||||
|
// as an error from the fallback model we just dispatched
|
||||||
|
model: "anthropic/claude-opus-4.7",
|
||||||
error: {
|
error: {
|
||||||
name: "ProviderModelNotFoundError",
|
name: "ProviderModelNotFoundError",
|
||||||
data: {
|
data: {
|
||||||
@@ -2764,11 +2770,15 @@ describe("runtime-fallback", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Simulate the fallback session completing before the next error arrives
|
||||||
|
await hook.event({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
|
|
||||||
//#when - second error occurs immediately; tries to switch back to original model but should be in cooldown
|
//#when - second error occurs immediately; tries to switch back to original model but should be in cooldown
|
||||||
await hook.event({
|
await hook.event({
|
||||||
event: {
|
event: {
|
||||||
type: "session.error",
|
type: "session.error",
|
||||||
properties: { sessionID, error: { statusCode: 429 } },
|
// model matches pendingFallbackModel so the awaiting-fallback gate lets this through
|
||||||
|
properties: { sessionID, model: "openai/gpt-5.4", error: { statusCode: 429 } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3158,14 +3168,21 @@ describe("runtime-fallback", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoRetryLog = logCalls.find((call) => call.msg.includes("No user message found for auto-retry"))
|
const autoRetryLog = logCalls.find((call) =>
|
||||||
|
call.msg.includes("No user message parts found for auto-retry") &&
|
||||||
|
call.msg.includes("using synthetic continuation"),
|
||||||
|
)
|
||||||
expect(autoRetryLog).toBeDefined()
|
expect(autoRetryLog).toBeDefined()
|
||||||
|
|
||||||
|
// Simulate the fallback session completing before the next error arrives
|
||||||
|
await hook.event({ event: { type: "session.idle", properties: { sessionID } } })
|
||||||
|
|
||||||
//#when - second error fires after retry completed (retryInFlight cleared)
|
//#when - second error fires after retry completed (retryInFlight cleared)
|
||||||
await hook.event({
|
await hook.event({
|
||||||
event: {
|
event: {
|
||||||
type: "session.error",
|
type: "session.error",
|
||||||
properties: { sessionID, error: { statusCode: 429, message: "Rate limit again" } },
|
// model matches pendingFallbackModel so the awaiting-fallback gate lets this through
|
||||||
|
properties: { sessionID, model: "provider-a/model-a", error: { statusCode: 429, message: "Rate limit again" } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user