fix(session): cancel stalled streams and preserve partial findings

AN
7h ago
+591-50

Issue for this PR

Closes #1484

Stacked on feat/analyst-harness-dab-2026-10 (#1441).

Type of change

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

What does this PR do?

Why: a provider stream that stops delivering events (and ignores cancellation) kept a run hung past its time budget, and partial findings from interrupted work were lost.

Root cause: LLM.stream() and the provider iterator were awaited directly, so abort was only observed after another event; hard abort published idle before partial output was committed; task discarded child findings when the child prompt rejected/returned empty; failure notes replaced earlier unfinished text.

Before -> after:

  • Stalled provider setup/read: hung indefinitely -> abort/deadline ends the wait (module-scope abortable-stream.ts, listeners removed, iterator cleanup requested without awaiting a stuck return()).
  • Hard abort: idle published first -> partial text/reasoning and task results committed, then idle.
  • Interrupted task: "Tool execution aborted" -> committed child findings returned, labelled incomplete (task-output.ts); metadata publishes child session and prompt message IDs before dispatch.
  • Budget finalization failure: note only -> note plus Partial assistant response (not finalized).

Deferred: no new post-tool timeout (no evidence of the historical transport cause); soft-interrupt polling unchanged. Review follow-ups (P2, not fixed): recovery after compaction/continuation turns only sees the original prompt lineage; batched tasks publish metadata on the outer batch part so recovery does not apply to them; a finalization stall before the first step leaves unfinished text without the warning note.

How did you verify your code works?

  • New regressions: stalled setup/post-tool reads under soft/hard cancel (0/4 before, 4/4 after), abort-time commit ordering, parent abort preserving child findings, task recovery cases, output-collector fallback.
  • Independent Codex review: no P0/P1; three P2s listed above.
  • bun run typecheck in packages/opencode: pass. git diff --check: clean.
  • Marker audit against the v1.17.9 baseline: processor.ts, task.ts, run.ts exist upstream; all altimate edits are inside balanced, non-nested altimate_change markers (one nested pair in processor.ts fixed).
  • Tests touching the changed modules (session, tool, cli, upstream, release-validation, etc.): the full combined run showed timeout failures under concurrent machine load (28); every failing file passes when re-run alone (prompt 52, registry 14, project-scan 86, db 1, processor-effect 59, abortable-stream 4, task 13, run-output 10, budget-finalization+time-budget+compaction 95, 0 fail).

Screenshots / recordings

N/A

Checklist

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

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Touches core session streaming, abort teardown, and task/subagent result paths where ordering bugs could drop output or leave sessions stuck; behavior is heavily tested but spans cancellation races and CLI finalization.

Overview
Fixes runs that hung past time budgets when provider streams ignored abort, and stops discarding useful partial output on cancellation or budget exhaustion.

Stream cancellation: LLM consumption now goes through abortableStream, which races abort against stream open and each next() so soft/hard deadlines can exit even when the provider iterator stalls; cleanup still requests iterator.return() without blocking shutdown.

Abort commit ordering: On interrupt, partial text/reasoning is finalized before session idle is published; interrupted tools go through interruptedToolPart, which for running task calls pulls committed child text via interruptedTaskOutput instead of a generic abort error.

Task tool: Child session and prompt messageID are published in metadata before the sub-prompt runs; aborted tasks return labelled partial findings and interrupted metadata, with guards so cancellation during metadata does not start child work.

CLI --output: When bounded finalization fails after a time budget, the harness note can include the last unfinished assistant step as “Partial assistant response (not finalized)”.

Tool bridges: Legacy/batch tool contexts await async metadata() publication instead of fire-and-forget voiding.

Reviewed by Cursor Bugbot for commit fd4b8eb72079e27d96464fe6938280f7f43174bd. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Fixes stalled provider streams leaving runs hung past their time budget and losing partial findings from interrupted work.

  • Wraps LLM.stream() in abortableStream so provider setup, reads, and iterator cleanup race the abort signal and cannot hang the session.
  • Hard aborts now commit partial text, tool results, and child task findings before publishing idle status; interrupted task outputs are labelled incomplete and recover committed child findings.
  • Budget-finalization failures now append the last unfinished assistant text under Partial assistant response (not finalized) instead of emitting a note alone.
  • Cancellation during task metadata publication aborts before starting child work; batched tools now await asynchronous metadata publication.

No migration steps. Deferred: recovery after compaction only sees the original prompt lineage, batched tasks still publish metadata on the outer batch part, and a finalization stall before the first step leaves unfinished text without a warning note.