Skip to content

fix(session): repair orphan reasoning/text stream parts instead of failing the turn - #39287

Open
DragonBaiMo wants to merge 2 commits into
anomalyco:devfrom
DragonBaiMo:tolerate-orphan-stream-parts
Open

fix(session): repair orphan reasoning/text stream parts instead of failing the turn#39287
DragonBaiMo wants to merge 2 commits into
anomalyco:devfrom
DragonBaiMo:tolerate-orphan-stream-parts

Conversation

@DragonBaiMo

@DragonBaiMo DragonBaiMo commented Jul 28, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #33934

Also reported as #36241 (same error shape, macOS/CLI instead of Windows/Desktop).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Sessions using reasoning models behind OpenAI-compatible / Anthropic proxies fail with
reasoning part <id> not found (or text part <id> not found). There are two distinct
problems behind that one message, and this PR fixes both.

1. The error was promoted to a fatal turn failure (commit 1)

In the AI SDK's streamText event processor, a reasoning-delta/reasoning-end whose id has
no matching reasoning-start enqueues an in-band part
{ type: "error", error: "reasoning part <id> not found" } and then returns, leaving the
stream running. The raw chunk itself was already forwarded downstream unconditionally at the
top of the same transform, so this part is bookkeeping about a missing start block, not a
provider failure. The same applies to the text path.

The adapter mapped every error part to Effect.fail(event.error), so this recoverable part
aborted the turn: SessionProcessor.halt() logged it (with stack=undefined, since the
payload is a plain string) and emitted a session error. Matching is deliberately strict --
prefix reasoning part /text part and suffix not found -- so genuine provider errors
(rate limits, Tool "x" not found, etc.) still fail exactly as before. If the SDK rewords the
message the matcher stops matching and behavior reverts to the previous fatal path, so this
fails loud rather than silently swallowing errors.

2. The reasoning text and its signature were being dropped (commit 2)

Tolerating the error is not sufficient, because the underlying delta is still lost.
SessionProcessor requires a start block before it will create a part --
if (!ctx.currentText) return for text, if (!(value.id in ctx.reasoningMap)) return for
reasoning -- so an orphan delta and any providerMetadata riding on it are discarded.

That matters beyond the missing text. MessageV2 replays a reasoning part as
{ type: "reasoning", text, providerMetadata }, and the Anthropic transform keeps a reasoning
part only when part.text.trim().length > 0 || providerOptions?.anthropic?.signature != null
(or redactedData). A dropped orphan delta means the part is never created, so its text and
its thinking signature are both absent from replayed history, breaking thinking-block replay on
subsequent requests.

The fix belongs in the adapter rather than the processor. The adapter is the
protocol-normalization layer and already synthesizes block ids via
currentTextID/currentReasoningID; it simply never synthesized the missing start event.
The processor applies one consistent "no start, no part" contract to both text and reasoning,
and it is the wrong place to reconstruct protocol the provider omitted. So the adapter now
tracks which block ids have emitted a start and, on an orphan delta or end, emits the missing
start (carrying that event's providerMetadata) before it. The processor is unchanged, and
streams that do emit starts are passed through identically.

This is the pattern the native runtime already uses, so the change aligns the AI SDK adapter
with existing behavior rather than introducing a new convention. In
packages/llm/src/protocols/utils/lifecycle.ts, reasoningDelta unconditionally routes
through reasoningStart first, and reasoningStart is idempotent via
if (state.reasoning.has(id)) return state (textDelta does the same for text, tracking ids
in a Set). The AI SDK path was the only runtime missing that guarantee.

How did you verify your code works?

  • bun test test/session/ai-sdk.test.ts -- 8 pass / 36 assertions. Covers: orphan
    reasoning part .../text part ... errors (including colon ids like rs_abc:0) are dropped
    and the turn continues; genuine errors still fail; an orphan reasoning-delta gets exactly
    one synthesized reasoning-start and subsequent deltas do not re-synthesize;
    providerMetadata (e.g. an Anthropic signature) is carried onto the synthesized start;
    orphan text-delta and orphan reasoning-end are likewise repaired; and a well-formed
    start/delta/end stream is emitted unchanged.
  • bun test test/session/ -- 47 pass vs 42 on the unpatched baseline (the +5 are the new
    tests). The 16 pre-existing failures / 9 errors are identical with and without this patch.
  • bun run typecheck in packages/opencode -- the pre-existing src/ diagnostic count is 33
    both with and without this patch, and no diagnostic mentions the changed file.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

The AI SDK enqueues a non-fatal "reasoning part <id> not found" / "text
part <id> not found" error part when a reasoning/text delta arrives with no
preceding *-start block (common with OpenAI-compatible and Anthropic
proxies). The adapter promoted every error part to a fatal Effect.fail,
aborting the turn and spamming logs. Recognize these SDK-internal orphan
stream-state errors and drop them instead of failing; genuine errors still
fail.
Proxies that stream deltas without a preceding *-start caused the
processor to drop the delta and any providerMetadata riding on it,
including Anthropic thinking signatures. Repair the stream in the
adapter by emitting the missing start before the delta.
@DragonBaiMo DragonBaiMo changed the title fix(session): tolerate orphan reasoning/text stream-state parts fix(session): repair orphan reasoning/text stream parts instead of failing the turn Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Windows/Desktop v1.17.11] OpenAI gpt-5.5-fast xhigh fails with reasoning part rs_*:0 not found

1 participant