fix: retire stale composer recovery drafts before retry

A failed-response Retry submits no composer text. Retire the earlier accepted text send's recovery fallback synchronously so a Retry model/key refusal cannot restore it over the current unsent draft. Never clear a later ordinary send's fallback at async settlement.

Add key/model503, superseding-draft and ordinary-refusal coverage. Verified 183 focused tests, typecheck, full ESLint, production build, and a red/green live production regression plus ordinary-draft recovery; no external provider calls.
This commit is contained in:
amosblomqvist
2026-10-27 18:56:55 +02:00
parent 5984eb4914
commit 3b29b5e9e7
2 changed files with 75 additions and 2 deletions
+13 -1
View File
@@ -162,6 +162,18 @@ export function parseProviderKeyRefusal(message: string | undefined): boolean {
}
}
/** Retry sends no composer text. Its refusal must not resurrect the draft
* from an earlier, already-durable text send over today's unsent composer.
* Retire that fallback synchronously, never when the async Retry settles:
* a later ordinary send may own a new recovery draft by then. */
export function retryFailedResponse(
send: (input: ChatInput) => void,
submittedDraft: { current: string | null },
): void {
submittedDraft.current = null;
void send({ type: "retry" });
}
/** useLayoutEffect that is silent during SSR (client components still render on the server in Next). */
const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -676,7 +688,7 @@ export function Chat({
// "error" too: a dropped stream leaves the model there, and send()
// recovers from it. Later compaction requires new input instead.
const retryable = (status === "ready" || status === "error") && !busy && !compacting && endsInFailedTurn(nativeMessages);
const retryFailedTurn = () => void send({ type: "retry" });
const retryFailedTurn = () => retryFailedResponse(send, draftRef);
const canSend =
(text.trim().length > 0 || dictation.active) && !pending && !activeProblem && !compacting;