Skip to content

fix(app): gate directory-store mutations for remote SSE events (#1539) - #1546

Merged
jeonghun-jj-lee merged 1 commit into
feature/free-tier-fleetfrom
opencode/1539-sse-fanin-badge
Sep 24, 2026
Merged

jeonghun-jj-lee merged 1 commit into
feature/free-tier-fleetfrom
opencode/1539-sse-fanin-badge

Conversation

@jeonghun-jj-lee

Copy link
Copy Markdown
Contributor

Summary

Gate applyDirectoryEvent and related directory-store mutations in server-sync.tsx so that SSE events relayed from remote fleet peers do not contaminate the local directory store — preserving fleet machine badges in the sessions dropdown.

The bug

The SSE fan-in correctly namespaces remote events via the composite cursor's id: field, but the app-layer event loop discarded that provenance before queuing. When both machines share the same workspace path, remote session.created/session.updated events matched a local child store's directoryKey and applyDirectoryEvent inserted the remote session into store.session — without the amicode_owner tag. On next dropdown open, mergePeerSessions deduplicates local-wins, discarding the correctly-tagged projection entry.

The fix

  1. New sse-origin.ts module — pure logic to resolve which namespace (machine) originated an SSE event by diffing the composite cursor against its previous state. Fleet-of-one (no = separator in the id) returns immediately with undefined origin — zero overhead.

  2. server-sdk.tsx — the SSE loop now calls resolveEventOrigin and threads an origin?: string field through QueuedServerEvent → the emitter → the listener.

  3. server-sync.tsx — four contamination points gated for remote-origin events:

    • homeSessions.apply (line 555)
    • indexSession calls from session.moved/session.forked (lines 593–601)
    • queue.push directory re-fetch triggers (lines 587, 614)
    • applyDirectoryEvent (line 618)

    session.apply/session.applyV2 are NOT gated — the per-session info and message caches the timeline reads from still process remote events.

Tests

  • sse-origin.test.ts — namespace extraction from composite cursor ids (local, remote, fleet-of-one)
  • event-reducer.test.ts — regression: reducer not called for remote-origin events
  • session-fleet-peers.test.ts — regression: dedup when session exists in both local store and projection
  • session-header-provenance.test.ts — badge persistence after simulated remote SSE events

Closes #1539

The SSE fan-in relays remote peer events into the app's single /event
SSE connection. The fan-in correctly namespaces events via composite
cursor id: fields, but the app-layer event handler was applying ALL
events to the directory store regardless of origin — contaminating
store.session with un-tagged remote sessions that mergePeerSessions
then preferred over the correctly-tagged projection entries.

Two changes, one invariant (the directory store is local-only):

Change 1: Propagate origin namespace through the event queue
- New sse-origin.ts module with parseCursorNamespaces() and
  resolveEventOrigin() — pure functions that diff the composite cursor
  id: field to determine which SSE namespace (local vs remote peer)
  originated each event. Fleet-of-one (bare scalar id) returns
  immediately with zero overhead.
- server-sdk.tsx wires origin resolution into the SSE loop: the
  ServerEvent type gains an optional origin field, set only for remote
  events.

Change 2: Gate directory-store mutations in the listener
- server-sync.tsx checks event.origin and skips for remote events:
  - applyDirectoryEvent (the primary contamination point)
  - homeSessions.apply (feeds into dedup list)
  - indexSession (synthetic session.created from session.moved/forked)
  - queue.push (triggers directory re-fetches)
- session.apply/session.applyV2 still process remote events (needed
  for per-session caches the timeline reads from)

Tests:
- sse-origin.test.ts: 12 unit tests for namespace extraction and
  origin resolution (fleet-of-one, composite local, composite remote,
  multi-peer, edge cases)
- session-fleet-peers.test.ts: regression guard for the contamination
  scenario + badge persistence when projection is sole source
- event-reducer.test.ts: gate contract tests documenting what the
  gate prevents
- session-header-provenance.test.ts: badge persistence across
  repeated dropdown opens
@jeonghun-jj-lee jeonghun-jj-lee self-assigned this Sep 24, 2026
@coderabbitai

coderabbitai Bot commented Sep 24, 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: 4cef897a-143d-4574-ac93-a6322451f2fe

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.

@jeonghun-jj-lee
jeonghun-jj-lee merged commit e055927 into feature/free-tier-fleet Sep 24, 2026
12 checks passed
@jeonghun-jj-lee
jeonghun-jj-lee deleted the opencode/1539-sse-fanin-badge branch September 24, 2026 19:20
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