Repository navigation
fix(responses): recover cold opaque state and harden replay, archive and lifecycle paths - #7388
Conversation
Preflight escaped-JSON size and reserve translator budget before materializing native Chat reasoning replay. Preserve existing budget errors, caller objects and retry/key-rotation ownership; retain synthetic regressions and update accounting contracts.
Distinguish the fixed serialized-request cap from the full final-size transient translator reservation. Document changed-replay ordering and finally release without changing production or test behavior.
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…the retry's
The guard holds pre-content events across an empty-completion retry and
releases them with the retry's output. With nothing between the two attempts,
an unsigned reasoning delta from the retry is appended to the discarded
attempt's still-open reasoning item, so the client sees one reasoning item
whose text runs both attempts together ("discardedretry").
Each retry point now queues an assistant_boundary after the held events (only
when something is held), which the bridge and the direct encoders already
treat as "close the open items".
Co-Authored-By: Claude Code <noreply@anthropic.com>
A drain past its deadline calls abortAndReleaseAllTurns, which aborts every controller bound to an admitted turn. trackStreamLifetime only stopped on a consumer cancel, and several callers (the adapter bridge, run-turn execution, client-encoder delivery, policy refusals, local SSE) pass a fresh AbortController that nothing else listens to. For those turns the abort was a no-op: the stream kept relaying upstream output and its source stayed open. trackStreamLifetime now listens for the controller's abort and, once tracking has started, finishes the turn, cancels the source and errors the stream. The listener is attached after registerTurn so the lease's own abort handling still runs first, and a controller registerTurn aborted synchronously (a settled lease) keeps the previous behaviour. Co-Authored-By: Claude Code <noreply@anthropic.com>
A throwing onDone callback must not leave the source open. Co-Authored-By: Claude Code <noreply@anthropic.com>
…transport The HTTP boundary already maps typed archive failures to a sanitized 503 with a stable code. The WebSocket pre-response catch sent them as a raw 502 proxy_error frame that carried the archive path. Apply the same mapping there.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
✅ Deterministic PR hygiene checks passed. |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 32
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 88473ed413
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return parsed._stripReasoningEncryptedContent !== true | ||
| && outboundResponsesBodyCarriesOpaqueBlob(outboundBody); |
There was a problem hiding this comment.
Preserve keepalives during cold opaque preflight
When an ordinary same-provider turn carries encrypted_content, this broader predicate now sends it through the awaited preflight in passthrough-dispatch.ts, even without the prior retention marker. preflightComboStreamResponse buffers response.created, response.heartbeat, and SSE keepalive comments until semantic output, a terminal, or the large buffer cap, so a slow first token can leave the downstream connection completely idle long enough for a client or intermediary timeout despite the upstream remaining healthy. Please either limit the cold path to histories that actually need recovery or use a preflight wrapper that can forward liveness keepalives while retaining the ability to replace a proven pre-output rejection.
AGENTS.md reference: src/AGENTS.md:L17-L19
Useful? React with 👍 / 👎.
Summary
Existing threads stopped working after a model or provider switch (#7369, a 2.88.0 regression). The upstream rejected the replayed encrypted reasoning inside an HTTP-200 SSE stream. Recovery only ran when the request carried the process-local retention marker. A thread restored after a restart or switch has no marker, so it never recovered and every later turn failed.
With this change, the bounded zero-output preflight runs whenever the outbound body actually carries opaque state. A flat SSE
type:errorevent that carries the dedicatedinvalid_encrypted_contentcode, or the exact unverifiable-ciphertext wording, now counts as a rejection. A 502 that carries the same rejection identity is accepted under the existing strict classifier. One sanitized rebuild follows a proven pre-output rejection. A repeated rejection is terminal, a generic 502 still authorizes nothing, and a native turn that succeeds keeps its ciphertext.The bundle also carries five more fixes on the same replay and lifecycle paths:
Items fixed
Closes #7369
Carried PRs
bcefb120a: cold-history recovery incore-opaque-recovery.tsandcombo-stream-preflight.ts, ADR-7369,native-reasoning-retention.md,responses-cold-opaque-recovery.test.ts, the Azure test tweak, layout mapsopenai-chat/reasoning-replay.ts,openai-chat/passthrough.ts,chat-native.ts), structure docs, scope testreasoning-retention.ts),reasoning-archive-error.ts, the HTTP mapping inserve-options.ts, quota and HTTP tests, docs-site server configopenai-responses/transport.tsplus its testempty-completion-guard.tsplus its testsrc/server/lifecycle.ts, plus ashutdown-draintestsrc/server/lifecycle.tsis an owner-only CODEOWNERS path. Exactly-once cleanup, pending-read cancellation and settled-lease tests intests/service/shutdown-drain.test.tsstay greenAll carried heads cherry-picked onto
79fa97be6with no conflicts. Original authorship is kept on every carried commit.Follow-up from review
A sol (gpt-6.1-sol) review of the full diff found no P0 or P1 issues. Its one P2 is fixed in
88473ed41. #7309 added the sanitized archive 503 at the HTTP boundary only. Over WebSocket,response.createhit the raw catch insrc/server/index/websocket-handler.tsand sent a 502proxy_errorwhose message included the absolute archive path and lacked the stable code. That catch now uses the samereasoningArchiveErrorResponsemapping and sends a 503 error frame. The newtests/responses/reasoning-archive-ws.test.tsdrives a real proxy WebSocket through quota, lock-busy and unsafe-directory refusals. It asserts a 503 frame with the stable code, no path or secret text, zero upstream sends and an unchanged archive directory. Before the fix it failed (received 502). It is registered inlayout.jsonandtest-layout-expected.json, andstructure/transports/responses-failover.mdnames it.Judgment calls
Held out
src/server/responses-phase-inference.ts:85-115, and the PR adds an editor-policy line tosrc/server/auth-cors.tsthat needs security review. Not carried.Verification
All on this branch at
88473ed41, rebased onorigin/dev79fa97be6(dev had not moved at push time).bun run typecheck: pass.bun test --isolate: 417 pass / 0 fail across 23 files. The files cover chat-inbound/native reasoning replay and scope, chat-native-spend, empty-completion core/guard/hardening, opaque-blob wrapped rejection, protocol opaque state, reasoning-archive-http, reasoning-retention, -quota, -routing and -v2, Azure, cold and standard opaque-blob recovery, retained-reasoning stream recovery, forward-transport base URL,tests/service/shutdown-drain.test.ts,tests/lab/core-lab-boundary.test.ts, and both test-layout tests.tests/responses/reasoning-archive-ws.test.ts: 3/3 with the fix and 0/3 with the fix stashed (it received 502). Layout, ratchet and structure tests after registration: 87/87.reasoning-retention-quota.test.ts: cross-process lock contention (a lock held by another process fails busy, writes nothing, and the retry archives, which runs a child process), over-cap legacy directory preserved, scan and close failure priority, non-regular, symlinked and uninspectable entries refusing closed.reasoning-archive-http.test.ts: the production router's quota, busy, I/O and partial-write refusals with zero sends, for v1 and v2 and for stream and non-stream.bun run test:changed: it resolved to 1531 files. 34155 pass / 42 skip / 1 fail. The one failure,stage audio: audio buffer and audio response frames cross the relay unchangedintests/server/server-live-realtime-fixtures.test.ts, is a timing flake: it passed 8/8 three times alone. The host load average was about 12 from concurrent worktrees. Nothing in this branch touches the realtime relay.bun run structure:check: pass.bun run privacy:scan: pass.tests/ci-workflows/file-size-ratchet.test.ts,structure-ssot.test.tsandrepo-hygiene.test.ts: 76/76.bun run test;test:changedabove already covered 1531 files.Ratchet hot files touched (line counts after this branch)
src/server/index/serve-options.tsscripts/test-layout/layout.jsonsrc/server/index/websocket-handler.tssrc/server/lifecycle.tssrc/server/responses/core-opaque-recovery.tssrc/adapters/openai-chat/passthrough.ts/reasoning-replay.tsFiles not touched:
passthrough-dispatch.ts,provider-routes.ts,openai-chat.ts,auth-context.ts,combos.test.ts.Checklist
native-reasoning-retention.md,responses-failover.md,inbound-compat.md,byte-accounting.md, and docs-site server config from fix(responses): cap aggregate reasoning archive storage #7309.Co-authored-by: Ingwannu 186453546+Ingwannu@users.noreply.github.com
Co-authored-by: luvs01 27862058+luvs01@users.noreply.github.com
Co-authored-by: vadymhimself 11277453+vadymhimself@users.noreply.github.com