Skip to content

Peer fleet: two-way serving, heartbeat sync, and live health indicators - #1407

Merged
jeonghun-jj-lee merged 260 commits into
mainfrom
feature/free-tier-fleet
Oct 12, 2026
Merged

jeonghun-jj-lee merged 260 commits into
mainfrom
feature/free-tier-fleet

Conversation

@jeonghun-jj-lee

@jeonghun-jj-lee jeonghun-jj-lee commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Transforms the fleet from a hub-and-spoke topology (one server, N thin clients) into a peer fleet where every machine runs its own engine, advertises serving, and maintains live cross-machine roster sync with heartbeat-driven health indicators.

210 files changed, ~31k lines added across 89 commits (all individually PR-reviewed and squash-merged into this branch).


Architecture decisions (ADRs 0023–0029)

ADR Title Status
0023 Base-tier fleet projection Accepted
0024 Fleet thin-client + pluggable transport Withdrawn (superseded by peer model)
0025 Remote-SSH default posture, thin-client as lifeboat Accepted
0026 Generalizable machine capabilities + host-owned roster Accepted
0027 Peer fleet studios + compute federation Accepted
0028 Self-reported device identity Accepted
0029 Unified fleet serving capability Accepted

What shipped

1. Peer fleet topology (ADR 0027 / 0029)

2. Heartbeat + cross-machine roster sync

3. Sidebar fleet section

4. Fleet health / drift popup

5. Thin-client infrastructure (H1 foundation)

6. Fleet setup + enrollment

7. Real-SSH e2e test

8. Session curation (piggyback)

9. Housekeeping


Testing

  • 405 fleet-specific tests (fleet_health, sidebar_fleet_section, sidebar_view) — all green
  • 4166+ total tests passing (the one cli_gate failure is a worktree build-artifact issue, not fleet-related)
  • Real-SSH e2e test: two engines over SSH, p95 = 167ms
  • Manually verified: both machines (Mac Studio + MacBook Pro) show green dots, roster syncs within 30s

Deployment notes

After merge, both machines need a rebuild from latest main to pick up the heartbeat cadence and drift-check changes. Run pnpm sync --fix on each machine.


Issues closed (54)

JJ's fleet issues — implemented (37)

Closes #1258
Closes #1260
Closes #1261
Closes #1262
Closes #1263
Closes #1264
Closes #1265
Closes #1267
Closes #1268
Closes #1269
Closes #1270
Closes #1271
Closes #1272
Closes #1273
Closes #1274
Closes #1275
Closes #1276
Closes #1277
Closes #1278
Closes #1279
Closes #1316
Closes #1318
Closes #1319
Closes #1320
Closes #1321
Closes #1340
Closes #1341
Closes #1342
Closes #1343
Closes #1344
Closes #1345
Closes #1346
Closes #1354
Closes #1358
Closes #1359
Closes #1363
Closes #1375

Aaron's fleet issues — superseded by peer fleet (17)

The peer fleet model (ADR 0029: every machine runs its own engine) makes the hub-client infrastructure these issues target obsolete. The thin-client relay, guard, tunnel, and hub-specific ops are no longer the active architecture.

Closes #780
Closes #792
Closes #651
Closes #775
Closes #776
Closes #781
Closes #782
Closes #1106
Closes #1131
Closes #1227
Closes #1283
Closes #1284
Closes #1288
Closes #1289
Closes #1290
Closes #1294
Closes #1302

NOT closed (future work)

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 412 files, which is 312 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d8080218-c62b-492b-97c3-90258bbb274e

📥 Commits

Reviewing files that changed from the base of the PR and between d9f77ad and 4f7981a.


⛔ Files ignored due to path filters (2)
  • packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts is excluded by !**/gen/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

