Skip to content

Multi-server: layout state not namespaced per server causes session loop after switching #18302

Closed as not planned

Description

@Skeptomenos

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

  1. Run opencode serve on a remote machine (e.g. http://remote:8472)
  2. In OpenCode Desktop, add the remote server via the server picker
  3. Switch to the remote server — open some projects, navigate to sessions
  4. Switch back to local sidecar
  5. Observe: ERROR service=server error=Session not found failed repeats every ~4 seconds in the sidecar log
  6. 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 serve on a separate machine
  • The session loop fires ~every 4 seconds and generates error notifications with audible chimes

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    webRelates to opencode on web / desktop
    on Mar 19, 2026
  2. removed
    webRelates to opencode on web / desktop
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  3. rocordov commented on May 5, 2026

    @rocordov

    We confirmed this bug affects opencode web instances 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.

  4. rocordov commented on May 5, 2026

    @rocordov

    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.

  5. Aerytgophilis commented on May 28, 2026

    @Aerytgophilis

    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:

    1. The vlocal sidecar starts synchronously and populates its project list immediately
    2. The remote WSL server connects asynchronously via SSE
    3. Because \lastProjectSession\ and \sidebar.workspaces\ are not namespaced by server origin, the vlocal sidecar's state overwrites or conflicts with the WSL server's state
    4. The WSL server's sessions (which exist in its database and are confirmed via API) never appear in the sidebar

    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:

    • \curl\ and \ etch()\ from DevTools both return 14 sessions from the WSL server
    • WSL server log shows zero session list requests from the GUI after handshake
    • \global.dat\ has \lastProjectSession\ keyed by bare directory path, not server origin
    • asar reverse-engineering confirms the \server.connected\ SSE handler iterates \M.children\ which is empty at that point

    This strongly supports the Option B fix (namespace by \origin()) described in this issue.

  6. github-actions commented on Jul 28, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions