Audit finding 3.1 (high, 3/3 reviewer consensus). Event ids restart at 1
when the backend restarts; a browser auto-reconnect carrying the old
numeric Last-Event-ID was honored whenever the new process had already
emitted that many events, silently suppressing fresh events (same ids,
different content). The previous guard only caught cursor > lastId.
Wire ids are now "<generation>:<seq>" (generation = per-hub instance
token; seq = the existing per-session monotonic counter). The hub parses
raw header/query candidates itself: other-generation and legacy bare-
number cursors are stale → replay from the beginning; same-generation
cursors keep the newest-valid-wins behavior. EventSource treats ids as
opaque, so no frontend change.
Finding 3.2 (eviction) resolved by NOT evicting: close keeps the seq
counter on purpose (sessions reopen; monotonicity is what makes old
cursors detectable) — documented at the call site; buffers are emptied by
clear() and ring-bounded at 200.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Pi process produces events immediately after session creation, but
the browser's SSE connection may not be open yet (React re-render delay,
navigation). The hub discarded events with no subscribers, so the user
saw a blank session.
- Ring-buffer up to 200 events per session; replay on subscribe
- hub.clear(id) on POST /sessions/:id/close frees memory
- spawnFor now tears down any existing runtime for the same session id before
the cap check, so resume/respawn neither leaks the old child nor falsely hits
maxPiProcesses
- exit handler is identity-checked (captures rt) so a stale child's late exit
cannot evict a newer runtime
- SSE pending re-emit now sends the full ClientEvent shape
{ type: "ui_request", ui_request } to match hub.publish live events
- tests: same-id respawn replaces runtime (count 1); old child exit does not
evict new runtime; sse-hub re-emit asserts unified shape
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>