📒 Files selected for processing (412)
  • .github/workflows/ci.yml
  • CONTEXT.md
  • config/specs/spec-20260914-205414-daemonize-standalone-server.md
  • docs/adr/0023-base-tier-fleet-projection.md
  • docs/adr/0024-fleet-thin-client-pluggable-transport.md
  • docs/adr/0025-remote-ssh-default-posture-thin-client-lifeboat.md
  • docs/adr/0026-generalizable-machine-capabilities-and-host-owned-roster.md
  • docs/adr/0027-peer-fleet-studios-and-compute-federation.md
  • docs/adr/0028-self-reported-device-identity.md
  • docs/adr/0029-unified-fleet-serving-capability.md
  • docs/adr/0030-connect-to-fleet-device-from-sidebar.md
  • docs/adr/0031-fleet-studio-in-window-multi-machine-sessions.md
  • docs/adr/0032-peer-trust-credential-and-accept-set.md
  • docs/adr/0033-fleet-studio-per-session-sse-fanout-and-resume.md
  • docs/adr/0034-base-peer-studio-control.md
  • docs/fleet/p2-companion-go-no-go-1274.md
  • docs/fleet/relocation-audit-1270.md
  • ops/fleet-status.sh
  • packages/amico-run/src/fleet_discover.ts
  • packages/amico-run/src/fleet_enroll_verb.ts
  • packages/amico-run/src/fleet_projection_verb.ts
  • packages/amico-run/src/fleet_revoke_verb.ts
  • packages/amico-run/src/fleet_verb.ts
  • packages/amico-run/src/verbs.ts
  • packages/amico-run/test/fleet_discover.test.ts
  • packages/amico-run/test/fleet_enroll_verb.test.ts
  • packages/amico-run/test/fleet_projection_verb.test.ts
  • packages/amico-run/test/fleet_revoke_verb.test.ts
  • packages/app-bundle/manifest.json
  • packages/app-bundle/overlay/packages/app/CONTEXT.md
  • packages/app-bundle/overlay/packages/app/src/components/engine-state-signal.ts
  • packages/app-bundle/overlay/packages/app/src/components/engine-toggle-utils.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/engine-toggle-utils.ts
  • packages/app-bundle/overlay/packages/app/src/components/new-session-machine-picker.ts
  • packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx
  • packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx
  • packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit-stream-gap.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/prompt-input/submit.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-arm.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-arm.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-header.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-header.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-preflight.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/remote-create-preflight.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/index.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/preview-file-view.tsx
  • packages/app-bundle/overlay/packages/app/src/components/session/preview-human-write-gate.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-archived-fleet.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-chats-dropdown.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-context-tab.tsx
  • packages/app-bundle/overlay/packages/app/src/components/session/session-fleet-control-projection.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-fleet-peers.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-fleet-peers.ts
  • packages/app-bundle/overlay/packages/app/src/components/session/session-header.test.tsx
  • packages/app-bundle/overlay/packages/app/src/components/session/session-header.tsx
  • packages/app-bundle/overlay/packages/app/src/components/settings-v2/settings-v2.css
  • packages/app-bundle/overlay/packages/app/src/components/status-popover-body.tsx
  • packages/app-bundle/overlay/packages/app/src/components/titlebar-layout.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/titlebar-portal-dedup.test.ts
  • packages/app-bundle/overlay/packages/app/src/components/titlebar-portal-registry.ts
  • packages/app-bundle/overlay/packages/app/src/components/titlebar-tab-nav.tsx
  • packages/app-bundle/overlay/packages/app/src/components/titlebar-tab-strip.tsx
  • packages/app-bundle/overlay/packages/app/src/context/directory-sync.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-reseed-scheduler.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-reseed-scheduler.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-session-status.e2e-fixture.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-session-status.e2e.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-session-status.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/fleet-session-status.ts
  • packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts
  • packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts
  • packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts
  • packages/app-bundle/overlay/packages/app/src/context/global-sync/types.ts
  • packages/app-bundle/overlay/packages/app/src/context/layout-side-panel-tabs.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/layout-side-panel-tabs.ts
  • packages/app-bundle/overlay/packages/app/src/context/layout-tabs.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx
  • packages/app-bundle/overlay/packages/app/src/context/server-session-child-active.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-session-orphan-part.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-session.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-session.ts
  • packages/app-bundle/overlay/packages/app/src/context/server-sync.tsx
  • packages/app-bundle/overlay/packages/app/src/context/session-status-reconcile.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/session-status-reconcile.ts
  • packages/app-bundle/overlay/packages/app/src/context/session-working.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/session-working.ts
  • packages/app-bundle/overlay/packages/app/src/context/sse-gap-frame.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/sse-gap-frame.ts
  • packages/app-bundle/overlay/packages/app/src/context/sse-origin.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/sse-origin.ts
  • packages/app-bundle/overlay/packages/app/src/context/stream-liveness.test.ts
  • packages/app-bundle/overlay/packages/app/src/context/stream-liveness.ts
  • packages/app-bundle/overlay/packages/app/src/design-polish.css
  • packages/app-bundle/overlay/packages/app/src/i18n/en.ts
  • packages/app-bundle/overlay/packages/app/src/pages/home-archive.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/home.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/layout-archive.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/layout-new.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/layout.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-machine-mount.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-machine-mount.ts
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-machine-picker-mount.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-machine-picker.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-machine-selection.ts
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-composer-region-controller.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/fleet-manager-tab.test.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session/fleet-manager-tab.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session/fleet-manager.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/fleet-manager.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/helpers.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/preview-human-write-gate.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline-archive.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/model.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/projection.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/rows.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/session-header-provenance.test.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/session-header-provenance.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/timeline/smooth-scroll.ts
  • packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2.tsx
  • packages/app-bundle/overlay/packages/app/src/utils/amicode-bridge.ts
  • packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.test.ts
  • packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.ts
  • packages/app-bundle/overlay/packages/opencode/src/effect/runner.ts
  • packages/app-bundle/overlay/packages/opencode/src/provider/provider.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/amicode/self-shutdown.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/auth.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/groups/file.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/middleware/file-write-workspace.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/server.ts
  • packages/app-bundle/overlay/packages/opencode/src/server/server.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/compaction.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/llm/request.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/run-state.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/session.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/status.ts
  • packages/app-bundle/overlay/packages/opencode/src/session/system.ts
  • packages/app-bundle/overlay/packages/opencode/src/tool/registry.ts
  • packages/app-bundle/overlay/packages/opencode/test/acp/service-session.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/cli/run/stream.transport.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/effect/runner-queue.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/effect/runner.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/provider/combine-abort-signals.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/server/auth-accept-set.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/server/httpapi-file-write-gate.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/session/instruction.test.ts
  • packages/app-bundle/overlay/packages/opencode/test/session/processor-effect.test.ts
  • packages/app-bundle/overlay/packages/schema/src/session-status-event.ts
  • packages/app-bundle/overlay/packages/ui/src/amicode/entity-rail.tsx
  • packages/app-bundle/overlay/packages/ui/src/amicode/problem.test.ts
  • packages/app-bundle/overlay/packages/ui/src/amicode/problem.ts
  • packages/app-bundle/overlay/packages/ui/src/components/amicode-entity-view.tsx
  • packages/app-bundle/overlay/packages/ui/src/hooks/create-auto-scroll.tsx
  • packages/app-bundle/overlay/packages/ui/src/v2/components/switch-v2.css
  • packages/app-bundle/overlay/packages/ui/src/v2/components/switch-v2.tsx
  • packages/companion/esbuild.config.mjs
  • packages/companion/package.json
  • packages/companion/src/auto_switch.ts
  • packages/companion/src/companion.ts
  • packages/companion/src/editor_carry.ts
  • packages/companion/src/link_sensor.ts
  • packages/companion/src/probe.ts
  • packages/companion/src/prompt_up.ts
  • packages/companion/src/reopen.ts
  • packages/companion/test/__mocks__/vscode.ts
  • packages/companion/test/auto_switch.test.ts
  • packages/companion/test/companion.test.ts
  • packages/companion/test/editor_carry.test.ts
  • packages/companion/test/link_sensor.test.ts
  • packages/companion/test/prompt_up.test.ts
  • packages/companion/tsconfig.json
  • packages/companion/vitest.config.ts
  • packages/extension/esbuild.config.mjs
  • packages/extension/opencode-plugin/amicode_tools.ts
  • packages/extension/opencode-plugin/session_spawn.ts
  • packages/extension/opencode-plugin/stack_state.ts
  • packages/extension/package.json
  • packages/extension/scripts/assert_additive_invariants.sh
  • packages/extension/scripts/build_binary.mjs
  • packages/extension/scripts/lint-skills-known.txt
  • packages/extension/skills/add-fleet-device/SKILL.md
  • packages/extension/skills/create-a-fleet/SKILL.md
  • packages/extension/skills/troubleshoot-fleet/SKILL.md
  • packages/extension/src/amicode_service/attach_action.ts
  • packages/extension/src/amicode_service/attach_lifecycle.ts
  • packages/extension/src/amicode_service/attachment_credential.ts
  • packages/extension/src/amicode_service/attachment_pointer.ts
  • packages/extension/src/amicode_service/attachment_transport.ts
  • packages/extension/src/amicode_service/control_gated_routing.ts
  • packages/extension/src/amicode_service/engine_proxy.ts
  • packages/extension/src/amicode_service/fleet_accept_set.ts
  • packages/extension/src/amicode_service/fleet_bootstrap_headers.ts
  • packages/extension/src/amicode_service/fleet_control_bootstrap.ts
  • packages/extension/src/amicode_service/fleet_control_lifecycle.ts
  • packages/extension/src/amicode_service/fleet_control_request.ts
  • packages/extension/src/amicode_service/fleet_enrollment_nonce.ts
  • packages/extension/src/amicode_service/fleet_focus_store.ts
  • packages/extension/src/amicode_service/fleet_headless_rehydration.ts
  • packages/extension/src/amicode_service/fleet_hub_service.ts
  • packages/extension/src/amicode_service/fleet_issued_tokens.ts
  • packages/extension/src/amicode_service/fleet_lifecycle_authority.ts
  • packages/extension/src/amicode_service/fleet_mint_route.ts
  • packages/extension/src/amicode_service/fleet_observe_bootstrap.ts
  • packages/extension/src/amicode_service/fleet_peer_provider.ts
  • packages/extension/src/amicode_service/fleet_peer_store.ts
  • packages/extension/src/amicode_service/fleet_session_status.ts
  • packages/extension/src/amicode_service/fleet_staging.ts
  • packages/extension/src/amicode_service/fleet_transport.ts
  • packages/extension/src/amicode_service/fleet_version_skew.ts
  • packages/extension/src/amicode_service/fleet_writes.ts
  • packages/extension/src/amicode_service/hub_proxy.ts
  • packages/extension/src/amicode_service/index.ts
  • packages/extension/src/amicode_service/keeper_pointer.ts
  • packages/extension/src/amicode_service/keyed_store.ts
  • packages/extension/src/amicode_service/merged_projection.ts
  • packages/extension/src/amicode_service/observation_read_plane.ts
  • packages/extension/src/amicode_service/observation_write_plane.ts
  • packages/extension/src/amicode_service/peer_home_base_fetch.ts
  • packages/extension/src/amicode_service/peer_transport_supervisor.ts
  • packages/extension/src/amicode_service/peer_workspace_route.ts
  • packages/extension/src/amicode_service/remote_activity_notifier.ts
  • packages/extension/src/amicode_service/remote_session_state.ts
  • packages/extension/src/amicode_service/remote_write_gate.ts
  • packages/extension/src/amicode_service/roster.ts
  • packages/extension/src/amicode_service/server.ts
  • packages/extension/src/amicode_service/session_creation_route.ts
  • packages/extension/src/amicode_service/session_creation_target.ts
  • packages/extension/src/amicode_service/session_event_resume.ts
  • packages/extension/src/amicode_service/session_multiplexer.ts
  • packages/extension/src/amicode_service/session_peer_home_base.ts
  • packages/extension/src/amicode_service/sse_composite_cursor.ts
  • packages/extension/src/amicode_service/sse_fanin_aggregator.ts
  • packages/extension/src/amicode_service/sse_fanin_driver.ts
  • packages/extension/src/amicode_service/trusted_observe_projection.ts
  • packages/extension/src/amicode_service_runner.ts
  • packages/extension/src/amicode_service_runner_cli.ts
  • packages/extension/src/amicode_service_wiring.ts
  • packages/extension/src/amicode_tools_core.ts
  • packages/extension/src/boot_attachment_recovery.ts
  • packages/extension/src/chat_bridge.ts
  • packages/extension/src/chat_panel.ts
  • packages/extension/src/chat_session_launcher.ts
  • packages/extension/src/deck/shell.ts
  • packages/extension/src/engine_self_shutdown.ts
  • packages/extension/src/engine_state_push.ts
  • packages/extension/src/extension.ts
  • packages/extension/src/fleet_connect_device.ts
  • packages/extension/src/fleet_connect_remote_ssh.ts
  • packages/extension/src/fleet_fallback.ts
  • packages/extension/src/fleet_focus.ts
  • packages/extension/src/fleet_health.ts
  • packages/extension/src/fleet_heartbeat.ts
  • packages/extension/src/fleet_host_fs/host_file_client.ts
  • packages/extension/src/fleet_host_fs/mount.ts
  • packages/extension/src/fleet_host_fs/mount_policy.ts
  • packages/extension/src/fleet_host_fs/provider.ts
  • packages/extension/src/fleet_manager_command.ts
  • packages/extension/src/fleet_panel.ts
  • packages/extension/src/fleet_posture_feed.ts
  • packages/extension/src/fleet_posture_state.ts
  • packages/extension/src/fleet_topology.ts
  • packages/extension/src/fleet_window_mode_state.ts
  • packages/extension/src/hub_fleet_wiring.ts
  • packages/extension/src/opencode_config.ts
  • packages/extension/src/quit_command.ts
  • packages/extension/src/rebuild/host_matrix.ts
  • packages/extension/src/server_auth.ts
  • packages/extension/src/server_handshake.ts
  • packages/extension/src/server_keepalive.ts
  • packages/extension/src/server_lifecycle.ts
  • packages/extension/src/server_manager.ts
  • packages/extension/src/sidebar_bridge.ts
  • packages/extension/src/sidebar_fleet_section.ts
  • packages/extension/src/sidebar_view.ts
  • packages/extension/src/sidebar_webview.ts
  • packages/extension/src/status_bar.ts
  • packages/extension/src/stop_server.ts
  • packages/extension/test/__mocks__/vscode.ts
  • packages/extension/test/additive_invariants_gate.test.ts
  • packages/extension/test/adopt_live_server.test.ts
  • packages/extension/test/amicode_service_attach_action.test.ts
  • packages/extension/test/amicode_service_attach_lifecycle.test.ts
  • packages/extension/test/amicode_service_attach_switch_latency.test.ts
  • packages/extension/test/amicode_service_attachment_pointer.test.ts
  • packages/extension/test/amicode_service_attachment_transport.test.ts
  • packages/extension/test/amicode_service_fleet_data_plane.test.ts
  • packages/extension/test/amicode_service_fleet_session_status_route.test.ts
  • packages/extension/test/amicode_service_keeper_pointer.test.ts
  • packages/extension/test/amicode_service_multiplex_dispatch.test.ts
  • packages/extension/test/amicode_service_multiplex_owner_routing.test.ts
  • packages/extension/test/amicode_service_observe_event_routing.test.ts
  • packages/extension/test/amicode_service_observe_read_routing.test.ts
  • packages/extension/test/amicode_service_observe_write_routing.test.ts
  • packages/extension/test/amicode_service_roster.test.ts
  • packages/extension/test/amicode_service_roster_relay.test.ts
  • packages/extension/test/amicode_service_runner.test.ts
  • packages/extension/test/amicode_service_solver_mode.test.ts
  • packages/extension/test/amicode_service_wiring.test.ts
  • packages/extension/test/amicode_session_relay.integration.test.ts
  • packages/extension/test/boot_attachment_recovery.test.ts
  • packages/extension/test/chat_bridge.test.ts
  • packages/extension/test/chat_panel.test.ts
  • packages/extension/test/chat_session_launcher.test.ts
  • packages/extension/test/control_gated_routing.test.ts
  • packages/extension/test/deck_shell_relay.test.ts
  • packages/extension/test/engine_readopt_inplace.test.ts
  • packages/extension/test/engine_restart_plan.test.ts
  • packages/extension/test/engine_self_shutdown.test.ts
  • packages/extension/test/engine_state_push.test.ts
  • packages/extension/test/fleet_accept_set.test.ts
  • packages/extension/test/fleet_activation_398.test.ts
  • packages/extension/test/fleet_amicode_proxy.test.ts
  • packages/extension/test/fleet_bound_bootstrap.test.ts
  • packages/extension/test/fleet_boundary_parity.test.ts
  • packages/extension/test/fleet_client_relay.test.ts
  • packages/extension/test/fleet_connect_device.test.ts
  • packages/extension/test/fleet_connect_remote_ssh.test.ts
  • packages/extension/test/fleet_control_bootstrap.test.ts
  • packages/extension/test/fleet_control_lifecycle.test.ts
  • packages/extension/test/fleet_control_request.test.ts
  • packages/extension/test/fleet_enroll_authority.test.ts
  • packages/extension/test/fleet_enroll_command.test.ts
  • packages/extension/test/fleet_focus.test.ts
  • packages/extension/test/fleet_focus_store.test.ts
  • packages/extension/test/fleet_header_wiring.test.ts
  • packages/extension/test/fleet_headless_rehydration.test.ts
  • packages/extension/test/fleet_health.test.ts
  • packages/extension/test/fleet_heartbeat.test.ts
  • packages/extension/test/fleet_host_fs_client.test.ts
  • packages/extension/test/fleet_host_fs_mount.test.ts
  • packages/extension/test/fleet_host_fs_provider.test.ts
  • packages/extension/test/fleet_hub_service.test.ts
  • packages/extension/test/fleet_manager_command.test.ts
  • packages/extension/test/fleet_never_fork.test.ts
  • packages/extension/test/fleet_panel.test.ts
  • packages/extension/test/fleet_peer_e2e.test.ts
  • packages/extension/test/fleet_posture_feed.test.ts
  • packages/extension/test/fleet_posture_single_writer.test.ts
  • packages/extension/test/fleet_posture_state.test.ts
  • packages/extension/test/fleet_reattach.test.ts
  • packages/extension/test/fleet_scripts_projection.test.ts
  • packages/extension/test/fleet_session_status.test.ts
  • packages/extension/test/fleet_session_status_e2e.test.ts
  • packages/extension/test/fleet_sse_resume.test.ts
  • packages/extension/test/fleet_transport.test.ts
  • packages/extension/test/fleet_version_skew.test.ts
  • packages/extension/test/fleet_window_mode_state.test.ts
  • packages/extension/test/fleet_ws_upgrade.test.ts
  • packages/extension/test/hub_fleet_wiring.test.ts
  • packages/extension/test/hub_handshake_owner_guard.test.ts
  • packages/extension/test/merged_projection_archived.test.ts
  • packages/extension/test/merged_projection_control.test.ts
  • packages/extension/test/merged_projection_controlled_by.test.ts
  • packages/extension/test/merged_projection_local_open_engine.test.ts
  • packages/extension/test/opencode_config.test.ts
  • packages/extension/test/ops/fleet_status_roster.test.ts
  • packages/extension/test/peer_home_base_fetch.test.ts
  • packages/extension/test/peer_transport_supervisor.test.ts
  • packages/extension/test/peer_transport_supervisor_alloc.test.ts
  • packages/extension/test/peer_workspace_route.test.ts
  • packages/extension/test/quit_command.test.ts
  • packages/extension/test/remote_activity_notifier.test.ts
  • packages/extension/test/remote_session_state.test.ts
  • packages/extension/test/remote_write_gate.test.ts
  • packages/extension/test/scroll_jank.test.ts
  • packages/extension/test/server_activation_plan.test.ts
  • packages/extension/test/server_lifecycle.test.ts
  • packages/extension/test/server_manager.test.ts
  • packages/extension/test/session_creation_route.test.ts
  • packages/extension/test/session_creation_target.test.ts
  • packages/extension/test/session_peer_home_base.test.ts
  • packages/extension/test/session_public_floor.test.ts
  • packages/extension/test/session_remote_create_peer_credential.test.ts
  • packages/extension/test/session_remote_create_wire.test.ts
  • packages/extension/test/session_spawn.test.ts
  • packages/extension/test/setup_hermetic_env.ts
  • packages/extension/test/sidebar_actions.test.ts
  • packages/extension/test/sidebar_fleet_section.test.ts
  • packages/extension/test/sidebar_view.test.ts
  • packages/extension/test/slow/fleet_remote_create_e2e.test.ts
  • packages/extension/test/slow/fleet_two_peer_e2e.test.ts
  • packages/extension/test/sse_fanin_aggregator.test.ts
  • packages/extension/test/sse_fanin_driver_pump.test.ts
  • packages/extension/test/ssh_test_harness.test.ts
  • packages/extension/test/stack_state.test.ts
  • packages/extension/test/status_bar.test.ts
  • packages/extension/test/stop_server.test.ts
  • packages/extension/test/support/ssh_test_harness.ts
  • packages/extension/test/support/stub_hub.ts
  • packages/extension/test/teardown_roster_hygiene.ts
  • packages/extension/test/trusted_observe_projection.test.ts
  • packages/extension/tools/fleet/install.sh
  • packages/extension/vitest.config.ts
  • packages/schema/src/fleet_config.ts
  • packages/schema/src/fleet_lifecycle_authority.ts
  • packages/schema/src/fleet_projection.ts
  • packages/schema/src/fleet_roster.ts
  • packages/schema/src/fleet_version_skew.ts
  • packages/schema/src/index.ts
  • packages/schema/test/fleet_config.test.ts
  • packages/schema/test/fleet_projection.test.ts
  • packages/schema/test/fleet_roster.test.ts
  • packages/schema/test/fleet_version_skew.test.ts
  • tools/fleet/README.md
  • tools/fleet/install.sh

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jack-champagne

