feat(proxy): carry prior-turn reasoning through to the chat upstream - #158
Merged
Merged
Conversation
Anthropic clients echo thinking blocks back on later turns, and the
`signature` on each block is what lets the server recover the reasoning
behind it. Codex replays Responses reasoning items for the same reason.
We dropped both, so a client following either contract lost its prior
reasoning as soon as it sent a second request.
We are the API as far as our clients are concerned, so mint and verify
these ourselves rather than waiting on Anthropic:
- Add `reasoning-seal`, sealing reasoning into an opaque, tamper-evident
blob with aes-256-gcm. Reuses the storage encryption secret, and falls
back to a plain encoding when it is unset so an unconfigured deployment
still proxies successfully. Unsealing never throws — an unopenable
value degrades to "no prior reasoning" instead of failing the request.
- Emit the signature on outbound thinking blocks (streamed as a
`signature_delta` just before the block closes) and on outbound
Responses reasoning items, so clients have something replayable.
- Recover inbound reasoning and send it upstream on the assistant
message it belongs to, as `reasoning` — the field the CodeBuddy chat
upstream round-trips, and the one its own client populates when it
replays a response.
- Handle `redacted_thinking`, which previously fell through to
`stringifyContent` and reached the model as a JSON dump of the opaque
payload.
- Stop turning replayed Responses reasoning items into empty user turns.
They had neither `role` nor `content`, so the fallback produced
`{role:'user', content:''}` — a turn the user never sent, repeated and
accumulated on every subsequent turn.
The signature is preferred but never a gate: a client may replay a
genuine Anthropic signature from a session started elsewhere, and that
should fall back to the summary rather than discard good reasoning.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f8e39b304
ℹ️ 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".
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #158 +/- ##
==========================================
+ Coverage 95.41% 95.52% +0.11%
==========================================
Files 37 37
Lines 6585 6706 +121
Branches 1899 1933 +34
==========================================
+ Hits 6283 6406 +123
+ Misses 302 300 -2
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
The reasoning we hand to clients is the upstream's own summary, and it already travels in the clear in the `thinking` field beside the signature. Encrypting a second copy protected nothing — the plaintext was right there — while adding a module, a key dependency, and a failure mode (a deployment without the secret silently changed behaviour). Drop `reasoning-seal` and pass the reasoning through as-is: - Anthropic path: thinking blocks carry their text in `thinking`, which is what clients echo back and what inbound handling reads. No signature is minted, because a replayable one would have to duplicate the text — and that duplication is not hypothetical: emitting it made the multi-hop server-tool tests see the reasoning twice. - Responses path: `encrypted_content` carries the reasoning verbatim. Codex treats the field as an opaque string it never opens, so plaintext round-trips exactly as well. - Values we minted are marked with a `cbreason1:` prefix. Not a security measure — it is what stops us reading a genuinely encrypted blob (an Anthropic signature, an OpenAI-issued blob) as reasoning text and forwarding ciphertext upstream. Unmarked values fall back to the summary.
orangeboyChen
enabled auto-merge (squash)
September 17, 2026 11:57
…e merge CI's patch-branch gate sits at 90%; the previous commit left the changed lines at 89.06% (57/64). Cover the branches that were missing: - Anthropic: several thinking blocks in one assistant turn (interleaved thinking puts one before each tool call) join in order. - Responses: a reasoning item attaching to the following assistant message; summaries holding bare strings; summary entries with no text; a non-string `encrypted_content` falling back to the summary; and one reasoning item emitted for many reasoning deltas rather than one per delta. Also drop the merge that combined a carried reasoning with a `reasoning` field already on the assistant message. `ResponsesInputItem` has no such field, so a client cannot send it and the branch was unreachable — the mapper is the only thing that sets it. Replaced with a plain assignment. Changed-branch coverage is now 98.44% (63/64).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Anthropic clients echo thinking blocks back on later turns, and the
signatureon each block is what lets the server recover the reasoning behind it. Codex replays Responses reasoning items for the same reason. We dropped both, so a client following either contract lost its prior reasoning as soon as it sent a second request.We are the API as far as our clients are concerned, so round-tripping reasoning is our job.
What changed
/v1/messages): inboundthinking/redacted_thinkingblocks are recovered and sent upstream on the assistant message they belong to, asreasoning— the field the CodeBuddy chat upstream round-trips. Previously they were dropped.redacted_thinkingis now matched explicitly; without that branch it fell through tostringifyContentand the model received a JSON dump of the opaque payload as if it were user prose./v1/responses): reasoning items no longer become empty user turns. They had neitherrolenorcontent, so the fallback produced{"role":"user","content":""}— a turn the user never sent, repeated and accumulated on every subsequent turn. Replayed reasoning now attaches to the assistant turn it accompanies.encrypted_content, in both streaming and non-streaming paths. Previously the only reasoning we handed back was a transientreasoning_text.delta, which no client can replay because it has no id.previous_response_idcontinuations persist reasoning, so a client that does not replayoutputkeeps its reasoning.Reasoning is carried in the clear, not encrypted
The reasoning here is the upstream's own summary, and it already travels in the clear in the
thinkingfield beside the signature. Encrypting a second copy would protect nothing while adding a module, a key dependency, and a failure mode (a deployment without the secret silently changing behaviour).Values we minted are marked with a
cbreason1:prefix. Not a security measure — it is what stops us reading a genuinely encrypted blob (an Anthropic signature, an OpenAI-issued blob) as reasoning text and forwarding ciphertext upstream. Unmarked values fall back to the summary, and are covered by tests.Verified against the CodeBuddy client bundled in
WorkBuddy.app: it sends prior-turn reasoning as a plainassistant.reasoningstring and reads it back frommessage.reasoning, so the field name matches what the upstream already speaks.Verification
tsc --noEmitandeslintclean.redacted_thinkingregressions, streamed reasoning items and their ordering, session persistence, and the ciphertext-not-forwarded cases.Caveat
Whether CodeBuddy accepts an inbound assistant
reasoningfield was not confirmed against the live endpoint — no credentials were available. It is inferred from the field its own client sends. Worth watching upstream errors after deploy.