Skip to content

Desktop: fatal "Notification server not found: wsl:<distro>" on launch — restored tab's server not gated on WSL sidecar readiness #37481

Closed

Description

@Shana-AE

Summary

On Windows, when the persisted window tab references a WSL sidecar server (wsl:<distro>), the desktop renderer fatally errors on every launch because it resolves the active server before that WSL sidecar finishes booting. The app is left unusable (blank/dead window) and relaunching always reproduces.

The existing readiness guard (availableStartupServer) only protects defaultServerUrl; the restored tab's server — which drives activeServer() via the route's serverKey — is never validated against sidecar readiness.

Environment

  • OpenCode Desktop 1.18.3 (latest; bug still present)
  • OS: Windows + WSL2 (distro: archlinux)
  • Server managed by the desktop's WSL sidecar subsystem

Reproduce

  1. Add a WSL server (wsl:<distro>) in the desktop app and open a session on it (so a tab with server: "wsl:<distro>" is persisted to opencode.window.<id>.dat).
  2. Quit the desktop app.
  3. Relaunch.

Actual: the renderer dies immediately with:

Error: Notification server not found: wsl:archlinux
    at ensure (…/main-…js)
    at selected (…/main-…js)
    at Object.ready (…/main-…js)
    …

The window is unusable and every relaunch reproduces. There is no way to recover from inside the app (you cannot reach the UI to switch servers).

Log timeline (single launch)

The WSL sidecar is healthy — it just boots ~4s after the renderer has already crashed:

18:47:35.693  wsl sidecar starting   { id: 'wsl:archlinux', distro: 'archlinux' }
18:47:37.096  native sidecar ready   { url: 'http://127.0.0.1:53493' }
18:47:37.552  FATAL renderer error:  Notification server not found: wsl:archlinux
18:47:41.372  wsl sidecar ready      { id: 'wsl:archlinux', url: 'http://127.0.0.1:64643' }   // ~3.8s AFTER the crash

Root cause

packages/app/src/context/notification.tsx — ensure() does a hard throw when the active server is absent from the server list:

const ensure = (key: ServerConnection.Key) => {
  const conn = global.servers.list().find((item) => ServerConnection.key(item) === key)
  if (!conn) throw new Error(`Notification server not found: ${key}`)   // ← fatal on launch
}
const selected = () => ensure(activeServer())
return { ready: () => selected().ready(), … }

activeServer() returns the restored route's serverKey (wsl:<distro>):

const activeServer = createMemo(() => {
  if (params.serverKey) return requireServerKey(params.serverKey)   // ← restored tab → wsl:<distro>
  if (search.draftId) { … }
  return server.key
})

At that moment wsl:<distro> is not yet in global.servers.list(), because packages/desktop/src/renderer/wsl/connections.ts only includes WSL servers whose runtime has reached ready:

export function readyWslConnections(state?: WslServersState) {
  return (state?.servers ?? []).flatMap((item) => {
    if (item.runtime.kind !== "ready") return []        // sidecar still "starting" → excluded
    return [ { … variant: "wsl", distro: item.config.distro, http: { url: item.runtime.url, … } } ]
  })
}

The availableStartupServer guard only rewrites defaultServerUrl, so it does not help when the active server comes from the restored tab/route:

export function availableStartupServer(defaultServer, state) {
  const key = defaultServer ?? "sidecar"
  if (!key.startsWith("wsl:")) return key
  if (state?.servers.some((item) => item.config.id === key && item.runtime.kind === "ready")) return key
  return "sidecar"
}

Because the throw happens inside a root SolidJS computation (Object.ready → runComputation), it is treated as a fatal renderer error (recorded via recordFatalRendererError), not a recoverable one.

Suggested fix

Either/both:

  1. Gate the renderer's initial render / "ready" on the active server actually being present — extend the readiness gating (cf. fix(desktop): include wslServers loading in ready state #37218) so the restored tab's serverKey is also covered, not only defaultServerUrl. E.g. resolve the active server through availableStartupServer (or skip/wait) when its target is a WSL sidecar that hasn't reached ready.
  2. Make ensure() non-fatal during init: fall back to a present server (e.g. the native sidecar) or defer the computation instead of throwing when the active server is transiently absent.

Workaround (for anyone hitting this now)

Quit the desktop app, then in %APPDATA%\ai.opencode.desktop\:

  1. In opencode.settings, set "defaultServerUrl": "sidecar".
  2. Delete opencode.window.<id>.dat (clears the restored tab that points at wsl:<distro>).

Relaunch → app boots on the native sidecar, then switch to the WSL server once it reports ready. Sessions are server-side and are not lost.

(Note: leaving a wsl:<distro> tab as the last active tab before quitting will reproduce the crash again until the readiness gating covers the restored tab.)

