Skip to content

fix: backfill interrupted tool results so stopped sessions stay resumable - #150

Merged
cnjack merged 1 commit into
mainfrom
fix/interrupted-tool-result-backfill
Jul 18, 2026
Merged

fix: backfill interrupted tool results so stopped sessions stay resumable#150
cnjack merged 1 commit into
mainfrom
fix/interrupted-tool-result-backfill

Conversation

@cnjack

@cnjack cnjack commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Problem

Stopping a run mid-tool (or a fatal tool-node failure) persisted tool_call entries without matching tool_result entries. Resuming such a session rebuilt a history the model API rejects:

an assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'

A second, related hole: ReconstructState skipped every entry between subagent_start and subagent_result. When the main agent issued a parallel batch (e.g. grep + subagent), the fast tool's result landed inside that window and was swallowed — again leaving a dangling tool_call on resume (session-86e09b12, tool_call_ids did not have response messages: grep:53).

On the web, /stop additionally emitted its own agent_done("stopped by user") that raced the runner's terminal event, producing a red error card for a deliberate user action.

Changes

  • runner (internal/runner/runner.go): drainDanglingToolResults backfills a result for every announced tool call that never produced one, on every early-exit path (user stop ×2, fatal event error). The backfill carries no error — an interrupted call is not a failed call, so front-ends render a calm row instead of a red context.Canceled card next to the stop notice. Tool-start tracking now also records the tool name so the backfill can attribute results correctly.
  • session (internal/session/history.go): ReconstructState repairs any remaining dangling tool_calls with placeholder tool messages (covers process kills and older recordings), exported as InterruptedToolOutput and shared with the runner. The subagent start/result/async markers are now informational-only — subagent-internal calls are never persisted, so entries in that window are the main agent's own parallel results and must be kept.
  • web backend (internal/web/chat.go, internal/handler/web.go): /stop no longer double-reports; the runner owns the single OnAgentDone(context.Canceled), which the web handler maps to agent_done{stopped:true}.
  • web frontend (store.ts, wsBridge.ts, ws.ts): a stopped run shows a muted "Stopped by user" notice instead of a red error card.

Tests

  • internal/runner/cancel_backfill_test.go: cancels a run mid-tool via a blocking tool; asserts exactly one result reaches the handler, the session pairs call/result on disk, and the reconstructed history satisfies the tool-call invariant.
  • internal/session/history_test.go: invariant assertion helper + repro of the session-86e09b12 parallel-window failure, interrupted-call backfill, and backfill positioning when later turns exist.

Verification

  • go test ./... — all pass
  • make lint — golangci-lint 0 issues; web + packages typecheck clean

Summary by CodeRabbit

  • New Features

    • Added a clear “Stopped by user” notice when an active run is canceled.
    • Preserved tool-call history when runs are interrupted, including placeholder results for incomplete calls.
    • Improved session history reconstruction for parallel tool activity.
  • Bug Fixes

    • Prevented duplicate completion notifications after stopping a run.
    • Ensured interrupted runs finish promptly and maintain valid conversation history.
    • Continued displaying detailed errors for failures unrelated to user cancellation.
  • Tests

    • Added coverage for cancellation, interrupted tool calls, and parallel tool-result reconstruction.

…able

A session stopped mid-tool (user stop, fatal tool-node failure) persisted
tool_call entries without matching tool_result entries; resuming rebuilt a
history the model API rejects with 'assistant message with tool_calls must
be followed by tool messages'.

- runner: drain dangling announced tool calls on every early-exit path,
  recording an '[Interrupted before result was recorded]' result. The
  backfill carries no error — an interrupted call is not a failed call, so
  front-ends render a calm row instead of a red 'context.Canceled' card.
- session: ReconstructState repairs any remaining dangling tool_calls with
  placeholder tool messages (covers process kills and older recordings),
  and no longer skips entries inside subagent start/result windows — the
  main agent's parallel tool results land there and were being swallowed,
  leaving dangling calls (session-86e09b12).
