Problem Statement
In devcontainer environments, the Amicode webview panel stops receiving responses
from the opencode server after container rebuilds or server restarts. The root
cause is a persistence lifetime mismatch: the webview's localStorage (containing
the server URL, session tabs, and connection state) lives on the host machine and
survives container rebuilds, while the server port is ephemeral and changes on
each rebuild.
The SSE event stream connects to the stale URL, fails indefinitely, and delivers
no events to the UI. The user sees no responses despite the harness functioning
correctly underneath.
#414
harmoniqs/opencode#212
Implemented Fixes
1. Server boot-ID generation
The opencode server generates a crypto.randomUUID() on each Server.listen()
call and emits it in the server.connected SSE event's properties field. This
gives clients a reliable mechanism to detect server restarts — even when the port
stays the same.
Files: packages/opencode/src/server/boot-id.ts, server.ts, SSE handlers.
2. Stale URL prevention in the Amicode webview
When running inside the Amicode webview (iframe), the defaultServerUrl
localStorage key is never consulted. location.origin is used unconditionally
because the iframe IS served by the running server — its origin is always the
correct URL. This prevents stale URLs from a previous session from overriding the
correct one.
File: packages/app/src/entry.tsx
3. SSE reconnect escalation
A consecutive-failure counter in the SSE reconnect loop stops retrying after 10
failures (2.5 seconds). This prevents infinite CPU burn on genuinely unreachable
servers and surfaces a "disconnected" state to the ConnectionBanner. A page
visibility cycle (switching away and back) restarts the loop.
File: packages/app/src/context/server-sdk.tsx
4. Boot-ID persistence and mismatch detection
The client persists the server's boot-ID in localStorage (per server scope). On
each server.connected event, it compares the new boot-ID to the stored value and
logs a warning on mismatch. The existing server-sync refresh logic (session list
refetch, directory re-bootstrap) already fires on server.connected — the boot-ID
provides additional observability.
Files: packages/app/src/context/server.tsx, server-sdk.tsx
5. Extension host URL push on server restart
The Amicode extension host pushes the server URL to the webview after every
successful server start (via serverManager.onReady). If the port changed
(ephemeral mode), the panel is disposed and recreated with the correct iframe src.
If the port is the same, a server-url-changed message is posted to inform the
webview that a restart occurred.
Files: packages/extension/src/chat_panel.ts, extension.ts, deck/shell.ts
6. Fixed port in devcontainer configuration
Both repos' .devcontainer/devcontainer.json now set amicode.opencodePort: 43117
in VS Code settings. This ensures the port is stable across container rebuilds,
making localStorage URLs valid indefinitely. Documentation covers three
mechanisms for injecting the port (VS Code settings, OPENCODE_CONFIG_CONTENT
env var, opencode.json) across three use cases (marketplace install, Dockerfile
build, CI/headless).
Files: .devcontainer/devcontainer.json (both repos),
docs/devcontainers.md (amicode)
Deferred Improvements
Multi-instance localStorage isolation
Multiple Amicode webviews on the same VS Code installation share a single
localStorage scope (keyed by extension ID origin). Two workspaces with different
servers can overwrite each other's connection state. The boot-ID mechanism
provides detection (each webview sees stale state and refreshes) but not
prevention.
Future direction: Key connection-related localStorage entries by a workspace
or container identifier. Global preferences (theme, zoom) remain shared.
Session tab pruning on boot-ID mismatch
Dead session tabs from previous server instances accumulate in localStorage. The
boot-ID mismatch is logged but tabs are not validated against the server. The
existing server-sync refresh handles sessions that still exist; dead entries
remain until the 50-key pruning threshold fires or the user manually closes them.
Future direction: On boot-ID mismatch, validate all open tabs by fetching
their session metadata. Move 404s to the closed list with a toast notification.
Credential invalidation on boot-ID mismatch
If OPENCODE_SERVER_PASSWORD changes across restarts, persisted credentials in
localStorage are stale. The current implementation does not clear them on boot-ID
change. In practice, the extension injects fresh credentials via the iframe's
?auth_token= query param on each panel creation, limiting exposure to long-lived
panels across manual restarts.
Future direction: On boot-ID mismatch, clear persisted credentials and
re-read from the iframe URL query parameter.
Quota-aware eviction priority
The localStorage eviction logic removes the largest keys first under quota
pressure. The server key (connection state) is a prime eviction target as it
grows. With the Phase 2 inAmicode() gate, eviction of this key is less critical
(the app falls back to location.origin), but it could still lose project and
session state.
Future direction: Maintain a protected-keys list that the eviction logic
never removes.
Connection banner guaranteed visibility
The ConnectionBanner reads streamStatus but its visibility depends on layout
configuration. A persistently disconnected state may go unnoticed if the banner
is scrolled off or hidden by a panel.
Future direction: After 5 seconds of continuous disconnection, surface a
VS Code notification (via postMessage to the extension host) in addition to the
in-webview banner.
Self-healing reconnect (upgrade from abort)
The current escalation strategy aborts the SSE loop after 10 failures. A future
upgrade posts server-url-changed to self (triggering the AmicodeServerBridge to
update the URL and reconnect), providing seamless recovery without user
intervention.
Future direction: Documented in plans/followup-self-healing-reconnect.md.
Terminal-mode extension fixes
The opencode terminal extension (sdks/vscode/) has separate issues: wrong
readiness probe URL (/app instead of /health), dead terminal reuse without
liveness check, RPC promise leaks in the worker transport, and silent error
swallowing in the worker process. These are on a different code path from the
Amicode webview and do not cause the "no GUI response" bug.
Future direction: Documented in notes/fix-plan.md.
Design Principles Established
-
The server URL is ephemeral, not a preference. It is determined at runtime
by the extension host and must not be persisted across server restarts.
-
The extension host is authoritative for runtime parameters. The webview
receives connection parameters from the host via the postMessage bridge;
localStorage is a cache for UI preferences only.
-
The boot-ID is the staleness signal. Any restart (container rebuild,
process crash, explicit restart) produces a new boot-ID. Clients that persist
and compare it can reliably detect that cached state is stale, regardless of
whether the port changed.
-
Fixed port is the primary defense. The default amicode.opencodePort = 43117 eliminates the stale-URL problem for most users. All other mechanisms
are defense-in-depth for ephemeral-port users and edge cases.
Problem Statement
In devcontainer environments, the Amicode webview panel stops receiving responses
from the opencode server after container rebuilds or server restarts. The root
cause is a persistence lifetime mismatch: the webview's localStorage (containing
the server URL, session tabs, and connection state) lives on the host machine and
survives container rebuilds, while the server port is ephemeral and changes on
each rebuild.
The SSE event stream connects to the stale URL, fails indefinitely, and delivers
no events to the UI. The user sees no responses despite the harness functioning
correctly underneath.
#414
harmoniqs/opencode#212
Implemented Fixes
1. Server boot-ID generation
The opencode server generates a
crypto.randomUUID()on eachServer.listen()call and emits it in the
server.connectedSSE event'spropertiesfield. Thisgives clients a reliable mechanism to detect server restarts — even when the port
stays the same.
Files:
packages/opencode/src/server/boot-id.ts,server.ts, SSE handlers.2. Stale URL prevention in the Amicode webview
When running inside the Amicode webview (iframe), the
defaultServerUrllocalStorage key is never consulted.
location.originis used unconditionallybecause the iframe IS served by the running server — its origin is always the
correct URL. This prevents stale URLs from a previous session from overriding the
correct one.
File:
packages/app/src/entry.tsx3. SSE reconnect escalation
A consecutive-failure counter in the SSE reconnect loop stops retrying after 10
failures (2.5 seconds). This prevents infinite CPU burn on genuinely unreachable
servers and surfaces a "disconnected" state to the ConnectionBanner. A page
visibility cycle (switching away and back) restarts the loop.
File:
packages/app/src/context/server-sdk.tsx4. Boot-ID persistence and mismatch detection
The client persists the server's boot-ID in localStorage (per server scope). On
each
server.connectedevent, it compares the new boot-ID to the stored value andlogs a warning on mismatch. The existing server-sync refresh logic (session list
refetch, directory re-bootstrap) already fires on
server.connected— the boot-IDprovides additional observability.
Files:
packages/app/src/context/server.tsx,server-sdk.tsx5. Extension host URL push on server restart
The Amicode extension host pushes the server URL to the webview after every
successful server start (via
serverManager.onReady). If the port changed(ephemeral mode), the panel is disposed and recreated with the correct iframe src.
If the port is the same, a
server-url-changedmessage is posted to inform thewebview that a restart occurred.
Files:
packages/extension/src/chat_panel.ts,extension.ts,deck/shell.ts6. Fixed port in devcontainer configuration
Both repos'
.devcontainer/devcontainer.jsonnow setamicode.opencodePort: 43117in VS Code settings. This ensures the port is stable across container rebuilds,
making localStorage URLs valid indefinitely. Documentation covers three
mechanisms for injecting the port (VS Code settings,
OPENCODE_CONFIG_CONTENTenv var,
opencode.json) across three use cases (marketplace install, Dockerfilebuild, CI/headless).
Files:
.devcontainer/devcontainer.json(both repos),docs/devcontainers.md(amicode)Deferred Improvements
Multi-instance localStorage isolation
Multiple Amicode webviews on the same VS Code installation share a single
localStorage scope (keyed by extension ID origin). Two workspaces with different
servers can overwrite each other's connection state. The boot-ID mechanism
provides detection (each webview sees stale state and refreshes) but not
prevention.
Future direction: Key connection-related localStorage entries by a workspace
or container identifier. Global preferences (theme, zoom) remain shared.
Session tab pruning on boot-ID mismatch
Dead session tabs from previous server instances accumulate in localStorage. The
boot-ID mismatch is logged but tabs are not validated against the server. The
existing server-sync refresh handles sessions that still exist; dead entries
remain until the 50-key pruning threshold fires or the user manually closes them.
Future direction: On boot-ID mismatch, validate all open tabs by fetching
their session metadata. Move 404s to the closed list with a toast notification.
Credential invalidation on boot-ID mismatch
If
OPENCODE_SERVER_PASSWORDchanges across restarts, persisted credentials inlocalStorage are stale. The current implementation does not clear them on boot-ID
change. In practice, the extension injects fresh credentials via the iframe's
?auth_token=query param on each panel creation, limiting exposure to long-livedpanels across manual restarts.
Future direction: On boot-ID mismatch, clear persisted credentials and
re-read from the iframe URL query parameter.
Quota-aware eviction priority
The localStorage eviction logic removes the largest keys first under quota
pressure. The
serverkey (connection state) is a prime eviction target as itgrows. With the Phase 2
inAmicode()gate, eviction of this key is less critical(the app falls back to
location.origin), but it could still lose project andsession state.
Future direction: Maintain a protected-keys list that the eviction logic
never removes.
Connection banner guaranteed visibility
The ConnectionBanner reads
streamStatusbut its visibility depends on layoutconfiguration. A persistently disconnected state may go unnoticed if the banner
is scrolled off or hidden by a panel.
Future direction: After 5 seconds of continuous disconnection, surface a
VS Code notification (via postMessage to the extension host) in addition to the
in-webview banner.
Self-healing reconnect (upgrade from abort)
The current escalation strategy aborts the SSE loop after 10 failures. A future
upgrade posts
server-url-changedto self (triggering the AmicodeServerBridge toupdate the URL and reconnect), providing seamless recovery without user
intervention.
Future direction: Documented in
plans/followup-self-healing-reconnect.md.Terminal-mode extension fixes
The opencode terminal extension (
sdks/vscode/) has separate issues: wrongreadiness probe URL (
/appinstead of/health), dead terminal reuse withoutliveness check, RPC promise leaks in the worker transport, and silent error
swallowing in the worker process. These are on a different code path from the
Amicode webview and do not cause the "no GUI response" bug.
Future direction: Documented in
notes/fix-plan.md.Design Principles Established
The server URL is ephemeral, not a preference. It is determined at runtime
by the extension host and must not be persisted across server restarts.
The extension host is authoritative for runtime parameters. The webview
receives connection parameters from the host via the postMessage bridge;
localStorage is a cache for UI preferences only.
The boot-ID is the staleness signal. Any restart (container rebuild,
process crash, explicit restart) produces a new boot-ID. Clients that persist
and compare it can reliably detect that cached state is stale, regardless of
whether the port changed.
Fixed port is the primary defense. The default
amicode.opencodePort = 43117eliminates the stale-URL problem for most users. All other mechanismsare defense-in-depth for ephemeral-port users and edge cases.