Repository navigation
Fix remote session SSE delivery + host-side presence awareness (#1564) - #1570
Merged
jeonghun-jj-lee merged 9 commits intoSep 26, 2026
Merged
Conversation
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
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
…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
merged commit Sep 26, 2026
fd5f41c
into
feature/free-tier-fleet
4 of 5 checks passed
2 tasks
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.
2 tasks done
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 wirereconcile()opens peer arms as they appear in the owner map/eventopens before the owner-map feed's first refreshA2: Fix dead arm zombie (#1566)
pump()now removes the dead source fromthis.sourceson exitreconcile()tick re-open the arm within 1 secondSafety valve (P1)
A3: Status reconciliation fallback (#1567)
Features (P1-P2)
B1: Remote-control presence indicator (#1568)
controlled_byoverlay when an active control grant targets the local machinedrivenByBanner()helper in session-fleet-peers.tsB2: Host auto-focus notification (#1569)
RemoteActivityNotifierwith dependency-injected VS Code notificationTest results
The single failure (production wiring timeout) is pre-existing on the base branch — confirmed by running the test on
origin/feature/free-tier-fleetwith no changes.