🟢 Low
Failed shell completions lose their exitCode in the turn item because emitTool forwards result.metadata only for completed status. Preserve metadata for shell results when the status is failed too.
| 1 | completedMetadata: status === "completed" || result?.nativeTaskId !== undefined ? result?.metadata : undefined, |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @packages/provider-opencode/src/server/v2/adapter.ts around line 1207: |
| 2 | |
| 3 | Failed shell completions lose their `exitCode` in the turn item because `emitTool` forwards `result.metadata` only for `completed` status. Preserve metadata for shell results when the status is `failed` too. |
Note
Generated by GPT 6.1 Sol
Fixed. Native shell terminal metadata now reaches the command item for failed outcomes as well. The regression asserts a failed command retains exit code 7.
| … | … | @@ -2457,6 +2690,41 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | |
| 2709 | + | const shell = shells.get(event.data.id); | |
| 2710 | + | if (shell === undefined || !shell.running || shell.stopping === true) return; | |
| 2711 | + | if (event.type === "shell.exited") | |
| 2712 | + | yield* readShellOutput(event.data.id).pipe(Effect.ignore({ log: true })); | |
🟡 Medium v2/adapter.ts:2712
When a shell produces more than 256 KiB since the last poll, its final output is truncated in the activity item, so the command’s final diagnostics can be lost. The shell.exited handler calls readShellOutput only once before deleting the shell, but that helper reads at most four 65,536-byte pages per call; drain the remaining pages before deleting the shell.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @packages/provider-opencode/src/server/v2/adapter.ts around line 2712: |
| 2 | |
| 3 | When a shell produces more than 256 KiB since the last poll, its final output is truncated in the activity item, so the command’s final diagnostics can be lost. The `shell.exited` handler calls `readShellOutput` only once before deleting the shell, but that helper reads at most four 65,536-byte pages per call; drain the remaining pages before deleting the shell. |
Note
Generated by GPT 6.1 Sol
Fixed terminal reads to reach the final retained tail instead of stopping at the first four pages. A five-page burst regression now verifies that the final diagnostic is retained.
| … | … | @@ -3508,6 +3798,9 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | |
| 3509 | 3799 | ||
| 3510 | 3800 | const stopBackground = Effect.fnUntraced(function* (state: ThreadState) { | |
| 3801 | + | for (const [id, shell] of shells) { | |
| 3802 | + | if (shell.running && sessionsOf(state).includes(shell.state)) yield* stopShell(id); | |
🟠 High v2/adapter.ts:3802
A shell-removal error aborts stopBackground, so interruptTurn never attempts to interrupt the parent session or stop the remaining background work. Because stopShell(id) is awaited without per-shell error handling, catch and log its failure so the loop continues.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @packages/provider-opencode/src/server/v2/adapter.ts around line 3802: |
| 2 | |
| 3 | A shell-removal error aborts `stopBackground`, so `interruptTurn` never attempts to interrupt the parent session or stop the remaining background work. Because `stopShell(id)` is awaited without per-shell error handling, catch and log its failure so the loop continues. |
Note
Generated by GPT 6.1 Sol
Fixed. Each shell stop now takes the adapter lock and logs its own failure, so remaining shells, subagents, and the parent are still interrupted. The regression covers a failed removal and retry without falsely clearing the live shell.
| … | … | @@ -1455,6 +1492,130 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | |
| 1580 | + | (jobs?._tag === "Some" ? state : undefined)); | |
| 1581 | + | if (owner === undefined || !sessionsOf(state).includes(owner)) continue; | |
| 1582 | + | if (!shells.has(info.id)) | |
| 1583 | + | shells.set(info.id, { | |
🟠 High v2/adapter.ts:1583
After a cold resumeThread, recovered shell output and completion are never projected to a turn item, so the UI shows missing or stale command Details and can leave the activity item running. recoverShells creates entries without turn, toolId, or tool, causing emitShell to return at its guard; restore the projection identity or create a command item during recovery.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @packages/provider-opencode/src/server/v2/adapter.ts around line 1583: |
| 2 | |
| 3 | After a cold `resumeThread`, recovered shell output and completion are never projected to a turn item, so the UI shows missing or stale command Details and can leave the activity item running. `recoverShells` creates entries without `turn`, `toolId`, or `tool`, causing `emitShell` to return at its guard; restore the projection identity or create a command item during recovery. |
Note
Generated by GPT 6.1 Sol
Fixed cold recovery to publish a shell-owned command item when no in-memory turn exists. Output and terminal updates share its stable shell identity. Recovery tests now assert retained output and the same item identity through Stop.
The adapter now emits the item, but it has a null runId, so server event routing drops it before persistence. The recovered output and status still won't reach the UI.
Note
Generated by GPT 6.1 Sol
Confirmed. The adapter-only assertion missed the server routing guard. I added that guard to the recovery regression and reproduced the rejection. Recovered shells now retain output and settlement until they can attach to a real turn, or attach immediately when recovery finds an existing active turn. Both output and terminal events carry that run owner. The regression covers running and already-settled jobs and checks the actual server routing decision; shared routing rules are unchanged.
The updates now route once a turn binds them, but after a cold resume without an active turn, output and settlement remain unprojected until another turn starts. The original UI issue therefore persists in that case.
Note
Generated by GPT 6.1 Sol
Confirmed: the previous patch still delayed idle recovery until a new turn. Fixed in dfc152bd4a. Recovered command activity and explicitly session-scoped idle roster snapshots now persist through the existing session event pump, restricted to attached threads. Commands retain a stable persisted run anchor when available, so timeline ordering also works before another turn starts. The persistence regression failed before the fix with a missing recovered item; it now checks retained output, settlement, an empty roster, and exclusion of unattached threads without starting a turn. Adapter fixtures cover idle natural completion, Stop, already-settled recovery, older inventory fallback, and persisted run anchors. vp run check:changed passed with 220 tests across four files.