fix(fallback): guard duplicate prompt injections
This commit is contained in:
@@ -12,6 +12,21 @@ import { dispatchFallbackRetry } from "./fallback-retry-dispatcher"
|
||||
import { createSessionStatusHandler } from "./session-status-handler"
|
||||
import { resolveMessageEventSessionID, resolveSessionEventID } from "../../shared/event-session-id"
|
||||
|
||||
function resolveEventModel(props: Record<string, unknown> | undefined): string | undefined {
|
||||
const model = props?.model
|
||||
if (typeof model === "string") {
|
||||
return model
|
||||
}
|
||||
|
||||
const providerID = props?.providerID
|
||||
const modelID = props?.modelID
|
||||
if (typeof providerID === "string" && typeof modelID === "string") {
|
||||
return `${providerID}/${modelID}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) {
|
||||
const { config, pluginConfig, sessionStates, sessionLastAccess, sessionRetryInFlight, sessionAwaitingFallbackResult, sessionFallbackTimeouts, sessionStatusRetryKeys } = deps
|
||||
const sessionStatusHandler = createSessionStatusHandler(deps, helpers, sessionStatusRetryKeys)
|
||||
@@ -137,6 +152,19 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionAwaitingFallbackResult.has(sessionID)) {
|
||||
const pendingFallbackModel = sessionStates.get(sessionID)?.pendingFallbackModel
|
||||
const eventModel = resolveEventModel(props)
|
||||
if (!pendingFallbackModel || eventModel !== pendingFallbackModel) {
|
||||
log(`[${HOOK_NAME}] session.error skipped - awaiting fallback result`, {
|
||||
sessionID,
|
||||
pendingFallbackModel,
|
||||
eventModel,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sessionAwaitingFallbackResult.delete(sessionID)
|
||||
helpers.clearSessionFallbackTimeout(sessionID)
|
||||
|
||||
|
||||
@@ -2650,7 +2650,7 @@ describe("runtime-fallback", () => {
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: { sessionID, error: { statusCode: 429, message: "Rate limit again" } },
|
||||
properties: { sessionID, model: "provider-a/model-a", error: { statusCode: 429, message: "Rate limit again" } },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2659,6 +2659,71 @@ describe("runtime-fallback", () => {
|
||||
expect(fallbackLogs.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
test("session.error is skipped while waiting for the dispatched fallback result", async () => {
|
||||
const promptCalls: Array<unknown> = []
|
||||
|
||||
//#given
|
||||
const hook = createRuntimeFallbackHook(
|
||||
createMockPluginInput({
|
||||
session: {
|
||||
messages: async () => ({
|
||||
data: [{ info: { role: "user" }, parts: [{ type: "text", text: "hello" }] }],
|
||||
}),
|
||||
promptAsync: async (args: unknown) => {
|
||||
promptCalls.push(args)
|
||||
return {}
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
config: createMockConfig({ notify_on_fallback: false }),
|
||||
pluginConfig: {
|
||||
git_master: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
categories: {
|
||||
test: {
|
||||
fallback_models: ["provider-a/model-a", "provider-b/model-b"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
const sessionID = "test-race-awaiting-fallback-result"
|
||||
SessionCategoryRegistry.register(sessionID, "test")
|
||||
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.created",
|
||||
properties: { info: { id: sessionID, model: "google/gemini-2.5-pro" } },
|
||||
},
|
||||
})
|
||||
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: { sessionID, error: { statusCode: 429, message: "Rate limit" } },
|
||||
},
|
||||
})
|
||||
|
||||
//#when - duplicate stale error fires after promptAsync resolved but before fallback output is visible
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.error",
|
||||
properties: { sessionID, error: { statusCode: 429, message: "Rate limit" } },
|
||||
},
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(promptCalls).toHaveLength(1)
|
||||
const fallbackLogs = logCalls.filter((call) => call.msg.includes("Preparing fallback"))
|
||||
expect(fallbackLogs).toHaveLength(1)
|
||||
const skipLog = logCalls.find((call) => call.msg.includes("session.error skipped - awaiting fallback result"))
|
||||
expect(skipLog).toBeDefined()
|
||||
})
|
||||
|
||||
test("session.stop aborts when sessionAwaitingFallbackResult is set", async () => {
|
||||
const abortCalls: Array<{ path?: { id?: string } }> = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user