fix(opencode): drop truncated reasoning from replayed history - #40148
Open
iceteaSA wants to merge 1 commit into
Open
fix(opencode): drop truncated reasoning from replayed history#40148iceteaSA wants to merge 1 commit into
iceteaSA wants to merge 1 commit into
Conversation
6 tasks
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.
Issue for this PR
Fixes #40147.
Related: #40146 / #40142 cover the session loop treating truncated turns as normal completions — same incident, one layer up, independent branch. #37946 covers empty assistant messages reaching the provider, which is directly relevant here (see below).
Type of change
What does this PR do?
A stored
reasoningpart on a same-model assistant message is replayed verbatim as a nativereasoningpart. Correct for Anthropic — signed thinking blocks must round-trip, and the separator workaround directly above exists for that.Wrong for a turn truncated at the output limit: its reasoning is an incomplete chain, and on a lane with no signature there is nothing to preserve by sending it back.
A subagent turn spent its whole output budget in the reasoning channel —
step-start,reasoning(129,961 chars),step-finish, no text, no tool call,finish: "length". Resuming replayed that chain and the model continued it, truncating again at the same limit: 132,948 characters, 4m19s, no text, no tools. A fresh dispatch of the same task on the same model completed normally in 274s, which isolates it to the replayed history.The change skips reasoning parts on a turn whose own
finishis"length", unless the part carries an Anthropic signature:The signature check reuses the existing discriminator from the workaround above rather than a provider allow-list, so a signature-less Anthropic-compatible proxy is handled correctly too. Scope is deliberately limited to turns that themselves truncated — a complete reasoning turn is not implicated by the evidence, and the
differentModelbranch (which already downgrades reasoning to text) is untouched.The second half of the change is the part worth reviewing. Dropping reasoning parts can leave a message whose only remaining content is an empty text part, which reaches the wire as:
{"role":"assistant","content":[{"type":"text","text":""}]}That is the failure mode in #37946. The existing guards do not catch it:
parts.length > 0counts the empty text part, and the final filter only excludes messages whose parts are allstep-start. So the fix carries a substantive-content check:Two deliberate choices there. It reads converted parts rather than stored ones — a first attempt read
msg.parts, which countsstep-finish(written on every completed turn, never converted) as substantive and let the empty message through anyway. Reading what actually reaches the provider makes stored-part taxonomy irrelevant.And it is gated on
droppedUnsignedReasoning, so it can only affect messages this change modified. A message where nothing was dropped short-circuits and replays byte-identically to today — including the pre-existing empty-assistant-message cases from #37946, which this PR deliberately does not touch. Fixing that class generally is a separate change.How did you verify your code works?
Four tests in
packages/opencode/test/session/message-v2.test.ts, written first and confirmed failing before the production change:lengthturn is droppedlengthturn is preservedstopturn is preservedlengthturn left with only empty text produces no assistant message at allMutation-checked in both directions. Reverting the reasoning-drop block turns two tests red; restoring returns them green.
A cross-family reviewer took three rounds and found two real defects that are worth stating rather than hiding, since both are the kind that pass a naive test:
step-finishmade the guard pass and the empty message still got through — and the regression test only passed because its fixture omittedstep-finish, which is not a shape the processor actually produces. The fixture now includes it.The reviewer also checked reachability empirically rather than assuming: querying a real session database found 102 messages with
finish: "length"and reasoning, of which 10 match the case this fix covers (no error, unsigned reasoning, non-empty text). It also verified the signature shape against the write path (packages/llm/src/protocols/anthropic-messages.ts→packages/opencode/src/session/processor.ts) rather than only the neighbouring read.An over-broad mutation — dropping the
finish === "length"condition — turns three tests red, including two pre-existing ones covering aborted assistant messages and OpenRouter reasoning details, each confirmed to fail individually rather than through ordering.bun testinpackages/opencode: 3232 pass / 0 fail (baseline ondevis 3228, measured on a clean checkout).bun typecheckclean inpackages/opencodeandpackages/core.Screenshots / recordings
Not a UI change.
Checklist