Skip to content

OpenCode threads lose background shell results after the 30-minute idle release #18880

Closed

Description

@1kaahu

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.

  • ProviderSessionManager releases a provider session DEFAULT_IDLE_TIMEOUT_MS (30 min) after its turn goes idle. The only exception is when the adapter's hasPendingBackgroundWork returns true, which pins the session for up to DEFAULT_MAX_IDLE_PIN_MS (4 h). This is in apps/server/src/orchestration-v2/ProviderSessionManager.ts:54-55 and releaseIfStillIdle (~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 and state.calls entries with background: true. state.calls only holds subagent tool calls. That is the [Bug]: session reaper counts opencode background agents activity as idle time on remote connection (headless) #13514 fix.
  • A background shell tool is tracked in turn.tools. OpenCode returns session.tool.success immediately with "Command moved to the background (shell ID: …)". The adapter then emits the tool as completed and calls turn.tools.delete(id) (L2307). Nothing records the running shell job, so owesWork is false and the session is released 30 minutes after the turn's idle.
  • OpenCode itself keeps the job running. When the job ends, it adds a synthetic message (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 hasPendingBackgroundWork covers background shells. Claude checks the pendingBackgroundTasksByNativeThread roster (ClaudeAdapterV2.ts:8844). Codex checks runningCommandItemsByTurn (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

  1. Start an OpenCode (v2) thread. Any model works; I used opencode-go/claude-haiku-5-5.
  2. Send: "Run sleep 2400; echo finished-2400 as a background command, then end your turn. When it finishes, tell me what it printed and the time."
  3. The agent starts the shell in the background and ends its turn. Leave the thread alone.
  4. After 30 minutes, T3 deletes the thread's OpenCode MCP registration and releases the provider session.
  5. 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

  1. juliusmarminge commented on Oct 13, 2026

    @juliusmarminge
    Member

    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, and state.calls entries with background: true. state.calls holds subagent calls only.
    • A shell called with background: true lives in turn.tools. On session.tool.success (which OpenCode sends right after launch) it's emitted as completed and deleted (~L2307). Nothing keeps track of the running job.
    • ProviderSessionManager.releaseIfStillIdle releases the session after 30 minutes unless hasPendingBackgroundWork is 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 gets background: true and the shell ID directly.

    Until this is fixed, keep OpenCode background commands under 30 minutes, or use a Claude/Codex thread for longer jobs.

  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 13, 2026
  3. added a commit that references this issue on Oct 16, 2026
    4bc0b21
  4. traktuner commented on Nov 1, 2026

    @traktuner

    Another observation that matches this diagnosis, from a deployment with many OpenCode 2 threads (OpenCode 2.2.3):

    • The consequence is wider than the lost reply. After the idle release removes the thread's MCP registration and revokes its credential, the follow-up turn that OpenCode starts for the finished background shell runs without the T3 tool namespace. The agent can no longer call T3 tools in that turn.
    • In the OpenCode server log, four idle threads logged an HTTP 401 and "failed to terminate MCP session" in the same millisecond, which fits the 30 minute idle release. No OpenCode-side session eviction happened at that time; the first one in that log came hours later.
    • Tracking background shell jobs in owesWork, as proposed above, would also keep the MCP registration alive for these threads.
  5. nkoynov commented on Nov 5, 2026

    @nkoynov
    Contributor

    #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 owesWork knows about.

  6. juliusmarminge commented on Nov 11, 2026

    @juliusmarminge
    Member

    Fixed by https://git.cafe/pingdotgg/t3code/pulls/34141, now on main: A background sleep 2400; echo finished-2400 now 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.

  7. nkoynov commented on Nov 13, 2026

    @nkoynov
    Contributor

    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?

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions