Repository navigation
Description
What happened
OpenCode threads lose the result of a background shell command that runs longer than 30 minutes.
In an OpenCode thread, I asked the agent to run sleep 2400; echo finished-2400 as a background command, end its turn, and report the output when the command finished. The reply never appeared in the T3 thread. The same prompt with sleep 120 or sleep 600 works fine. Claude Code and Codex threads don't have this problem.
Diagnosis
The provider session's idle release doesn't wait for background shell commands in the OpenCode 2 adapter. It only waits for background subagents.
ProviderSessionManagerreleases a provider sessionDEFAULT_IDLE_TIMEOUT_MS(30 min) after its turn goes idle. The only exception is when the adapter'shasPendingBackgroundWorkreturns true, which pins the session for up toDEFAULT_MAX_IDLE_PIN_MS(4 h). This is inapps/server/src/orchestration-v2/ProviderSessionManager.ts:54-55andreleaseIfStillIdle(~L1048).- The OpenCode adapter implements this as
[...threads.values()].some(owesWork)(packages/provider-opencode/src/server/v2/adapter.ts:3771).owesWork(L1039) counts wakes, queued reports, busy subagent sessions andstate.callsentries withbackground: true.state.callsonly holdssubagenttool calls. That is the [Bug]: session reaper counts opencode background agents activity as idle time on remote connection (headless) #13514 fix. - A background
shelltool is tracked inturn.tools. OpenCode returnssession.tool.successimmediately with "Command moved to the background (shell ID: …)". The adapter then emits the tool as completed and callsturn.tools.delete(id)(L2307). Nothing records the running shell job, soowesWorkis false and the session is released 30 minutes after the turn'sidle. - OpenCode itself keeps the job running. When the job ends, it adds a
syntheticmessage (metadata.source: "shell",state: "completed") and starts a follow-up turn on the session. That follow-up happens after T3 has already deleted its MCP registration and released the session, so the reply never reaches the thread. - Claude and Codex are unaffected because their
hasPendingBackgroundWorkcovers background shells. Claude checks thependingBackgroundTasksByNativeThreadroster (ClaudeAdapterV2.ts:8844). Codex checksrunningCommandItemsByTurn(CodexAdapterV2.ts:6423).
Possible fix: track background shell jobs in the OpenCode adapter. They are identified by input.background === true on session.tool.called, or by the shell ID in the success content/metadata. Count them in owesWork and clear them when the synthetic shell-completion message (metadata.shellID / jobID) arrives or the follow-up turn runs. This matches how background subagents are handled.
The code is unchanged on main as of 2026-10-13: owesWork is identical, and the last commits to adapter.ts are #14752 and #14744 on 2026-10-11.
Steps to reproduce
- Start an OpenCode (v2) thread. Any model works; I used
opencode-go/claude-haiku-5-5. - Send: "Run
sleep 2400; echo finished-2400as a background command, then end your turn. When it finishes, tell me what it printed and the time." - The agent starts the shell in the background and ends its turn. Leave the thread alone.
- After 30 minutes, T3 deletes the thread's OpenCode MCP registration and releases the provider session.
- At 40 minutes, OpenCode wakes the agent and it replies, as recorded in
opencode.db. The reply never appears in the T3 thread.
Control: with sleep 120 or sleep 600, the reply arrives normally, because the wake happens before the 30-minute release.
Version
0.0.46-nightly.20261012.3035 (511d20c) on the remote environment where this happened. Same code on main and 0.0.46-nightly.20261013.3042.
Environment
Remote T3 environment: Ubuntu 26.04.1 LTS, Node 24.21.0, opencode v2.0.27. Client: T3 desktop on Windows 11.
Evidence
# T3 server trace on the remote env: the MCP session is deleted exactly 30 min after each thread's last OpenCode `idle`
2026-10-13T00:31:22Z DELETE /api/experimental/mcp/t3-code-a375a814-… # sleep 120: last idle 00:01:22, reply delivered
2026-10-13T00:40:20Z DELETE /api/experimental/mcp/t3-code-09c09d91-… # sleep 600: last idle 00:10:20, reply delivered
2026-10-13T00:30:37Z DELETE /api/experimental/mcp/t3-code-34b83bc9-… # sleep 2400: idle after first turn 00:00:37
2026-10-13T00:30:37Z DELETE http://127.0.0.1:3773/mcp?codemode=false (204)
# opencode.db, session for the 2400 s thread (session_message, timestamps UTC)
00:00:34 assistant tool shell {"command":"sleep 2400; echo finished-2400","background":true}
-> "Command moved to the background (shell ID: sh_…)"
00:00:37 idle {"outcome":"succeeded"}
00:40:34 synthetic {"metadata":{"source":"shell","state":"completed","exit":0}, "text":"<shell …>finished-2400</shell>"}
00:40:37 assistant "The background command finished and printed: finished-2400 … 13:40:37 NZDT"
00:40:39 idle {"outcome":"succeeded"}Related issues
#13514 (closed) fixed the same idle release for OpenCode background subagents by adding state.calls to owesWork. Background shell commands go through a different code path (turn.tools) and were not covered. #11730 (closed) dealt with background delegations showing as Settled. Neither covers background shells.
Fix applied or workaround
None applied. The 30-minute idle timeout isn't user-configurable; only tests override idleTimeoutMs. Workaround: in OpenCode threads, keep background commands under 30 minutes, or use a Claude/Codex thread for longer background jobs.
Filed by
Claude Code (claude-opus-5-5) via t3 triage
Activity
juliusmarminge commentedon Oct 13, 2026
Note
Grok responding on behalf of Julius.
Confirmed on
main: this is a bug. The OpenCode 2 adapter doesn't pin the idle release for background shells.owesWork(packages/provider-opencode/src/server/v2/adapter.ts~L1039) only counts wakes, queued reports, busy subagent sessions, andstate.callsentries withbackground: true.state.callsholdssubagentcalls only.background: truelives inturn.tools. Onsession.tool.success(which OpenCode sends right after launch) it's emitted as completed and deleted (~L2307). Nothing keeps track of the running job.ProviderSessionManager.releaseIfStillIdlereleases the session after 30 minutes unlesshasPendingBackgroundWorkis true. That check is what pins the session for up to 4 hours.Claude (
pendingBackgroundTasksByNativeThread) and Codex (runningCommandItemsByTurn) both count running background commands, and #13514 already pinned OpenCode background subagents. Shells are the gap. This isn't the #18090 case: there, Pi's background work wasn't visible to T3 at all, while here the adapter getsbackground: trueand the shell ID directly.Until this is fixed, keep OpenCode background commands under 30 minutes, or use a Claude/Codex thread for longer jobs.
fix(opencode): a running background shell keeps its session until it …
traktuner commentedon Nov 1, 2026
Another observation that matches this diagnosis, from a deployment with many OpenCode 2 threads (OpenCode 2.2.3):
owesWork, as proposed above, would also keep the MCP registration alive for these threads.nkoynov commentedon Nov 5, 2026
#18920 fixes this for OpenCode's own background shell job (VM numbers in #18920). The case from #31307 still loses the turn, because there the shell ran in cursor-opencode-provider's own runner, which T3 can't see as a running job. Would a fallback where T3 picks up a turn OpenCode starts in an unloaded thread's session (reload the thread so the turn shows up) be acceptable? That would cover any source of self-started turns, not just the ones
owesWorkknows about.juliusmarminge commentedon Nov 11, 2026
Fixed by https://git.cafe/pingdotgg/t3code/pulls/34141, now on main: A background
sleep 2400; echo finished-2400now counts as pending work, keeping its OpenCode session attached past the 30-minute idle release so its completion and follow-up reply can reach the thread.nkoynov commentedon Nov 13, 2026
A case #34141 doesn't reach, from 2026-11-12 (T3 nightly 4340, OpenCode 2.2.6, Cursor models through cursor-opencode-provider). T3 released the thread after 30 idle minutes while the model's background shells were still running. Those shells run in the provider's own runner, not as OpenCode shell jobs, so #34141 doesn't track them either. I checked that in a VM on ed52af3: no roster entry, and the thread was released the same way.
When the shells finished, OpenCode started four turns from their completion notes (18:29, 18:37, 19:13 and 20:15 UTC), and none of them showed up in T3. The fifth turn asked a question at 20:31. T3 had already removed the thread's MCP server and doesn't pick up a form from an unloaded thread, so the question sat unseen for 400 minutes and blocked the session until it was dismissed.
Job tracking can't cover work T3 can't see. Would you take a fallback where T3 reattaches an unloaded thread when OpenCode starts a turn in its session, or at least surfaces a form asked there?