diff --git a/src/auto-reply/reply/agent-runner-auth-profile.ts b/src/auto-reply/reply/agent-runner-auth-profile.ts index ec014ee162b0..4b9c70bb5b43 100644 --- a/src/auto-reply/reply/agent-runner-auth-profile.ts +++ b/src/auto-reply/reply/agent-runner-auth-profile.ts @@ -41,3 +41,33 @@ export function resolveRunAuthProfile( workspaceDir: run.workspaceDir, }); } + +/** Applies an auto-fallback probe's pinned auth to its fallback candidate. */ +export function resolveFallbackCandidateRun( + run: FollowupRun["run"], + provider: string, + model: string, +): FollowupRun["run"] { + const probe = run.autoFallbackPrimaryProbe; + const isPrimaryProbeCandidate = probe && provider === probe.provider && model === probe.model; + if ( + !probe || + provider !== probe.fallbackProvider || + isPrimaryProbeCandidate || + !probe.fallbackAuthProfileId + ) { + return run; + } + const candidateRun: FollowupRun["run"] = { + ...run, + provider, + model, + authProfileId: probe.fallbackAuthProfileId, + }; + if (probe.fallbackAuthProfileIdSource) { + candidateRun.authProfileIdSource = probe.fallbackAuthProfileIdSource; + } else { + delete candidateRun.authProfileIdSource; + } + return candidateRun; +} diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 85ebda612954..3f1703a3676e 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -27,6 +27,7 @@ import { getReplyPayloadMetadata } from "../reply-payload.js"; import type { TemplateContext } from "../templating.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; +import { resolveFallbackCandidateRun } from "./agent-runner-auth-profile.js"; import { buildEmptyInteractiveReplyPayload, buildKnownAgentRunFailureReplyPayload, @@ -2081,7 +2082,7 @@ describe("runAgentTurnWithFallback", () => { expect(rechecked.hasAutoFallbackProvenance).toBeUndefined(); }); - it("keeps fallback auth available when a primary probe falls back", async () => { + it("keeps fallback auth available when a primary probe falls back", () => { const probe = { provider: "anthropic", model: "claude-sonnet-4-6", @@ -2096,21 +2097,7 @@ describe("runAgentTurnWithFallback", () => { followupRun.run.authProfileId = "anthropic:primary"; followupRun.run.authProfileIdSource = "auto"; followupRun.run.autoFallbackPrimaryProbe = probe; - state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({ - result: await params.run("google", "gemini-3-pro"), - provider: "google", - model: "gemini-3-pro", - attempts: [{ provider: "anthropic", model: "claude-sonnet-4-6", error: "rate limit" }], - })); - state.runEmbeddedAgentMock.mockResolvedValueOnce({ - payloads: [{ text: "fallback" }], - meta: {}, - }); - - const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); - await runAgentTurnWithFallback(createMinimalRunAgentTurnParams({ followupRun })); - - expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run", { + expect(resolveFallbackCandidateRun(followupRun.run, "google", "gemini-3-pro")).toMatchObject({ provider: "google", model: "gemini-3-pro", authProfileId: "google:fallback", diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 56eec08bbf12..77bfedaa28fc 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -121,7 +121,7 @@ import { resolveAgentLifecycleTerminalMetadata, type AgentLifecycleTerminalBackstop, } from "./agent-lifecycle-terminal.js"; -import { resolveRunAuthProfile } from "./agent-runner-auth-profile.js"; +import { resolveFallbackCandidateRun, resolveRunAuthProfile } from "./agent-runner-auth-profile.js"; import { clearDroppedCliSessionBinding, createCliReasoningStreamBridge, @@ -1490,30 +1490,6 @@ async function runAgentTurnWithFallbackInternal( const preserveUserFacingSessionState = shouldPreserveUserFacingSessionStateForInputProvenance( effectiveRun.inputProvenance, ); - const resolveRunForFallbackCandidate = (provider: string, model: string): FollowupRun["run"] => { - const probe = effectiveRun.autoFallbackPrimaryProbe; - const isPrimaryProbeCandidate = probe && provider === probe.provider && model === probe.model; - if ( - probe && - provider === probe.fallbackProvider && - !isPrimaryProbeCandidate && - probe.fallbackAuthProfileId - ) { - const candidateRun: FollowupRun["run"] = { - ...effectiveRun, - provider, - model, - authProfileId: probe.fallbackAuthProfileId, - }; - if (probe.fallbackAuthProfileIdSource) { - candidateRun.authProfileIdSource = probe.fallbackAuthProfileIdSource; - } else { - delete candidateRun.authProfileIdSource; - } - return candidateRun; - } - return effectiveRun; - }; let liveModelSwitchRuntimeEntry: | Pick | undefined; @@ -1945,7 +1921,7 @@ async function runAgentTurnWithFallbackInternal( queuedUserMessagePersistedAcrossFallback; const suppressAssistantErrorPersistenceForCandidate = assistantErrorPersistedAcrossFallback; - const candidateRun = resolveRunForFallbackCandidate(provider, model); + const candidateRun = resolveFallbackCandidateRun(effectiveRun, provider, model); const candidateThinkLevel = resolveCandidateThinkingLevel({ cfg: runtimeConfig, provider, diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index 2a50a254cb0d..faf3ef9f4f05 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -6752,7 +6752,10 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { onAgentEvent?: (evt: { stream: string; data: Record }) => Promise; }) => { await args.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); - return { payloads: [], meta: {} }; + return { + payloads: [{ text: DELIVERY_NO_REPLY_RUNTIME_CONTRACT.silentText }], + meta: {}, + }; }, ); const runner = createFollowupRunner({