Skip to content

Background service resumes sessions still running on another server that shares the database #51213

Open

Description

@alohaninja

Description

When a second OpenCode server shares the data directory, the managed service's startup recovery resumes sessions that the other server is still running. It marks their in-flight tool calls as aborted, adds "The server restarted while you were working", and starts its own step, so two servers drive one session.

Execution claims only store a timestamp, so the sweep can't tell a dead owner from a live one. restart.ts assumes "unregistered servers sharing the database never sweep", but their live claims still get swept by the service.

What I saw: a desktop client runs its own bundled opencode serve --port <n>, and the background service started while that server was mid-turn. When the original server's subagents finished, its next request failed with:

Anthropic Messages system updates cannot split a local tool call from its tool result

because the history now had the service's unresolved tool calls in it. The session stayed stuck until I sent another message. I didn't hit this on V1.

Plugins

None (isolated repro)

OpenCode version

2.0.16 (also seen with a 2.0.15 client-bundled server next to a 2.0.16 service)

Steps to reproduce

  1. Use fresh XDG_CONFIG_HOME / XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME dirs and an Anthropic model.
  2. Start server A: opencode serve --port 4801.
  3. On A, prompt: "Call the subagent tool twice in parallel. Each subagent runs sleep 150 && echo done."
  4. While the subagents are running, start server B on the same dirs: opencode serve --service --port 4799.
  5. B marks A's subagent calls as errors, adds the restart notice, and starts new subagents.
  6. When A's subagents finish, A's next step fails with the error above.

Screenshot and/or share link

No response

Operating System

macOS 26 (Darwin 25.3.0, arm64)

Terminal

N/A (API-driven repro)

Activity

  1. gszep commented on Sep 28, 2026

    @gszep

    Thanks for the detailed report and #51215. I confirmed overlapping executions on a local 2.0.15-based build sharing its DB between an unregistered server and the managed service; each failed request preceded the other server’s tool completion. I am carrying both commits from #51215 (46 execution tests pass). There is also a separate deterministic lowering problem: an update followed by an available or repaired error result still throws. Repro in #51764; fixing that does not replace the live-owner recovery guard.

  2. nkoynov commented on Oct 7, 2026

    @nkoynov

    Hit this from T3 Code (2.0.22; restart.ts is unchanged in 2.0.24). The second server was started by an agent: in a thread served by T3's own opencode serve --hostname 127.0.0.1 --port N, the model ran opencode mcp list to debug an MCP login, and the CLI auto-started opencode serve --service. Its boot sweep resumed both sessions T3's server was running ("The server restarted while you were working" 0.15 s after start), and for 3h22m the two servers alternated steps. Each had a different MCP set, so one session got 437 full Code Mode catalog notices (~4 MB stored) and was auto-compacted.

    Repro on 2.0.24 (VM): T3 thread runs sleep 120; date >> runs.txt → opencode mcp list in a shell → the service marks the running shell call "Tool execution interrupted" while sleep keeps running, the resumed model starts the same command again (two sleep processes, two lines in runs.txt, two idle rows for one turn).

    Workaround until #51215 lands: {"disabled": true} in ~/.config/opencode/service.json. CLI commands then use a standalone serve --stdio, which never sweeps; same repro: one sleep, one line, no notice. Only opencode service start/restart, pair and serve --service still start the managed service.

  3. Starefossen commented on Oct 8, 2026

    @Starefossen

    Also on 2.0.24 (macOS arm64): two managed services with separate XDG state/config but a shared opencode.db. Service B's boot log shows Failed to drain Session for two sessions still running in A.

    Another effect: the resumed turn runs with B's tools, cwd and sandbox. A turn started in a sandboxed server can continue in an unsandboxed one.

    Source: restart.ts:192-231 only checks the local execution.active. Fix: record the claim owner (server id + heartbeat) and skip claims whose owner is alive.

  4. jorgeferreira1 commented on Oct 25, 2026

    @jorgeferreira1

    Still happening on 2.1.4 (Linux). This time the trigger was an auto-update, and the session that got taken over belonged to a live opencode run --standalone.

    • A headless worker was running as opencode run --standalone -m <model> "<task>", with its own serve --stdio. It was 7 minutes into a long shell call.
    • A newly opened TUI auto-updated 2.1.3 → 2.1.4 (updated OpenCode from=2.1.3 to=2.1.4 method=curl). The old serve --service shut down, and a 2.1.4 serve --service started 5 s later.
    • Within 0.2 s of starting, the new service sent "The server restarted while you were working" to 4 sessions. One of them was the standalone worker's. It marked the worker's in-flight shell call aborted: Tool execution interrupted: shell and started its own step.
    • The standalone process was still alive and kept driving the same session. For about 9 minutes both servers ran tools for one session id in the same working tree. The service's copy ran without the environment the run was started with, and outside the process group that the caller's timeout kills. The service's copy made the final commit.

    So a standalone run can't be bounded by its caller while any serve --service shares the DB, and an auto-update is enough to set that off without anyone restarting anything. #51215 (skip claims whose owner is alive) would have covered this case.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced this on 2.0.16, and it still happens on the latest dev (9adc42b).

    I used a mock OpenAI-compatible model instead of Anthropic and drove both servers through the HTTP API:

    1. Start server A with opencode serve --port 4801 on fresh XDG dirs, and prompt a session so that it runs a shell command, sleep 40 && echo done.
    2. While the command is still running, start server B with opencode serve --service --port 4799 on the same dirs.
    3. Within seconds, B's startup recovery marks A's still-running tool call as aborted ("Tool execution interrupted: shell"), adds "The server restarted while you were working…" and runs its own model step, while A's sleep process keeps running.
    4. When A's command finishes, A's next request is built on the history B changed.

    I couldn't see the Anthropic-specific error without Anthropic credentials, but the two servers driving one session, which is what leads to it, reproduces. This contradicts the assumption in packages/core/src/session/execution/restart.ts that every unreleased execution claim belongs to a dead process. The script below reproduces it; it needs the small mock LLM server it starts (mock.ts) in the same directory.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #51213: a `serve --service` started on the same data dirs sweeps a session
    # that an unregistered `opencode serve` is still running.
    # Usage: OC="opencode" bash repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    HERE=$(cd "$(dirname "$0")" && pwd)
    R=$(mktemp -d /tmp/opencode/r51213.XXXX)
    mkdir -p $R/config/opencode $R/data $R/state $R/cache $R/proj
    cat > $R/config/opencode/opencode.json <<JSON
    { "model": "mock/m", "permission": { "*": "allow" },
      "provider": { "mock": { "npm": "@ai-sdk/openai-compatible", "name": "Mock",
        "options": { "baseURL": "http://127.0.0.1:4900/v1", "apiKey": "x" },
        "models": { "m": { "name": "m", "tool_call": true } } } } }
    JSON
    export XDG_CONFIG_HOME=$R/config XDG_DATA_HOME=$R/data XDG_STATE_HOME=$R/state XDG_CACHE_HOME=$R/cache
    cd $R/proj && git init -q
    bun $HERE/mock.ts > $R/mock.log 2>&1 & MOCK=$!
    $OC serve --port 4801 > $R/a.log 2>&1 & A=$!
    for i in $(seq 60); do grep -q "server password" $R/a.log && break; sleep 0.5; done
    PA=$(grep -o "server password .*" $R/a.log | awk '{print $3}')
    SID=$(curl -s -u opencode:$PA -XPOST localhost:4801/api/session -H 'content-type: application/json' \
      -d "{\"location\":{\"directory\":\"$R/proj\"},\"model\":{\"id\":\"m\",\"providerID\":\"mock\"}}" | sed 's/.*"id":"\(ses_[^"]*\)".*/\1/')
    echo "session $SID"
    curl -s -u opencode:$PA -XPOST localhost:4801/api/session/$SID/prompt -H 'content-type: application/json' -d '{"text":"run sleep"}' >/dev/null
    sleep 5
    echo "--- shell processes after A started its tool call:"; pgrep -fa "sleep 40" | grep -v pgrep
    echo "--- starting server B (--service) on the same dirs while A is mid-turn"
    $OC serve --service --port 4799 > $R/b.log 2>&1 & B=$!
    sleep 15
    echo "--- shell processes now:"; pgrep -fa "sleep 40" | grep -v pgrep
    echo "--- mock LLM requests:"; cat $R/mock.log
    echo "--- session messages (as seen by A):"
    curl -s -u opencode:$PA "localhost:4801/api/session/$SID/message" | head -c 6000; echo
    echo "--- waiting for A's tool to finish"; sleep 30
    echo "--- mock LLM requests (final):"; cat $R/mock.log
    echo "--- A log tail:"; tail -20 $R/a.log
    kill $A $B $MOCK 2>/dev/null; pkill -f "sleep 40"
    echo "dir: $R"

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions