Skip to content

fix: drain tool results after stream disconnect#1252

Open
Astro-Han wants to merge 29 commits into
devfrom
codex/i927-tool-drain
Open

fix: drain tool results after stream disconnect#1252
Astro-Han wants to merge 29 commits into
devfrom
codex/i927-tool-drain

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Decouple local tool execution from the provider stream abort signal during session processor runs.
  • Add tool lifecycle callbacks so completed and failed tool executions persist through the existing processor state even if the provider stream never yields a tool-result or tool-error event.
  • Drain started tools on stream cleanup with a bounded timeout and direct session-cancel abort propagation, while preserving interrupted semantics for user cancel and lifecycle close.

Why

A provider stream timeout or transport disconnect after tool execution starts could abort the tool's local signal and let cleanup mark the tool part as interrupted before the completed result was persisted. PR1 for #927 needs the tool execution/result path to survive stream lifecycle failure without adding recovery UI or replay behavior.

Related Issue

Part of #927.

Human Review Status

Pending

Review Focus

Please focus on the tool abort signal split, the idempotent lifecycle callbacks, the tool-call readiness gate before terminal lifecycle writes, the bounded cleanup drain for started tools after stream failure, and the run-state cancel observer that aborts tools even while cleanup is draining.

Risk Notes

Runtime behavior changes for session tool execution on both macOS and Windows, but no OS-specific paths or packaging surfaces changed. The main behavior tradeoff is intentional: after a non-interrupt stream failure, started tools are allowed to finish up to the recovery drain timeout instead of being forced into the old one-second cleanup path; genuinely dead tools are still finalized instead of hanging the session indefinitely, and user cancel/lifecycle close can abort the drain directly.

Fresh-eye and Claude review found races around fast tool completion, abort-aware tool failure, lifecycle-close drain semantics, unbounded drain, drain-time cancellation, started-but-not-materialized tool calls, leftover Deferred cleanup, and early failure persistence. This PR includes follow-up commits covering those with the readiness gate, abort-aware failure guard, lifecycle-close abort, bounded drain, session cancel observer, remaining Deferred cleanup, pending-start reconciliation, and regression tests.

Skipped conditional checklist items: screenshots are not applicable because there are no visible UI or copy changes; docs/release/dependencies are not applicable because none were touched.

Migration note: this PR intentionally keeps the recovery fix in the legacy packages/opencode processor/LLM bridge. The behavior aligns with the upstream Core v2 runtime direction: local tool execution is tracked independently from provider stream events, tool settlements are drained before continuation or recovery decisions, and unresolved tools are finalized instead of being left pending. If PawWork later ports the upstream packages/core session runner and unified tool runtime, the right migration path is to carry these regression scenarios into the Core runner and delete this bridge, not grow it into a parallel runtime.

How To Verify

Install: bun install --frozen-lockfile completed successfully in the isolated worktree
Diff check: git diff --check passed
Processor effect tests: bun --cwd packages/opencode test test/session/processor-effect.test.ts -> 39 pass, 0 fail
Prompt effect tests: bun --cwd packages/opencode test test/session/prompt-effect.test.ts -> 63 pass, 0 fail
Run state tests: bun --cwd packages/opencode test test/session/run-state.test.ts -> 22 pass, 0 fail
Compaction tests: bun --cwd packages/opencode test test/session/compaction.test.ts -> 51 pass, 0 fail
LLM tests: bun --cwd packages/opencode test test/session/llm.test.ts -> 21 pass, 0 fail
Tool failure tests: bun --cwd packages/opencode test test/session/tool-failure.test.ts -> 3 pass, 0 fail
Opencode typecheck: (cd packages/opencode && bun run typecheck) -> tsgo --noEmit passed

Screenshots or Recordings

Not applicable. No visible UI changes.

Checklist

How to use this checklist:

  • Tick a box by replacing [ ] with [x]. Do not edit, add, or remove items.
  • The bot-applied label items can only be honestly ticked AFTER the PR is opened and the labeler / priority-triage bots have run — return to the PR description and tick them then.
  • Most items are required. The few that are conditional are explicitly marked (conditional); for those, leave unticked if they truly do not apply and explain why in Risk Notes. All other items must be ticked before requesting human review.
  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override if wrong, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • I reviewed the final diff for unrelated changes and suspicious dependency changes.
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English.

