Skip to content

Fix remote session SSE delivery + host-side presence awareness (#1564) - #1570

Merged
jeonghun-jj-lee merged 9 commits into
feature/free-tier-fleetfrom
amico/issue-1564-remote-sse-fixes
Sep 26, 2026
Merged

jeonghun-jj-lee merged 9 commits into
feature/free-tier-fleetfrom
amico/issue-1564-remote-sse-fixes

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

Summary

Fixes the "Thinking churns forever" bug when prompting remote sessions, and adds host-side presence awareness for multi-machine collaboration.

Closes #1564, closes #1565, closes #1566, closes #1567, closes #1568, closes #1569

Changes

Bug fixes (P0)

A1: Fix SSE fan-in race at connection time (#1565)

  • SseFanInDriver.handle() no longer declines when zero non-local owners exist — it always accepts on the observation wire
  • Starts in fleet-of-one mode; reconcile() opens peer arms as they appear in the owner map
  • Fixes the permanent binding to local engine when /event opens before the owner-map feed's first refresh

A2: Fix dead arm zombie (#1566)

  • pump() now removes the dead source from this.sources on exit
  • Lets the next reconcile() tick re-open the arm within 1 second
  • Prevents silent permanent disconnection of peer SSE arms

Safety valve (P1)

A3: Status reconciliation fallback (#1567)

  • Starts a 30-second fallback timer on prompt submit
  • If the SSE resolution event never arrives, polls the session endpoint and reconciles the status
  • Cancelled when the SSE event arrives normally

Features (P1-P2)

B1: Remote-control presence indicator (#1568)

  • Fleet projection includes controlled_by overlay when an active control grant targets the local machine
  • Session tab shows an eye icon with "Driven by {machine}" tooltip for remotely-controlled local sessions
  • drivenByBanner() helper in session-fleet-peers.ts

B2: Host auto-focus notification (#1569)

  • RemoteActivityNotifier with dependency-injected VS Code notification
  • Rate-limited: one notification per session per 60-second burst
  • "View" action to open the session tab

Test results

Suite Pass Fail Notes
sse_fanin_aggregator 26 0 Unchanged
sse_fanin_driver_pump (new) 1 0 Dead arm recovery
observe_event_routing 5 1 Pre-existing production wiring timeout
merged_projection_controlled_by (new) 3 0 controlled_by overlay
remote_activity_notifier (new) 8 0 Notification + rate limiting
session-status-reconcile (new, app) 6 0 Timer lifecycle
session-fleet-peers + session-header (app) 55 0 B1 UI tests

The single failure (production wiring timeout) is pre-existing on the base branch — confirmed by running the test on origin/feature/free-tier-fleet with no changes.

amicode-ci added 8 commits September 25, 2026 21:28
The SseFanInDriver.handle() method declined (returned false) when the
SessionOwnerMap had zero non-local owner-peers. On the observation path,
the OwnerMapFeed's first refreshOnce() is fire-and-forget — if the app
opens /event before it completes, the driver declines permanently and
peer events never arrive.

Remove the zero-owner gate: handle() now always accepts (returns true).
Zero non-local owners starts in fleet-of-one mode where the aggregator's
§D4 byte-identity relay delivers local frames verbatim, and reconcile
opens peer arms as the OwnerMapFeed discovers them.

- Remove lines 306-311 from handle() (the owners filter + early return)
- Update JSDoc on EventFanInDriver interface and module-level comments
- Update server.ts dispatch comment and index.ts wiring comment
- Rewrite AC1 tests: zero-owner → driver accepts in fleet-of-one mode,
  verified via injected upstream (controllableSource) instead of oracle
- Rewrite lastEventID test: cursor reaches local arm via composite parse
- Add late peer discovery test (#1565 contract): zero-owner start →
  ownerMap gains a peer → reconcile opens the peer arm mid-stream
#1566)

When an SSE upstream source ended or threw, pump() exited without removing the
source from the internal Map. reconcile() skipped re-opening arms it found in
the Map, so a dead arm became a zombie that blocked reconnection indefinitely.

Fix: delete the namespace from this.sources after the pump loop exits. The next
reconcile() tick detects the gap and calls openArm() to re-establish the peer.

Test: sse_fanin_driver_pump.test.ts — end a peer source, tick, reconcile,
assert the arm re-opens and delivers new frames downstream.
Add RemoteActivityNotifier — the host auto-focus notification when a remote
machine starts driving a local session. The notifier is dependency-injected
(showInformationMessage, onViewSession, clock) so it is fully testable under
vitest without the extension host.

- Rate-limited: one notification per session per 60s cooldown
- Names the controlling machine and session title in the message
- 'View' action calls the injected onViewSession callback
- Falls back to sessionId when sessionTitle is absent
- Per-session rate-limiting (different sessions fire independently)

8 unit tests covering AC1–AC5.
When the optimistic 'busy' status is set on prompt submit but the SSE
resolution event never arrives (fan-in gap, network issue), 'Thinking'
persists forever. Add a reconciliation timer (30s) that falls back to
idle when the SSE event is delayed.

- New module: session-status-reconcile.ts — timer management with
  start/cancel/cancelAll/has exports
- submit.ts: start timer on setBusy(), cancel on setIdle() and command
  error handler
- server-session.ts: cancel timer in applyV2() when execution.succeeded,
  execution.failed, or execution.interrupted arrives
- 6 bun:test cases covering timer fire, cancellation, replacement,
  bulk clear, no-op cancel, and session independence
When a remote machine holds an active control grant targeting the local
machine, the fleet projection now stamps local session entries with
`amicode_controlled_by` ({ machine_id, machine_name }). The session tab
renders an eye icon with a 'Driven by {machine}' tooltip — the visual
reverse of the 'Driving {machine}' monitor icon for outbound control.

Extension side:
- Add `ControlledByTag` type and `resolveControlledBy` option to
  `FleetProjectionOptions` (merged_projection.ts)
- `tagSessionsWithOwner` stamps the overlay on LOCAL entries only
- Back-compat: absent resolver ⇒ no field

App side:
- Add `drivenByBanner()` and `drivenByBannerFromProjection()` to
  session-fleet-peers.ts (tolerant readers, never throw)
- Titlebar tab strip derives `drivenByMachine` from the shared control
  projection and passes it through to `TabNavItem`
- Tab renders a distinct eye icon (vs monitor for driving) with
  'Driven by {machineName}' tooltip

Tests:
- Extension: merged_projection_controlled_by.test.ts (3 cases)
- App: drivenByBanner tests in session-fleet-peers.test.ts (5 cases)
- App: wiring assertions in session-header.test.tsx (2 cases)
…te-sse-fixes

# Conflicts:
#	packages/app-bundle/manifest.json
@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: f88adb48-5f70-4965-bad7-bf14e5f91481

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

…ion stub path

13 test failures were caused by two issues:

1. AMICO_FLEET_MULTIPLEX=1 leaking from the host fleet environment into the
   test runner. This activated the session multiplexer code path in dispatch(),
   which returned 'local' for non-session paths like /amicode/vaults —
   bypassing the fleet proxy entirely. Fixed by saving/clearing the env var
   in beforeAll/beforeEach and restoring in afterAll/afterEach.

2. buildFleetProjection switched from /session to /experimental/session but
   the test stubs only handled /session. The /experimental/session requests
   fell through to the default handler (returning non-array JSON), so
   fetchSessions reported 'session list is not an array' and the owner-map
   never populated. Fixed by accepting both paths in the stubs.

Affected test files:
- fleet_client_relay.test.ts (6 tests)
- amicode_service_wiring.test.ts (1 test)
- amicode_service_attach_lifecycle.test.ts (2 tests)
- amicode_service_observe_read_routing.test.ts (1 test)
- amicode_service_observe_write_routing.test.ts (2 tests)
- amicode_service_observe_event_routing.test.ts (1 test)
@jeonghun-jj-lee
jeonghun-jj-lee merged commit fd5f41c into feature/free-tier-fleet Sep 26, 2026
4 of 5 checks passed
@jeonghun-jj-lee
jeonghun-jj-lee deleted the amico/issue-1564-remote-sse-fixes branch September 26, 2026 14:16
jeonghun-jj-lee pushed a commit that referenced this pull request Sep 27, 2026
…verlay typecheck

Base defect from #1570 (byte-identical to base HEAD): mock(() => {}) types
mock.calls[0] as [] so the .toEqual(["s1"]) assertion failed the fail-closed
overlay typecheck gate on feature/free-tier-fleet independent of #1583. Type the
mock param (id: string) so the assertion typechecks. Integration repair recorded
in the session ledger as a finding.
jeonghun-jj-lee added a commit that referenced this pull request Sep 27, 2026
* fix(app): client-side SSE liveness watchdog + wake re-arm (#1584)

The overlay app's live SSE stream had no client-side liveness detection: when
the socket goes silently half-open (sleep/wake, Wi-Fi blip, engine replaced
without FIN/RST), the v1 event iterator neither throws nor completes, so
onSseError never fires, the for-await parks forever, streamStatus stays
"connected", and the transcript freezes until a manual reload.

- New zero-dependency pure module stream-liveness.ts exporting
  shouldReconnectIdleStream (unit-tested in-place via bun:test) — reconnect iff
  connected AND now - lastFrameAt > staleMs (strict).
- Watchdog in createServerSdkContextBase: track lastFrameAt (set on connect and
  each frame), a 5s setInterval force-reconnects a stale-but-connected stream at
  30s by aborting the per-attempt controller (same generation reconnects with
  the lastEventID cursor — lossless); cleared in onCleanup. Intentional abort is
  treated as closed by the existing catch, so the banner does not flip.
- Wake re-arm in onMount: visibilitychange (visible + stale >10s) and online
  (when connected) force a reconnect via a shared forceReconnect() closure,
  guarded on started && !abort.signal.aborted. A healthy stream (fresh
  lastFrameAt) causes no reconnect on a quick tab toggle.
- Refresh manifest hashes for the edited/added overlay files.

* fix(manifest): restore correct hashes for server-session.ts + message-timeline.tsx

The slice-1 manifest patch corrupted two unrelated overlay entries to
stale pre-#1579 hashes, reddening drift_gate.mjs. Restore both to their
committed-disk hashes (disk is the source of truth). drift-gate PASS.

* fix(webview): entity rail self-heals /amicode/problem on stream reconnect (#1585)

Add pure shouldRefetchOnReconnect(prev, next) to the UI package's problem.ts
(true iff false→true — a rising edge after a real disconnect; initial connect
undefined→true does not refetch). Wire AmicodeEntityRail with an optional
streamConnected?: () => boolean prop and a createEffect that tracks the prior
value in a plain closure let and refetches on the reconnect edge, never reading
the resource's own state (no feedback loop). The app mount supplies the app's
live SSE status via serverSDK().event.status().

* fix(app): type pre-existing session-status-reconcile mock — unblock overlay typecheck

Base defect from #1570 (byte-identical to base HEAD): mock(() => {}) types
mock.calls[0] as [] so the .toEqual(["s1"]) assertion failed the fail-closed
overlay typecheck gate on feature/free-tier-fleet independent of #1583. Type the
mock param (id: string) so the assertion typechecks. Integration repair recorded
in the session ledger as a finding.

* fix(extension): drop unused isUnarmedHandshake import — unblock fast-gate typecheck

Pre-existing base defect (from #1579 lineage): the import at extension.ts:114
went unused when its last consumer was removed, so 'pnpm -r run typecheck'
(the CI 'fast' gate) failed with TS6133 on feature/free-tier-fleet, independent
of #1583. UNARMED_PASSWORD on the same line stays (used at ~L1307).

* test: make the extension suite hermetic against ambient dev env (#1589)

The suite inherits the Amicode terminal's operational env (AMICODE_SERVICE_AUTH=open,
AMICO_FLEET_MULTIPLEX=1, AMICODE_ENGINE_UNARMED, AMICODE_SERVICE_PORT, AMICODE_APP_DIST,
OPENCODE_DB, OPENCODE_CONFIG_CONTENT, ...) that CI never sets — flipping service auth
mode, fleet data-plane routing, app-shelf boot shape, and terminal env injection under
the tests, so ~17 tests red locally while green in CI.

- add test/setup_hermetic_env.ts (vitest setupFiles): snapshot + clear AMICO*/AMICODE*/
  OPENCODE* per file, restore on teardown; explicit per-test values still win.
- cli_gate: skip the really-staged-bins check on a staged LAUNCHER, not just bin/
  (bin/launcher/* are gitignored build artifacts; a fresh worktree false-failed it).

Verified: full fast suite green in a dirty dev env (4991 passed, 0 failed); CI clean-env
behavior unchanged.

---------

Co-authored-by: amicode-ci <ci@amicode.local>
Co-authored-by: amico-director <director@amico.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant