Skip to content

fix(harness): drop an orphan reasoning-end instead of letting it kill the run - #6702

Merged
pedrofrxncx merged 1 commit into
mainfrom
fix/orphan-reasoning-end
Aug 28, 2026
Merged

fix(harness): drop an orphan reasoning-end instead of letting it kill the run#6702
pedrofrxncx merged 1 commit into
mainfrom
fix/orphan-reasoning-end

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

An agent cloned its repo, made two tool calls, and its card went to In Review reading "Completed — I'll start by cloning the repo." The agent was still running in its pod, and kept running for minutes after Studio had torn its stream down and moved on.

[Decopilot] stream pump error for thread thrd_OXa6vEC0kfR3O_-jsB9NA:
  Received reasoning-end for missing reasoning part with ID "stream-2".
  Ensure a "reasoning-start" chunk is sent before any "reasoning-end" chunks.

The AI SDK reducer throws on an end for a part it does not have open, and that throw kills the stream.

Two ways a start goes missing

  • A restart drops it. pending buffers every chunk until the turn's message id is known, and an abandoned attempt clears it wholesale (pending.length = 0) along with the coalescer — but UiChunkTranslator keeps its openStreamBlocks, so the next message_start closes a block whose start went in the bin.
  • A finish-step lands between them. The reducer CLEARS its open parts on that boundary, so every end after it is an orphan by definition. The step-boundary call sites already close open blocks first for exactly this reason — this is the backstop for the next call site that forgets.

The fix

Rather than chase each path, the guard sits at the one place every chunk reaches the consumer and enforces the reducer's own rule directly: a start opens an id, an end closes it, finish-step clears them all. An end with no open id is dropped — the part it referred to is already closed or was never opened, so dropping it costs nothing and keeps the run alive.

Keyed on kind and id, like the coalescer's merge check: ids are minted distinctly, so a reasoning-end landing on a text part's id means something upstream is confused, and closing that part on its behalf would corrupt it rather than just misorder it.

Buffered chunks are guarded at the startTurn flush, not on the way into pending — an end whose start is still buffered is not an orphan, and pending is dropped wholesale on a restart, so what survives is only decidable at the emit that actually sends it.

Testing

packages/harness-runner/orphan-end-guard.test.ts — 6 cases: the matched pair passes untouched, the bare end is dropped, finish-step orphans a still-open part, a double end is dropped, kinds are tracked independently, and non-lifecycle chunks (including null and a bare string) pass through.

bun test packages/harness-runner/ → 78 pass / 0 fail. bun run --cwd=apps/api check, bun run lint (0 errors), bun run fmt clean.

Known, not fixed here

The pump rethrows correctly, but the run still landed as completed rather than failed, which is what let the card advance to In Review while the agent was mid-work. That mislabel is a separate defect — worth its own change, and it only bites when a crash happens, which this PR is meant to stop.


Summary by cubic

Adds an orphan-end guard in packages/harness-runner so a reasoning-end or text-end for a part the AI SDK doesn't have open is dropped instead of throwing and killing the stream. Previously such a chunk tore down the run mid-work; now the run continues and the chunk is ignored.

  • Tracks open parts by kind and id, matching the coalescer's merge check, and clears them on finish-step.
  • Guards buffered chunks at flush time, so an end whose start is still buffered isn't treated as orphaned.
  • Adds tests for matched pairs, orphaned ends, double ends, finish-step, kind separation, and pass-through chunks.
  • Does not fix the separate issue where a crashed run is labeled completed instead of failed.

Written for commit 1694299. Summary will update on new commits.

Review in cubic

… the run

The AI SDK reducer throws on an end for a part it does not have open —
`Received reasoning-end for missing reasoning part with ID "stream-2"` — and
that throw tears down the Studio-side stream mid-run. Observed live: an agent
that had cloned its repo and was two tool calls in had its run marked finished
and its card advanced, while the pod kept working for minutes afterwards,
oblivious.

Two things upstream can separate an end from its start:

- a restart drops the start. `pending` buffers every chunk until the turn's
  message id is known, and an abandoned attempt clears it wholesale along with
  the coalescer — but `UiChunkTranslator` keeps its `openStreamBlocks`, so the
  next `message_start` closes a block whose start went in the bin;
- a `finish-step` lands between them. The reducer CLEARS its open parts on
  that boundary, so any end after it is an orphan by definition. The
  step-boundary call sites close open blocks first for exactly this reason —
  this is the backstop for the next call site that forgets.

Rather than chase each path, the guard sits at the one place every chunk
reaches the consumer and enforces the reducer's own rule: a start opens an id,
an end closes it, `finish-step` clears them all, and an end with no open id is
dropped. The part it referred to is already closed or was never opened, so
dropping it costs nothing and keeps the run alive.

Keyed on kind AND id, like the coalescer's merge check: ids are minted
distinctly, so a reasoning-end landing on a text part's id means something
upstream is confused and closing that part would corrupt it.

Buffered chunks are guarded at the `startTurn` flush, not on the way into
`pending`: an end whose start is still buffered is not an orphan, and
`pending` is dropped wholesale on a restart, so what survives is only
decidable at the emit that actually sends it.
@pedrofrxncx
pedrofrxncx merged commit 3925510 into main Aug 28, 2026
33 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/orphan-reasoning-end branch August 28, 2026 17:07
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6702 fix(harness): drop an orphan reasoning-end instead of letting it kill the run
Bump type: patch

- @decocms/harness-runner (packages/harness-runner/package.json): 0.9.0 -> 0.9.1

Deploy-Scope: both
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.

1 participant