Summary by CodeRabbit

  • New Features

    • Added tool lifecycle callbacks (started, completed, failed) to monitor tool execution progress and receive execution details.
    • Added ability to abort in-flight tool executions during session interrupts and cleanup.
  • Bug Fixes

    • Improved session cancellation handling to gracefully interrupt and drain active tool executions.
    • Enhanced cleanup logic to prevent stuck tool execution deferrals during session interruption.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Astro-Han, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 28 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b7061190-6a94-4b33-a166-6803e41a78e2

📥 Commits

Reviewing files that changed from the base of the PR and between 1f92abb and 8b023a3.

📒 Files selected for processing (5)
  • packages/opencode/src/session/llm.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/processor-effect.test.ts
📝 Walkthrough

Walkthrough

Adds tool lifecycle callbacks (started, completed, failed) and abort-signal propagation to the LLM stream layer. The session processor is extended with ToolLifecycleRecord tracking, bounded cleanup drain, abortTools() on the Handle, and idempotent terminal state helpers. Run-state gains per-session cancel observers; prompt.loop hooks those observers to abort active tool execution on cancellation.

Changes

Tool lifecycle and cancellation-driven abort flow

Layer / File(s) Summary
LLM lifecycle contract and wrapper
packages/opencode/src/session/llm.ts, packages/opencode/test/session/llm.test.ts
StreamInput gains toolAbortSignal and toolLifecycle fields. wrapToolsWithLifecycle wraps each tool's execute to invoke started/completed/failed callbacks and optionally inject the abort signal. Output normalization utilities (normalizeToolLifecycleOutput, stringifyToolOutput, isPlainRecord) are added. Unit tests cover callback invocation, error propagation from completed, and non-JSON output stringification.
Processor lifecycle records and terminal state helpers
packages/opencode/src/session/processor.ts
ToolCall is replaced by ToolLifecycleRecord holding partCreated/ready/done deferreds and idempotence flags. ProcessInput adds toolDrainTimeoutMs. Handle adds abortTools() and widens recordToolExecutionStarted to include raw input. Terminal completion/failure helpers become idempotent and persist { start, end } timing. ensureToolPartForTerminal materializes missing tool parts for terminal writes.
Processor stream event handling
packages/opencode/src/session/processor.ts
tool-input-start, tool-call, and tool-error handlers are updated to create/refresh ToolLifecycleRecord, mark materialization, resolve ready deferreds at the correct times, and persist running/terminal tool states.
Processor abort wiring and bounded cleanup drain
packages/opencode/src/session/processor.ts
Adds a session-level abort-callback registry and abortHandleTools. Per-process toolAbortController is wired into llm.stream via toolAbortSignal. finalizeToolLifecycles drains done deferreds with bounded timeouts, resolves stranded lifecycles with interruption-phase errors, and releases remaining waiters. Synthetic block/stop events trigger stopCurrentStream(). Handle.abortTools is wired to abortHandleTools.
Run-state cancel observers and prompt abort hook
packages/opencode/src/session/run-state.ts, packages/opencode/src/session/prompt.ts
ensureRunning accepts onCancel to register a per-session cancel observer; cancel notifies observers before cancelling. Queued-cancel handling covers cancellation before a runner exists. prompt.loop tracks the active processor handle via onProcessor and invokes abortTools() from the onCancel handler. Both AI SDK and MCP tool start events now include the tool input payload.
Timeout, disconnect, and pre-materialization lifecycle tests
packages/opencode/test/session/processor-effect.test.ts, packages/opencode/test/session/compaction.test.ts, packages/opencode/test/session/message-v2.test.ts
Adds TestDeferred/defer helpers and live tests covering: lifecycle events before stream materialization, tool completion/failure synthesis on stream disconnect, dead executions, late lifecycle callbacks after finalization, and cleanup waiting for terminal state. Compaction stub gains no-op abortTools. A message-v2 test validates fatal assistant errors with tool parts are filtered from model messages.
Abort propagation and drain-bound tests
packages/opencode/test/session/processor-effect.test.ts, packages/opencode/test/session/prompt-effect.test.ts
Adds tests for: abort-aware tool interruption on processor abort, bounded drain after stream timeout with dead tools, handle.abortTools() completing within a time bound, prompt-cancel preventing a queued run from starting, and cancel aborting an executing tool via the run-state observer with the AbortSignal observed in the tool.