Copy link
Copy Markdown
Member

holy mother reviewing bits of this now

The guard backstop was darwin-only in two places. Make it cross-platform:

- install.sh: enrollment writes the platform-correct VS Code settings path —
  macOS Application Support; linux/WSL ~/.config/Code/User, plus the Remote-WSL/
  SSH server-side ~/.vscode-server/data/Machine/settings.json when present — so
  amicode.opencodeBinary/opencodePort are set on a client of any OS. The guard
  --check host verification runs on every OS now (no non-darwin skip); the
  launchd TUNNEL stays darwin-only (the linux tunnel is #1260). Packaged copy
  kept byte-identical.
- fleet_health.ts: checkFleetGuard/Settings/Role RUN on linux (only
  checkFleetTunnel stays darwin-specific); the aggregate standalone-floor is
  cross-platform.
- extension.ts: the activation-time drift warning fires on linux too (the
  plist read stays darwin-only; the tunnel check self-skips).
…cal dispatch (AC1/AC2/AC5)

In fleet CLIENT mode the host owns all /amicode/* state. dispatch() now
bypasses the ENTIRE local /amicode/* dispatch (the exact-match route table
AND the :262 catch-all) so a REGISTERED route (GET /amicode/problems, POST
/amicode/connections) no longer shadows the fleet branch — the request routes
to the host's authoritative amicode_service (reads via HubProxy, non-GETs via
the write-failure contract). /amicode/fleet/* stays LOCAL (the client's own
posture/mode/staging honesty surface, #1261 AC6). Gated on the client role:
standalone AND the engine-armed base machine are byte-identical.

Extends #1261's stub-host relay test: a registered /amicode/* GET returns the
host sentinel (AC1); the fleet honesty surface stays local (AC1); a mutation
lands + is accepted host-side with the hub mint translated (AC2); standalone
still serves /amicode/* locally (AC5).
…ency pin (AC3/AC4)

The write-failure contract's deriveRefetchPath was session-shaped: an
/amicode/* write fell through to /${segs[0]} = /amicode, dropping the mutated
family and yielding a refetch that hides a partial write. Add
deriveAmicodeRefetchPath — the /amicode/* variant that refetches the FAMILY
GET twin (POST /amicode/connections/credential → GET /amicode/connections),
and dispatch to it from deriveRefetchPath. So a proxied /amicode/* non-GET
resolves through delivered/failed/ambiguous with an /amicode/*-appropriate
refetch, never a raw proxy that hides a partial write (AC3).

Concurrency policy (AC4): LAST-WRITE-WINS with whole-request atomic delivery.
The write pipeline buffers each body fully and sends it in ONE request, so two
clients' bodies never interleave; the single-threaded host applies whole
requests, so the resource holds exactly one client's COMPLETE value. No
lost-update DETECTION (no CAS/ETag) — a client that read-modify-wrote stale
state can silently lose. A two-client concurrent-POST test pins it.
… (AC1/AC2/AC3)

The #1261 relay proxied HTTP + SSE, but both proxies are body-pipe-only and
the relay's HTTP server had no `upgrade` handler — so the integrated terminal
(the engine's WS PTY route, GET /pty/:id/connect) was dead on a thin client:
the handshake fell through to dispatch → the body-pipe HubProxy, which drops
`Connection`, so the host never upgraded (answered 426).

Extend the SAME fleet branch (fleetPlane.client === true && routingMode ===
"fleet") with an upgrade path:

- server.ts: an `upgrade` listener on the relay's http.Server. A fleet CLIENT
  tunnels the upgrade to the host engine via HubProxy.handleUpgrade; every
  other posture (engine-armed base, standalone, no fleet plane) destroys the
  socket — the prior no-listener behavior, so loopback/never-fork is unchanged.
- hub_proxy.ts: HubProxy.handleUpgrade re-implements credential translation for
  the 101/raw-socket path (the body-pipe proxies have no upgrade path to reuse).
  It forwards the engine's OWN 101 verbatim — status line + rawHeaders (the
  engine-computed Sec-WebSocket-Accept) + the head buffer — NEVER a synthesized
  101; keeps Connection/Upgrade/Sec-WebSocket-* so the host upgrade fires;
  strips the client mint + ?auth_token= and attaches the hub mint; preserves
  ?ticket=/?cursor=/Origin. The host's PTY route authorizes by ticket OR Basic.
- Clean teardown (AC3): any end/close/error on either side destroys BOTH sockets
  (pipe with { end: false }) so a dropped WS never leaks the counterpart via a
  half-open FIN; a pending pre-upgrade drop aborts the upstream request.
- server.ts stop(): closeAllConnections() so a live WS/PTY tunnel at shutdown
  can't wedge server.close() on a hijacked socket.

New test fleet_ws_upgrade.test.ts: a WS-echo test against a stub WebSocket
upstream (no `ws` dep — hand-rolled framing). The stub returns a SENTINEL
Sec-WebSocket-Accept a synthesized 101 could never produce, so the client
observing it proves verbatim 101 forward. Covers handshake + both-way frames
(AC1), the PTY route with hub-mint translation + ?ticket=/?cursor=/Origin
preserved (AC2), leak-free teardown on an isolated host (AC3), and the
scope-fence that a non-fleet-client relay never proxies the upgrade.
…undary dedupe (AC1/AC2/AC3/AC4)

The client relay's event stream sent no cursor and the engine's multiplexed
streams emit id: undefined, so a tunnel blip dropped every event in the gap.
The engine's per-session route /api/session/{id}/event?after=<seq> is resumable
(replay durable events after an aggregate seq) but unused.

Make the relay resume it losslessly, client-side (no engine change):
- SessionEventResume tracks the last delivered aggregate seq PER SESSION
  (per aggregate, never one global cursor), carried across relay reconnects.
- HubProxy injects ?after=<seq> on a per-session reconnect and routes the SSE
  body through a byte-verbatim dedupe/track filter (a replayed seq at or below
  the resume point is dropped — idempotent resume, no double-delivery).
- SSE headers are flushed on connect so a fully-caught-up resume still opens
  the stream (Node sends headers lazily; a zero-byte resume would otherwise
  hang the client).
- The non-resumable /event and /global/event streams have no cursor form and
  are LEFT AS-IS (never given ?after=, never assumed resumable) — documented
  out of scope in ADR 0024; making them resumable is separate engine work
  coordinated with #775, not forked from it.

Only armed for a fleet CLIENT (never-fork); the engine-armed base machine
falls back to its local engine on hub-down, so its steady state is unchanged.

Tests (test/fleet_sse_resume.test.ts, reusing #1261's stub-host harness):
AC1 drop-mid-stream loses zero events, gap replayed, boundary deduped;
AC2 per-session cursors resume independently (A@3, B@7);
AC3 /event + /global/event never given ?after=; AC4 steady-state byte-verbatim.
…p — single writer, transition-only (AC1/AC3)
…ed render (AC1/AC2/AC3)

The relay's FleetPostureDetector computes the hub-up-but-slow DEGRADED steady
state the extension's own up/down probe cannot; #780 left feeding it into the
posture state file as an explicit seam. Wire it THROUGH #780's single writer:

- fleet_posture_feed.ts: the RELAY→WRITER seam. Maps the relay posture
  vocabulary (fleet|degraded|standalone) onto #780's PostureFacts and drives
  the SAME FleetPostureStateWriter — no fs write of its own, no second writer.
  Transition-only is the writer's own signature discipline. degraded is
  hub-UP-but-slow (reachable=true), never rendered as fell-back.
- extension.ts: checkFleet's fleet/standalone writes and the manual
  goStandalone fallback both go through the seam (recordPostureState) — one
  fact-builder, the one writer. goStandalone now persists standalone posture
  so the render stops claiming 'attached' the instant the user chooses
  standalone (AC1: never a stale attached claim).
- stack_state.test.ts: locks the honest DEGRADED render (machine, hub,
  reachable-but-slow, timestamped, never as-if-attached) and OS-neutral copy
  (no launchd/plist) — coverage the relay-distinctive state lacked. #780's
  renderPostureLines already handled it; these guard it.

AC1 render for fleet/standalone/missing/corrupt/stale was #780's; this adds
the degraded-state coverage + the relay feed. The client-relay boot that
co-locates the detector with checkFleet is a later #792 world-merge.
The pluggable-transport seam (ADR 0024) as a new module, fully unit-tested:

- FleetTransportProvider interface (the Data Contract the tailscale/direct
  slices consume): resolveBaseUrl() -> URL|undefined, health() -> status.
  undefined base URL = the honest 'no forward bound' (hub-down), never a
  fallback to another provider's URL.
- createSshProvider: the current launchd/systemd -L forward behind the seam.
  resolveBaseUrl() is the loopback hub URL (read late); health() is an active
  probe (reachable+latency+version | unreachable+reason). (AC2)
- resolveFleetTransportKind: the amicode.fleetTransport selector — default ssh,
  independently disableable, a NAMED not-ok for a disabled/unshipped/unknown
  kind, NEVER a silent cross-provider fallback. (AC3)
- transportHealthToOutcome: the SHARED drop-vs-slow-link posture contract this
  slice OWNS — maps any provider's health into the existing FleetPostureDetector
  (reachable -> responded -> p95 decides degraded; unreachable -> no-response ->
  streak decides hub-down); the provider kind rides the reason. (AC4)
- sshForwardArgs + systemdTunnelUnit: the ssh provider's OS forms — the Linux
  systemd form of the launchd plist, loopback-only -L on both ends (ADR 0002/0005).
- hubUrlStringFromProvider: the byte-identical adapter back to the getUrl seam.

Provider-conformance tests reuse the #792 relay stub-hub contract (test/support/
stub_hub.ts). 23 tests.
…no fallback (AC1/AC3)

startAmicodeService constructs the transport provider from the fleetTransport.kind
setting (default ssh) and derives hub.getUrl from provider.resolveBaseUrl() through
the byte-identical adapter:

- unset/ssh reproduces today's launchd-forward behavior exactly — the 13
  existing activation-wiring tests stay green (the no-regression gate). (AC1)
- a not-yet-shipped provider (tailscale/direct) binds NO base URL -> the honest
  'hub upstream not available' 503, NEVER a dial of the configured ssh URL
  (no silent cross-provider fallback). (AC3)
- the boot log names the transport selection outcome (never a silent no-op).

The late-bound hub-URL getter is preserved (a de-armed activation still yields
the honest upstream absence). 3 new end-to-end wiring tests; 16 total in the file.
… (AC1)

- package.json declares amicode.fleetTransport (enum ssh|tailscale|direct,
  default ssh) — the one knob; the description states the no-fallback law and
  that unset/ssh reproduces today's behavior.
- extension.ts reads it (readFleetTransportOption) and passes fleetTransport.kind
  to both startAmicodeService call sites. Empty/unset -> the wiring defaults ssh.
…ifecycle

The Data Contract lists resolveBaseUrl() + health() + start()/stop(). Add the
lifecycle members to the provider interface. The ssh tunnel is OS-managed
(launchd/systemd, installed by the fleet installer) in this build, so its
start()/stop() are honest no-ops that defer to the service manager — the seam
members exist so the tailscale/direct slices (which own their lifecycle)
implement them behind the SAME interface. 24 tests.
…bind preservation (AC tailscale slice)

The host runs `tailscale serve` fronting its loopback service; the client
resolves the host's MagicDNS origin as the base URL. createTailscaleProvider
implements the SAME FleetTransportProvider interface as ssh (resolveBaseUrl →
the MagicDNS origin, read LATE; health() an active probe; start/stop honest
no-ops — serve is host-side, tailscaled is a system daemon). MagicDNS + the
`tailscale serve` mapping are modeled behind injectable seams (no `tailscale`
binary in CI), exactly as the ssh provider models its `-L` forward.

- resolveFleetTransportKind ships `tailscale` in the available set; it is
  independently disableable. `direct` remains the unshipped seam member that
  pins the no-cross-provider-fallback law.
- transportForSelection maps a selection → ITS OWN provider (tailscale → the
  tailscale provider, never a silent ssh substitution); the host wiring uses it.
- tailscaleServeMapping fronts the MagicDNS origin onto a LOOPBACK target
  (127.0.0.1), so the host engine/service bind stays loopback and the mutation
  guard never trips — asserted TOGETHER with MagicDNS resolution in the
  bind-host guard suite, with the rejected direct-tailnet-bind (100.x) contrast.
- health() reuses transportHealthToOutcome (the ONE shared posture contract);
  an unreachable host is the honest hub-down, never a reroute to ssh.
- #775 front-door composition: the serve target is the same loopback front-door
  endpoint the ssh forward reaches, never a bypass.
- Refactor: extracted the shared probeTransportHealth (ssh + tailscale).
…e / systemd WantedBy+Restart, run the #955 runner not a bespoke serve (one writer, ADR 0005)
…only) — launchd/systemd-user unit runs the #955 runner with RunAtLoad+KeepAlive, one writer (ADR 0005), never-fork gate on the parsed role (ADR 0023)
… supersedes 0005 Remote-SSH rejection; flip gated on FSP (#1267) + UI-kind extension split
…s host files (with the mandatory capability label)

In fleet-client (thin-client) posture, mount a dedicated amico-host://
FileSystemProvider so the native Explorer, open-in-editor, and save operate
on the HOST filesystem over the ALREADY-PROXIED /amicode/* relay data plane —
reversing nothing about the thin-client posture (a new host-file VIEW, not a
file: override).

Design (per the terrain's honesty constraint):
- host_file_client.ts — the PURE core (no vscode, NO node:fs): path→route
  mapping, auth injection, response decode, error→typed-code translation. The
  READ plane (list/read/stat) maps onto the REAL vault-browser contract the
  engine already serves (GET /amicode/vaults + /amicode/vault-files +
  /amicode/vault-file) so AC1/AC2-read/AC5/AC6 are backed by the real engine.
  The WRITE plane (write/rename/delete/mkdir) maps onto the INTENDED host
  mutation routes the engine does NOT yet serve; an absent route 404s and the
  client surfaces an explicit RouteAbsent — never a fabricated success, never a
  local write. Having no node:fs at all makes a silent local fallback
  STRUCTURALLY impossible (AC5): baseUrl()===undefined throws HubDown before any
  fetch.
- mount_policy.ts — the dedicated scheme, the fleet-client gate (AC6), and the
  MANDATORY disclosure label (AC4: Explorer=host while terminal/LSP/SCM/search=
  local, points to Remote-SSH).
- mount.ts — the injectable, vscode-free wiring seam (register provider + add
  folder + show label), so AC4/AC6 are pinned without the editor host.
- provider.ts — the thin vscode.FileSystemProvider adapter: HostFsError →
  vscode.FileSystemError (HubDown → Unavailable, honest degraded posture).
- extension.ts — wires the mount ONLY in the fleet-client attach branch, base
  URL = the tunnel origin (undefined when down → honest hub-down), auth = the
  relay-translated header. Opt-out via amicode.fleet.hostExplorer.

Tests reuse and EXTEND the stub-hub harness (Testing Decisions) with the host
file routes; hostReadOnly emulates the current real engine (mutation routes
404) to pin the honest RouteAbsent path. 33 new tests; neighboring
fleet/relay/posture/proxy suites unaffected; typecheck clean.

HONEST GAP (host-side follow-up): the real engine has NO generic host
write/rename/delete/mkdir route today — only the vault-browser read routes.
AC2-write/AC3 are green against the stub that serves the intended routes; they
go green against the real engine once the host adds POST /amicode/vault-file{,
/rename,/delete,/mkdir}. Until then the provider surfaces RouteAbsent honestly.

Part of #792. Refs #1267, #1262.
…cing

Window mode (Remote-SSH vs editor-local) is an axis ORTHOGONAL to the
link-health posture (#780). Overloading the posture field would break its
single-writer / transition-only contract and SILENTLY SWALLOW a
window-mode-only change (the mode flips while hub reachability is unchanged,
so the posture signature never moves). So window mode gets its OWN field, its
OWN writer with its OWN transition signature, and its OWN state file.

- src/fleet_window_mode_state.ts (new): WindowMode = "remote-ssh" | "local"
  (AC1: never the posture `standalone` token). WindowModeStateWriter mirrors
  #780's FleetPostureStateWriter — injectable clock/sink, atomic write,
  never-throw, transition-only — but its signature is the WINDOW-MODE VALUE
  ALONE, independent of the posture signature (AC2). windowModeFromRemoteName
  is a PURE derivation from vscode.env.remoteName (ssh-remote* → remote-ssh,
  else local) so it TDDs with no live editor. State file:
  ~/.amico/ops/fleet/window-mode.json (env AMICO_FLEET_WINDOW_MODE_STATE).
- opencode-plugin/stack_state.ts: read the window-mode file shape-tolerantly
  and render one honest, timestamped line in ## Fleet (live) (AC3 — agent
  context). Additive & optional: absent/corrupt/unknown → nothing.
- src/status_bar.ts: statusBarLabel gains an optional windowMode param that
  reflects a remote-ssh window in the text + tooltip; local/undefined leave
  the label unchanged (AC3 — status). StatusBarManager.setWindowMode wires it.
- src/extension.ts: at activation, derive the window mode from
  vscode.env.remoteName, record it via its own writer, reflect it in status.
  Runs for EVERY window regardless of fleet role.

The link-health posture writer/signature is UNTOUCHED (AC4) — verified by a
cross-writer test: a window-mode-only transition writes via the window-mode
writer while the posture writer, fed unchanged posture facts, does NOT write.

Judgment calls:
- Signature = window_mode value only; remote_name is provenance recorded in
  the file but EXCLUDED from the signature — re-attaching to a different SSH
  host at the same window mode is not a transition.
- A corrupt/unknown window-mode file renders NOTHING (unlike posture it is not
  a "role is config, not live" correction, so it emits no degraded note).

Tests: +19 (fleet_window_mode_state) +5 (stack_state surfacing) +4 (status_bar).
Fast gate green (only pre-existing skill_freshness_orchestrator failures remain);
typecheck clean.
…e hub, never forks (+ relocation audit template)

Slice #1270 (ADR 0025): the walking skeleton for host-side attach over
Remote-SSH. The never-fork guarantee ALREADY exists via adoptOrSpawn + its
activation wiring (extension.ts:986 surfaces foreign-error/incompatible-error
and skips the spawn) — so the honest, correct delta is the PROVING scenario
test, not an invented change. No production code changed; no gap found.

- test/adopt_live_server.test.ts: add the #1270 host-side-relocation scenario
  (4 tests), composing the three seams end to end — windowModeFromRemoteName
  (ssh-remote -> remote-ssh, the relocation signal, #1272) + divertToFleetRelay
  (hub host is role=server -> no relay divert, proceeds to adopt) + adoptOrSpawn
  against a live durable-hub survivor. Asserts AC2 (adopts at the handshake; no
  second engine, no second store) and AC3 (unadoptable handshake -> surfaced
  foreign-error, never a silent cold-spawn). Reuses the #1185 fixtures verbatim.

- docs/fleet/relocation-audit-1270.md: the AC4 HITL review artifact — a TEMPLATE
  enumerating each surface (activation, engine adopt/spawn, unadoptable-surfaced,
  durable-hub service, native Explorer, terminal, runs, chat) with PASS/BROKEN
  boxes + the exact manual check for a live Remote-SSH window. No results
  invented; awaits a live run.

AC1 (runtime host-side activation) is runtime/HITL — not unit-testable here;
covered by surface 1 of the audit template.

Gate: 152 fleet/adopt/never-fork suite tests pass (4 new); typecheck clean.
Pre-existing skill_freshness_orchestrator failures (6) unrelated to this slice.
…e projection

Add 'Amicode: Connect to Hub over Remote-SSH' (amicode.fleet.connectRemoteSsh):
resolve the hub coordinates from the projection via readFleetTopology (the ONE
topology reader, ADR 0023 — never a hand-built host string or a second config
source) and open a Remote-SSH window onto the hub workspace.

- src/fleet_connect_remote_ssh.ts: pure resolveRemoteSshTarget (canonical coords
  + workspace path -> vscode-remote://ssh-remote+<alias>/<path> URI, or a typed
  cannot-resolve reason), the resolveRemoteSshFromTopology adapter (absent/broken
  surface the reader's own actionable message verbatim), and the thin
  connectToHubOverRemoteSsh handler (opens exactly once on success; on every
  error path shows the honest message and opens nothing — no half-window, AC2).
- extension.ts: register the command; read the remote folder from the new
  setting; log the outcome to the opencode channel.
- package.json: declare the command + amicode.fleet.hubWorkspacePath.

Workspace-path decision: the projection's canonical carries {host,port,sshAlias}
only (no remote path), so the folder defaults to the hub home ~ (where ~/.amico/
lives), overridable via amicode.fleet.hubWorkspacePath; a configured-but-relative
path is an honest AC2 error. The typed RemoteSshResolution (authority/path/uri)
is the stable contract #1274 will drive programmatically.

Tests: test/fleet_connect_remote_ssh.test.ts (22, green). Typecheck clean.
…ove + honest thin-client fallback (no-regression baseline)

#1268-S3. A COMPOSITION / proving slice in the shape of #1270 — it adds NO
production code. The single-writer guarantee (ADR 0025 / ADR 0005) is already
carried by the primitives; this pins the cross-posture scenario that composes
them, so a regression that forked a second writer or invented a new degraded
fallback path fails here.

AC1 — single-writer across the move (asserted): a round-trip thin-client ⇄
Remote-SSH move composes windowModeFromRemoteName (relocation signal) +
divertToFleetRelay + adoptOrSpawn against a live password-armed durable-hub
fixture. One shared spawn counter across every step stays 0: host-side the
window reads role=server → does NOT divert → ADOPTS the hub (never a second
engine/store); client-side it DIVERTS to the relay → spawns nothing. Exactly
one canonical writer (the hub) at every step. The unhappy edge (stale/
unadoptable handshake) SURFACES foreign — never a silent cold-spawn of a rival.

AC2 — Remote-SSH unavailable/unconfigured → the EXISTING thin-client posture:
a not-ok resolution (no-ssh-alias unconfigured; open-failed unavailable, via
connectToHubOverRemoteSsh which never throws; topology-absent) leaves a client
diverted to the relay (divert=true) — no new state, no second writer. The
NAMED no-regression baseline is test/fleet_client_relay.test.ts (the relay
contract: UI-local, engine-data-plane proxy, credential translation, honest
hub-down 503, NO local engine); this slice does NOT modify or fork it — the
gate runs it green alongside, unchanged.

Reuses the #1185 live-server fixtures verbatim (ephemeral port, tmp handshake —
never 43117, never the real ~/.amico). Non-vacuity confirmed by a mutation
check (adopted→cold-spawned / spawns 0→1 goes RED). Gate: 131/131 across the
fleet/adopt/never-fork/relay suites + baseline; typecheck clean.
… go/no-go

New workspace package `packages/companion` (`amicode-companion`,
`extensionKind: ["ui"]`) — the always-local companion that activates on the
CLIENT and stays alive while the main extension relocates to the host under
Remote-SSH. Load-bearing feasibility gate for ADR 0025's default-flip (P2).

Proves the three unit-testable feasibility halves (25 tests, all green):
- AC1: `activate()` runs client-side, registers `amicode.companion.reopenWindow`,
  wires the probe + reopen (src/companion.ts). "Survives live relocation" is
  runtime/HITL — recorded in the go/no-go, not faked.
- AC2: client-side probe (src/probe.ts) — reads ONLY the client hubUrl setting +
  an injected/global fetch, hits /global/health, host-independent by construction.
  Minimal seam #1275 bundles the full detector on top of.
- AC3: pure bidirectional reopen URI builders + the
  executeCommand("vscode.openFolder", …) wiring (src/reopen.ts). Live reopen is
  runtime.
- AC4: docs/fleet/p2-companion-go-no-go-1274.md — the ADR 0025 P2 kill-switch,
  proofs cited, live proof + final feasible/abort decision PENDING human (NOT
  pre-decided).

Never-fork preserved: the companion holds no engine/store (zero runtime deps).

Judgment calls:
- Duplicated the ~10-line pure Remote-SSH URI builder from the main extension's
  fleet_connect_remote_ssh.ts rather than importing — the companion is a separate
  package and must not take a cyclic dep on the workspace extension. Contract
  fields kept identical so #1275 can extract a shared helper trivially.
- Second VSIX is a NEW release-matrix artifact: deliberately NOT wired into
  release.yml / the main VSIX / packaging (flagged to release owners in the doc).
  Inert to vsix-gate + packaging.test.ts.
- pnpm-lock.yaml updated (new importer) so `pnpm install --frozen-lockfile` (CI)
  passes; companion has working build/typecheck/test for `pnpm -r`.

Part of #1269 · ADR 0025 (P2).
@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor Author

Carries the fix for #1649 — the thought rail's foreground-subagent liveness (the fifth root of the #1617 staleness family).

Commit 4b4dbf21. The rail's running dot was driven by the RAW session_status[parent] busy leaf, which a foreground subagent leaves un-refreshed (parent blocks in the task tool; neither the turnActive nor streamActiveParts floor applies) and a reconcile can stray-idle. Fix adds a reactive ancestor-of-active-child floor (store leaf session_child_active, propagated like diff_version) and points the rail's turnIsRunning at the floored session_working instead of the raw leaf. Client-side only.

Gates green locally: app tsgo clean for the 4 touched files (one pre-existing unrelated error on this branch at session-composer-region-controller.ts:144); fast suite 314 files / 5198 tests 0 fail; drift-gate PASS (913 files); binary builds (dev/1.18.30). New tests server-session-child-active.test.ts 6/6. Remaining human check: launch a subagent and confirm the rail stays live through the child run.

amicode-ci and others added 9 commits September 29, 2026 22:14
… remote delete

The titlebar Sessions flyout had three defects on remote (peer) rows:

- Laggy list: the <For> rendered every loaded session across all projects
  plus all fleet peers; `activeLimit` only bounded the backend fetch, never
  the DOM, so hundreds of rows each carried the per-row
  useSessionTabAvatarState subscriptions and re-diffed on every 3s fleet poll.
  Slice the rendered list to `activeLimit()` (after the search filter, so
  search still spans everything loaded) via `pagedActiveSessions`, and
  redefine `hasMoreActive` to mean "rows exist beyond the slice, or the fetch
  cap was hit". Caps the DOM at 50 rows (+50 per Show more).

- Archive overlaid the machine tag: the owner tag now fades out on
  hover/focus so the right-edge affordance never covers it (active + archived
  remote rows).

- Delete on active remote rows: removed the owner-routed remote-delete
  arm→confirm + its plumbing (onRemoteDelete, remoteDeleteSession). Remote now
  matches local — archive from the active row, delete from the Archived tab
  (deleteArchivedSession is already owner-routed + control-gated).

Also fix a latent bug the overlay typecheck gate surfaced:
session-composer-region-controller.ts called `resolve` on the
directory-scoped session store, which only re-exported `sync`. At runtime the
#1647 S7 owner-side archived-flag convergence threw every 5s (caught/ignored)
and never fired. Add an info-only `resolve` passthrough to directory-sync.ts
delegating to serverSync.session.resolve (NOT `sync`, which force-reloads
messages and would disrupt a live stream).

Manifest refreshed for the three overlay files (drift gate verifies).
Two defects on tagged (peer/owner) rows in the titlebar Sessions flyout:

- Hover flash: while the flyout is open, the 3s fleet poll re-fetches the
  projection and mergePeerSessions mints a NEW object for every peer/owner-
  tagged row on each fetch. SolidJS <For> keys by referential identity, so a
  new reference at an index disposes + remounts the row — resetting its CSS
  :hover state and replaying the machine-tag opacity transition under a resting
  pointer. The archived tab hits the same on its own 5s poll. Add a pure
  stabilizeSessionIdentity(next, cache) helper: an id-keyed cache that returns
  the PREVIOUS row object when a row's render signature is unchanged, so
  unchanged rows keep their reference (and DOM node) across the poll. The
  signature (sessionRowSignature) excludes time.updated — it churns per streamed
  token but changes nothing the row paints — and tracks title/directory/owner/
  control. Wired into the active (activeIdentityCache) and archived
  (archivedIdentityCache) memos feeding <For>; component-scoped so it survives
  memo re-runs; self-evicts departed ids.

- Owner tag vanished on hover: instead of fading the machine tag to nothing to
  reveal the action cluster, teleport it — the resting badge still fades out,
  and the SAME tag reappears inside the (fade-in) action cluster to the left of
  the archive/delete controls. Reads as the tag sliding aside for the action,
  not disappearing. Applied to the active row (local + remote clusters) and the
  archived row; marked data-slot="…-teleported".

Tests: 5 stabilizeSessionIdentity/signature unit tests (same-ref-on-unchanged,
new-ref-on-content-change, time.updated is ignored, order + eviction) and
source assertions for the teleported badges + the identity wiring. Manifest
refreshed for the four overlay files (drift gate verifies).
…n hover

Follow-up to 62e45b6, which fixed the wrong way: it faded the resting tag out
and rendered a SECOND, max-w-[40%]-truncated copy inside the archive's own
absolute cluster — so the tag AND the archive appeared to jump into place, and
the copy was shortened.

Now there is ONE tag element that stays full length and slides left by exactly
the action cluster's rendered width, with the archive pinned at the row's right
edge (it never moves). The cluster is no longer an absolute right-edge overlay;
it reveals IN FLOW via a CSS-grid `grid-cols-[0fr]→[1fr]` column, so it reserves
the controls' exact width — the row button (now flex-1, was w-full) shrinks and
pushes the tag over by precisely that amount. Width-agnostic: no hardcoded pixel
shift (the large icon-button width lives in a compiled stylesheet). The tag is
never faded and never truncated on hover; the title gives up the space instead.
Applied to the active row (local + remote clusters) and the archived row.

The #1652 identity-cache flash fix from 62e45b6 is unchanged.

Tests updated: one owner badge per row (no teleported copy), the tag is not
faded on hover, and the cluster reveals via the 0fr→1fr grid (the old absolute
overlay is gone). session-header 18/18; fleet-peers/dropdown/archived 100/100;
overlay typecheck + drift green. Manifest refreshed.
…leet-main-v3

# Conflicts:
#	packages/schema/src/index.ts
…orded

Three code comments and one doc line added by the feature branch still
referenced the deleted amico-run bin. Reworded to the package-spec form
(@amicode/amico-run) or the five-verb surface (amico run) so the #1667
retirement test and #1668 doc-residue lint pass post-merge.
…n-v3

Reconcile feature/free-tier-fleet with main (v3)
… not sync (#1646)

Root cause of the local-archive lag: every archive handler ended with session.sync(id,{force:true}) whose only job is to flip the composer's archived read-only banner (metadata). But sync(force) also re-fetches the viewed/warmed session's full message page + every parent (the #1297 chain) — seconds on a loaded LOCAL session, instant on a REMOTE one with nothing loaded. That asymmetry was the reported bug.

Swap all 4 archive handlers (dropdown, home flyout, Rail, chat menu) and 2 unarchive handlers (chat menu, composer banner) to the info-only resolve(id,{force:true}) — session.get -> remember, no message reload — matching the composer controller's own convergence. timeline/model.ts forceSyncViewed stays on sync(force): it is the SSE-reconnect transcript self-heal where the reload is the point, not a flag flip.

Tests: 4 source-assertion files retargeted to resolve + a negative assertion locking out the heavy sync(force). manifest.json refreshed for the drift gate.
…wn; drop subagent rows

Root cause (measured in headless Chromium against the live service): the
dropdown merges the CACHED fleet projection, and allSessionsFromProjection
(bee23ff) keeps LOCAL rows too. After a local archive removed the row from
the local store, mergePeerSessions re-filled it from the stale projection as
a twin row until the next 3s poll landed — visible 1.3–2.2s. Remote archive
only felt instant because its path refetched the projection.

- Archive tombstones (hideArchivedTombstones / settleArchivedTombstones):
  hide the id at click, before the PATCH, for local and remote rows; retire
  when a fresh projection stops listing it, on unarchive, on a failed PATCH,
  or after a 15s TTL. Local archive now refetches the projection too.
  Verified: row gone in 4–10ms and never resurrected (was 1.3–2.2s).
- Projection extractors drop CHILD sessions (parentID), mirroring roots():
  subagent sessions were flooding the dropdown (76 of 100 rows).
- Dropdown + Home archive splice the row from its directory store instead of
  re-fetching and reconciling the whole session list (wasted work).
amicode-ci and others added 16 commits October 11, 2026 22:47
…rol (#1764)

Two review follow-ups to the FLEET header work:

- Add machine now routes BOTH standalone and empty section states to
  /create-a-fleet (no fleet exists yet, so start one); populated,
  unreachable, and the not-yet-posted case keep /add-fleet-device.
  Unreachable stays on add-fleet-device because a down host can't be
  known to be empty.

- Remove the per-row focus control (the target-glyph button + its CSS)
  for now — it did not earn its place in the UI. The focus SUBSYSTEM is
  kept intact: the focus-machine message/handler, FleetFocusStore,
  applyFocusRequest, focus-follows-tab, the overlay push, and the
  focused-row marking all stay, so restoring the control is a re-add.

Tests updated accordingly; full fast suite green (5299 passed).
…-actions

feat(sidebar): FLEET header Add-machine + Refresh actions (#1764)
…point 1)

SessionOwnerMap.update() now also records each owned session's working
directory from the same owner-tagged projection entry it reads the owner
from (the entry's own `directory`, owner-tag fallback), byte-for-byte.
resolveDirectory() never invents one: unknown, unowned, empty, or a
pending create-time binding all resolve undefined. Existing owner-map
behavior is unchanged.
…1766, checkpoint 2)

New focused module fleet_session_status.ts:
- resolution: ids (trim / drop-empty / de-dup, 64-id cap) + the live owner
  map → per-(owner, directory) query plan in request order (16-query cap)
  + unresolved (unknown / no-directory / over-cap); never invents a
  directory.
- fan-out: each owner's /session/status?directory=<d> exactly once per
  distinct directory, reached as the fleet projection reaches it (local
  engine password or bare loopback; peer base URL + its own reader token;
  never the hub credential); concurrent, per-query timeout, 3 s overall
  deadline; entries validated against the engine's SessionStatus union;
  idle by absence for a successfully queried directory; honest omission
  + named per-source / per-directory reasons otherwise. Never throws.

Gate: test/fleet_session_status.test.ts (49 cases).
…nt 3)

Registered on every boot under the never-proxied /amicode/fleet/*
prefix. It reads the live owner map of whichever branch built one (the
premium staged plane or the observation-only base peer) via hoisted
deps, assembling peers / tokens / trust / blocked identities per request
exactly as /amicode/fleet/sessions does. Where no fleet-peer provider /
owner map exists it answers 404 {ok:false, reason:"no-fleet-peers"} —
the app's silent-fallback signal — instead of the generic no-route 404.
Auth is the shared service gate (identical refusal to fleet/sessions).

Gate: test/amicode_service_fleet_session_status_route.test.ts (namespace,
auth parity, not-found without fleet peers on 3 boot shapes, live read
on both owner-map branches, empty request).
…ckpoint 4)

- context/fleet-session-status.ts (pure, import-free): the route builder
  (one comma-separated `ids` param), the documented-shape parse (per-entry
  drop of invalid status / unresolved entries; whole-body fallback only for
  a non-documented top level), per-cause rate-limited diagnostics, and
  runFleetReseed: fetchedAt stamped before the request, 5 s client timeout,
  late-response discard, apply via the recency-guarded reconcile, or
  today's local /session/status re-seed (silent for 404 no-fleet-peers,
  diagnosed otherwise).
- server-session: pinnedIds() + onPinned() (0 → 1 pin edge).
- utils/amicode-fetch: amicodeGetResponse — amicodeGet's credential idiom,
  resolving status + body of a non-2xx (the not-served 404 names its reason).
- server-sync: reconcileFromStatus sends the pinned ids to the fleet read;
  the old local re-seed is kept, unchanged, as the fallback.
- manifest re-stamped (3 modified + 3 added overlay files).

Gate: fleet-session-status.test.ts, amicode-fetch.test.ts, the extended
#1637 reconcile tests in server-session.test.ts.
… checkpoint 5)

- context/fleet-reseed-scheduler.ts (pure, injected timers): the
  connected-edge and sync-gap triggers, a debounced newly-pinned trigger,
  `unknown` retries (2 s, ≤3 per trigger), and the 10 s self-heal tick
  (connected + visible + pinned, served machines only). Single-flight with
  pending-flag coalescing; viewed session first, then least-recently-
  reseeded; diagnostics after 3 consecutive all-omitted re-seeds and for a
  partial failure persisting 3 re-seeds per (owner, directory); teardown
  clears every timer and cancels the in-flight read. Once the read answers
  404 no-fleet-peers, the tick and pin triggers stand down (today's
  behavior); the edges still re-probe.
- runFleetReseed: injectable timers + a teardown signal (cancelled outcome).
- server-sync: the scheduler replaces the direct reconcile on the edges;
  session.onPinned feeds the debounced trigger; stopped on cleanup.
- end to end, two halves bound by one import-free fixture:
  extension/test/fleet_session_status_e2e.test.ts drives the REAL route
  against a stubbed peer owner, asserts its answer equals the fixture, and
  runs it through the app's own parse (imported across packages);
  fleet-session-status.e2e.test.ts (bun) takes the fixture through parse +
  runFleetReseed + the real ServerSession reconcile → busy in the store.
- manifest re-stamped (2 modified + 4 added overlay files).

Gate: fleet-reseed-scheduler.test.ts (fake clock) + both e2e halves.
The cold-owner-map retry budget is per TRIGGER (connected edge, sync gap,
new pin). Re-arming it on every 10 s self-heal tick made a permanently
unknown pinned id (a tab for a deleted session, or for a peer that stopped
serving) cost the tick's read plus three retries each interval: about four
fleet reads per 10 s instead of one. Only edge and pin runs re-arm now.

RED first: the new test measured 12 reads over 3 intervals before the fix,
and 3 after it. Found by CodeRabbit review on PR #1768.
…n-status

fix(fleet): remote session keeps its working rail + tab state after reopen (fleet-aware status re-seed) (#1766)
@jeonghun-jj-lee
jeonghun-jj-lee marked this pull request as ready for review October 12, 2026 15:42
@jeonghun-jj-lee
jeonghun-jj-lee merged commit cb3f6ad into main Oct 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment