Skip to content

Client machines silently fork the chat DB: extension always spawns a local server (needs attach-only mode) #279

Description

@aarontrowbridge

Summary

The Amicode extension always spawns its own opencode serve (server_manager.ts — there is no attach-only path). On a fleet client machine, the intended design is that the panel reaches the canonical server through an SSH local-forward (amicode.opencodePort: 4096). That only works by accident: the spawned server must lose the port race (EADDRINUSE) so the health probe ends up riding the tunnel.

On 2026-08-07 we found the MacBook client had been running a forked chat server for ~3 weeks (279 sessions, Jul 20 → Aug 7), invisible to the canonical server, culminating in a user-facing "can't do chats" when the fork's per-boot OPENCODE_SERVER_PASSWORD stranded the panel on 401s.

Root cause chain

  1. Extension spawns opencode serve --port=4096 unconditionally when a binary is present (extension.ts boot → ServerManager.start()).
  2. The client's tunnel (ssh -N amico-mini, LocalForward 4096 127.0.0.1:4096) bound IPv6 [::1]:4096 only — OpenSSH resolves localhost to ::1 first on macOS and binds the first address that works.
  3. IPv4 127.0.0.1:4096 stayed free → every VS Code launch spawned a fork that won the race. The panel (IPv4-first DNS) talked to the fork ever after.
  4. Nothing detects this: the health probe is satisfied by any 200 on the port, so a forked server reads as healthy.

Immediate mitigations applied (ops-side)

  • Client ssh config now binds explicitly: LocalForward 127.0.0.1:4096 127.0.0.1:4096.
  • Fork killed, tunnel restarted; forked history merged into canonical (collision-checked, position-guarded on the event stream); failure mode + merge nuances recorded in the fleet playbook.

These are fragile: if the tunnel is ever down when VS Code launches, the extension silently forks again.

Proposed fix

An attach-only client mode for the extension, e.g.:

  • amicode.serverMode: "spawn" | "attach" (default "spawn"), or
  • when amicode.opencodePort is set and a healthy server already answers on it with the expected identity, attach instead of spawning — and if nothing answers, fail closed (chat disabled with a clear message), never spawn on a client.

Concretely in ServerManager: probe http://127.0.0.1:<port>/health (or an identity endpoint) before spawning; in attach mode, skip cp.spawn entirely and surface a distinctive status when the tunnel is down.

Also worth considering

  • A server identity handshake (canonical server reports a fleet/instance id; the extension warns when the answering server's id ≠ expected) so any future fork is loudly detected instead of silently used.
  • Documenting that the per-boot server password + stale restored webviews present as "401 on everything" after a respawn — that symptom sent the user hunting in the wrong place first.

Incident reference

Fleet recovery of 2026-08-07: canonical DB 634 → 637 sessions after merging the fork (3 recovered sessions, incl. one that was itself troubleshooting this bug — meta). Recovery artifacts: ~/.amico/fleet-recovery/2026-08-07/ on the server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions