SSE Connection Reliability Improvements #413

Description

@gennadiryan

Actionable fixes to prevent the "no GUI response" failure class and harden the
webview-server connection lifecycle. Each item includes acceptance criteria.

See also:

  • ./issue-no-gui-response.md — the specific bug these fixes address
  • ../notes/local-storage-customization.md — parameter inventory and design analysis
  • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

Tier 1 — Critical Path

These fixes directly prevent or mitigate the "no GUI response" bug. They should be
prioritized for immediate implementation.


1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

File:packages/app/src/entry.tsx (lines 157–161)

Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
localStorage before consulting location.origin. In the Amicode webview, the
iframe is served by the running server — location.origin is always the correct
URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
port), it permanently overrides the correct origin.

Fix: When running inside the Amicode webview (detectable via inAmicode() from
utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
entirely. Use location.origin unconditionally.

Acceptance criteria:

  • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
    without consulting localStorage.
  • The defaultServerUrl localStorage key is still honored in the standalone web
    app context (when inAmicode() is false).
  • After a server restart (new port), reopening the Amicode webview connects to the
    new server without requiring localStorage to be cleared.

2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

File:packages/app/src/context/server-sdk.tsx (lines 268–338)

Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
It does not distinguish "connection refused" (server gone) from "server error"
(server exists). If the URL is stale, this loops forever without recovery.

Fix: Add an escalation path: after N consecutive connection-refused failures
(suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
instead of the persisted server URL. If location.origin succeeds, update the
persisted server store to reflect the correct URL.

Acceptance criteria:

  • After 10 consecutive connection-refused errors (not HTTP errors — specifically
    TypeError: Failed to fetch or equivalent network failure), the loop switches to
    location.origin as the target URL.
  • If location.origin connects successfully, the persisted server store entry is
    updated in-place so subsequent reconnections use the correct URL directly.
  • If both the persisted URL and location.origin fail, the loop continues retrying
    location.origin at 250 ms intervals (as today, but to the correct URL).
  • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
    the ConnectionBanner to display).

3. [CRITICAL] Server boot-ID stamping

Files:

  • Server: packages/opencode/src/server/server.ts or the SSE event handler
  • Client: packages/app/src/context/server-sdk.tsx and server.tsx

Problem: The client cannot distinguish "same server reconnected" from "different
server on same port" from "stale URL, server gone." Without a server identity
token, all reconnection heuristics are fragile.

Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
includes it in:

  1. The server.connected SSE event payload (so the client receives it on every
    reconnection).
  2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
    client can detect mid-session server restarts even outside the SSE stream.

The client persists the boot-ID alongside the server URL in localStorage. On
reconnection:

  • If boot-ID matches → normal reconnect; session state is valid.
  • If boot-ID differs → server restarted; invalidate session tab list, refetch all
    state, and optionally re-validate credentials.
  • If boot-ID is absent (older server version) → treat as "unknown," fall back to
    current behavior.

Acceptance criteria:

  • Server emits a bootId field in the server.connected event.
  • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
  • Client stores lastBootId per server entry in the persisted store.
  • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
    prune tabs with 404 sessions, reset workspace caches.
  • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
    auth-required prompt (rather than silently failing).

4. [CRITICAL] Configurable fixed port per container

Files:

  • Config schema: packages/core/src/v1/config/server.ts
  • Devcontainer: .devcontainer/devcontainer.json
  • Documentation

Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
Because the webview's localStorage persists across container rebuilds (it lives on
the host), a new port on restart means a stale persisted URL. Users in devcontainer
workflows hit this on every rebuild.

Fix: Allow users to pin a stable port per container via either:

  1. opencode.json{ "server": { "port": 4096 } } (already supported by the
    schema but not widely documented or surfaced).
  2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
    the Amicode extension host when spawning the server).

Document the recommendation: in devcontainer-based workflows, set a fixed port so
that localStorage's persisted URL remains valid across container rebuilds.

Acceptance criteria:

  • Documentation (README or extension settings description) explicitly recommends
    setting server.port in opencode.json for devcontainer workflows.
  • The Amicode extension host reads OPENCODE_PORT from the container environment
    (if available) and uses it when launching the server.
  • When server.port is set in opencode.json, the server binds to exactly that
    port (no fallback) and fails loudly if the port is in use (rather than silently
    falling back to a random port).
  • The .devcontainer/devcontainer.json in this repo is updated to include a
    commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

Tier 2 — High Importance

These fixes prevent related failure modes and harden the connection lifecycle.
Implement after Tier 1.


5. Multi-instance localStorage isolation

File:packages/app/src/utils/persist.ts

Problem: All Amicode webview instances on the same VS Code installation share a
single localStorage scope (keyed by extension ID origin). Two windows with
different servers overwrite each other's server entries.

Fix: Key all connection-related localStorage entries by a workspace
identifier
(e.g., a hash of the container's filesystem root or the server URL at
first successful connection). Non-connection state (theme, zoom) remains global.

Acceptance criteria:

  • Two VS Code windows with Amicode, connected to different servers, do not
    interfere with each other's connection state.
  • Opening a new window for a previously-unknown workspace starts fresh (no stale
    entries from another workspace).
  • Global preferences (theme, solver mode) remain shared across all instances.
  • Migration: on first load with the new keying scheme, existing global state is
    migrated into the appropriate workspace bucket.

6. Session tab validation on load

File:packages/app/src/context/tabs.tsx

Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
Dead session references from previous server instances accumulate, causing burst
fetches to stale/non-existent endpoints on reload.

Fix: On the server.connected event (which fires on every SSE reconnection),
validate all open session tabs by checking their existence against the server. Tabs
whose session IDs return 404 are moved to the closed list (not deleted — user can
re-open if the session reappears after a migration/restore).

Acceptance criteria:

  • Within 5 seconds of server.connected, all open tabs are validated.
  • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
  • A toast notification summarizes: "N sessions from a previous server instance were
    closed."
  • The validation is non-blocking (does not prevent the app from rendering).
  • If the server is unreachable during validation (e.g., the server.connected
    event was a false positive), validation is skipped gracefully.

7. Credential invalidation on boot-ID change

File:packages/app/src/context/server.tsx (inside resolveServerList)

Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
persisted credentials in the localStorage server.list entry are stale. Every API
call returns 401, but the client does not surface this or attempt to refresh.

