Repository navigation
Description
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).
opencodeon 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_v2holds the sessions; the legacysessiontable is empty.
Reproduction
- Start the desktop app so its service runs (
opencode serve --service), and note the URL/password in~/.local/state/opencode/service.json. - Start a second server on the same data dir:
opencode serve --hostname 127.0.0.1 --port 55731. - On server A, open the desktop app. Its session list is populated correctly.
- Create a session on server B (e.g.
POST /api/sessionwith{"title":"xproc"}againsthttp://127.0.0.1:55731, Basic authopencode:<B's password>). - Watch the desktop app. The new session does not appear.
- Confirm both servers really see the data:
GET /api/session?limit=200returns 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/sessionon B (127.0.0.1:55731) → the listener receives onlyserver.connectedplus heartbeats. Nosession.createdof any kind. - Listener on B, create on A → identical: only
server.connectedplus 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:
- An incoming
session.createdon its own stream. The handler invalidates and thensyncs 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. - A refetch of the react-query
home-sessionsquery.
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
refetchIntervalanywhere — there is no periodic poll of the session list. - No
refetchOnWindowFocuson this query — the tanstack focus manager is wired (visibilitychangelisteners exist), but nothing refetcheshome-sessionson 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.
- Client side (desktop app): make the session list refresh independently of the event stream — a modest
refetchIntervalonhome-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. - Server side: publish events on a cross-process bus (or have
/api/eventtail the DB journal) so asession.createdwritten 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
- Frequency: 100% reproducible; it is a topology property, not a race.
- Both servers are
serveon the same data dir with no per-project scoping involved; the failure also holds when the created session is scoped to a directory (location: { directory }in the create body is honoured by both servers — the session lands in the right project either way, it is still not announced). - Workaround that works today: restart the desktop app.
- Related: session: two server processes sharing one opencode.db allocate session_message.seq independently - UNIQUE(seq) collisions fail sessions #53146 (two servers sharing
opencode.dbcollide onsession_message.seq), Background service resumes sessions still running on another server that shares the database #51213 (the background service resumes sessions still running on another server that shares the DB), [FEATURE]: Cross-project session event bus + a documented plugin-safe way to subscribe to it #45549 (request for a cross-process session event bus). Given those, it may be worth deciding whether two servers sharing one data dir is meant to be supported at all — if it is not, the desktop app should probably detect and warn rather than silently serve a stale list.
Activity
opencode-agent commentedon Oct 13, 2026
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 serveprocesses sharing one data dir (so oneopencode.db), subscribed to/api/eventon both, and created a session on the second one:session.created.server.connected.GET /api/sessionon the other server already listed the new session.The script below reproduces it. On
dev, the/api/eventfeed is built from the in-process event bus. The app'shome-sessionsquery 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)
Run it:
bash repro.shpatanasov commentedon Nov 11, 2026
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(sameHOME, no XDG overrides):opencode serve --serviceon0.0.0.0:49374— the managed service; the user's TUI attaches to itopencode acp→ childopencode serve --stdio --port 0— the ACP server an external gateway talks toObserved, consistent with the rest of this issue:
opencode -s <id>) shows the full new history — but the TUI never receives livesession.*events for them, and the running spinner never turns on.GET /api/session/activeon the managed service returns{}for those sessions; only the ACP process's in-memory coordinator knows they are running.serve --portcases above, because there is no configuration knob at all:opencode acphas no--server/URL flag to make it execute against the managed service (unlike the TUI's default path, which lands on the service viaService.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.