fix(session): cancel stalled streams and preserve partial findings
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 stuckreturn()). - 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 typecheckinpackages/opencode: pass.git diff --check: clean.- Marker audit against the
v1.17.9baseline:processor.ts,task.ts,run.tsexist upstream; all altimate edits are inside balanced, non-nestedaltimate_changemarkers (one nested pair inprocessor.tsfixed). - 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 (
prompt52,registry14,project-scan86,db1,processor-effect59,abortable-stream4,task13,run-output10,budget-finalization+time-budget+compaction95, 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()inabortableStreamso 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.