Fix: When the boot-ID changes (see item #3), clear persisted credentials for
that server entry and re-read them from the iframe URL query param (auth_token).
If no auth_token is present in the URL and the server requires auth, surface an
auth prompt.

Acceptance criteria:

  • On boot-ID mismatch, the persisted username/password for the affected server
    entry are cleared.
  • The app re-reads auth_token from location.search (the iframe URL injected by
    the extension host).
  • If auth is required and no valid credentials are available, a modal prompts the
    user (rather than silently failing with 401s).

8. Extension host → webview URL push on server restart

File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

Problem: When the extension host restarts the server (via
amicode.restartServer command), it re-launches the opencode process on a
potentially different port. The webview SSE stream is connected to the old port and
must wait for connection-refused → escalation (item #2) to recover.

Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
iframe immediately after the new server is confirmed listening. The webview handles
this message by updating its active server URL and immediately reconnecting SSE to
the new URL.

Acceptance criteria:

  • The webview registers a listener for kind: "server-url-changed" messages.
  • On receiving this message, the webview updates its persisted server store and
    reconnects SSE within 1 second (no 250 ms retry loop needed).
  • If the message arrives while the webview is already connected (race condition),
    it is a no-op.
  • The webview emits a route-info message back to confirm it received the update.

Tier 3 — Improvements

These are well-advised hardening measures. They do not directly prevent the "no GUI
response" bug but reduce adjacent failure surfaces.


9. Quota-aware eviction priority

File:packages/app/src/utils/persist.ts (lines 112–165)

Problem: The localStorage eviction logic removes the largest opencode.* keys
first. The server key (connection state) and tabs key (session history) grow
over time and become prime eviction targets.

Fix: Maintain a "protected keys" list that the eviction logic never removes.
At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

Acceptance criteria:

  • opencode.global.dat:server is never evicted by the quota handler.
  • Eviction preferentially targets workspace and session-scoped keys.
  • If eviction cannot free enough space without touching protected keys, the write
    fails gracefully (the app continues to function with the existing state).

10. Connection banner always visible on persistent disconnection

File:packages/app/src/components/connection-banner.tsx

Problem: The ConnectionBanner component shows when streamStatus is
"disconnected", but its visibility depends on layout configuration. If the banner
is scrolled off or hidden by a panel, the user has no indication that the
connection is broken.

Fix: After 5 seconds of continuous "disconnected" state, surface a
VS Code-style notification (via postMessage to the extension host, which calls
vscode.window.showWarningMessage) in addition to the in-webview banner.

Acceptance criteria:

  • If the SSE stream is disconnected for > 5 continuous seconds, a warning
    notification appears in VS Code's notification area.
  • The notification includes an action button: "Reconnect" (which triggers URL
    rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
  • The notification is not repeated more than once per 60 seconds.

11. Terminal extension: readiness probe fix (/app/health)

File:sdks/vscode/src/extension.ts (line 78)

Problem: The extension probes GET /app (a catch-all UI route) with no
status-code check. Should probe GET /health and check response.ok.

Fix: Change the URL to /health and gate connected = true on response.ok.

Acceptance criteria:

  • The probe hits GET /health.
  • connected is only set to true if the response status is 2xx.
  • A 404 or 500 from a partially-initialized server does not set connected = true.

12. Terminal extension: dead terminal detection

File:sdks/vscode/src/extension.ts (lines 15–19)

Problem:opencode.openTerminal reuses a terminal by name without checking if
the process has exited.

Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
exitStatus === undefined.

Acceptance criteria:

  • A terminal whose process has exited is not reused.
  • The user gets a fresh terminal with a new server instance.

13. RPC error propagation

File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

Problem: If a worker-side RPC method throws, the pending promise in
client.call() is never settled. The caller hangs forever.

Fix:

  • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
    with the error details and request ID.
  • Client side: store { resolve, reject } pairs in pending; handle rpc.error
    messages by calling reject(new Error(...)).

Acceptance criteria:

  • If a worker method throws, the client-side promise rejects with an Error
    containing the original error message.
  • The pending Map entry is cleaned up (no memory leak).
  • Existing callers of client.call() that do not handle rejection see an unhandled
    rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

14. Worker error logging

File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

Problem:unhandledRejection and uncaughtException handlers discard all
errors silently, making worker failures invisible.

Fix: Log errors to stderr with a [worker] prefix.

Acceptance criteria:

  • Unhandled rejections log the error object to stderr.
  • Uncaught exceptions log the error message and stack to stderr.
  • The worker process does NOT exit on these errors (existing keep-alive behavior
    is preserved).

15. Terminal extension: retry window + user-visible warning

File:sdks/vscode/src/extension.ts (lines 73–90)

Problem: The retry loop tries 10 times (2 s total). If the server doesn't
respond (port collision, slow startup), the file reference is silently dropped.

Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
vscode.window.showWarningMessage(...) so the user knows something went wrong.

Acceptance criteria:

  • The retry window is 4 seconds (20 * 200 ms).
  • If connected is still false after the loop, a warning message is shown.
  • The warning message suggests "try again" or "the port may be in use."

Metadata

Metadata

Assignees

Labels

duplicateThis issue or pull request already exists

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    SSE Connection Reliability Improvements #413

    Description

    @gennadiryan

    Actionable fixes to prevent the "no GUI response" failure class and harden the
    webview-server connection lifecycle. Each item includes acceptance criteria.

    See also:

    • ./issue-no-gui-response.md — the specific bug these fixes address
    • ../notes/local-storage-customization.md — parameter inventory and design analysis
    • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

    Tier 1 — Critical Path

    These fixes directly prevent or mitigate the "no GUI response" bug. They should be
    prioritized for immediate implementation.


    1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

    File:packages/app/src/entry.tsx (lines 157–161)

    Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
    localStorage before consulting location.origin. In the Amicode webview, the
    iframe is served by the running server — location.origin is always the correct
    URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
    port), it permanently overrides the correct origin.

    Fix: When running inside the Amicode webview (detectable via inAmicode() from
    utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
    entirely. Use location.origin unconditionally.

    Acceptance criteria:

    • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
      without consulting localStorage.
    • The defaultServerUrl localStorage key is still honored in the standalone web
      app context (when inAmicode() is false).
    • After a server restart (new port), reopening the Amicode webview connects to the
      new server without requiring localStorage to be cleared.

    2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

    File:packages/app/src/context/server-sdk.tsx (lines 268–338)

    Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
    It does not distinguish "connection refused" (server gone) from "server error"
    (server exists). If the URL is stale, this loops forever without recovery.

    Fix: Add an escalation path: after N consecutive connection-refused failures
    (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
    instead of the persisted server URL. If location.origin succeeds, update the
    persisted server store to reflect the correct URL.

    Acceptance criteria:

    • After 10 consecutive connection-refused errors (not HTTP errors — specifically
      TypeError: Failed to fetch or equivalent network failure), the loop switches to
      location.origin as the target URL.
    • If location.origin connects successfully, the persisted server store entry is
      updated in-place so subsequent reconnections use the correct URL directly.
    • If both the persisted URL and location.origin fail, the loop continues retrying
      location.origin at 250 ms intervals (as today, but to the correct URL).
    • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
      the ConnectionBanner to display).

    3. [CRITICAL] Server boot-ID stamping

    Files:

    • Server: packages/opencode/src/server/server.ts or the SSE event handler
    • Client: packages/app/src/context/server-sdk.tsx and server.tsx

    Problem: The client cannot distinguish "same server reconnected" from "different
    server on same port" from "stale URL, server gone." Without a server identity
    token, all reconnection heuristics are fragile.

    Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
    includes it in:

    1. The server.connected SSE event payload (so the client receives it on every
      reconnection).
    2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
      client can detect mid-session server restarts even outside the SSE stream.

    The client persists the boot-ID alongside the server URL in localStorage. On
    reconnection:

    • If boot-ID matches → normal reconnect; session state is valid.
    • If boot-ID differs → server restarted; invalidate session tab list, refetch all
      state, and optionally re-validate credentials.
    • If boot-ID is absent (older server version) → treat as "unknown," fall back to
      current behavior.

    Acceptance criteria:

    • Server emits a bootId field in the server.connected event.
    • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
    • Client stores lastBootId per server entry in the persisted store.
    • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
      prune tabs with 404 sessions, reset workspace caches.
    • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
      auth-required prompt (rather than silently failing).

    4. [CRITICAL] Configurable fixed port per container

    Files:

    • Config schema: packages/core/src/v1/config/server.ts
    • Devcontainer: .devcontainer/devcontainer.json
    • Documentation

    Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
    Because the webview's localStorage persists across container rebuilds (it lives on
    the host), a new port on restart means a stale persisted URL. Users in devcontainer
    workflows hit this on every rebuild.

    Fix: Allow users to pin a stable port per container via either:

    1. opencode.json{ "server": { "port": 4096 } } (already supported by the
      schema but not widely documented or surfaced).
    2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
      the Amicode extension host when spawning the server).

    Document the recommendation: in devcontainer-based workflows, set a fixed port so
    that localStorage's persisted URL remains valid across container rebuilds.

    Acceptance criteria:

    • Documentation (README or extension settings description) explicitly recommends
      setting server.port in opencode.json for devcontainer workflows.
    • The Amicode extension host reads OPENCODE_PORT from the container environment
      (if available) and uses it when launching the server.
    • When server.port is set in opencode.json, the server binds to exactly that
      port (no fallback) and fails loudly if the port is in use (rather than silently
      falling back to a random port).
    • The .devcontainer/devcontainer.json in this repo is updated to include a
      commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

    Tier 2 — High Importance

    These fixes prevent related failure modes and harden the connection lifecycle.
    Implement after Tier 1.


    5. Multi-instance localStorage isolation

    File:packages/app/src/utils/persist.ts

    Problem: All Amicode webview instances on the same VS Code installation share a
    single localStorage scope (keyed by extension ID origin). Two windows with
    different servers overwrite each other's server entries.

    Fix: Key all connection-related localStorage entries by a workspace
    identifier
    (e.g., a hash of the container's filesystem root or the server URL at
    first successful connection). Non-connection state (theme, zoom) remains global.

    Acceptance criteria:

    • Two VS Code windows with Amicode, connected to different servers, do not
      interfere with each other's connection state.
    • Opening a new window for a previously-unknown workspace starts fresh (no stale
      entries from another workspace).
    • Global preferences (theme, solver mode) remain shared across all instances.
    • Migration: on first load with the new keying scheme, existing global state is
      migrated into the appropriate workspace bucket.

    6. Session tab validation on load

    File:packages/app/src/context/tabs.tsx

    Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
    Dead session references from previous server instances accumulate, causing burst
    fetches to stale/non-existent endpoints on reload.

    Fix: On the server.connected event (which fires on every SSE reconnection),
    validate all open session tabs by checking their existence against the server. Tabs
    whose session IDs return 404 are moved to the closed list (not deleted — user can
    re-open if the session reappears after a migration/restore).

    Acceptance criteria:

    • Within 5 seconds of server.connected, all open tabs are validated.
    • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
    • A toast notification summarizes: "N sessions from a previous server instance were
      closed."
    • The validation is non-blocking (does not prevent the app from rendering).
    • If the server is unreachable during validation (e.g., the server.connected
      event was a false positive), validation is skipped gracefully.

    7. Credential invalidation on boot-ID change

    File:packages/app/src/context/server.tsx (inside resolveServerList)

    Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
    persisted credentials in the localStorage server.list entry are stale. Every API
    call returns 401, but the client does not surface this or attempt to refresh.

    Fix: When the boot-ID changes (see item #3), clear persisted credentials for
    that server entry and re-read them from the iframe URL query param (auth_token).
    If no auth_token is present in the URL and the server requires auth, surface an
    auth prompt.

    Acceptance criteria:

    • On boot-ID mismatch, the persisted username/password for the affected server
      entry are cleared.
    • The app re-reads auth_token from location.search (the iframe URL injected by
      the extension host).
    • If auth is required and no valid credentials are available, a modal prompts the
      user (rather than silently failing with 401s).

    8. Extension host → webview URL push on server restart

    File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

    Problem: When the extension host restarts the server (via
    amicode.restartServer command), it re-launches the opencode process on a
    potentially different port. The webview SSE stream is connected to the old port and
    must wait for connection-refused → escalation (item #2) to recover.

    Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
    iframe immediately after the new server is confirmed listening. The webview handles
    this message by updating its active server URL and immediately reconnecting SSE to
    the new URL.

    Acceptance criteria:

    • The webview registers a listener for kind: "server-url-changed" messages.
    • On receiving this message, the webview updates its persisted server store and
      reconnects SSE within 1 second (no 250 ms retry loop needed).
    • If the message arrives while the webview is already connected (race condition),
      it is a no-op.
    • The webview emits a route-info message back to confirm it received the update.

    Tier 3 — Improvements

    These are well-advised hardening measures. They do not directly prevent the "no GUI
    response" bug but reduce adjacent failure surfaces.


    9. Quota-aware eviction priority

    File:packages/app/src/utils/persist.ts (lines 112–165)

    Problem: The localStorage eviction logic removes the largest opencode.* keys
    first. The server key (connection state) and tabs key (session history) grow
    over time and become prime eviction targets.

    Fix: Maintain a "protected keys" list that the eviction logic never removes.
    At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

    Acceptance criteria:

    • opencode.global.dat:server is never evicted by the quota handler.
    • Eviction preferentially targets workspace and session-scoped keys.
    • If eviction cannot free enough space without touching protected keys, the write
      fails gracefully (the app continues to function with the existing state).

    10. Connection banner always visible on persistent disconnection

    File:packages/app/src/components/connection-banner.tsx

    Problem: The ConnectionBanner component shows when streamStatus is
    "disconnected", but its visibility depends on layout configuration. If the banner
    is scrolled off or hidden by a panel, the user has no indication that the
    connection is broken.

    Fix: After 5 seconds of continuous "disconnected" state, surface a
    VS Code-style notification (via postMessage to the extension host, which calls
    vscode.window.showWarningMessage) in addition to the in-webview banner.

    Acceptance criteria:

    • If the SSE stream is disconnected for > 5 continuous seconds, a warning
      notification appears in VS Code's notification area.
    • The notification includes an action button: "Reconnect" (which triggers URL
      rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
    • The notification is not repeated more than once per 60 seconds.

    11. Terminal extension: readiness probe fix (/app/health)

    File:sdks/vscode/src/extension.ts (line 78)

    Problem: The extension probes GET /app (a catch-all UI route) with no
    status-code check. Should probe GET /health and check response.ok.

    Fix: Change the URL to /health and gate connected = true on response.ok.

    Acceptance criteria:

    • The probe hits GET /health.
    • connected is only set to true if the response status is 2xx.
    • A 404 or 500 from a partially-initialized server does not set connected = true.

    12. Terminal extension: dead terminal detection

    File:sdks/vscode/src/extension.ts (lines 15–19)

    Problem:opencode.openTerminal reuses a terminal by name without checking if
    the process has exited.

    Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
    exitStatus === undefined.

    Acceptance criteria:

    • A terminal whose process has exited is not reused.
    • The user gets a fresh terminal with a new server instance.

    13. RPC error propagation

    File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

    Problem: If a worker-side RPC method throws, the pending promise in
    client.call() is never settled. The caller hangs forever.

    Fix:

    • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
      with the error details and request ID.
    • Client side: store { resolve, reject } pairs in pending; handle rpc.error
      messages by calling reject(new Error(...)).

    Acceptance criteria:

    • If a worker method throws, the client-side promise rejects with an Error
      containing the original error message.
    • The pending Map entry is cleaned up (no memory leak).
    • Existing callers of client.call() that do not handle rejection see an unhandled
      rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

    14. Worker error logging

    File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

    Problem:unhandledRejection and uncaughtException handlers discard all
    errors silently, making worker failures invisible.

    Fix: Log errors to stderr with a [worker] prefix.

    Acceptance criteria:

    • Unhandled rejections log the error object to stderr.
    • Uncaught exceptions log the error message and stack to stderr.
    • The worker process does NOT exit on these errors (existing keep-alive behavior
      is preserved).

    15. Terminal extension: retry window + user-visible warning

    File:sdks/vscode/src/extension.ts (lines 73–90)

    Problem: The retry loop tries 10 times (2 s total). If the server doesn't
    respond (port collision, slow startup), the file reference is silently dropped.

    Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
    vscode.window.showWarningMessage(...) so the user knows something went wrong.

    Acceptance criteria:

    • The retry window is 4 seconds (20 * 200 ms).
    • If connected is still false after the loop, a warning message is shown.
    • The warning message suggests "try again" or "the port may be in use."

    Metadata

    Metadata

    Assignees

    Labels

    duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      SSE Connection Reliability Improvements #413

      Description

      @gennadiryan

      Actionable fixes to prevent the "no GUI response" failure class and harden the
      webview-server connection lifecycle. Each item includes acceptance criteria.

      See also:

      • ./issue-no-gui-response.md — the specific bug these fixes address
      • ../notes/local-storage-customization.md — parameter inventory and design analysis
      • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

      Tier 1 — Critical Path

      These fixes directly prevent or mitigate the "no GUI response" bug. They should be
      prioritized for immediate implementation.


      1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

      File:packages/app/src/entry.tsx (lines 157–161)

      Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
      localStorage before consulting location.origin. In the Amicode webview, the
      iframe is served by the running server — location.origin is always the correct
      URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
      port), it permanently overrides the correct origin.

      Fix: When running inside the Amicode webview (detectable via inAmicode() from
      utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
      entirely. Use location.origin unconditionally.

      Acceptance criteria:

      • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
        without consulting localStorage.
      • The defaultServerUrl localStorage key is still honored in the standalone web
        app context (when inAmicode() is false).
      • After a server restart (new port), reopening the Amicode webview connects to the
        new server without requiring localStorage to be cleared.

      2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

      File:packages/app/src/context/server-sdk.tsx (lines 268–338)

      Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
      It does not distinguish "connection refused" (server gone) from "server error"
      (server exists). If the URL is stale, this loops forever without recovery.

      Fix: Add an escalation path: after N consecutive connection-refused failures
      (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
      instead of the persisted server URL. If location.origin succeeds, update the
      persisted server store to reflect the correct URL.

      Acceptance criteria:

      • After 10 consecutive connection-refused errors (not HTTP errors — specifically
        TypeError: Failed to fetch or equivalent network failure), the loop switches to
        location.origin as the target URL.
      • If location.origin connects successfully, the persisted server store entry is
        updated in-place so subsequent reconnections use the correct URL directly.
      • If both the persisted URL and location.origin fail, the loop continues retrying
        location.origin at 250 ms intervals (as today, but to the correct URL).
      • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
        the ConnectionBanner to display).

      3. [CRITICAL] Server boot-ID stamping

      Files:

      • Server: packages/opencode/src/server/server.ts or the SSE event handler
      • Client: packages/app/src/context/server-sdk.tsx and server.tsx

      Problem: The client cannot distinguish "same server reconnected" from "different
      server on same port" from "stale URL, server gone." Without a server identity
      token, all reconnection heuristics are fragile.

      Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
      includes it in:

      1. The server.connected SSE event payload (so the client receives it on every
        reconnection).
      2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
        client can detect mid-session server restarts even outside the SSE stream.

      The client persists the boot-ID alongside the server URL in localStorage. On
      reconnection:

      • If boot-ID matches → normal reconnect; session state is valid.
      • If boot-ID differs → server restarted; invalidate session tab list, refetch all
        state, and optionally re-validate credentials.
      • If boot-ID is absent (older server version) → treat as "unknown," fall back to
        current behavior.

      Acceptance criteria:

      • Server emits a bootId field in the server.connected event.
      • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
      • Client stores lastBootId per server entry in the persisted store.
      • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
        prune tabs with 404 sessions, reset workspace caches.
      • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
        auth-required prompt (rather than silently failing).

      4. [CRITICAL] Configurable fixed port per container

      Files:

      • Config schema: packages/core/src/v1/config/server.ts
      • Devcontainer: .devcontainer/devcontainer.json
      • Documentation

      Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
      Because the webview's localStorage persists across container rebuilds (it lives on
      the host), a new port on restart means a stale persisted URL. Users in devcontainer
      workflows hit this on every rebuild.

      Fix: Allow users to pin a stable port per container via either:

      1. opencode.json{ "server": { "port": 4096 } } (already supported by the
        schema but not widely documented or surfaced).
      2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
        the Amicode extension host when spawning the server).

      Document the recommendation: in devcontainer-based workflows, set a fixed port so
      that localStorage's persisted URL remains valid across container rebuilds.

      Acceptance criteria:

      • Documentation (README or extension settings description) explicitly recommends
        setting server.port in opencode.json for devcontainer workflows.
      • The Amicode extension host reads OPENCODE_PORT from the container environment
        (if available) and uses it when launching the server.
      • When server.port is set in opencode.json, the server binds to exactly that
        port (no fallback) and fails loudly if the port is in use (rather than silently
        falling back to a random port).
      • The .devcontainer/devcontainer.json in this repo is updated to include a
        commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

      Tier 2 — High Importance

      These fixes prevent related failure modes and harden the connection lifecycle.
      Implement after Tier 1.


      5. Multi-instance localStorage isolation

      File:packages/app/src/utils/persist.ts

      Problem: All Amicode webview instances on the same VS Code installation share a
      single localStorage scope (keyed by extension ID origin). Two windows with
      different servers overwrite each other's server entries.

      Fix: Key all connection-related localStorage entries by a workspace
      identifier
      (e.g., a hash of the container's filesystem root or the server URL at
      first successful connection). Non-connection state (theme, zoom) remains global.

      Acceptance criteria:

      • Two VS Code windows with Amicode, connected to different servers, do not
        interfere with each other's connection state.
      • Opening a new window for a previously-unknown workspace starts fresh (no stale
        entries from another workspace).
      • Global preferences (theme, solver mode) remain shared across all instances.
      • Migration: on first load with the new keying scheme, existing global state is
        migrated into the appropriate workspace bucket.

      6. Session tab validation on load

      File:packages/app/src/context/tabs.tsx

      Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
      Dead session references from previous server instances accumulate, causing burst
      fetches to stale/non-existent endpoints on reload.

      Fix: On the server.connected event (which fires on every SSE reconnection),
      validate all open session tabs by checking their existence against the server. Tabs
      whose session IDs return 404 are moved to the closed list (not deleted — user can
      re-open if the session reappears after a migration/restore).

      Acceptance criteria:

      • Within 5 seconds of server.connected, all open tabs are validated.
      • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
      • A toast notification summarizes: "N sessions from a previous server instance were
        closed."
      • The validation is non-blocking (does not prevent the app from rendering).
      • If the server is unreachable during validation (e.g., the server.connected
        event was a false positive), validation is skipped gracefully.

      7. Credential invalidation on boot-ID change

      File:packages/app/src/context/server.tsx (inside resolveServerList)

      Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
      persisted credentials in the localStorage server.list entry are stale. Every API
      call returns 401, but the client does not surface this or attempt to refresh.

      Fix: When the boot-ID changes (see item #3), clear persisted credentials for
      that server entry and re-read them from the iframe URL query param (auth_token).
      If no auth_token is present in the URL and the server requires auth, surface an
      auth prompt.

      Acceptance criteria:

      • On boot-ID mismatch, the persisted username/password for the affected server
        entry are cleared.
      • The app re-reads auth_token from location.search (the iframe URL injected by
        the extension host).
      • If auth is required and no valid credentials are available, a modal prompts the
        user (rather than silently failing with 401s).

      8. Extension host → webview URL push on server restart

      File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

      Problem: When the extension host restarts the server (via
      amicode.restartServer command), it re-launches the opencode process on a
      potentially different port. The webview SSE stream is connected to the old port and
      must wait for connection-refused → escalation (item #2) to recover.

      Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
      iframe immediately after the new server is confirmed listening. The webview handles
      this message by updating its active server URL and immediately reconnecting SSE to
      the new URL.

      Acceptance criteria:

      • The webview registers a listener for kind: "server-url-changed" messages.
      • On receiving this message, the webview updates its persisted server store and
        reconnects SSE within 1 second (no 250 ms retry loop needed).
      • If the message arrives while the webview is already connected (race condition),
        it is a no-op.
      • The webview emits a route-info message back to confirm it received the update.

      Tier 3 — Improvements

      These are well-advised hardening measures. They do not directly prevent the "no GUI
      response" bug but reduce adjacent failure surfaces.


      9. Quota-aware eviction priority

      File:packages/app/src/utils/persist.ts (lines 112–165)

      Problem: The localStorage eviction logic removes the largest opencode.* keys
      first. The server key (connection state) and tabs key (session history) grow
      over time and become prime eviction targets.

      Fix: Maintain a "protected keys" list that the eviction logic never removes.
      At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

      Acceptance criteria:

      • opencode.global.dat:server is never evicted by the quota handler.
      • Eviction preferentially targets workspace and session-scoped keys.
      • If eviction cannot free enough space without touching protected keys, the write
        fails gracefully (the app continues to function with the existing state).

      10. Connection banner always visible on persistent disconnection

      File:packages/app/src/components/connection-banner.tsx

      Problem: The ConnectionBanner component shows when streamStatus is
      "disconnected", but its visibility depends on layout configuration. If the banner
      is scrolled off or hidden by a panel, the user has no indication that the
      connection is broken.

      Fix: After 5 seconds of continuous "disconnected" state, surface a
      VS Code-style notification (via postMessage to the extension host, which calls
      vscode.window.showWarningMessage) in addition to the in-webview banner.

      Acceptance criteria:

      • If the SSE stream is disconnected for > 5 continuous seconds, a warning
        notification appears in VS Code's notification area.
      • The notification includes an action button: "Reconnect" (which triggers URL
        rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
      • The notification is not repeated more than once per 60 seconds.

      11. Terminal extension: readiness probe fix (/app/health)

      File:sdks/vscode/src/extension.ts (line 78)

      Problem: The extension probes GET /app (a catch-all UI route) with no
      status-code check. Should probe GET /health and check response.ok.

      Fix: Change the URL to /health and gate connected = true on response.ok.

      Acceptance criteria:

      • The probe hits GET /health.
      • connected is only set to true if the response status is 2xx.
      • A 404 or 500 from a partially-initialized server does not set connected = true.

      12. Terminal extension: dead terminal detection

      File:sdks/vscode/src/extension.ts (lines 15–19)

      Problem:opencode.openTerminal reuses a terminal by name without checking if
      the process has exited.

      Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
      exitStatus === undefined.

      Acceptance criteria:

      • A terminal whose process has exited is not reused.
      • The user gets a fresh terminal with a new server instance.

      13. RPC error propagation

      File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

      Problem: If a worker-side RPC method throws, the pending promise in
      client.call() is never settled. The caller hangs forever.

      Fix:

      • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
        with the error details and request ID.
      • Client side: store { resolve, reject } pairs in pending; handle rpc.error
        messages by calling reject(new Error(...)).

      Acceptance criteria:

      • If a worker method throws, the client-side promise rejects with an Error
        containing the original error message.
      • The pending Map entry is cleaned up (no memory leak).
      • Existing callers of client.call() that do not handle rejection see an unhandled
        rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

      14. Worker error logging

      File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

      Problem:unhandledRejection and uncaughtException handlers discard all
      errors silently, making worker failures invisible.

      Fix: Log errors to stderr with a [worker] prefix.

      Acceptance criteria:

      • Unhandled rejections log the error object to stderr.
      • Uncaught exceptions log the error message and stack to stderr.
      • The worker process does NOT exit on these errors (existing keep-alive behavior
        is preserved).

      15. Terminal extension: retry window + user-visible warning

      File:sdks/vscode/src/extension.ts (lines 73–90)

      Problem: The retry loop tries 10 times (2 s total). If the server doesn't
      respond (port collision, slow startup), the file reference is silently dropped.

      Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
      vscode.window.showWarningMessage(...) so the user knows something went wrong.

      Acceptance criteria:

      • The retry window is 4 seconds (20 * 200 ms).
      • If connected is still false after the loop, a warning message is shown.
      • The warning message suggests "try again" or "the port may be in use."

      Metadata

      Metadata

      Assignees

      Labels

      duplicateThis issue or pull request already exists

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        SSE Connection Reliability Improvements #413

        Description

        @gennadiryan

        Actionable fixes to prevent the "no GUI response" failure class and harden the
        webview-server connection lifecycle. Each item includes acceptance criteria.

        See also:

        • ./issue-no-gui-response.md — the specific bug these fixes address
        • ../notes/local-storage-customization.md — parameter inventory and design analysis
        • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

        Tier 1 — Critical Path

        These fixes directly prevent or mitigate the "no GUI response" bug. They should be
        prioritized for immediate implementation.


        1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

        File:packages/app/src/entry.tsx (lines 157–161)

        Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
        localStorage before consulting location.origin. In the Amicode webview, the
        iframe is served by the running server — location.origin is always the correct
        URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
        port), it permanently overrides the correct origin.

        Fix: When running inside the Amicode webview (detectable via inAmicode() from
        utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
        entirely. Use location.origin unconditionally.

        Acceptance criteria:

        • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
          without consulting localStorage.
        • The defaultServerUrl localStorage key is still honored in the standalone web
          app context (when inAmicode() is false).
        • After a server restart (new port), reopening the Amicode webview connects to the
          new server without requiring localStorage to be cleared.

        2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

        File:packages/app/src/context/server-sdk.tsx (lines 268–338)

        Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
        It does not distinguish "connection refused" (server gone) from "server error"
        (server exists). If the URL is stale, this loops forever without recovery.

        Fix: Add an escalation path: after N consecutive connection-refused failures
        (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
        instead of the persisted server URL. If location.origin succeeds, update the
        persisted server store to reflect the correct URL.

        Acceptance criteria:

        • After 10 consecutive connection-refused errors (not HTTP errors — specifically
          TypeError: Failed to fetch or equivalent network failure), the loop switches to
          location.origin as the target URL.
        • If location.origin connects successfully, the persisted server store entry is
          updated in-place so subsequent reconnections use the correct URL directly.
        • If both the persisted URL and location.origin fail, the loop continues retrying
          location.origin at 250 ms intervals (as today, but to the correct URL).
        • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
          the ConnectionBanner to display).

        3. [CRITICAL] Server boot-ID stamping

        Files:

        • Server: packages/opencode/src/server/server.ts or the SSE event handler
        • Client: packages/app/src/context/server-sdk.tsx and server.tsx

        Problem: The client cannot distinguish "same server reconnected" from "different
        server on same port" from "stale URL, server gone." Without a server identity
        token, all reconnection heuristics are fragile.

        Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
        includes it in:

        1. The server.connected SSE event payload (so the client receives it on every
          reconnection).
        2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
          client can detect mid-session server restarts even outside the SSE stream.

        The client persists the boot-ID alongside the server URL in localStorage. On
        reconnection:

        • If boot-ID matches → normal reconnect; session state is valid.
        • If boot-ID differs → server restarted; invalidate session tab list, refetch all
          state, and optionally re-validate credentials.
        • If boot-ID is absent (older server version) → treat as "unknown," fall back to
          current behavior.

        Acceptance criteria:

        • Server emits a bootId field in the server.connected event.
        • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
        • Client stores lastBootId per server entry in the persisted store.
        • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
          prune tabs with 404 sessions, reset workspace caches.
        • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
          auth-required prompt (rather than silently failing).

        4. [CRITICAL] Configurable fixed port per container

        Files:

        • Config schema: packages/core/src/v1/config/server.ts
        • Devcontainer: .devcontainer/devcontainer.json
        • Documentation

        Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
        Because the webview's localStorage persists across container rebuilds (it lives on
        the host), a new port on restart means a stale persisted URL. Users in devcontainer
        workflows hit this on every rebuild.

        Fix: Allow users to pin a stable port per container via either:

        1. opencode.json{ "server": { "port": 4096 } } (already supported by the
          schema but not widely documented or surfaced).
        2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
          the Amicode extension host when spawning the server).

        Document the recommendation: in devcontainer-based workflows, set a fixed port so
        that localStorage's persisted URL remains valid across container rebuilds.

        Acceptance criteria:

        • Documentation (README or extension settings description) explicitly recommends
          setting server.port in opencode.json for devcontainer workflows.
        • The Amicode extension host reads OPENCODE_PORT from the container environment
          (if available) and uses it when launching the server.
        • When server.port is set in opencode.json, the server binds to exactly that
          port (no fallback) and fails loudly if the port is in use (rather than silently
          falling back to a random port).
        • The .devcontainer/devcontainer.json in this repo is updated to include a
          commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

        Tier 2 — High Importance

        These fixes prevent related failure modes and harden the connection lifecycle.
        Implement after Tier 1.


        5. Multi-instance localStorage isolation

        File:packages/app/src/utils/persist.ts

        Problem: All Amicode webview instances on the same VS Code installation share a
        single localStorage scope (keyed by extension ID origin). Two windows with
        different servers overwrite each other's server entries.

        Fix: Key all connection-related localStorage entries by a workspace
        identifier
        (e.g., a hash of the container's filesystem root or the server URL at
        first successful connection). Non-connection state (theme, zoom) remains global.

        Acceptance criteria:

        • Two VS Code windows with Amicode, connected to different servers, do not
          interfere with each other's connection state.
        • Opening a new window for a previously-unknown workspace starts fresh (no stale
          entries from another workspace).
        • Global preferences (theme, solver mode) remain shared across all instances.
        • Migration: on first load with the new keying scheme, existing global state is
          migrated into the appropriate workspace bucket.

        6. Session tab validation on load

        File:packages/app/src/context/tabs.tsx

        Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
        Dead session references from previous server instances accumulate, causing burst
        fetches to stale/non-existent endpoints on reload.

        Fix: On the server.connected event (which fires on every SSE reconnection),
        validate all open session tabs by checking their existence against the server. Tabs
        whose session IDs return 404 are moved to the closed list (not deleted — user can
        re-open if the session reappears after a migration/restore).

        Acceptance criteria:

        • Within 5 seconds of server.connected, all open tabs are validated.
        • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
        • A toast notification summarizes: "N sessions from a previous server instance were
          closed."
        • The validation is non-blocking (does not prevent the app from rendering).
        • If the server is unreachable during validation (e.g., the server.connected
          event was a false positive), validation is skipped gracefully.

        7. Credential invalidation on boot-ID change

        File:packages/app/src/context/server.tsx (inside resolveServerList)

        Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
        persisted credentials in the localStorage server.list entry are stale. Every API
        call returns 401, but the client does not surface this or attempt to refresh.

        Fix: When the boot-ID changes (see item #3), clear persisted credentials for
        that server entry and re-read them from the iframe URL query param (auth_token).
        If no auth_token is present in the URL and the server requires auth, surface an
        auth prompt.

        Acceptance criteria:

        • On boot-ID mismatch, the persisted username/password for the affected server
          entry are cleared.
        • The app re-reads auth_token from location.search (the iframe URL injected by
          the extension host).
        • If auth is required and no valid credentials are available, a modal prompts the
          user (rather than silently failing with 401s).

        8. Extension host → webview URL push on server restart

        File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

        Problem: When the extension host restarts the server (via
        amicode.restartServer command), it re-launches the opencode process on a
        potentially different port. The webview SSE stream is connected to the old port and
        must wait for connection-refused → escalation (item #2) to recover.

        Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
        iframe immediately after the new server is confirmed listening. The webview handles
        this message by updating its active server URL and immediately reconnecting SSE to
        the new URL.

        Acceptance criteria:

        • The webview registers a listener for kind: "server-url-changed" messages.
        • On receiving this message, the webview updates its persisted server store and
          reconnects SSE within 1 second (no 250 ms retry loop needed).
        • If the message arrives while the webview is already connected (race condition),
          it is a no-op.
        • The webview emits a route-info message back to confirm it received the update.

        Tier 3 — Improvements

        These are well-advised hardening measures. They do not directly prevent the "no GUI
        response" bug but reduce adjacent failure surfaces.


        9. Quota-aware eviction priority

        File:packages/app/src/utils/persist.ts (lines 112–165)

        Problem: The localStorage eviction logic removes the largest opencode.* keys
        first. The server key (connection state) and tabs key (session history) grow
        over time and become prime eviction targets.

        Fix: Maintain a "protected keys" list that the eviction logic never removes.
        At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

        Acceptance criteria:

        • opencode.global.dat:server is never evicted by the quota handler.
        • Eviction preferentially targets workspace and session-scoped keys.
        • If eviction cannot free enough space without touching protected keys, the write
          fails gracefully (the app continues to function with the existing state).

        10. Connection banner always visible on persistent disconnection

        File:packages/app/src/components/connection-banner.tsx

        Problem: The ConnectionBanner component shows when streamStatus is
        "disconnected", but its visibility depends on layout configuration. If the banner
        is scrolled off or hidden by a panel, the user has no indication that the
        connection is broken.

        Fix: After 5 seconds of continuous "disconnected" state, surface a
        VS Code-style notification (via postMessage to the extension host, which calls
        vscode.window.showWarningMessage) in addition to the in-webview banner.

        Acceptance criteria:

        • If the SSE stream is disconnected for > 5 continuous seconds, a warning
          notification appears in VS Code's notification area.
        • The notification includes an action button: "Reconnect" (which triggers URL
          rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
        • The notification is not repeated more than once per 60 seconds.

        11. Terminal extension: readiness probe fix (/app/health)

        File:sdks/vscode/src/extension.ts (line 78)

        Problem: The extension probes GET /app (a catch-all UI route) with no
        status-code check. Should probe GET /health and check response.ok.

        Fix: Change the URL to /health and gate connected = true on response.ok.

        Acceptance criteria:

        • The probe hits GET /health.
        • connected is only set to true if the response status is 2xx.
        • A 404 or 500 from a partially-initialized server does not set connected = true.

        12. Terminal extension: dead terminal detection

        File:sdks/vscode/src/extension.ts (lines 15–19)

        Problem:opencode.openTerminal reuses a terminal by name without checking if
        the process has exited.

        Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
        exitStatus === undefined.

        Acceptance criteria:

        • A terminal whose process has exited is not reused.
        • The user gets a fresh terminal with a new server instance.

        13. RPC error propagation

        File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

        Problem: If a worker-side RPC method throws, the pending promise in
        client.call() is never settled. The caller hangs forever.

        Fix:

        • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
          with the error details and request ID.
        • Client side: store { resolve, reject } pairs in pending; handle rpc.error
          messages by calling reject(new Error(...)).

        Acceptance criteria:

        • If a worker method throws, the client-side promise rejects with an Error
          containing the original error message.
        • The pending Map entry is cleaned up (no memory leak).
        • Existing callers of client.call() that do not handle rejection see an unhandled
          rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

        14. Worker error logging

        File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

        Problem:unhandledRejection and uncaughtException handlers discard all
        errors silently, making worker failures invisible.

        Fix: Log errors to stderr with a [worker] prefix.

        Acceptance criteria:

        • Unhandled rejections log the error object to stderr.
        • Uncaught exceptions log the error message and stack to stderr.
        • The worker process does NOT exit on these errors (existing keep-alive behavior
          is preserved).

        15. Terminal extension: retry window + user-visible warning

        File:sdks/vscode/src/extension.ts (lines 73–90)

        Problem: The retry loop tries 10 times (2 s total). If the server doesn't
        respond (port collision, slow startup), the file reference is silently dropped.

        Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
        vscode.window.showWarningMessage(...) so the user knows something went wrong.

        Acceptance criteria:

        • The retry window is 4 seconds (20 * 200 ms).
        • If connected is still false after the loop, a warning message is shown.
        • The warning message suggests "try again" or "the port may be in use."

        Metadata

        Metadata

        Assignees

        Labels

        duplicateThis issue or pull request already exists

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          SSE Connection Reliability Improvements #413

          Description

          @gennadiryan

          Actionable fixes to prevent the "no GUI response" failure class and harden the
          webview-server connection lifecycle. Each item includes acceptance criteria.

          See also:

          • ./issue-no-gui-response.md — the specific bug these fixes address
          • ../notes/local-storage-customization.md — parameter inventory and design analysis
          • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

          Tier 1 — Critical Path

          These fixes directly prevent or mitigate the "no GUI response" bug. They should be
          prioritized for immediate implementation.


          1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

          File:packages/app/src/entry.tsx (lines 157–161)

          Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
          localStorage before consulting location.origin. In the Amicode webview, the
          iframe is served by the running server — location.origin is always the correct
          URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
          port), it permanently overrides the correct origin.

          Fix: When running inside the Amicode webview (detectable via inAmicode() from
          utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
          entirely. Use location.origin unconditionally.

          Acceptance criteria:

          • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
            without consulting localStorage.
          • The defaultServerUrl localStorage key is still honored in the standalone web
            app context (when inAmicode() is false).
          • After a server restart (new port), reopening the Amicode webview connects to the
            new server without requiring localStorage to be cleared.

          2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

          File:packages/app/src/context/server-sdk.tsx (lines 268–338)

          Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
          It does not distinguish "connection refused" (server gone) from "server error"
          (server exists). If the URL is stale, this loops forever without recovery.

          Fix: Add an escalation path: after N consecutive connection-refused failures
          (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
          instead of the persisted server URL. If location.origin succeeds, update the
          persisted server store to reflect the correct URL.

          Acceptance criteria:

          • After 10 consecutive connection-refused errors (not HTTP errors — specifically
            TypeError: Failed to fetch or equivalent network failure), the loop switches to
            location.origin as the target URL.
          • If location.origin connects successfully, the persisted server store entry is
            updated in-place so subsequent reconnections use the correct URL directly.
          • If both the persisted URL and location.origin fail, the loop continues retrying
            location.origin at 250 ms intervals (as today, but to the correct URL).
          • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
            the ConnectionBanner to display).

          3. [CRITICAL] Server boot-ID stamping

          Files:

          • Server: packages/opencode/src/server/server.ts or the SSE event handler
          • Client: packages/app/src/context/server-sdk.tsx and server.tsx

          Problem: The client cannot distinguish "same server reconnected" from "different
          server on same port" from "stale URL, server gone." Without a server identity
          token, all reconnection heuristics are fragile.

          Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
          includes it in:

          1. The server.connected SSE event payload (so the client receives it on every
            reconnection).
          2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
            client can detect mid-session server restarts even outside the SSE stream.

          The client persists the boot-ID alongside the server URL in localStorage. On
          reconnection:

          • If boot-ID matches → normal reconnect; session state is valid.
          • If boot-ID differs → server restarted; invalidate session tab list, refetch all
            state, and optionally re-validate credentials.
          • If boot-ID is absent (older server version) → treat as "unknown," fall back to
            current behavior.

          Acceptance criteria:

          • Server emits a bootId field in the server.connected event.
          • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
          • Client stores lastBootId per server entry in the persisted store.
          • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
            prune tabs with 404 sessions, reset workspace caches.
          • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
            auth-required prompt (rather than silently failing).

          4. [CRITICAL] Configurable fixed port per container

          Files:

          • Config schema: packages/core/src/v1/config/server.ts
          • Devcontainer: .devcontainer/devcontainer.json
          • Documentation

          Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
          Because the webview's localStorage persists across container rebuilds (it lives on
          the host), a new port on restart means a stale persisted URL. Users in devcontainer
          workflows hit this on every rebuild.

          Fix: Allow users to pin a stable port per container via either:

          1. opencode.json{ "server": { "port": 4096 } } (already supported by the
            schema but not widely documented or surfaced).
          2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
            the Amicode extension host when spawning the server).

          Document the recommendation: in devcontainer-based workflows, set a fixed port so
          that localStorage's persisted URL remains valid across container rebuilds.

          Acceptance criteria:

          • Documentation (README or extension settings description) explicitly recommends
            setting server.port in opencode.json for devcontainer workflows.
          • The Amicode extension host reads OPENCODE_PORT from the container environment
            (if available) and uses it when launching the server.
          • When server.port is set in opencode.json, the server binds to exactly that
            port (no fallback) and fails loudly if the port is in use (rather than silently
            falling back to a random port).
          • The .devcontainer/devcontainer.json in this repo is updated to include a
            commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

          Tier 2 — High Importance

          These fixes prevent related failure modes and harden the connection lifecycle.
          Implement after Tier 1.


          5. Multi-instance localStorage isolation

          File:packages/app/src/utils/persist.ts

          Problem: All Amicode webview instances on the same VS Code installation share a
          single localStorage scope (keyed by extension ID origin). Two windows with
          different servers overwrite each other's server entries.

          Fix: Key all connection-related localStorage entries by a workspace
          identifier
          (e.g., a hash of the container's filesystem root or the server URL at
          first successful connection). Non-connection state (theme, zoom) remains global.

          Acceptance criteria:

          • Two VS Code windows with Amicode, connected to different servers, do not
            interfere with each other's connection state.
          • Opening a new window for a previously-unknown workspace starts fresh (no stale
            entries from another workspace).
          • Global preferences (theme, solver mode) remain shared across all instances.
          • Migration: on first load with the new keying scheme, existing global state is
            migrated into the appropriate workspace bucket.

          6. Session tab validation on load

          File:packages/app/src/context/tabs.tsx

          Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
          Dead session references from previous server instances accumulate, causing burst
          fetches to stale/non-existent endpoints on reload.

          Fix: On the server.connected event (which fires on every SSE reconnection),
          validate all open session tabs by checking their existence against the server. Tabs
          whose session IDs return 404 are moved to the closed list (not deleted — user can
          re-open if the session reappears after a migration/restore).

          Acceptance criteria:

          • Within 5 seconds of server.connected, all open tabs are validated.
          • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
          • A toast notification summarizes: "N sessions from a previous server instance were
            closed."
          • The validation is non-blocking (does not prevent the app from rendering).
          • If the server is unreachable during validation (e.g., the server.connected
            event was a false positive), validation is skipped gracefully.

          7. Credential invalidation on boot-ID change

          File:packages/app/src/context/server.tsx (inside resolveServerList)

          Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
          persisted credentials in the localStorage server.list entry are stale. Every API
          call returns 401, but the client does not surface this or attempt to refresh.

          Fix: When the boot-ID changes (see item #3), clear persisted credentials for
          that server entry and re-read them from the iframe URL query param (auth_token).
          If no auth_token is present in the URL and the server requires auth, surface an
          auth prompt.

          Acceptance criteria:

          • On boot-ID mismatch, the persisted username/password for the affected server
            entry are cleared.
          • The app re-reads auth_token from location.search (the iframe URL injected by
            the extension host).
          • If auth is required and no valid credentials are available, a modal prompts the
            user (rather than silently failing with 401s).

          8. Extension host → webview URL push on server restart

          File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

          Problem: When the extension host restarts the server (via
          amicode.restartServer command), it re-launches the opencode process on a
          potentially different port. The webview SSE stream is connected to the old port and
          must wait for connection-refused → escalation (item #2) to recover.

          Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
          iframe immediately after the new server is confirmed listening. The webview handles
          this message by updating its active server URL and immediately reconnecting SSE to
          the new URL.

          Acceptance criteria:

          • The webview registers a listener for kind: "server-url-changed" messages.
          • On receiving this message, the webview updates its persisted server store and
            reconnects SSE within 1 second (no 250 ms retry loop needed).
          • If the message arrives while the webview is already connected (race condition),
            it is a no-op.
          • The webview emits a route-info message back to confirm it received the update.

          Tier 3 — Improvements

          These are well-advised hardening measures. They do not directly prevent the "no GUI
          response" bug but reduce adjacent failure surfaces.


          9. Quota-aware eviction priority

          File:packages/app/src/utils/persist.ts (lines 112–165)

          Problem: The localStorage eviction logic removes the largest opencode.* keys
          first. The server key (connection state) and tabs key (session history) grow
          over time and become prime eviction targets.

          Fix: Maintain a "protected keys" list that the eviction logic never removes.
          At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

          Acceptance criteria:

          • opencode.global.dat:server is never evicted by the quota handler.
          • Eviction preferentially targets workspace and session-scoped keys.
          • If eviction cannot free enough space without touching protected keys, the write
            fails gracefully (the app continues to function with the existing state).

          10. Connection banner always visible on persistent disconnection

          File:packages/app/src/components/connection-banner.tsx

          Problem: The ConnectionBanner component shows when streamStatus is
          "disconnected", but its visibility depends on layout configuration. If the banner
          is scrolled off or hidden by a panel, the user has no indication that the
          connection is broken.

          Fix: After 5 seconds of continuous "disconnected" state, surface a
          VS Code-style notification (via postMessage to the extension host, which calls
          vscode.window.showWarningMessage) in addition to the in-webview banner.

          Acceptance criteria:

          • If the SSE stream is disconnected for > 5 continuous seconds, a warning
            notification appears in VS Code's notification area.
          • The notification includes an action button: "Reconnect" (which triggers URL
            rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
          • The notification is not repeated more than once per 60 seconds.

          11. Terminal extension: readiness probe fix (/app/health)

          File:sdks/vscode/src/extension.ts (line 78)

          Problem: The extension probes GET /app (a catch-all UI route) with no
          status-code check. Should probe GET /health and check response.ok.

          Fix: Change the URL to /health and gate connected = true on response.ok.

          Acceptance criteria:

          • The probe hits GET /health.
          • connected is only set to true if the response status is 2xx.
          • A 404 or 500 from a partially-initialized server does not set connected = true.

          12. Terminal extension: dead terminal detection

          File:sdks/vscode/src/extension.ts (lines 15–19)

          Problem:opencode.openTerminal reuses a terminal by name without checking if
          the process has exited.

          Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
          exitStatus === undefined.

          Acceptance criteria:

          • A terminal whose process has exited is not reused.
          • The user gets a fresh terminal with a new server instance.

          13. RPC error propagation

          File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

          Problem: If a worker-side RPC method throws, the pending promise in
          client.call() is never settled. The caller hangs forever.

          Fix:

          • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
            with the error details and request ID.
          • Client side: store { resolve, reject } pairs in pending; handle rpc.error
            messages by calling reject(new Error(...)).

          Acceptance criteria:

          • If a worker method throws, the client-side promise rejects with an Error
            containing the original error message.
          • The pending Map entry is cleaned up (no memory leak).
          • Existing callers of client.call() that do not handle rejection see an unhandled
            rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

          14. Worker error logging

          File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

          Problem:unhandledRejection and uncaughtException handlers discard all
          errors silently, making worker failures invisible.

          Fix: Log errors to stderr with a [worker] prefix.

          Acceptance criteria:

          • Unhandled rejections log the error object to stderr.
          • Uncaught exceptions log the error message and stack to stderr.
          • The worker process does NOT exit on these errors (existing keep-alive behavior
            is preserved).

          15. Terminal extension: retry window + user-visible warning

          File:sdks/vscode/src/extension.ts (lines 73–90)

          Problem: The retry loop tries 10 times (2 s total). If the server doesn't
          respond (port collision, slow startup), the file reference is silently dropped.

          Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
          vscode.window.showWarningMessage(...) so the user knows something went wrong.

          Acceptance criteria:

          • The retry window is 4 seconds (20 * 200 ms).
          • If connected is still false after the loop, a warning message is shown.
          • The warning message suggests "try again" or "the port may be in use."

          Metadata

          Metadata

          Assignees

          Labels

          duplicateThis issue or pull request already exists

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            SSE Connection Reliability Improvements #413

            Description

            @gennadiryan

            Actionable fixes to prevent the "no GUI response" failure class and harden the
            webview-server connection lifecycle. Each item includes acceptance criteria.

            See also:

            • ./issue-no-gui-response.md — the specific bug these fixes address
            • ../notes/local-storage-customization.md — parameter inventory and design analysis
            • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

            Tier 1 — Critical Path

            These fixes directly prevent or mitigate the "no GUI response" bug. They should be
            prioritized for immediate implementation.


            1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

            File:packages/app/src/entry.tsx (lines 157–161)

            Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
            localStorage before consulting location.origin. In the Amicode webview, the
            iframe is served by the running server — location.origin is always the correct
            URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
            port), it permanently overrides the correct origin.

            Fix: When running inside the Amicode webview (detectable via inAmicode() from
            utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
            entirely. Use location.origin unconditionally.

            Acceptance criteria:

            • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
              without consulting localStorage.
            • The defaultServerUrl localStorage key is still honored in the standalone web
              app context (when inAmicode() is false).
            • After a server restart (new port), reopening the Amicode webview connects to the
              new server without requiring localStorage to be cleared.

            2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

            File:packages/app/src/context/server-sdk.tsx (lines 268–338)

            Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
            It does not distinguish "connection refused" (server gone) from "server error"
            (server exists). If the URL is stale, this loops forever without recovery.

            Fix: Add an escalation path: after N consecutive connection-refused failures
            (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
            instead of the persisted server URL. If location.origin succeeds, update the
            persisted server store to reflect the correct URL.

            Acceptance criteria:

            • After 10 consecutive connection-refused errors (not HTTP errors — specifically
              TypeError: Failed to fetch or equivalent network failure), the loop switches to
              location.origin as the target URL.
            • If location.origin connects successfully, the persisted server store entry is
              updated in-place so subsequent reconnections use the correct URL directly.
            • If both the persisted URL and location.origin fail, the loop continues retrying
              location.origin at 250 ms intervals (as today, but to the correct URL).
            • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
              the ConnectionBanner to display).

            3. [CRITICAL] Server boot-ID stamping

            Files:

            • Server: packages/opencode/src/server/server.ts or the SSE event handler
            • Client: packages/app/src/context/server-sdk.tsx and server.tsx

            Problem: The client cannot distinguish "same server reconnected" from "different
            server on same port" from "stale URL, server gone." Without a server identity
            token, all reconnection heuristics are fragile.

            Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
            includes it in:

            1. The server.connected SSE event payload (so the client receives it on every
              reconnection).
            2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
              client can detect mid-session server restarts even outside the SSE stream.

            The client persists the boot-ID alongside the server URL in localStorage. On
            reconnection:

            • If boot-ID matches → normal reconnect; session state is valid.
            • If boot-ID differs → server restarted; invalidate session tab list, refetch all
              state, and optionally re-validate credentials.
            • If boot-ID is absent (older server version) → treat as "unknown," fall back to
              current behavior.

            Acceptance criteria:

            • Server emits a bootId field in the server.connected event.
            • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
            • Client stores lastBootId per server entry in the persisted store.
            • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
              prune tabs with 404 sessions, reset workspace caches.
            • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
              auth-required prompt (rather than silently failing).

            4. [CRITICAL] Configurable fixed port per container

            Files:

            • Config schema: packages/core/src/v1/config/server.ts
            • Devcontainer: .devcontainer/devcontainer.json
            • Documentation

            Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
            Because the webview's localStorage persists across container rebuilds (it lives on
            the host), a new port on restart means a stale persisted URL. Users in devcontainer
            workflows hit this on every rebuild.

            Fix: Allow users to pin a stable port per container via either:

            1. opencode.json{ "server": { "port": 4096 } } (already supported by the
              schema but not widely documented or surfaced).
            2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
              the Amicode extension host when spawning the server).

            Document the recommendation: in devcontainer-based workflows, set a fixed port so
            that localStorage's persisted URL remains valid across container rebuilds.

            Acceptance criteria:

            • Documentation (README or extension settings description) explicitly recommends
              setting server.port in opencode.json for devcontainer workflows.
            • The Amicode extension host reads OPENCODE_PORT from the container environment
              (if available) and uses it when launching the server.
            • When server.port is set in opencode.json, the server binds to exactly that
              port (no fallback) and fails loudly if the port is in use (rather than silently
              falling back to a random port).
            • The .devcontainer/devcontainer.json in this repo is updated to include a
              commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

            Tier 2 — High Importance

            These fixes prevent related failure modes and harden the connection lifecycle.
            Implement after Tier 1.


            5. Multi-instance localStorage isolation

            File:packages/app/src/utils/persist.ts

            Problem: All Amicode webview instances on the same VS Code installation share a
            single localStorage scope (keyed by extension ID origin). Two windows with
            different servers overwrite each other's server entries.

            Fix: Key all connection-related localStorage entries by a workspace
            identifier
            (e.g., a hash of the container's filesystem root or the server URL at
            first successful connection). Non-connection state (theme, zoom) remains global.

            Acceptance criteria:

            • Two VS Code windows with Amicode, connected to different servers, do not
              interfere with each other's connection state.
            • Opening a new window for a previously-unknown workspace starts fresh (no stale
              entries from another workspace).
            • Global preferences (theme, solver mode) remain shared across all instances.
            • Migration: on first load with the new keying scheme, existing global state is
              migrated into the appropriate workspace bucket.

            6. Session tab validation on load

            File:packages/app/src/context/tabs.tsx

            Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
            Dead session references from previous server instances accumulate, causing burst
            fetches to stale/non-existent endpoints on reload.

            Fix: On the server.connected event (which fires on every SSE reconnection),
            validate all open session tabs by checking their existence against the server. Tabs
            whose session IDs return 404 are moved to the closed list (not deleted — user can
            re-open if the session reappears after a migration/restore).

            Acceptance criteria:

            • Within 5 seconds of server.connected, all open tabs are validated.
            • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
            • A toast notification summarizes: "N sessions from a previous server instance were
              closed."
            • The validation is non-blocking (does not prevent the app from rendering).
            • If the server is unreachable during validation (e.g., the server.connected
              event was a false positive), validation is skipped gracefully.

            7. Credential invalidation on boot-ID change

            File:packages/app/src/context/server.tsx (inside resolveServerList)

            Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
            persisted credentials in the localStorage server.list entry are stale. Every API
            call returns 401, but the client does not surface this or attempt to refresh.

            Fix: When the boot-ID changes (see item #3), clear persisted credentials for
            that server entry and re-read them from the iframe URL query param (auth_token).
            If no auth_token is present in the URL and the server requires auth, surface an
            auth prompt.

            Acceptance criteria:

            • On boot-ID mismatch, the persisted username/password for the affected server
              entry are cleared.
            • The app re-reads auth_token from location.search (the iframe URL injected by
              the extension host).
            • If auth is required and no valid credentials are available, a modal prompts the
              user (rather than silently failing with 401s).

            8. Extension host → webview URL push on server restart

            File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

            Problem: When the extension host restarts the server (via
            amicode.restartServer command), it re-launches the opencode process on a
            potentially different port. The webview SSE stream is connected to the old port and
            must wait for connection-refused → escalation (item #2) to recover.

            Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
            iframe immediately after the new server is confirmed listening. The webview handles
            this message by updating its active server URL and immediately reconnecting SSE to
            the new URL.

            Acceptance criteria:

            • The webview registers a listener for kind: "server-url-changed" messages.
            • On receiving this message, the webview updates its persisted server store and
              reconnects SSE within 1 second (no 250 ms retry loop needed).
            • If the message arrives while the webview is already connected (race condition),
              it is a no-op.
            • The webview emits a route-info message back to confirm it received the update.

            Tier 3 — Improvements

            These are well-advised hardening measures. They do not directly prevent the "no GUI
            response" bug but reduce adjacent failure surfaces.


            9. Quota-aware eviction priority

            File:packages/app/src/utils/persist.ts (lines 112–165)

            Problem: The localStorage eviction logic removes the largest opencode.* keys
            first. The server key (connection state) and tabs key (session history) grow
            over time and become prime eviction targets.

            Fix: Maintain a "protected keys" list that the eviction logic never removes.
            At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

            Acceptance criteria:

            • opencode.global.dat:server is never evicted by the quota handler.
            • Eviction preferentially targets workspace and session-scoped keys.
            • If eviction cannot free enough space without touching protected keys, the write
              fails gracefully (the app continues to function with the existing state).

            10. Connection banner always visible on persistent disconnection

            File:packages/app/src/components/connection-banner.tsx

            Problem: The ConnectionBanner component shows when streamStatus is
            "disconnected", but its visibility depends on layout configuration. If the banner
            is scrolled off or hidden by a panel, the user has no indication that the
            connection is broken.

            Fix: After 5 seconds of continuous "disconnected" state, surface a
            VS Code-style notification (via postMessage to the extension host, which calls
            vscode.window.showWarningMessage) in addition to the in-webview banner.

            Acceptance criteria:

            • If the SSE stream is disconnected for > 5 continuous seconds, a warning
              notification appears in VS Code's notification area.
            • The notification includes an action button: "Reconnect" (which triggers URL
              rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
            • The notification is not repeated more than once per 60 seconds.

            11. Terminal extension: readiness probe fix (/app/health)

            File:sdks/vscode/src/extension.ts (line 78)

            Problem: The extension probes GET /app (a catch-all UI route) with no
            status-code check. Should probe GET /health and check response.ok.

            Fix: Change the URL to /health and gate connected = true on response.ok.

            Acceptance criteria:

            • The probe hits GET /health.
            • connected is only set to true if the response status is 2xx.
            • A 404 or 500 from a partially-initialized server does not set connected = true.

            12. Terminal extension: dead terminal detection

            File:sdks/vscode/src/extension.ts (lines 15–19)

            Problem:opencode.openTerminal reuses a terminal by name without checking if
            the process has exited.

            Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
            exitStatus === undefined.

            Acceptance criteria:

            • A terminal whose process has exited is not reused.
            • The user gets a fresh terminal with a new server instance.

            13. RPC error propagation

            File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

            Problem: If a worker-side RPC method throws, the pending promise in
            client.call() is never settled. The caller hangs forever.

            Fix:

            • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
              with the error details and request ID.
            • Client side: store { resolve, reject } pairs in pending; handle rpc.error
              messages by calling reject(new Error(...)).

            Acceptance criteria:

            • If a worker method throws, the client-side promise rejects with an Error
              containing the original error message.
            • The pending Map entry is cleaned up (no memory leak).
            • Existing callers of client.call() that do not handle rejection see an unhandled
              rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

            14. Worker error logging

            File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

            Problem:unhandledRejection and uncaughtException handlers discard all
            errors silently, making worker failures invisible.

            Fix: Log errors to stderr with a [worker] prefix.

            Acceptance criteria:

            • Unhandled rejections log the error object to stderr.
            • Uncaught exceptions log the error message and stack to stderr.
            • The worker process does NOT exit on these errors (existing keep-alive behavior
              is preserved).

            15. Terminal extension: retry window + user-visible warning

            File:sdks/vscode/src/extension.ts (lines 73–90)

            Problem: The retry loop tries 10 times (2 s total). If the server doesn't
            respond (port collision, slow startup), the file reference is silently dropped.

            Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
            vscode.window.showWarningMessage(...) so the user knows something went wrong.

            Acceptance criteria:

            • The retry window is 4 seconds (20 * 200 ms).
            • If connected is still false after the loop, a warning message is shown.
            • The warning message suggests "try again" or "the port may be in use."

            Metadata

            Metadata

            Assignees

            Labels

            duplicateThis issue or pull request already exists

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              SSE Connection Reliability Improvements #413

              Description

              @gennadiryan

              Actionable fixes to prevent the "no GUI response" failure class and harden the
              webview-server connection lifecycle. Each item includes acceptance criteria.

              See also:

              • ./issue-no-gui-response.md — the specific bug these fixes address
              • ../notes/local-storage-customization.md — parameter inventory and design analysis
              • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

              Tier 1 — Critical Path

              These fixes directly prevent or mitigate the "no GUI response" bug. They should be
              prioritized for immediate implementation.


              1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

              File:packages/app/src/entry.tsx (lines 157–161)

              Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
              localStorage before consulting location.origin. In the Amicode webview, the
              iframe is served by the running server — location.origin is always the correct
              URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
              port), it permanently overrides the correct origin.

              Fix: When running inside the Amicode webview (detectable via inAmicode() from
              utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
              entirely. Use location.origin unconditionally.

              Acceptance criteria:

              • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
                without consulting localStorage.
              • The defaultServerUrl localStorage key is still honored in the standalone web
                app context (when inAmicode() is false).
              • After a server restart (new port), reopening the Amicode webview connects to the
                new server without requiring localStorage to be cleared.

              2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

              File:packages/app/src/context/server-sdk.tsx (lines 268–338)

              Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
              It does not distinguish "connection refused" (server gone) from "server error"
              (server exists). If the URL is stale, this loops forever without recovery.

              Fix: Add an escalation path: after N consecutive connection-refused failures
              (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
              instead of the persisted server URL. If location.origin succeeds, update the
              persisted server store to reflect the correct URL.

              Acceptance criteria:

              • After 10 consecutive connection-refused errors (not HTTP errors — specifically
                TypeError: Failed to fetch or equivalent network failure), the loop switches to
                location.origin as the target URL.
              • If location.origin connects successfully, the persisted server store entry is
                updated in-place so subsequent reconnections use the correct URL directly.
              • If both the persisted URL and location.origin fail, the loop continues retrying
                location.origin at 250 ms intervals (as today, but to the correct URL).
              • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
                the ConnectionBanner to display).

              3. [CRITICAL] Server boot-ID stamping

              Files:

              • Server: packages/opencode/src/server/server.ts or the SSE event handler
              • Client: packages/app/src/context/server-sdk.tsx and server.tsx

              Problem: The client cannot distinguish "same server reconnected" from "different
              server on same port" from "stale URL, server gone." Without a server identity
              token, all reconnection heuristics are fragile.

              Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
              includes it in:

              1. The server.connected SSE event payload (so the client receives it on every
                reconnection).
              2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
                client can detect mid-session server restarts even outside the SSE stream.

              The client persists the boot-ID alongside the server URL in localStorage. On
              reconnection:

              • If boot-ID matches → normal reconnect; session state is valid.
              • If boot-ID differs → server restarted; invalidate session tab list, refetch all
                state, and optionally re-validate credentials.
              • If boot-ID is absent (older server version) → treat as "unknown," fall back to
                current behavior.

              Acceptance criteria:

              • Server emits a bootId field in the server.connected event.
              • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
              • Client stores lastBootId per server entry in the persisted store.
              • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
                prune tabs with 404 sessions, reset workspace caches.
              • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
                auth-required prompt (rather than silently failing).

              4. [CRITICAL] Configurable fixed port per container

              Files:

              • Config schema: packages/core/src/v1/config/server.ts
              • Devcontainer: .devcontainer/devcontainer.json
              • Documentation

              Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
              Because the webview's localStorage persists across container rebuilds (it lives on
              the host), a new port on restart means a stale persisted URL. Users in devcontainer
              workflows hit this on every rebuild.

              Fix: Allow users to pin a stable port per container via either:

              1. opencode.json{ "server": { "port": 4096 } } (already supported by the
                schema but not widely documented or surfaced).
              2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
                the Amicode extension host when spawning the server).

              Document the recommendation: in devcontainer-based workflows, set a fixed port so
              that localStorage's persisted URL remains valid across container rebuilds.

              Acceptance criteria:

              • Documentation (README or extension settings description) explicitly recommends
                setting server.port in opencode.json for devcontainer workflows.
              • The Amicode extension host reads OPENCODE_PORT from the container environment
                (if available) and uses it when launching the server.
              • When server.port is set in opencode.json, the server binds to exactly that
                port (no fallback) and fails loudly if the port is in use (rather than silently
                falling back to a random port).
              • The .devcontainer/devcontainer.json in this repo is updated to include a
                commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

              Tier 2 — High Importance

              These fixes prevent related failure modes and harden the connection lifecycle.
              Implement after Tier 1.


              5. Multi-instance localStorage isolation

              File:packages/app/src/utils/persist.ts

              Problem: All Amicode webview instances on the same VS Code installation share a
              single localStorage scope (keyed by extension ID origin). Two windows with
              different servers overwrite each other's server entries.

              Fix: Key all connection-related localStorage entries by a workspace
              identifier
              (e.g., a hash of the container's filesystem root or the server URL at
              first successful connection). Non-connection state (theme, zoom) remains global.

              Acceptance criteria:

              • Two VS Code windows with Amicode, connected to different servers, do not
                interfere with each other's connection state.
              • Opening a new window for a previously-unknown workspace starts fresh (no stale
                entries from another workspace).
              • Global preferences (theme, solver mode) remain shared across all instances.
              • Migration: on first load with the new keying scheme, existing global state is
                migrated into the appropriate workspace bucket.

              6. Session tab validation on load

              File:packages/app/src/context/tabs.tsx

              Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
              Dead session references from previous server instances accumulate, causing burst
              fetches to stale/non-existent endpoints on reload.

              Fix: On the server.connected event (which fires on every SSE reconnection),
              validate all open session tabs by checking their existence against the server. Tabs
              whose session IDs return 404 are moved to the closed list (not deleted — user can
              re-open if the session reappears after a migration/restore).

              Acceptance criteria:

              • Within 5 seconds of server.connected, all open tabs are validated.
              • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
              • A toast notification summarizes: "N sessions from a previous server instance were
                closed."
              • The validation is non-blocking (does not prevent the app from rendering).
              • If the server is unreachable during validation (e.g., the server.connected
                event was a false positive), validation is skipped gracefully.

              7. Credential invalidation on boot-ID change

              File:packages/app/src/context/server.tsx (inside resolveServerList)

              Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
              persisted credentials in the localStorage server.list entry are stale. Every API
              call returns 401, but the client does not surface this or attempt to refresh.

              Fix: When the boot-ID changes (see item #3), clear persisted credentials for
              that server entry and re-read them from the iframe URL query param (auth_token).
              If no auth_token is present in the URL and the server requires auth, surface an
              auth prompt.

              Acceptance criteria:

              • On boot-ID mismatch, the persisted username/password for the affected server
                entry are cleared.
              • The app re-reads auth_token from location.search (the iframe URL injected by
                the extension host).
              • If auth is required and no valid credentials are available, a modal prompts the
                user (rather than silently failing with 401s).

              8. Extension host → webview URL push on server restart

              File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

              Problem: When the extension host restarts the server (via
              amicode.restartServer command), it re-launches the opencode process on a
              potentially different port. The webview SSE stream is connected to the old port and
              must wait for connection-refused → escalation (item #2) to recover.

              Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
              iframe immediately after the new server is confirmed listening. The webview handles
              this message by updating its active server URL and immediately reconnecting SSE to
              the new URL.

              Acceptance criteria:

              • The webview registers a listener for kind: "server-url-changed" messages.
              • On receiving this message, the webview updates its persisted server store and
                reconnects SSE within 1 second (no 250 ms retry loop needed).
              • If the message arrives while the webview is already connected (race condition),
                it is a no-op.
              • The webview emits a route-info message back to confirm it received the update.

              Tier 3 — Improvements

              These are well-advised hardening measures. They do not directly prevent the "no GUI
              response" bug but reduce adjacent failure surfaces.


              9. Quota-aware eviction priority

              File:packages/app/src/utils/persist.ts (lines 112–165)

              Problem: The localStorage eviction logic removes the largest opencode.* keys
              first. The server key (connection state) and tabs key (session history) grow
              over time and become prime eviction targets.

              Fix: Maintain a "protected keys" list that the eviction logic never removes.
              At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

              Acceptance criteria:

              • opencode.global.dat:server is never evicted by the quota handler.
              • Eviction preferentially targets workspace and session-scoped keys.
              • If eviction cannot free enough space without touching protected keys, the write
                fails gracefully (the app continues to function with the existing state).

              10. Connection banner always visible on persistent disconnection

              File:packages/app/src/components/connection-banner.tsx

              Problem: The ConnectionBanner component shows when streamStatus is
              "disconnected", but its visibility depends on layout configuration. If the banner
              is scrolled off or hidden by a panel, the user has no indication that the
              connection is broken.

              Fix: After 5 seconds of continuous "disconnected" state, surface a
              VS Code-style notification (via postMessage to the extension host, which calls
              vscode.window.showWarningMessage) in addition to the in-webview banner.

              Acceptance criteria:

              • If the SSE stream is disconnected for > 5 continuous seconds, a warning
                notification appears in VS Code's notification area.
              • The notification includes an action button: "Reconnect" (which triggers URL
                rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
              • The notification is not repeated more than once per 60 seconds.

              11. Terminal extension: readiness probe fix (/app/health)

              File:sdks/vscode/src/extension.ts (line 78)

              Problem: The extension probes GET /app (a catch-all UI route) with no
              status-code check. Should probe GET /health and check response.ok.

              Fix: Change the URL to /health and gate connected = true on response.ok.

              Acceptance criteria:

              • The probe hits GET /health.
              • connected is only set to true if the response status is 2xx.
              • A 404 or 500 from a partially-initialized server does not set connected = true.

              12. Terminal extension: dead terminal detection

              File:sdks/vscode/src/extension.ts (lines 15–19)

              Problem:opencode.openTerminal reuses a terminal by name without checking if
              the process has exited.

              Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
              exitStatus === undefined.

              Acceptance criteria:

              • A terminal whose process has exited is not reused.
              • The user gets a fresh terminal with a new server instance.

              13. RPC error propagation

              File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

              Problem: If a worker-side RPC method throws, the pending promise in
              client.call() is never settled. The caller hangs forever.

              Fix:

              • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
                with the error details and request ID.
              • Client side: store { resolve, reject } pairs in pending; handle rpc.error
                messages by calling reject(new Error(...)).

              Acceptance criteria:

              • If a worker method throws, the client-side promise rejects with an Error
                containing the original error message.
              • The pending Map entry is cleaned up (no memory leak).
              • Existing callers of client.call() that do not handle rejection see an unhandled
                rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

              14. Worker error logging

              File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

              Problem:unhandledRejection and uncaughtException handlers discard all
              errors silently, making worker failures invisible.

              Fix: Log errors to stderr with a [worker] prefix.

              Acceptance criteria:

              • Unhandled rejections log the error object to stderr.
              • Uncaught exceptions log the error message and stack to stderr.
              • The worker process does NOT exit on these errors (existing keep-alive behavior
                is preserved).

              15. Terminal extension: retry window + user-visible warning

              File:sdks/vscode/src/extension.ts (lines 73–90)

              Problem: The retry loop tries 10 times (2 s total). If the server doesn't
              respond (port collision, slow startup), the file reference is silently dropped.

              Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
              vscode.window.showWarningMessage(...) so the user knows something went wrong.

              Acceptance criteria:

              • The retry window is 4 seconds (20 * 200 ms).
              • If connected is still false after the loop, a warning message is shown.
              • The warning message suggests "try again" or "the port may be in use."

              Metadata

              Metadata

              Assignees

              Labels

              duplicateThis issue or pull request already exists

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                SSE Connection Reliability Improvements #413

                Description

                @gennadiryan

                Actionable fixes to prevent the "no GUI response" failure class and harden the
                webview-server connection lifecycle. Each item includes acceptance criteria.

                See also:

                • ./issue-no-gui-response.md — the specific bug these fixes address
                • ../notes/local-storage-customization.md — parameter inventory and design analysis
                • ../notes/fix-plan.md — terminal-mode fixes (subset included here as Tier 3)

                Tier 1 — Critical Path

                These fixes directly prevent or mitigate the "no GUI response" bug. They should be
                prioritized for immediate implementation.


                1. [CRITICAL] Remove defaultServerUrl localStorage override in webview context

                File:packages/app/src/entry.tsx (lines 157–161)

                Problem:getDefaultUrl() reads opencode.settings.dat:defaultServerUrl from
                localStorage before consulting location.origin. In the Amicode webview, the
                iframe is served by the running server — location.origin is always the correct
                URL. But if a previous session wrote defaultServerUrl (pointing to a now-dead
                port), it permanently overrides the correct origin.

                Fix: When running inside the Amicode webview (detectable via inAmicode() from
                utils/amicode-bridge.ts), skip the localStorage defaultServerUrl lookup
                entirely. Use location.origin unconditionally.

                Acceptance criteria:

                • When inAmicode() returns true, getDefaultUrl() returns getCurrentUrl()
                  without consulting localStorage.
                • The defaultServerUrl localStorage key is still honored in the standalone web
                  app context (when inAmicode() is false).
                • After a server restart (new port), reopening the Amicode webview connects to the
                  new server without requiring localStorage to be cleared.

                2. [CRITICAL] SSE reconnect escalation: fall back to location.origin after persistent failures

                File:packages/app/src/context/server-sdk.tsx (lines 268–338)

                Problem: The SSE reconnect loop retries the same URL every 250 ms indefinitely.
                It does not distinguish "connection refused" (server gone) from "server error"
                (server exists). If the URL is stale, this loops forever without recovery.

                Fix: Add an escalation path: after N consecutive connection-refused failures
                (suggested: 10, i.e., 2.5 seconds), attempt to reconnect using location.origin
                instead of the persisted server URL. If location.origin succeeds, update the
                persisted server store to reflect the correct URL.

                Acceptance criteria:

                • After 10 consecutive connection-refused errors (not HTTP errors — specifically
                  TypeError: Failed to fetch or equivalent network failure), the loop switches to
                  location.origin as the target URL.
                • If location.origin connects successfully, the persisted server store entry is
                  updated in-place so subsequent reconnections use the correct URL directly.
                • If both the persisted URL and location.origin fail, the loop continues retrying
                  location.origin at 250 ms intervals (as today, but to the correct URL).
                • A streamStatus value of "reconnecting" or "discovering" is surfaced (for
                  the ConnectionBanner to display).

                3. [CRITICAL] Server boot-ID stamping

                Files:

                • Server: packages/opencode/src/server/server.ts or the SSE event handler
                • Client: packages/app/src/context/server-sdk.tsx and server.tsx

                Problem: The client cannot distinguish "same server reconnected" from "different
                server on same port" from "stale URL, server gone." Without a server identity
                token, all reconnection heuristics are fragile.

                Fix: The server generates a random boot-ID (e.g., UUID v4) at startup and
                includes it in:

                1. The server.connected SSE event payload (so the client receives it on every
                  reconnection).
                2. A response header on all API responses (e.g., X-OpenCode-Boot-ID), so the
                  client can detect mid-session server restarts even outside the SSE stream.

                The client persists the boot-ID alongside the server URL in localStorage. On
                reconnection:

                • If boot-ID matches → normal reconnect; session state is valid.
                • If boot-ID differs → server restarted; invalidate session tab list, refetch all
                  state, and optionally re-validate credentials.
                • If boot-ID is absent (older server version) → treat as "unknown," fall back to
                  current behavior.

                Acceptance criteria:

                • Server emits a bootId field in the server.connected event.
                • Server includes X-OpenCode-Boot-ID: <uuid> header on all HTTP responses.
                • Client stores lastBootId per server entry in the persisted store.
                • On boot-ID mismatch, client runs a "full refresh" path: refetch sessions list,
                  prune tabs with 404 sessions, reset workspace caches.
                • On boot-ID mismatch, if auth credentials produce 401, the client surfaces an
                  auth-required prompt (rather than silently failing).

                4. [CRITICAL] Configurable fixed port per container

                Files:

                • Config schema: packages/core/src/v1/config/server.ts
                • Devcontainer: .devcontainer/devcontainer.json
                • Documentation

                Problem: In a devcontainer, the server port is ephemeral (random or 4096+N).
                Because the webview's localStorage persists across container rebuilds (it lives on
                the host), a new port on restart means a stale persisted URL. Users in devcontainer
                workflows hit this on every rebuild.

                Fix: Allow users to pin a stable port per container via either:

                1. opencode.json{ "server": { "port": 4096 } } (already supported by the
                  schema but not widely documented or surfaced).
                2. .devcontainer.json"containerEnv": { "OPENCODE_PORT": "4096" } (read by
                  the Amicode extension host when spawning the server).

                Document the recommendation: in devcontainer-based workflows, set a fixed port so
                that localStorage's persisted URL remains valid across container rebuilds.

                Acceptance criteria:

                • Documentation (README or extension settings description) explicitly recommends
                  setting server.port in opencode.json for devcontainer workflows.
                • The Amicode extension host reads OPENCODE_PORT from the container environment
                  (if available) and uses it when launching the server.
                • When server.port is set in opencode.json, the server binds to exactly that
                  port (no fallback) and fails loudly if the port is in use (rather than silently
                  falling back to a random port).
                • The .devcontainer/devcontainer.json in this repo is updated to include a
                  commented-out example: "containerEnv": { "OPENCODE_PORT": "4096" }.

                Tier 2 — High Importance

                These fixes prevent related failure modes and harden the connection lifecycle.
                Implement after Tier 1.


                5. Multi-instance localStorage isolation

                File:packages/app/src/utils/persist.ts

                Problem: All Amicode webview instances on the same VS Code installation share a
                single localStorage scope (keyed by extension ID origin). Two windows with
                different servers overwrite each other's server entries.

                Fix: Key all connection-related localStorage entries by a workspace
                identifier
                (e.g., a hash of the container's filesystem root or the server URL at
                first successful connection). Non-connection state (theme, zoom) remains global.

                Acceptance criteria:

                • Two VS Code windows with Amicode, connected to different servers, do not
                  interfere with each other's connection state.
                • Opening a new window for a previously-unknown workspace starts fresh (no stale
                  entries from another workspace).
                • Global preferences (theme, solver mode) remain shared across all instances.
                • Migration: on first load with the new keying scheme, existing global state is
                  migrated into the appropriate workspace bucket.

                6. Session tab validation on load

                File:packages/app/src/context/tabs.tsx

                Problem: Session tabs persist indefinitely (pruning only fires at 50+ keys).
                Dead session references from previous server instances accumulate, causing burst
                fetches to stale/non-existent endpoints on reload.

                Fix: On the server.connected event (which fires on every SSE reconnection),
                validate all open session tabs by checking their existence against the server. Tabs
                whose session IDs return 404 are moved to the closed list (not deleted — user can
                re-open if the session reappears after a migration/restore).

                Acceptance criteria:

                • Within 5 seconds of server.connected, all open tabs are validated.
                • Tabs with 404 sessions are moved to closed with a reason: "not_found" marker.
                • A toast notification summarizes: "N sessions from a previous server instance were
                  closed."
                • The validation is non-blocking (does not prevent the app from rendering).
                • If the server is unreachable during validation (e.g., the server.connected
                  event was a false positive), validation is skipped gracefully.

                7. Credential invalidation on boot-ID change

                File:packages/app/src/context/server.tsx (inside resolveServerList)

                Problem: If the server regenerates OPENCODE_SERVER_PASSWORD on restart,
                persisted credentials in the localStorage server.list entry are stale. Every API
                call returns 401, but the client does not surface this or attempt to refresh.

                Fix: When the boot-ID changes (see item #3), clear persisted credentials for
                that server entry and re-read them from the iframe URL query param (auth_token).
                If no auth_token is present in the URL and the server requires auth, surface an
                auth prompt.

                Acceptance criteria:

                • On boot-ID mismatch, the persisted username/password for the affected server
                  entry are cleared.
                • The app re-reads auth_token from location.search (the iframe URL injected by
                  the extension host).
                • If auth is required and no valid credentials are available, a modal prompts the
                  user (rather than silently failing with 401s).

                8. Extension host → webview URL push on server restart

                File: The Amicode extension host (in harmoniqs/amicode repo — not this repo)

                Problem: When the extension host restarts the server (via
                amicode.restartServer command), it re-launches the opencode process on a
                potentially different port. The webview SSE stream is connected to the old port and
                must wait for connection-refused → escalation (item #2) to recover.

                Fix: The extension host should postMessage a { source: "amicode", kind: "server-url-changed", url: "http://localhost:NEW_PORT" } message to the webview
                iframe immediately after the new server is confirmed listening. The webview handles
                this message by updating its active server URL and immediately reconnecting SSE to
                the new URL.

                Acceptance criteria:

                • The webview registers a listener for kind: "server-url-changed" messages.
                • On receiving this message, the webview updates its persisted server store and
                  reconnects SSE within 1 second (no 250 ms retry loop needed).
                • If the message arrives while the webview is already connected (race condition),
                  it is a no-op.
                • The webview emits a route-info message back to confirm it received the update.

                Tier 3 — Improvements

                These are well-advised hardening measures. They do not directly prevent the "no GUI
                response" bug but reduce adjacent failure surfaces.


                9. Quota-aware eviction priority

                File:packages/app/src/utils/persist.ts (lines 112–165)

                Problem: The localStorage eviction logic removes the largest opencode.* keys
                first. The server key (connection state) and tabs key (session history) grow
                over time and become prime eviction targets.

                Fix: Maintain a "protected keys" list that the eviction logic never removes.
                At minimum: opencode.global.dat:server, opencode.settings.dat:defaultServerUrl.

                Acceptance criteria:

                • opencode.global.dat:server is never evicted by the quota handler.
                • Eviction preferentially targets workspace and session-scoped keys.
                • If eviction cannot free enough space without touching protected keys, the write
                  fails gracefully (the app continues to function with the existing state).

                10. Connection banner always visible on persistent disconnection

                File:packages/app/src/components/connection-banner.tsx

                Problem: The ConnectionBanner component shows when streamStatus is
                "disconnected", but its visibility depends on layout configuration. If the banner
                is scrolled off or hidden by a panel, the user has no indication that the
                connection is broken.

                Fix: After 5 seconds of continuous "disconnected" state, surface a
                VS Code-style notification (via postMessage to the extension host, which calls
                vscode.window.showWarningMessage) in addition to the in-webview banner.

                Acceptance criteria:

                • If the SSE stream is disconnected for > 5 continuous seconds, a warning
                  notification appears in VS Code's notification area.
                • The notification includes an action button: "Reconnect" (which triggers URL
                  rediscovery from item β.2 — Vendor + pin opencode in the VSIX #2).
                • The notification is not repeated more than once per 60 seconds.

                11. Terminal extension: readiness probe fix (/app/health)

                File:sdks/vscode/src/extension.ts (line 78)

                Problem: The extension probes GET /app (a catch-all UI route) with no
                status-code check. Should probe GET /health and check response.ok.

                Fix: Change the URL to /health and gate connected = true on response.ok.

                Acceptance criteria:

                • The probe hits GET /health.
                • connected is only set to true if the response status is 2xx.
                • A 404 or 500 from a partially-initialized server does not set connected = true.

                12. Terminal extension: dead terminal detection

                File:sdks/vscode/src/extension.ts (lines 15–19)

                Problem:opencode.openTerminal reuses a terminal by name without checking if
                the process has exited.

                Fix: Filter vscode.window.terminals by both name === TERMINAL_NAME and
                exitStatus === undefined.

                Acceptance criteria:

                • A terminal whose process has exited is not reused.
                • The user gets a fresh terminal with a new server instance.

                13. RPC error propagation

                File:packages/opencode/src/util/rpc.ts (lines 5–12, 23, 47)

                Problem: If a worker-side RPC method throws, the pending promise in
                client.call() is never settled. The caller hangs forever.

                Fix:

                • Server side: wrap rpc[method](input) in try/catch; post rpc.error message
                  with the error details and request ID.
                • Client side: store { resolve, reject } pairs in pending; handle rpc.error
                  messages by calling reject(new Error(...)).

                Acceptance criteria:

                • If a worker method throws, the client-side promise rejects with an Error
                  containing the original error message.
                • The pending Map entry is cleaned up (no memory leak).
                • Existing callers of client.call() that do not handle rejection see an unhandled
                  rejection (which is logged by item feat: plot the pulse with Piccolo plot_pulse (not hand-rolled Makie) #14) rather than hanging forever.

                14. Worker error logging

                File:packages/opencode/src/cli/tui/worker.ts (lines 16–21)

                Problem:unhandledRejection and uncaughtException handlers discard all
                errors silently, making worker failures invisible.

                Fix: Log errors to stderr with a [worker] prefix.

                Acceptance criteria:

                • Unhandled rejections log the error object to stderr.
                • Uncaught exceptions log the error message and stack to stderr.
                • The worker process does NOT exit on these errors (existing keep-alive behavior
                  is preserved).

                15. Terminal extension: retry window + user-visible warning

                File:sdks/vscode/src/extension.ts (lines 73–90)

                Problem: The retry loop tries 10 times (2 s total). If the server doesn't
                respond (port collision, slow startup), the file reference is silently dropped.

                Fix: Increase to 20 retries (4 s). After the loop, if not connected, show
                vscode.window.showWarningMessage(...) so the user knows something went wrong.

                Acceptance criteria:

                • The retry window is 4 seconds (20 * 200 ms).
                • If connected is still false after the loop, a warning message is shown.
                • The warning message suggests "try again" or "the port may be in use."

                Metadata

                Metadata

                Assignees

                Labels

                duplicateThis issue or pull request already exists

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions