Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,10 +1144,6 @@ export async function handleResponses(
}
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;

if (isThreadSpawnRequest(req.headers)) {
await maybePrimeSubagentQuota(config);
}

let route: RouteResult;
try {
route = routeModel(config, parsed.modelId);
Expand All @@ -1158,6 +1154,17 @@ export async function handleResponses(
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
}

const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
&& parsed._previousResponseInputExpanded !== true;
// A canonical replay miss must not poll quota upstream before the final fail-closed decision.
// Cached fallback state can still select a provider with native continuation support below.
if (
isThreadSpawnRequest(req.headers)
&& !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
) {
await maybePrimeSubagentQuota(config);
Comment on lines +1161 to +1165

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prime quota before deciding the continuation route

When a thread_spawn continuation was previously served by an API-key fallback, then local replay and quota caches become stale (for example after a proxy restart), this condition suppresses maybePrimeSubagentQuota. Unknown quota is treated as available by fallback selection, so the request remains on the canonical route and is rejected below, even though priming would identify the exhausted primary and return the continuation to the API-key provider that owns its upstream response ID. Preserve quota-aware fallback selection, or otherwise evaluate continuation-capable fallbacks, before rejecting.

Useful? React with 👍 / 👎.

}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

let authCtx: CodexAuthContext = { kind: "main", accountId: null };
let selectedForwardHeaders = req.headers;
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
Expand Down Expand Up @@ -1206,6 +1213,20 @@ export async function handleResponses(
return unreadableEncryptedAgentTaskResponse();
}

// The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
// safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
// I/O instead of stripping the id and silently forwarding a context-free delta (#702).
if (
hasUnexpandedPreviousResponse
&& isCanonicalOpenAiForwardProvider(route.provider)
) {
Comment on lines +1220 to +1222

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject misses for every forward Responses provider

When a custom or noncanonical openai-responses provider uses authMode: "forward", this canonical-URL check is false, so an expired continuation still proceeds. The adapter strips previous_response_id for every forward provider, not only the canonical URL, and consequently sends only the current delta upstream—the same silent context loss this change is intended to prevent. Align this fail-closed predicate with the adapter's forward-mode stripping predicate, or preserve native continuation IDs for forward providers that actually support them.

Useful? React with 👍 / 👎.

return formatErrorResponse(
Comment on lines +1219 to +1223

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue combo failover to the provider that owns the response

When a combo continuation's local replay has expired and the current target is canonical OpenAI, this local 400 invalid_request_error is classified as stop by comboFailureDecision, so no later target is attempted. This breaks a round-robin/failover chain when the response ID was created by a later API-key Responses target, which could natively resume it. Skip incapable canonical targets for replay misses, or make this target-specific failure hop so the owning provider can be tried.

Useful? React with 👍 / 👎.

400,
"invalid_request_error",
"OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new continuation-expiry failure

This adds a user-visible terminal condition: after replay TTL expiry, eviction, or loss of the local snapshot, canonical continuations now return 400 and require a new session, but docs-site/src/content/docs/reference/architecture.md only says remembered previous_response_id input is expanded when available and no guide describes this failure or recovery. Update the English documentation and keep its translated counterparts consistent so users can diagnose the new response.

AGENTS.md reference: AGENTS.md:L152-L153

Useful? React with 👍 / 👎.

);
}

await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx });

{
Expand Down
Loading
Loading