Repository navigation
Multi-server: layout state not namespaced per server causes session loop after switching #18302
Description
Bug Description
When connecting OpenCode Desktop to multiple servers (local sidecar + remote opencode serve), switching between them leaves stale state from the previous server in persisted layout storage. This causes a "Session not found" polling loop when the Desktop tries to restore sessions that only exist on the other server.
Reproduction Steps
- Run
opencode serveon a remote machine (e.g.http://remote:8472) - In OpenCode Desktop, add the remote server via the server picker
- Switch to the remote server — open some projects, navigate to sessions
- Switch back to local sidecar
- Observe:
ERROR service=server error=Session not found failedrepeats every ~4 seconds in the sidecar log - Error notifications accumulate with red badges on projects
Root Cause
Three pieces of persisted state in global.dat are keyed by directory path only, not by server origin + directory path:
1. lastProjectSession (packages/app/src/pages/layout.tsx:96-108)
// layout.page.v1 — global, not per-server
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } }rememberSessionRoute() (line 1189) stores session IDs keyed by directory. When switching servers, the previous server's session IDs remain. On return to local, navigateToProject() (line 1215) tries to restore these sessions via openSession() (line 1236) — fails because the session ID belongs to the remote server's database.
2. sidebar.workspaces (packages/app/src/pages/layout.tsx:227-261)
// layout.v6 — global, not per-server
sidebar: {
workspaces: {} as Record<string, boolean>, // keyed by directory path
}Remote server workspace paths (e.g. /Users/remote-user/repos/project) persist in the sidebar layout after switching back to local. The Desktop then tries to bootstrap child stores for these non-existent local paths.
3. notifications (packages/app/src/context/notification.tsx:126-131)
// notification.v1 — global, single flat list
list: [] as Notification[]Error notifications from the session restoration failures accumulate in a single list capped at 500, pushing out legitimate turn-complete notifications.
What IS correctly isolated
The project list itself is correctly namespaced — server.tsx:24-29 uses projectsKey(origin) to key store.projects[origin]. This is why the sidebar shows different projects per server. The runtime state is also isolated via <ServerKey keyed> which remounts the entire component tree on switch.
Proposed Fix
Namespace the three affected storage keys by server origin, matching the pattern already used in server.tsx:
Option A (minimal): Clear lastProjectSession entries and sidebar.workspaces entries for the previous server's paths when switching servers.
Option B (proper): Key these stores by origin() the same way store.projects already is:
// Instead of:
lastProjectSession: {} as { [directory: string]: SessionRoute }
// Use:
lastProjectSession: {} as { [origin: string]: { [directory: string]: SessionRoute } }Environment
- Version: 1.2.27
- Two servers: local sidecar + remote
opencode serveon a separate machine - The session loop fires ~every 4 seconds and generates error notifications with audible chimes
Activity
rocordov commentedon May 5, 2026
We confirmed this bug affects
opencode webinstances on different ports sharing the same state directory. Even with identical CSP, identical JS bundles, and no auth, the web UI on the secondary instance fails to render session content (blank page). The main instance (port 4096) works fine.Workaround: don't open the secondary server's web UI. Poll its REST API directly for session messages and render progress in a custom UI.
rocordov commentedon May 5, 2026
Workaround for multi-server UI bug: We confirmed this affects any opencode web instance on a non-default port. Even with identical CSP, JS bundles, and auth, the web UI fails to render session content on secondary ports.
Our solution: don't open the secondary server's web UI at all. Instead, we proxy session messages through the main server and display CoT in a custom chat window with 3s polling. The isolated server runs purely as an API backend.
Aerytgophilis commentedon May 28, 2026
Additional evidence from a WSL2 + Desktop GUI setup: #29766
This issue manifests in a particularly insidious way when the Desktop GUI's built-in vlocal sidecar and a user-configured remote server (WSL2 opencode serve) are both active:
The dual-server architecture (vlocal sidecar + remote server) exacerbates this bug because both servers are active simultaneously, not just switched between. The frontend state collision happens in real-time rather than only during server switching.
Concrete evidence from #29766:
This strongly supports the Option B fix (namespace by \origin()) described in this issue.
github-actions commentedon Jul 28, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.