- web: /stop no longer emits its own agent_done (the runner reports the
  single context.Canceled); the web handler maps cancellation to a
  stopped:true payload and the timeline shows a muted 'Stopped by user'
  notice instead of a red error card.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Cancellation now produces a single stopped completion event, backfills missing tool results, preserves tool results across subagent markers, repairs reconstructed histories, and renders user stops as notices instead of errors.

Changes

Cancellation and tool-history lifecycle

Layer / File(s) Summary
Drain interrupted tool calls
internal/runner/runner.go, internal/runner/cancel_backfill_test.go
runInner records tool names, emits interrupted results for pending calls on termination, and tests persisted and reconstructed cancellation history.
Preserve and repair reconstructed history
internal/session/history.go, internal/session/history_test.go
ReconstructState retains tool results between subagent markers and inserts InterruptedToolOutput messages for unmatched tool calls.
Propagate stopped completion state
internal/handler/web.go, internal/web/chat.go, web/src/app/store.ts, web/src/app/wsBridge.ts, web/src/lib/ws.ts
Canceled runs emit stopped: true, stop handling avoids duplicate completion events, and the client displays a stopped notice instead of an error card.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant handleStop
  participant runInner
  participant WebHandler
  participant WebSocket
  participant chatStore
  User->>handleStop: request stop
  handleStop->>runInner: cancel context
  runInner->>runInner: backfill pending tool result
  runInner->>WebHandler: OnAgentDone(context.Canceled)
  WebHandler->>WebSocket: agent_done stopped payload
  WebSocket->>chatStore: dispatch stopped state
  chatStore->>chatStore: append notice
Loading

Possibly related PRs

  • cnjack/jcode#8: Related tool-call/result persistence and reconstruction changes.
  • cnjack/jcode#79: Related single OnAgentDone(ctx.Err()) cancellation flow.
  • cnjack/jcode#147: Related WebSocket and client agent_done payload handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: backfilling interrupted tool results so stopped sessions remain resumable.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/interrupted-tool-result-backfill

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/runner/cancel_backfill_test.go (1)

156-163: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the interruption-specific result contract.

The test only checks count and ID, so it still passes if the framework emits context.Canceled as the tool error—the red-row regression this PR intends to prevent. Assert the placeholder output, Err == nil, and preserved tool name.

Proposed assertions
 if h.results[0].ToolCallID != "call-block-1" {
     t.Errorf("result ToolCallID = %q, want call-block-1", h.results[0].ToolCallID)
 }
+if h.results[0].Name != "block" {
+    t.Errorf("result Name = %q, want block", h.results[0].Name)
+}
+if h.results[0].Output != session.InterruptedToolOutput {
+    t.Errorf("result Output = %q, want %q", h.results[0].Output, session.InterruptedToolOutput)
+}
+if h.results[0].Err != nil {
+    t.Errorf("result Err = %v, want nil", h.results[0].Err)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/cancel_backfill_test.go` around lines 156 - 163, Strengthen
the assertions in the result validation for the announced call so it verifies
the interruption-specific contract: assert the placeholder output, require Err
to be nil, and confirm the original tool name is preserved, in addition to the
existing count and ToolCallID checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/runner/cancel_backfill_test.go`:
- Around line 156-163: Strengthen the assertions in the result validation for
the announced call so it verifies the interruption-specific contract: assert the
placeholder output, require Err to be nil, and confirm the original tool name is
preserved, in addition to the existing count and ToolCallID checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05bf867a-5c6f-4789-a7e1-092758ddee94

📥 Commits

Reviewing files that changed from the base of the PR and between c7c05c5 and f77c405.

📒 Files selected for processing (9)
  • internal/handler/web.go
  • internal/runner/cancel_backfill_test.go
  • internal/runner/runner.go
  • internal/session/history.go
  • internal/session/history_test.go
  • internal/web/chat.go
  • web/src/app/store.ts
  • web/src/app/wsBridge.ts
  • web/src/lib/ws.ts

@cnjack
cnjack merged commit e7ab226 into main Jul 18, 2026
4 checks passed
@cnjack
cnjack deleted the fix/interrupted-tool-result-backfill branch July 18, 2026 03:59
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