fix(orchestrator): Close OpenCode tool items when a turn ends under them - #4759
fix(orchestrator): Close OpenCode tool items when a turn ends under them#4759mwolson wants to merge 223 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: Approved b010622 Bug fix that properly closes tool items when a turn ends mid-flight, preventing stuck spinners in the UI. Changes are self-contained to the OpenCode adapter with clear intent and comprehensive test coverage. You can customize Macroscope's approvability policy. Learn more. |
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
9f2a8a2 to
0e50a43
Compare
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
0e50a43 to
d9e8afa
Compare
Dismissing prior approval to re-evaluate d9e8afa
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
d9e8afa to
1e994fd
Compare
Dismissing prior approval to re-evaluate 1e994fd
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
1e994fd to
b010622
Compare
Dismissing prior approval to re-evaluate b010622
…eysetIndex Main owns migration numbering: 037_ProjectionTurnsKeysetIndex landed on main (pingdotgg#5493), so the v2 migrations shift from 037-045 to 038-046. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native subagent observability (pingdotgg#5219), wired per its spec's v2 merge plan: - getWorkflowScript RPC re-homed onto the v2 WS surface (contracts, rpc group, ws handler, auth scope, client atom). - AgentsPanel fed by the spec's mapper swap: projectedSubagentsToRuntime maps orchestration-v2 subagent entities into the panel model; deriveAgentPanelModel's v2Projection leg is now live and the v1 fold never runs. Agents surface wired into ChatView + RightPanelTabs. Other ports and reconciliations: - Shell reconnect-loop fix (pingdotgg#5561) ported into the v2 shell sync (same-session resubscribes resume from the in-memory cursor), with the cursor-resume regression test adapted to v2 fixtures. - Mobile end-follow latch (pingdotgg#5566) ported onto the v2 ThreadFeed. - Claude ede_diagnostic interrupt classification (pingdotgg#5557) ported into ClaudeAdapterV2 (aborted_tools/aborted_streaming => interrupted; CLI telemetry never becomes the failure banner). pingdotgg#5559 needs no v2 port (unknown system subtypes are already ignored). - Plan sidebar removed from the v2 ChatView/ChatComposer per main's plans-fold-into-chat rework (pingdotgg#5558); rightPanelStore stays at main's surface set. - SettingsPanels rebuilt as main's refactored version plus the branch's composer-context setting; sidebar snooze respects the time format (pingdotgg#4438 follow-through). - v1-only leftovers deleted: zombie v1 adapters/ingestion/tests the v2 rewrite removes, the v1-bound transfer-budget CI harness (pingdotgg#5350, needs a v2 rebuild), and main's v1 client pagination machinery (pingdotgg#5493 client side; the 037 keyset migration is kept — server-side v2 windowing is a follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase kept the LegendList 3.3.3 upgrade and patch from pingdotgg#5449 and the mobile end-follow latch from pingdotgg#5566, but the v2 MessagesTimeline/ChatView still carried the branch's blunt any-gesture-breaks-follow listeners. Port main's pingdotgg#5566 web mechanics onto the v2 follow architecture: - resolveTimelineIsAtEnd measures the 40px follow re-arm band from real geometry (contentLength/scroll/scrollLength minus the composer inset), keeping the isNearEnd fallback for older state shapes. - Follow now breaks only on gestures that can actually leave the live edge: upward wheel with overflowing content, touch drags that exited the end band, scrollbar drags vs content clicks, and keyboard navigation (PageUp/Home/ArrowUp) — previously keyboard scrolling never broke follow and the next stream chunk yanked the view back down. - Listener attach retries across frames so a thread switch cannot mount the list without its opt-out listeners. Deliberately not ported: pingdotgg#5449's shouldRestorePosition disclosure anchoring and follow-gated maintainScrollAtEnd — the v2 timeline keeps maintainScrollAtEnd={false} with its own follow scrolls and anchor system; flipping that core is a separate change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gdotgg#5449) Complete the pingdotgg#5449 architecture on the v2 timeline, following the LegendList author's direction to lean on the list's native mechanisms instead of app-side scroll layers: - maintainScrollAtEnd is enabled and owned by LegendList, gated off only while the user reads history (liveFollowEnabled), while a sent turn anchors near the top (anchoredEndSpace), or during the two-frame settle of a fold toggle. - maintainVisibleContentPosition compensates size changes natively ({data, size, shouldRestorePosition}); fold toggles anchor compensation to the toggled row via a disclosure anchor key, so the trigger stays under the pointer instead of the viewport chasing the end. - ChatView's hand-rolled streaming follow (double-rAF scrollToEnd on every data change) is gone; the app now only owns streaming adjustments during anchored-end-space mode, mirroring main. - timelineLiveFollowEnabled state mirrors the follow refs so the render-visible gate switches native follow off when a gesture breaks follow and back on when the viewport returns to the end band. Timeline tests updated to assert the native-ownership invariants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Keep success feedback visible in the Git action control for 10 seconds - Move the running elapsed timer into the panel menu slot
…s with v2 Post-rebase reconciliation sweep: - Sidebar: main's folded Sidebar.tsx/Sidebar.logic.ts adapted to v2 shells (latestRun/runtime naming, waiting status instead of monitoring), with subagent-thread filtering and main's pinned-reorder helpers re-exported - Pinned drag reorder (pingdotgg#5581) ported into v2: thread.pin orderKey + thread.pin.reorder command, thread.pin-reordered event, Orchestrator fold, ProjectionStore/Maintenance, client-runtime commands and shell mapping - Project favicon (pingdotgg#4849-era) and defaultThreadEnvMode flowed through v2 contracts (OrchestrationProjectShell, application event payloads) - ChatView: main's pingdotgg#5592 header props, pull-request right-panel surfaces, liveAgentCount badge (pingdotgg#5745) wired into the v2 panel layout - enableAssistantStreaming -> enableLegacyTokenStreaming rename applied to v2 RunExecutionService and replay testkit - Removed v1 zombies resurrected by the rebase (provider service/reaper/ ingestion + v1 layer tests, server.test.ts, integration harness) - routeTree: main's tree + branch's /settings/scheduled-tasks route - Misc marker-sweep syntax repairs (rpc.ts, entities.ts, localApi.test.ts, rightPanelStore.test.ts, GitManager.test.ts, mobile model menu helpers) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 038-040 Main released ProjectionThreadsPinOrderKey (038), ProjectionProjectsDefaultThreadEnvMode (039) and ProjectionProjectFaviconPath (040), so the branch-private v2 stack shifts up by three. Registry ids were already 41-49; this renames the files and identifiers to match and updates the ledger expectations and through-id boundaries in the migration tests (released boundary 37 -> 40). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a6c9b41 (agents open pasted images): ClaudeAdapterV2 now grants the attachments dir alongside cwd via additionalDirectories and appends '[Attached ... is saved at: path]' lines to the turn text so tools can dereference pasted images (pixels alone are not tool-readable). - 5bb8c03 (settle leaves monitors running): thread.settle now joins archive/delete in the provider-session detach set, so PR monitors, dev servers and subagent fleets stop when the user parks the thread. The settle guard already rejects active runs, and serialized dispatch closes the re-engage race the v1 fix handled with onlyIfSettled. - e70cdb4 (Claude resume handshakes) and 2c7267a (reaper vs live background subagents) are already covered structurally in v2: results are turn-scoped with explicit zero-turn handshake drops, and idle release is pinned while background work is pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e panel-visibility merge The keep-both merge nested main's plan-surface migration test inside a branch popover test and dropped the threadPanelVisibilityByThreadKey key from the migration results. Restore main's test body and include the branch's (empty) visibility map in the expected persisted shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n text Follow-up to the pingdotgg#5757 port: start and steer turns now append the '[Attached ... is saved at: path]' line, so the adapter fixtures assert it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
22bd872 to
a27c1cc
Compare
The round-3 reconciliation took main's ChatHeader wholesale and wired its full prop set, resurrecting the scripts/open-in/git-actions cluster the branch had deliberately relocated into the thread panel. Restore the 79-line slim header (project favicon + name + thread title) and its minimal ChatView call. pingdotgg#5592's header actions stay a documented v2 follow-up, as decided in round 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
- Add ready, merge, and conflict-resolution actions to the PR row - Share pull request action and handoff logic with the detail panel - Fix thread details scrolling and row alignment
`finalizeTurn` flushed only `text` and `reasoning` parts, so a tool still mid-flight when the turn ended kept whatever state it was last observed in. OpenCode's `session.abort` stops the run without reporting a final state for the running tool, so nothing ever closed the item: after a Stop, the interrupted run's `command_execution` stayed `running` permanently and the row spun forever in the UI. Sweeps non-terminal tool parts during finalize and emits them with the turn's terminal status, threaded through `emitToolPart` and `emitSubagent` as an optional override so the existing projection logic is reused rather than duplicated. `OrchestrationV2TurnItemStatus` and `OrchestrationV2Subagent` declare the same status literals, so an interrupted subagent stays interrupted rather than collapsing to failed. Found by the new `opencode-interrupt-direct-stop` live pack, which reproduced it as `command_execution:running` surviving on an `interrupted` run. The same pack now reports `command_execution:interrupted` with zero running commands.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
2230488 to
a33de3b
Compare
a186d64 to
5b1a115
Compare
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
|
Absorbed into #4786 cleanup-drain. That PR now carries the OpenCode 1.x interrupt-tool commits plus the cleanup drain on current t3code/codex-turn-mapping. Review the OpenCode 1.x work there. |
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
The existing tests covered only the pure status mapping, so they would still pass with the sweep deleted, applied to already-terminal tools, or failing to forward the override. Codex review of pingdotgg#4759 called that out. Adds an OpenCode provider to the `turn_interrupt_mid_tool` replay fixture, which drives the real adapter and asserts the projection. The transcript reproduces the ordering from a live run rather than an idealised one: the tool goes running, the abort goes out, then `session.error` and idle arrive, and only afterwards does OpenCode's cleanup emit its own tool part update. That late update lands on an already-finalized turn and is ignored, so the item's `interrupted` status has to come from the sweep. Verified the fixture catches the regression: with the sweep commented out it fails with `expected 'running' to equal 'interrupted'`, and nothing else in the suite breaks.
Summary
Interrupting an OpenCode turn left whatever tool was mid-flight projected as
runningpermanently. In the UI that is a spinner that never stops, on a runthe user deliberately stopped.
Stop itself was never broken. The run projects as
interruptedand sequentialrecoveries map to their own completed runs. Only the tool item leaked.
Problem and Fix
finalizeTurnflushed onlytextandreasoningparts and never tool parts, so a tool still running whensession.abortlanded kept its last observedrunningstate. OpenCode reports no final state for an aborted tool, so nothing else ever closed it and the row spun forever.finalizeTurnand emit them with the turn's terminal status, via an optionalterminaloverride threaded throughemitToolPart.failed, since that was the only non-success status reachable without a mapping.emitSubagenttakes the same override.OrchestrationV2SubagentandOrchestrationV2TurnItemcarry the same status literals, so an interrupted subagent stays interrupted.Validation
Typecheck, lint, unit tests, and a live interrupt scenario against a real
opencode serve, all on this branch rather than on the branch where the fix wasoriginally written.
vpr typecheckacross all 15 packages: 0 errorsvp check: 0 errorsvp test src/orchestration-v2/Adapters/OpenCodeAdapterV2.test.ts: 10 passedopencode/glm-5.2: passThe live run captures the projection at several points. Counting
command_executionitems by status:The boundary capture shows the tools genuinely in flight; the next one shows
them closed. Before this change they stayed
runningin every subsequentcapture, which is the reported symptom.
Manual re-test scenarios, in the published guide at
https://nam7nt0rbtm6.postplan.dev:
Scenario 1 is the one this fix changes. Scenarios 2 and 3 passed before and
after, and are included because they are what would catch an over-broad fix that
closed the interrupted run's items by tearing down the provider thread.
Replay coverage
turn_interrupt_mid_toolgains an OpenCode provider, so the fix is driventhrough the real adapter rather than asserted on a pure helper. The transcript
reproduces the ordering from a live run rather than an idealised one: the tool
goes running, the abort goes out,
session.errorand idle arrive, and onlyafterwards does OpenCode's cleanup emit its own tool part update. That late
update lands on an already-finalized turn and is ignored, so the item's
interruptedstatus has to come from the sweep.Confirmed the fixture catches the regression: with the sweep commented out it
fails with
expected 'running' to equal 'interrupted', and nothing else breaks.Notes for review
The fix was found during an audit of OpenCode background-work and interrupt
coverage that also produced an OpenCode 2.0 driver. That driver is a separate,
much larger change and is deliberately not in this PR; this one is independently
shippable and fixes a bug users can hit today.
OpenCode 1.x has no background work that can outlive the root turn, so
hasPendingBackgroundWorkandhasPendingBackgroundWorkForThreadremainingunimplemented on this adapter is correct rather than an oversight.
Current CI Note
The branch-local validation above passes. GitHub Check, Release Smoke, Mobile
Native Static Analysis, Cursor Bugbot, CodeRabbit, and both Macroscope code
checks pass. Test currently fails in five unchanged queued-turn replay
assertions from the newer
t3code/codex-turn-mappingmerge result; this PR doesnot touch queued-turn behavior.
Note
Fix OpenCode tool items left in running state when a turn ends
terminalToolStatusin OpenCodeAdapterV2.ts to map a terminal turn status (completed,interrupted,cancelled,failed) to matching node/item statuses for tools that never reported their own terminal state.finalizeTurnnow sweeps any tool parts still inpendingorrunningstate, callingemitToolPartwith the turn's terminal status so they close with a matching status and a non-nullcompletedAt.emitSubagentandemitToolPartaccept an optional terminal status parameter; when present, they useterminalToolStatusoutput instead of the observedtoolStatus/toolCompletedAt.turn_interrupt_mid_toolfixture with an OpenCode transcript and assertion function to cover the interrupted-mid-tool scenario end-to-end.runningindefinitely.Macroscope summarized a33de3b.
Note
Medium Risk
Changes turn-finalization and tool/subagent projection in the OpenCode adapter—a user-visible orchestration path—but scope is narrow and covered by new unit and replay tests.
Overview
Fixes OpenCode runs where Stop left mid-flight tools projected as
runningforever (stuck UI spinners) even though the run correctly showedinterrupted.finalizeTurnnow sweeps tool parts stillpendingorrunningwhen the turn ends (e.g. aftersession.abort) and re-emits them viaemitToolPart/emitSubagentwith an optional terminal override. NewterminalToolStatusmaps the turn’s terminal outcome (interrupted,cancelled,failed,completed) onto matching item/node statuses; subagents use the same literals instead of collapsing non-success tofailed.Adds unit tests for
terminalToolStatusand an OpenCodeturn_interrupt_mid_toolreplay fixture (including late provider cleanup events on an already-finalized turn).Reviewed by Cursor Bugbot for commit a33de3b. Bugbot is set up for automated code reviews on this repo. Configure here.