Skip to content

fix(server): enforce event stream ownership and diagnostics#37585

Open
armancharan wants to merge 26 commits into
anomalyco:v2from
armancharan:event-stream-ownership
Open

fix(server): enforce event stream ownership and diagnostics#37585
armancharan wants to merge 26 commits into
anomalyco:v2from
armancharan:event-stream-ownership

Conversation

@armancharan

@armancharan armancharan commented Jul 18, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #36445

#36445

Parent: #36441

Related: #36311 (minimal Bun SSE disconnect bridge on the event path only — not a platform-wide fix)

✧ raised · ○ in review · ● approved · ✓ merged

#36441 epic: scope streams + bound payloads

Merge in order:
v2
└ ○ #37486 location interest
   └ ○ #37487 session interest + fan-out bench
      └ ○ #37585 stream ownership / diagnostics ← this PR
         └ ○ #37586 reconnect herd

Siblings under the epic (not git parents of the stack tip):
└ ○ #37559 session blobs + payloadHash
└ ○ #37570 ensure-on-first-failure → feeds #37586

Stacked on #37487 (fix/event-subscription-sessions). GitHub base is v2 because the parent branch is fork-only; ownership-only diff: armancharan/opencode@fix/event-subscription-sessions...event-stream-ownership — retarget base onto that branch after #37487 merges (or when the branch exists on anomalyco).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Guarantees one active event stream per TUI generation, prompt resource release on failure/teardown, and content-free diagnostics for leak detection.

  • TUI ownership: monotonic generation + await prior subscribe loop before opening the next; abort request AbortController on every exit; reconnect wait 1000 + random(0..500) ms so N clients do not phase-lock
  • Promise SSE: abort signal cancels the body reader (and response.body) so Bun sees client disconnect under custom fetch
  • Server: EventFeed.diagnostics() counters (active / opens / closes / serializedEvents / serializedBytes / overflows); debug GET /api/debug/event-feed; IncomingMessage aborted/closeServerResponse.destroy() so acquireRelease runs (2.0: standalone SSE disconnect can wedge Bun server at 100% CPU #36311 bridge, event path only)

How did you verify your code works?

  • packages/server test/event-feed.test.ts — eleven-client churn → baseline; overflow counts; content-free dump
  • packages/server test/event-feed-fanout.bench.test.ts — from stack base: fan-out accounting laws (clients × sizes)
  • packages/server test/event-feed-ownership.test.ts — scope cancel → baseline; real-socket abort closes body; eleven sequential scopes never accumulate
  • packages/tui test/cli/tui/client-ownership.test.tsx — rapid interest ≤1 stream; teardown → 0; prior generation does not receive events after reconnect
  • packages/tui test/cli/tui/client-scope.test.tsx — connect / reconnect on change / no-op when unchanged (split)
  • typecheck in packages/server, packages/tui, and packages/cli

Acceptance → test

Claim Test
One mounted TUI → one stream; zero after teardown teardown leaves zero active event streams + rapid interest changes never exceed one active event stream
Reconnect churn never exceeds one stream per TUI rapid interest changes never exceed one active event stream
Eleven clients → eleven streams (no accumulation) returns active subscriber count to baseline when scopes close after eleven-client churn + eleven sequential subscribe scopes never accumulate active subscribers
Cancel real network client → response closed, baseline canceling a subscriber scope… + aborting a real Node HTTP client returns EventFeed.active to baseline
Prior generation does not receive events after interest reconnect event IDs from a prior generation are not delivered after interest reconnect
Diagnostics expose leaks without payloads returns active… dump asserts + overflow counters

Screenshots / recordings

N/A — not a UI layout change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

armancharan and others added 14 commits July 17, 2026 21:58
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Narrow unknown event data with object/`in` checks so sessionID is read without an assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>
Helpers only need directory; no test supplies a workspace.

Co-authored-by: Cursor <cursoragent@cursor.com>
No test supplies a workspace; avoid an unnecessary brand cast.

Co-authored-by: Cursor <cursoragent@cursor.com>
Filter interested subscribers once before encode, require directory for
location scope (reject workspace-only queries), and cover those cases
in tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collect matching subscribers first so encode/offer share one pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extract pure interest helpers and cover subscribeInput/equality plus
ClientProvider.event.scope reconnecting only when interest changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize TUI subscribe generations, cancel SSE readers on abort, bridge
Bun disconnects so EventFeed queues release, and expose content-free
feed counters for leak detection.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid a hard EventFeed.Service requirement on the debug handler so CLI
serve Effect inference stays compatible with the runtime handler map.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on my search, the PRs found are all explicitly mentioned as related work in PR #37585's description (parent stack and sibling work under #36441). These are not duplicates but intentionally related:

No duplicate PRs found

Comment thread packages/schema/src/event.ts Outdated
Replace the hand-rolled unknown guards with decodeUnknownOption so
ephemeral sessionID extraction matches host Schema patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
@armancharan
armancharan marked this pull request as draft July 18, 2026 02:09
armancharan and others added 3 commits July 18, 2026 12:10
Map HttpApi-decoded ctx.query through interestFromSubscribeQuery, and route
deepObject URLSearchParams through the same Schema before building Interest.

Co-authored-by: Cursor <cursoragent@cursor.com>
Also derive EventFeed.Diagnostics from the protocol Schema and lock the
generation boundary so prior-generation event IDs are not delivered.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/tui/test/cli/tui/client-scope.test.tsx
armancharan and others added 2 commits July 18, 2026 12:37
One test was asserting connect, interest change, and no-op scope together.
Split into three named cases with a shared mount helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@armancharan
armancharan marked this pull request as ready for review July 18, 2026 02:44
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread packages/tui/src/context/event-interest.ts
Comment thread packages/tui/test/cli/tui/client-scope.test.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant