Skip to content

desktop: session list does not update when another server sharing the database creates a session #55098

Description

@milesbuckton

Summary

When a second opencode serve process runs alongside the desktop app's background service — both sharing the one opencode.db — the desktop app's session list never learns about sessions created by that other server. It only detects them after a full restart of the app. The data is correct in the shared DB (GET /api/session returns the identical list on both servers); the failure is purely that /api/event does not deliver session.created across process boundaries, and the desktop client has no other refresh path.

This is the same "two servers, one database" topology as #53146 and #51213, but the failure is on the event/live-update side rather than seq allocation or restart recovery.

Environment

  • opencode version: 2.0.28 (desktop app bundled, latest); the second server was 2.0.27 (bundled by the desktop client OpenChamber). opencode on PATH is an unrelated 1.19.0.
  • OS: Darwin 27.0.0 (darwin arm64)
  • Terminal: TERM=xterm-256color, TERM_PROGRAM unset, COLORTERM unset (N/A — API-driven repro)
  • Shell: /bin/zsh
  • Install/channel: release/latest. Desktop app version 2.0.28, bundle id ai.opencode.desktop.
  • Active plugins: global ~/.config/opencode/opencode.json → billion-context; OpenChamber's managed config ~/.config/openchamber/opencode.managed.json → -opencode.browser, openchamber-agent-tool

Topology being reported

  • Server A — desktop app's background service: opencode serve --service on 127.0.0.1:49374, spawned detached by the Electron main process, recorded in ~/.local/state/opencode/service.json. Env: OPENCODE_CLIENT=desktop, OPENCODE_DISABLE_EMBEDDED_WEB_UI=true.
  • Server B — a second opencode serve (spawned by another desktop client, here OpenChamber): opencode serve --hostname 127.0.0.1 --port 55731.
  • Both open ~/.local/share/opencode/opencode.db. session_v2 holds the sessions; the legacy session table is empty.

Reproduction

  1. Start the desktop app so its service runs (opencode serve --service), and note the URL/password in ~/.local/state/opencode/service.json.
  2. Start a second server on the same data dir: opencode serve --hostname 127.0.0.1 --port 55731.
  3. On server A, open the desktop app. Its session list is populated correctly.
  4. Create a session on server B (e.g. POST /api/session with {"title":"xproc"} against http://127.0.0.1:55731, Basic auth opencode:<B's password>).
  5. Watch the desktop app. The new session does not appear.
  6. Confirm both servers really see the data: GET /api/session?limit=200 returns the identical list on A and B, including the new session.

Expected Behavior

The desktop app shows the new session shortly after it is created — ideally immediately, via a session.created event on its SSE stream.

Actual Behavior

The new session stays invisible in the desktop app until the app is closed and reopened. Only a remount (or the 30 s staleness-triggered refetch, if the app happens to sit open that long) picks it up. In-app navigation, project switching and window focus do not.

Event delivery measured across the process boundary

Two symmetric experiments, each attaching an SSE listener to one server and creating a session on the other:

  • Listener on A (127.0.0.1:49374), POST /api/session on B (127.0.0.1:55731) → the listener receives only server.connected plus heartbeats. No session.created of any kind.
  • Listener on B, create on A → identical: only server.connected plus heartbeats.

So /api/event is delivered per-process; the in-process event bus does not cross processes. The DB is genuinely shared, so data visibility is fine — it is the live notification that does not travel.

Why the desktop app has no fallback

Read out of the shipped renderer bundle (/Applications/OpenCode.app/Contents/Resources/app.asar, 2.0.28), the session list has exactly two triggers:

  1. An incoming session.created on its own stream. The handler invalidates and then syncs the single record (C.delete(e.data.sessionID), N.session.invalidate(e.data.sessionID), r(()=>N.session.sync(e.data.sessionID))) — it GETs that one session, it never repopulates the list.
  2. A refetch of the react-query home-sessions query.

The query is defined as:

{
  queryKey: ["home-sessions", server.focused()],
  enabled: !!t && t.sdk.connection.status() === "connected",
  queryFn: ({ signal }) => $6((e, n) => t.sdk.api.session.list(e, n), signal),
  retry: false,
  staleTime: 3e4,
  refetchOnMount: true,
  refetchOnReconnect: true,
  select: g8,
}

i.e. cursor-paginated, staleTime: 30000, refetch on mount/reconnect. Verified in the bundle:

  • No refetchInterval anywhere — there is no periodic poll of the session list.
  • No refetchOnWindowFocus on this query — the tanstack focus manager is wired (visibilitychange listeners exist), but nothing refetches home-sessions on focus.
  • No invalidation on an incoming session.created — invalidateQueries({ queryKey: ["home-sessions", r], exact: true }) appears only on mutation paths (e.g. after a delete). An externally created session never invalidates it.

So the only ways the list catches up are remount, reconnect, or the staleness timer — which is exactly why a restart is the reliable trigger.

Suggested fixes

Either side closes the hole; the first is the more general one.

  1. Client side (desktop app): make the session list refresh independently of the event stream — a modest refetchInterval on home-sessions, or an invalidation when the window/app regains focus. This makes the desktop app resilient to any second writer on the shared DB, not just this client.
  2. Server side: publish events on a cross-process bus (or have /api/event tail the DB journal) so a session.created written by one server reaches subscribers of every other server sharing the database. Compare [FEATURE]: Cross-project session event bus + a documented plugin-safe way to subscribe to it #45549, which asks for the cross-project variant of the same thing.

Additional Context

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced the server side of this on 2.0.28, and it still happens on the latest dev (4ffede4).

    I started two opencode serve processes sharing one data dir (so one opencode.db), subscribed to /api/event on both, and created a session on the second one:

    • The creating server's stream got session.created.
    • The other server's stream got only server.connected.
    • GET /api/session on the other server already listed the new session.

    The script below reproduces it. On dev, the /api/event feed is built from the in-process event bus. The app's home-sessions query refetches only on mount, on reconnect, or when its 30 s stale time has passed: there's no interval and no refetch on window focus. That matches what you found in the bundle. I didn't run the desktop app itself.

    Related: #45549 (cross-process event bus request), #53146 and #51213 (other problems with two servers sharing one DB), and #53752 (open PR that refetches the session list on reconnect).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Two `opencode serve` processes sharing one data dir (one opencode.db).
    # Subscribe to /api/event on A, create a session on B, and check A's stream.
    # Usage: OC="opencode" ./repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    ROOT=$(mktemp -d /tmp/opencode/xproc.XXXX)
    export XDG_DATA_HOME=$ROOT/data XDG_CONFIG_HOME=$ROOT/config XDG_STATE_HOME=$ROOT/state XDG_CACHE_HOME=$ROOT/cache
    export OPENCODE_SERVER_PASSWORD=pw
    mkdir -p $ROOT/proj && cd $ROOT/proj && git init -q
    $OC serve --hostname 127.0.0.1 --port 4811 >$ROOT/a.log 2>&1 &
    PA=$!
    sleep 8
    $OC serve --hostname 127.0.0.1 --port 4812 >$ROOT/b.log 2>&1 &
    PB=$!
    sleep 8
    AUTH="-u opencode:pw"
    curl -sN $AUTH http://127.0.0.1:4811/api/event >$ROOT/a.events &
    CA=$!
    curl -sN $AUTH http://127.0.0.1:4812/api/event >$ROOT/b.events &
    CB=$!
    sleep 2
    echo "== create session on B"
    curl -s $AUTH -H 'content-type: application/json' -X POST http://127.0.0.1:4812/api/session \
      -d "{\"title\":\"xproc\",\"location\":{\"directory\":\"$ROOT/proj\"}}"; echo
    sleep 5
    echo "== session list on A contains xproc?"
    curl -s $AUTH "http://127.0.0.1:4811/api/session?limit=200" | grep -o '"title":"xproc"' || echo "NO"
    echo "== event types seen by A's stream (subscriber on the other process)"
    grep -o '"type":"[a-z.]*"' $ROOT/a.events | sort | uniq -c
    echo "== event types seen by B's stream (same process as the create)"
    grep -o '"type":"[a-z.]*"' $ROOT/b.events | sort | uniq -c
    kill $CA $CB $PA $PB 2>/dev/null

    Run it: bash repro.sh

  2. patanasov commented on Nov 11, 2026

    @patanasov

    One more variant of this topology, from a voice-assistant setup (opencode driven over ACP; opencode 2.2.7, latest release, Ubuntu 26.04):

    Process tree on one machine, all sharing one ~/.local/share/opencode/opencode.db (same HOME, no XDG overrides):

    • opencode serve --service on 0.0.0.0:49374 — the managed service; the user's TUI attaches to it
    • opencode acp → child opencode serve --stdio --port 0 — the ACP server an external gateway talks to

    Observed, consistent with the rest of this issue:

    • Sessions executed by the ACP server (delegated coding tasks) appear in the shared DB, and re-attaching to them in the TUI (opencode -s <id>) shows the full new history — but the TUI never receives live session.* events for them, and the running spinner never turns on.
    • GET /api/session/active on the managed service returns {} for those sessions; only the ACP process's in-memory coordinator knows they are running.
    • This variant is structurally worse than the serve --port cases above, because there is no configuration knob at all: opencode acp has no --server/URL flag to make it execute against the managed service (unlike the TUI's default path, which lands on the service via Service.ensure). Any ACP-driven orchestrator is therefore permanently a "second server" on a shared-DB machine, and ACP-executed work is never live-visible to first-party clients.

    So a fix for this issue (cross-process wake for durable events, as requested in #45549) would cover ACP as well; alternatively an opencode acp --server <url> that delegates execution to an existing service would fix the ACP case directly. Related ACP-side ask: #62986 (internal ACP server port).

    Happy to provide process-tree/log evidence on request.

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