Activity

  1. github-actions commented on Jul 17, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. izumiChan16 commented on Jul 17, 2026

    @izumiChan16

    Methods available for the time being:
    Open the software -> encounter a crash -> press Ctrl+Shift+I -> just press F5 in the developer tools to refresh it

  3. Shana-AE commented on Jul 17, 2026

    @Shana-AE
    Author

    Update — found the exact trigger (and a verified workaround).

    The crash isn't driven by the persisted window tab (opencode.window.<id>.dat) as I first wrote — it's the router's restored last URL from localStorage.

    packages/desktop/src/renderer/index.tsx:

    function getLastActiveUrl(windowID) {
      const value = localStorage.getItem(`opencode.desktop.window.${windowID}.last-active-url`)
      if (value?.startsWith("/") && !value.startsWith("//")) return value
      return "/"
    }
    // DesktopMemoryRouter seeds createMemoryHistory() with that URL

    That URL looks like /server/<base64("wsl:archlinux")>/session/<id> — the serverKey is base64-encoded in the path (easy to miss when grepping for wsl:). On launch, params.serverKey decodes to wsl:<distro>, which feeds activeServer() in notification.tsx → ensure() throws before the WSL sidecar reaches ready.

    Because params.serverKey takes priority over defaultServer, the existing availableStartupServer guard on defaultServerUrl can't help here.

    Verified on the affected machine:

    • Before: every launch logged a fatal Notification server not found: wsl:archlinux ~3.8s before the sidecar reported ready (e.g. crash 20:49:56.678 vs sidecar ready 20:50:00.734).
    • After clearing localStorage (router now starts at /): clean launch with an empty renderer.log, sidecar becomes ready normally (20:57:16.709), no fatal error.

    Suggested fix (refined): validate the restored serverKey (from getLastActiveUrl) against the available/ready servers before the router commits to it — i.e. apply availableStartupServer-style logic to the restored route's server, or make ensure() non-fatal during init when the active server is a not-yet-ready sidecar.

    Workaround: quit the app → delete %APPDATA%\ai.opencode.desktop\Local Storage\ (optionally also set defaultServerUrl: "sidecar" in opencode.settings). Relaunch, then switch to the WSL server once it's ready. Caveat: leaving a WSL session as the last active view before quitting repopulates localStorage and reproduces the crash.

  4. fermumen commented on Jul 21, 2026

    @fermumen

    Regression in Desktop 1.18.4 (Windows 11 + WSL2, Ubuntu-26.04). The fix from #37890 is included in the installed release, but startup still fatally crashes with the same error.

    Current exported debug bundle timeline:

    20:33:16.587  Desktop 1.18.4 starting
    20:33:18.055  WSL sidecar starting: wsl:Ubuntu-26.04
    20:33:19.106  FATAL renderer: Notification server not found: wsl:Ubuntu-26.04
    20:33:20.199  WSL sidecar ready: wsl:Ubuntu-26.04
    

    So this is the same transient sidecar-readiness condition, not a failing WSL server. The stack is now through ProjectAvatar / HomeSessionRow and identifies ensure as ensureServerState:

    Error: Notification server not found: wsl:Ubuntu-26.04
      at Object.ensure [as ensureServerState] (...)
      at Object.fn (...)
      at ProjectAvatar (...)
      at SessionTabAvatar (...)
      at HomeSessionRow (...)
    

    #37890 only changed selected() to fall back when the active key is not in global.servers.list(). However packages/app/src/pages/layout/project-avatar-state.ts bypasses selected():

    notification.ensureServerState(server()).session.unseenCount(sessionId())

    and in v1.18.4, ensure() still contains:

    if (!conn) throw new Error(`Notification server not found: ${key}`)

    Thus any restored/home session row for a configured WSL server can still crash the renderer before that sidecar is registered. The direct API needs the same non-throwing fallback/no-op behavior (or callers must guard the connection), rather than relying on the selected() fallback alone.

    The previously proposed fallback in unmerged/closed #37190 appears to address this direct-call path. A regression test should exercise useSessionTabAvatarState (or another direct ensureServerState(wslKey) consumer) while the WSL sidecar is configured but not yet ready.

  5. Hona commented on Jul 21, 2026

    @Hona
    Member

    Weird - I got someone to test and it was fixed.
    Investigating

  6. fermumen commented on Jul 21, 2026

    @fermumen

    tbh it worked briefly after the update but then same issue.

    I did get it to work again by doing some resetting:

    • Removed the configured wsl:Ubuntu-26.04 entry from opencode.settings
    • Cleared persisted WSL workspace/project state (opencode.workspace.wsl-*.dat and server/project state)
    • Cleared Desktop UI/router state (window/tab state and Chromium Local Storage)

    This was a clean reset of Desktop state; the WSL OpenCode session database was retained. The configured WSL server entry itself was valid and generated by Desktop, not manually added.

    But now there is still an intermittent (related?) syntomp: Desktop reports that sessions no longer exist and asks to close their tabs. That suggests stale persisted tab/session references can survive or be recreated independently of the notification race. =

  7. Hona commented on Jul 22, 2026

    @Hona
    Member

    Yeah follow up commit done for this crash

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