perf(tiles): no SSE terminal stream for the focused tile while tiles own the terminal

With the grid open the page's SSE filter still named the focused tile's
session, so the server streamed that session's output over SSE as well.
The main terminal is parked (its socket closed, _wsReady false), so every
frame was JSON.parsed and then dropped by the park guard; the tile has the
same output over its own socket. The filter now names TILE_GRID_SSE_FILTER
(constants.js, a fixed id no session takes) while tiles own the terminal,
at both places that set it: the live re-subscribe every tile focus runs
(_updateSseSubscription) and the connect URL an SSE reconnect rebuilds
(connectSSE), through one helper, _sseFilterSessionId(). Leaving the grid
gives the filter back to the session shown (selectSession re-subscribes).

Why it is safe, server side: the filter is read in exactly one place,
SseStreamManager.flushSessionTerminalBatch. The connect route parses it,
POST /api/events/subscribe replaces it (updateClientFilter); broadcast(),
the multi-user ownership check (canDeliver), the heartbeat, the order and
tab-layout frames and the shutdown notice never read it, and no push,
viewing or acknowledgement logic does. The only page consumer of
session:terminal is _onSSETerminal -> _onSessionTerminal, a no-op while
tiles own the terminal.

Measured at checkpoint 1 (6 tiles, focused tile a printing shell): 16 to
18 frames/s, 2.2 to 2.4 KB/s parsed and dropped -> 0.

Live, this code (6 tiles, shells printing), SSE terminal frames per 5 s:
0 with the grid open; 0 after an SSE reconnect with the grid open (connect
URL sessions=tile-grid); 86 in the single view after closing the grid and
86 after a reload into it (that connect URL names no session, as before;
selectSession's re-subscribe names the shown one). Just before this commit:
18 frames/s, 2.4 KB/s. A tile focus runs no connectSSE and no handleInit;
it posts the grid id.

Tests: the page subscribes with the grid id on open and on every tile
focus, gives the session back on close and on a reload into the single
view, and connectSSE asks the same helper. Server, live, multi-user: the
id is taken on the connect query and on a re-subscribe, and then withholds
terminal output while session:updated and hook events still reach their
owner (and only their owner). Mutation-checked five ways (helper ignoring
the grid, connectSSE on the raw id, the server dropping non-UUID ids on
subscribe and on connect, the filter gating every event).

Scope: PR 2 (constants.js, the app.js SSE seam).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 07:11:08 +02:00
parent d7f6047529
commit 50e0d22def
4 changed files with 323 additions and 2 deletions
+8
View File
@@ -1662,6 +1662,13 @@ const TILE_GRID_WIDE_3X1 = 1800;
const TILE_SCROLLBACK = 10000;
// Tiles have their own per-device font size (a tile is a fraction of the screen).
const TILE_FONT_SIZE_DEFAULT = 13;
// What the page's SSE filter names while tiles own the terminal: a value no
// session id takes (ids are UUIDs), so the server, whose filter gates only
// session:terminal batches, sends none. The tiles carry their own output over
// their own sockets, and the parked main terminal only parsed those frames to
// drop them (16 to 18 a second for one busy shell). Every other event still
// arrives (test/sse-tile-grid-filter.test.ts pins the server's side of this).
const TILE_GRID_SSE_FILTER = 'tile-grid';
/**
* Columns and rows for `count` tiles, by count (the spec's table), and whether
@@ -2156,6 +2163,7 @@ if (typeof window !== 'undefined') {
TILE_MIN_H,
TILE_SCROLLBACK,
TILE_FONT_SIZE_DEFAULT,
TILE_GRID_SSE_FILTER,
};
window.CodemanRenderLiveness = { shouldKickRenderer, RENDER_STALL_MS, RENDER_LIVENESS_POLL_MS };
window.CodemanFetchDeadline = {