Permission-agent test fixture cleanup

Layer / File(s) Summary
Permission-agent tmpdir option cleanup
packages/opencode/test/permission-agent.test.ts
Removes git: true from six permission-agent integration test fixtures that create temporary config directories.

Sequence Diagram(s)

sequenceDiagram
  participant PromptLoop
  participant SessionRunState
  participant SessionProcessor
  participant LLMStream as LLM.stream
  participant ToolExecute as Tool.execute

  PromptLoop->>SessionRunState: ensureRunning({ onCancel: abortTools })
  SessionProcessor->>LLMStream: stream(toolAbortSignal, toolLifecycle)
  LLMStream->>ToolExecute: execute(args, { abortSignal })
  Note over ToolExecute: tool running
  PromptLoop->>SessionRunState: cancel(sessionID, meta)
  SessionRunState->>PromptLoop: onCancel(meta)
  PromptLoop->>SessionProcessor: abortTools()
  SessionProcessor->>LLMStream: abortProcessTools() / toolAbortController.abort()
  LLMStream-->>ToolExecute: abortSignal fires
  ToolExecute-->>SessionProcessor: lifecycle.failed(toolCallID, AbortError)
  SessionProcessor->>SessionProcessor: finalizeToolLifecycles() with bounded drain
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Astro-Han/pawwork#812: Modifies processor.ts around tool-part lifecycle observability and cleanup/interruption handling, directly overlapping with this PR's tool lifecycle record and drain refactor.
  • Astro-Han/pawwork#1251: Modifies run-state.ts SessionRunState.cancel semantics and the idle-vs-busy cancel boundary, directly related to this PR's cancel-observer additions to the same cancel path.

Suggested labels

bug

🐇 A rabbit hops through signals and streams,
Wrapping each tool with started and done gleams.
When cancel arrives, abortTools cries out,
Deferreds drain bounded — no stuck tool, no doubt!
The lifecycle record keeps every flag tight,
So tools never linger past cleanup's last light. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: draining tool results after stream disconnect, which is the core purpose of the PR.
Description check ✅ Passed The description is comprehensive and well-structured, following the template with complete Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, and How To Verify sections with actual verification results.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/i927-tool-drain

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 and usage tips.

@Astro-Han Astro-Han added enhancement New feature or request P1 High priority app Application behavior and product flows harness Model harness, prompts, tool descriptions, and session mechanics labels Jun 10, 2026
@github-actions github-actions Bot removed the app Application behavior and product flows label Jun 10, 2026

@github-actions github-actions 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.

Suggested priority: P2 (includes non-doc, non-test paths outside the low-risk bucket).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces tool lifecycle hooks (started, completed, failed) and abort signals to manage tool execution, tracking, and cleanup during LLM streams. It also adds robust state tracking to prevent duplicate execution records and includes a test verifying tool completion persistence after stream timeouts. The review feedback highlights two important improvements: decoupling the tool execution try-catch block from the completion callback to prevent incorrect failure reporting, and wrapping JSON.stringify in a try-catch block to handle potential serialization errors safely.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/opencode/src/session/llm.ts
Comment thread packages/opencode/src/session/llm.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/opencode/src/session/run-state.ts (1)

217-225: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Queued ensureRunning() calls never receive the new onCancel hook.

ensureRunning() registers options.onCancel before withActiveRun(...), but cancel() returns on Line 220 unless this session already has a busy runner. A run that's still waiting on the directory lock has no runner yet, so its observer is skipped and the queued work can still start after the user cancels. Either notify observers before this early return, or register onCancel only once the run is actually active.

🤖 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 `@packages/opencode/src/session/run-state.ts` around lines 217 - 225, The
cancel() implementation in SessionRunState currently returns early when there is
no busy runner, skipping notifyCancelObservers and leaving queued
ensureRunning() tasks without the new onCancel hook; update cancel(sessionID,
meta) to ensure observers get notified even when no existing.busy runner exists
(e.g., call data.notifyCancelObservers(sessionID, meta) before the early return)
or alternatively change ensureRunning()/withActiveRun so onCancel is registered
only after a runner is created; reference functions/objects: cancel,
ensureRunning, withActiveRun, InstanceState.get(state), data.runners, and
data.notifyCancelObservers to locate and apply the fix.
packages/opencode/src/session/processor.ts (1)

913-939: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap this switch case in its own block.

