fix(FN-5584): restore planning fallback when primary provider API key is missing
The top-level promptWithFallback bypassed the session-attached rich fallback path that runs isRetryableModelSelectionError + swapPromptSession, so errors like "No API key for provider: anthropic" propagated without trying the configured planning fallback. Restore the dispatch with a WeakSet re-entry guard that preserves the FN-4900 recursion fix for plugin-runtime sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -571,7 +571,14 @@ describe("promptWithFallback auto-compaction", () => {
|
||||
expect(mockPrompt.mock.calls[1]).toEqual(["test prompt", options]);
|
||||
});
|
||||
|
||||
it("uses standalone prompt path even when session.promptWithFallback exists", async () => {
|
||||
it("delegates to session.promptWithFallback when available so rich fallback logic runs", async () => {
|
||||
// The session-attached promptWithFallback (set by createFnAgent at pi.ts:2012)
|
||||
// is the only path that swaps to the configured fallbackModel on
|
||||
// isRetryableModelSelectionError matches like "api key", 401/403, rate-limit, etc.
|
||||
// Bypassing it (as the old standalone-only behavior did) silently dropped
|
||||
// missing-API-key triage failures with no fallback attempt — see FN-5584.
|
||||
// A re-entry guard in promptWithFallback prevents the recursion that
|
||||
// FN-4900 originally guarded against.
|
||||
const mockSessionPromptWithFallback = vi.fn().mockResolvedValue(undefined);
|
||||
const mockPrompt = vi.fn().mockResolvedValue(undefined);
|
||||
const mockCompact = vi.fn();
|
||||
@@ -583,8 +590,9 @@ describe("promptWithFallback auto-compaction", () => {
|
||||
|
||||
await promptWithFallback(session, "test prompt");
|
||||
|
||||
expect(mockPrompt).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionPromptWithFallback).not.toHaveBeenCalled();
|
||||
expect(mockSessionPromptWithFallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockSessionPromptWithFallback).toHaveBeenCalledWith("test prompt", undefined);
|
||||
expect(mockPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles context error patterns from various providers", async () => {
|
||||
|
||||
@@ -313,7 +313,32 @@ export async function promptSessionAndCheck(session: AgentSession, prompt: strin
|
||||
}
|
||||
}
|
||||
|
||||
// Re-entry guard for the top-level dispatcher below. When `session.promptWithFallback`
|
||||
// is attached (e.g. by `createFnAgent` at pi.ts:2012, where the rich model-swap +
|
||||
// `isRetryableModelSelectionError` path lives), we want top-level callers like
|
||||
// triage/executor to flow through it so missing-API-key, 401/403, rate-limit, etc.
|
||||
// trigger the configured `fallbackModel`. The WeakSet prevents infinite recursion
|
||||
// in case a session-attached `promptWithFallback` ever calls back into the
|
||||
// top-level export with the same session.
|
||||
const promptWithFallbackInFlight = new WeakSet<object>();
|
||||
|
||||
export async function promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
|
||||
const sessionWithDispatch = session as AgentSession & {
|
||||
promptWithFallback?: (prompt: string, options?: unknown) => Promise<void>;
|
||||
};
|
||||
if (
|
||||
typeof sessionWithDispatch.promptWithFallback === "function" &&
|
||||
!promptWithFallbackInFlight.has(session as unknown as object)
|
||||
) {
|
||||
promptWithFallbackInFlight.add(session as unknown as object);
|
||||
try {
|
||||
await sessionWithDispatch.promptWithFallback(prompt, options);
|
||||
return;
|
||||
} finally {
|
||||
promptWithFallbackInFlight.delete(session as unknown as object);
|
||||
}
|
||||
}
|
||||
|
||||
piLog.log(`promptWithFallback: calling session.prompt (prompt length=${prompt.length})`);
|
||||
try {
|
||||
await promptSessionAndCheck(session, prompt, options);
|
||||
|
||||
Reference in New Issue
Block a user