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

Merged
AN
1d 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 lifecycle, and task delegation—behavior changes on cancel and time-budget exits, though covered by new tests and scoped upstream_fix markers.

Overview
Fixes runs that could hang past time budgets when a provider stream stalled, and stops losing partial work on cancellation.

Provider cancellation: LLM.stream() consumption now goes through abortableStream, so abort/deadline wins races on stream open, pending next(), and triggers iterator cleanup without waiting on a stuck return().

Abort commit ordering: On hard abort, partial text/reasoning and in-flight tools are finalized first (including task tools that recover child session text via interruptedTaskOutput / messageID in metadata); SessionStatus idle is deferred until after those commits. Soft generation interrupt still closes open parts without clearing finish when only the request was aborted.

Task tool & adapters: Cancelled tasks return labelled partial child findings instead of a bare abort error; metadata is published (with child messageID) before the child prompt runs, with throwIfAborted after async metadata. Legacy/batch tool bridges now await async metadata() instead of fire-and-forget.

CLI --output: When bounded finalization cannot produce an answer, the harness note can append Partial assistant response (not finalized) from the last non-final step text (OutputCollector.preserveLastText).

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


Summary by cubic

Fixes runs hung past their time budget when provider streams stall or ignore cancellation, and preserves partial findings from interrupted work instead of discarding them.

  • 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.