const executionStarted = ... is declared directly under case "tool-input-start":, so Biome's noSwitchDeclarations rule flags this as an error. If lint is gating CI, this change won't merge until the case is braced.

Suggested fix
-          case "tool-input-start":
+          case "tool-input-start": {
             if (ctx.assistantMessage.summary) {
               throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
             }
             const part = yield* session.updatePart({
               id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(),
@@
             }
             yield* applyPendingToolUpdates(value.id)
             return
+          }

Static analysis already reports this as a Biome noSwitchDeclarations error.

🤖 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 `@packages/opencode/src/session/processor.ts` around lines 913 - 939, The
switch case for "tool-input-start" declares variables (e.g., const
executionStarted) directly under the case which triggers the Biome
noSwitchDeclarations rule; fix it by wrapping the entire case body in its own
block (add { ... } after case "tool-input-start":) so declarations like
executionStarted, part, and the ctx.toolcalls assignment are scoped properly;
ensure the existing calls to session.updatePart,
ctx.pendingStartedToolCalls.delete, Deferred.make, and
applyPendingToolUpdates(value.id) remain inside that new block.

Source: Linters/SAST tools

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

Outside diff comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 913-939: The switch case for "tool-input-start" declares variables
(e.g., const executionStarted) directly under the case which triggers the Biome
noSwitchDeclarations rule; fix it by wrapping the entire case body in its own
block (add { ... } after case "tool-input-start":) so declarations like
executionStarted, part, and the ctx.toolcalls assignment are scoped properly;
ensure the existing calls to session.updatePart,
ctx.pendingStartedToolCalls.delete, Deferred.make, and
applyPendingToolUpdates(value.id) remain inside that new block.

In `@packages/opencode/src/session/run-state.ts`:
- Around line 217-225: The cancel() implementation in SessionRunState currently
returns early when there is no busy runner, skipping notifyCancelObservers and
leaving queued ensureRunning() tasks without the new onCancel hook; update
cancel(sessionID, meta) to ensure observers get notified even when no
existing.busy runner exists (e.g., call data.notifyCancelObservers(sessionID,
meta) before the early return) or alternatively change
ensureRunning()/withActiveRun so onCancel is registered only after a runner is
created; reference functions/objects: cancel, ensureRunning, withActiveRun,
InstanceState.get(state), data.runners, and data.notifyCancelObservers to locate
and apply the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 882645fc-04dd-4d16-a884-ad6e677ad5c8

📥 Commits

Reviewing files that changed from the base of the PR and between 036f1e7 and fba902d.

📒 Files selected for processing (6)
  • packages/opencode/src/session/llm.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/run-state.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/processor-effect.test.ts

Astro-Han added a commit that referenced this pull request Jun 10, 2026
Provider-side ContextOverflowError handling now respects compaction.auto: false for normal assistant turns instead of setting needsCompaction and silently compacting.

This imports the Line B #30749 runtime correctness fix while keeping the scope isolated from #927/tool-drain/recovery work and from PR #1252, whose processor.ts collision was owner-accepted as rebase-later.

Changes:
- Stop normal assistant provider overflow with the provider ContextOverflowError when auto compaction is disabled.
- Preserve summary/compaction assistant overflow behavior so SessionCompaction still writes its explicit compact failure error.
- Add focused coverage for disabled-auto provider overflow and the summary-compaction boundary.

Verification:
- RED: bun --cwd packages/opencode test test/session/processor-effect.test.ts -t "stops structured context overflow when auto compaction is disabled" failed because the processor returned "compact".
- bun --cwd packages/opencode test test/session/processor-effect.test.ts -t "stop structured context overflow when auto compaction is disabled" -> pass.
- bun --cwd packages/opencode test test/session/processor-effect.test.ts -t "compact on structured context overflow" -> pass.
- bun --cwd packages/opencode test test/session/compaction.test.ts -t "marks summary message as errored on compact result when auto compaction is disabled" -> pass.
- bun --cwd packages/opencode test test/session/processor-effect.test.ts -> 36 pass.
- bun --cwd packages/opencode test test/session/compaction.test.ts -> 51 pass.
- bun run --cwd packages/opencode typecheck -> pass.
- git diff --check -> pass.
- Fresh-eye reviewer rerun after semantic diff change -> no issues found.
- CI run 27288199573 -> success.
- windows-advisory run 27288199605 -> success.
Astro-Han added a commit that referenced this pull request Jun 10, 2026
Root cause / goal:
SessionPrompt persists an assistant scaffold before processor handling fully owns the turn. Cancellation in that early window could leave an empty unfinished assistant message with no abort error or completion timestamp, poisoning later session history. This ports the focused upstream anomalyco/opencode#27254 behavior only.

Change boundary:
- Finalize the current unfinished assistant scaffold from the shared prompt-loop interrupt cleanup path when cancellation happens before processor cleanup owns the assistant.
- Preserve existing cancel semantics and diagnostics, and avoid importing #1252 tool-drain behavior, #30804 filtering, provider/MCP/UI/package changes, or adjacent refactors.
- Add regression coverage for cancellation after assistant scaffold persistence and after processor handle creation but before process starts.

Verification:
- RED proof: new regression first failed because the canceled scaffold had no MessageAbortedError.
- Focused regression: bun test --timeout 30000 ./test/session/prompt-effect.test.ts -t "cancel after assistant scaffold save finalizes before processor handle|cancel after processor handle creation finalizes before process starts" -> 2 passed.
- Prompt interruption suite: bun test --timeout 30000 ./test/session/prompt-effect.test.ts -> 65 passed.
- Processor abort slice: bun test --timeout 30000 ./test/session/processor-effect.test.ts -t "abort|interrupt" -> 6 passed.
- Typecheck: bun run typecheck in packages/opencode -> passed.
- Whitespace: git diff --check origin/dev...HEAD -> passed.
- PR CI: required checks green; full current check set including windows-advisory green.

Reviews:
- Terminal Claude review: PASS after final diff.
- Independent terminal Occam fresh-eye review: PASS, no P0/P1/P2 after addressing the post-handle/pre-process cancellation window.
- GitHub review threads: none unresolved.

Residual risk:
Canceled assistants still follow existing semantics and do not set finish; this PR only ensures aborted scaffolds carry MessageAbortedError and time.completed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/session/processor.ts (1)

1056-1092: ⚠️ Potential issue | 🟠 Major

Wrap the tool-input-start switch case in braces

case "tool-input-start": declares const bindings (lifecycle, part, current) directly in the case without a block, which trips Biome’s noSwitchDeclarations pattern and can create shared TDZ/scope issues across cases. Wrap the case in { ... }.

🧩 Minimal fix
-          case "tool-input-start":
+          case "tool-input-start": {
             if (ctx.assistantMessage.summary) {
               throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
             }
             const lifecycle = yield* getToolLifecycleRecord(value.id)
             if (lifecycle.settled) return
             const part = yield* session.updatePart({
               id: lifecycle.partID ?? PartID.ascending(),
               messageID: ctx.assistantMessage.id,
               sessionID: ctx.assistantMessage.sessionID,
               type: "tool",
               tool: value.toolName,
               callID: value.id,
               state: { status: "pending", input: {}, raw: "" },
               metadata: value.providerExecuted ? { providerExecuted: true } : undefined,
             } satisfies MessageV2.ToolPart)
             ctx.toolcalls[value.id] = {
               ...lifecycle,
               partID: part.id,
               messageID: part.messageID,
               sessionID: part.sessionID,
               attemptID,
             }
             const current = ctx.toolcalls[value.id]
             if (current.executionStarted && current.startedToolName && !current.executionStartedRecorded) {
               current.executionStartedRecorded = true
               ctx.runTrace.recordToolExecutionStarted({
                 attemptID,
                 at: Date.now(),
                 monotonicMs: performance.now(),
                 toolName: RunObservability.safeToolName(current.startedToolName),
                 effect: RunObservability.toolEffect(current.startedToolName),
               })
             }
             yield* Deferred.succeed(current.partCreated, undefined).pipe(Effect.ignore)
             yield* applyPendingToolUpdates(value.id)
             return
+          }
🤖 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 `@packages/opencode/src/session/processor.ts` around lines 1056 - 1092, The
switch case for "tool-input-start" declares block-scoped consts (lifecycle,
part, current) without its own block which violates noSwitchDeclarations; wrap
the entire case body in braces { ... } so those const bindings are scoped to
this case, preserving the existing flow/returns and ensuring you still call
getToolLifecycleRecord, session.updatePart, set ctx.toolcalls[value.id], record
executionStarted with ctx.runTrace, Deferred.succeed(...), and
applyPendingToolUpdates(value.id) as before.

Source: Linters/SAST tools

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

Inline comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 413-425: getToolLifecycleRecord currently creates new Deferreds
even after cleanup() clears ctx.toolcalls, causing callers like
completeToolExecution()/failToolExecution() and LLM.wrapToolsWithLifecycle() to
wait forever; change getToolLifecycleRecord to detect teardown (e.g.,
ctx.toolcalls missing or a ctx.draining/teardown flag set by cleanup) and return
a pre-resolved/pre-rejected lifecycle record whose partCreated/ready/done
deferreds are already completed so downstream awaits never block after teardown;
update cleanup() to set the teardown flag (or clear ctx.toolcalls) consistently
and ensure completeToolExecution()/failToolExecution() will work with the
pre-resolved record.

---

Outside diff comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 1056-1092: The switch case for "tool-input-start" declares
block-scoped consts (lifecycle, part, current) without its own block which
violates noSwitchDeclarations; wrap the entire case body in braces { ... } so
those const bindings are scoped to this case, preserving the existing
flow/returns and ensuring you still call getToolLifecycleRecord,
session.updatePart, set ctx.toolcalls[value.id], record executionStarted with
ctx.runTrace, Deferred.succeed(...), and applyPendingToolUpdates(value.id) as
before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd72894e-8aed-4e57-a1e8-a0ce7703b562

📥 Commits

Reviewing files that changed from the base of the PR and between fba902d and f272304.

📒 Files selected for processing (10)
  • packages/opencode/src/session/llm.ts
  • packages/opencode/src/session/message-v2.ts
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/prompt.ts
  • packages/opencode/src/session/run-state.ts
  • packages/opencode/test/permission-agent.test.ts
  • packages/opencode/test/session/compaction.test.ts
  • packages/opencode/test/session/llm.test.ts
  • packages/opencode/test/session/processor-effect.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
💤 Files with no reviewable changes (1)
  • packages/opencode/test/permission-agent.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/session/run-state.ts

Comment thread packages/opencode/src/session/processor.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/session/processor.ts (1)

1071-1108: ⚠️ Potential issue | 🔴 Critical

Wrap the case body in braces to fix the Biome noSwitchDeclarations error.

Line 1075 declares const lifecycle directly in an unbraced switch case, which violates Biome's lint/correctness/noSwitchDeclarations rule (enforced as error). Wrap the case body in braces to scope the declaration correctly.

Proposed fix
-          case "tool-input-start":
+          case "tool-input-start": {
             if (ctx.assistantMessage.summary) {
               throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
             }
             const lifecycle = yield* getToolLifecycleRecord(value.id, attemptID)
             if (!lifecycle) return
@@
             yield* Deferred.succeed(current.partCreated, undefined).pipe(Effect.ignore)
             yield* applyPendingToolUpdates(value.id)
             return
+          }
 
           case "tool-input-delta":
             return
🤖 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 `@packages/opencode/src/session/processor.ts` around lines 1071 - 1108, The
case "tool-input-start" block violates Biome's noSwitchDeclarations rule because
variable declarations like const lifecycle are made directly in the unbraced
case without proper scoping. Wrap the entire body of the case "tool-input-start"
statement in curly braces to create a proper scope for these declarations and
resolve the linting error.

Source: Linters/SAST tools

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

Outside diff comments:
In `@packages/opencode/src/session/processor.ts`:
- Around line 1071-1108: The case "tool-input-start" block violates Biome's
noSwitchDeclarations rule because variable declarations like const lifecycle are
made directly in the unbraced case without proper scoping. Wrap the entire body
of the case "tool-input-start" statement in curly braces to create a proper
scope for these declarations and resolve the linting error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34df687b-e889-40b1-8b12-8aca27c18d68

📥 Commits

Reviewing files that changed from the base of the PR and between f272304 and 1f92abb.

📒 Files selected for processing (5)
  • packages/opencode/src/session/processor.ts
  • packages/opencode/src/session/run-state.ts
  • packages/opencode/test/session/message-v2.test.ts
  • packages/opencode/test/session/processor-effect.test.ts
  • packages/opencode/test/session/prompt-effect.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opencode/src/session/run-state.ts
  • packages/opencode/test/session/processor-effect.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request harness Model harness, prompts, tool descriptions, and session mechanics P1 High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant