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.
Assumptions — darwin 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 + previousPort→4096, 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 setting — amicode.* 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.net → 100.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 review before merge

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions