Skip to content

Fleet: Local fallback — deliberate offline hatch + rejoin (CONTEXT.md) #332

Description

@aarontrowbridge

Fleet: Local fallback — deliberate offline hatch + rejoin (CONTEXT.md)

Important

Problem — Fleet client (MacBook) currently has no deliberate escape hatch when the canonical (Aarons-Mac-mini, co.harmoniqs.amicode-server, tunnel 127.0.0.1:4096) is unreachable (tailscale Online:false, ssh amico-mini timeout — as seen 2026-08-11 01:25, mini LastSeen 23:20, erlich still Online:true). Guard ~/.local/bin/amico-opencode-fleet-guard exit 1 is correct for never-fork (ADR 0005 #279) but with no fallback the panel is just stranded dead; Local fallback per CONTEXT.md (“explicit, user-invoked, visible state; sessions merge on Rejoin”) has no product surface and normal ssh (Match host amico-mini LAN-first) is conflated with fleet down.
Approach — Ship the ADR 0005 hatch as user-visible product: a machine-scoped marker ~/.amico/ops/fleet/fallback.json ({active,since,previousBinary,previousPort}) checked by the guard before its exit 1 (fallback → fall through to frozen/VSIX/dev), plus three commands (Enter Local Fallback, Exit Local Fallback, Rejoin), a warning status bar (Fleet: LOCAL FALLBACK → click to Rejoin), and healthcheck awareness (guard/settings/tunnel skipped while fallback active, Fleet fallback: ACTIVE shown). Rejoin probes 127.0.0.1:4096/4096 and archives the local shard (~/.local/share/opencode/opencode.db → ~/.amico/fleet-recovery/<date>/) before restoring amicode.opencodeBinary+opencodePort (guard+4096) per the “preserve, never delete on unmappable drift” rule. Tunnel stays 15/2+TCPKeepAlive.
Scope — in: fleet_fallback.ts + fleet_fallback.test.ts (4), guard fallback check (FALLBACK="$HOME/.amico/ops/fleet/fallback.json"), fleet_health.ts fallback row + aggregate skip (now 4 checks), extension.ts fallback status bar + amicode.fleet.fallback.enter/exit + amicode.fleet.rejoin (archive shard), package.json 3 commands, assert_fleet_guard.sh fallback assertion, tools/fleet/README.md Local fallback docs, VSIX !tools/** already ships it. out: full Rejoin server-side merge route + identity/Fleet-token (#284, #290) — this is a local-only shard with manual archive, not the fixture-locked merge.
Assumptionsdarwin fleet only; marker is machine-scoped file, never synced; tailscale/erlich proves normal ssh is healthy when mini is up; fallback port is ephemeral (opencodePort=0) while tunnel is 4096.

Acceptance Criteria

  • Guard tools/fleet/amico-opencode-fleet-guard checks ~/.amico/ops/fleet/fallback.json before scutil --get LocalHostName; with marker present amico-mini's exit 1 is bypassed and frozen/VSIX/dev is execd even on MacBook-Pro-de-Aaron (verified touch fallback.json && guard --help shows opencode not refusing).
  • Amicode: Fleet — Enter Local Fallback (palette, modal confirm) creates fallback.json with since + previousBinary/previousPort, clears amicode.opencodeBinary="" + opencodePort=0 (Global), restarts to vendored local server (ephemeral), status bar $(warning) Fleet: LOCAL FALLBACK (warning bg, tooltip fallbackStatusLabel, click → Rejoin) and amicode.healthcheck shows Fleet fallback: ACTIVE while Fleet guard/settings/tunnel are skipped — fallback active.
  • Amicode: Fleet — Exit Local Fallback (modal) deletes marker, restores previousBinary~/.local/bin/amico-opencode-fleet-guard + previousPort4096, restarts to ride tunnel (health probe fails closed if mini still offline, with Rejoin hint).
  • Amicode: Fleet — Rejoin when fallback active probes http://127.0.0.1:4096/ (and amicode.opencodePort); if tunnel still down offers Stay/Exit anyway/Check again; if up archives ~/.local/share/opencode/opencode.db~/.amico/fleet-recovery/<YYYY-MM-DD>/opencode-fallback-<ts>.db, then exits fallback and runs healthcheck.
  • tailscale status aarons-mac-mini Online:false vs erlich Online:true proves normal ssh (ssh erlich ok) is unrelated to fleet ssh amico-mini timeout; Match host amico-mini LAN-first (ping Aarons-Mac-mini.local.local vs 100.125.66.86) remains.
  • bash packages/extension/scripts/assert_fleet_guard.sh checks fallback marker/hint; pnpm test includes fleet_fallback.test.ts (4) + fleet_health fallback aggregate (1), typecheck clean, vsce package includes extension/tools/fleet/amico-opencode-fleet-guard 2147B with fallback.

Testing Decisions

  • Pure fleet_fallback.ts (enter/exit/isActive/read + atomic tmp→rename, mkdir -p) unit-tested with temp dir, no scutil/launchctl. Guard tested via touch fallback.json + guard --help (exec vs exit 1). Health aggregate tested with injected read returning fallback JSON → 4 checks with skipped rows. Manual: enter fallback on MacBook while tailscale ping timeout, verify lsof -iTCP:4096 no ssh LISTEN but local opencode LISTEN on ephemeral, status bar warning, health ACTIVE; rejoin after tailscale recovers.

Key Decisions

  • Marker is a file, not a settingamicode.* settings are machine but still JSON in settings.json; a file is the same machine-scoped contract as the Fleet token 0600 (ADR 0001) and is trivial for the bash guard to check without jq.
  • Guard check is first: if [ -f "$FALLBACK" ]; then :; elif [ "$(scutil …)" != ... ]; then exit 1; fi — fallback is explicit, not a silent bypass of never-fork.
  • Port in fallback is ephemeral (0) — avoids 4096 collision when the tunnel recovers; canonical 4096 is restored on exit.
  • Rejoin is archive, not merge for now — copies opencode.db to fleet-recovery per “preserve, never delete” (ADR 0005 unmappable drift); full server-side fixture-locked merge (Local fallback + Rejoin merge (fixture-locked) #290) lands with identity routes.

Constraints & Invariants

  • Exactly one canonical per fleet; client never spawns outside fallback; marker + settings never ride Settings Sync; loopback-only binding persists; shard preserved until verified merge.

Prior Art

Source

Part of Managed Fleet hatch — explicit offline work.

Notes

  • Normal ssh (erlich.tail1c461a.ts.net100.104.59.70 ok) proves ~/.ssh/config tweak is not the fleet down cause; fleet down is aarons-mac-mini Online:false (Tailscale).

Metadata

Metadata

Assignees

No one assigned

    Labels

    hitlNeeds human decision/review before merge

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions