Repository navigation
perf(sessions): drop spinner-only screen frames and alias duplicate strings on the wire - #761
Conversation
… duplicate strings Refs #746 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
session:screen was the highest-volume IPC channel and almost all of it was redundant: one recorded Claude session produced 4,728 frames in 13 minutes and 99% of consecutive frames differed only in the spinner glyph, the elapsed timer, the token counter or the /rc connecting… blink. Each one was cloned through the coalescer, committed as a new runtime object in the renderer, forwarded to the remote client and stringified by the recorder. The headless terminal's exact-equality gate can never fire while a spinner animates. The manager's re-emit — the single feed for the forwarder, the remote server and the recorder — now compares frames after normalising the recorded volatile shapes and drops a frame whose normalised text equals the last emitted one. Emitted frames stay raw, so consumers still see the real spinner on every real change; the raw latest is still cached for MCP and debug readers; condition parsers, composer-ready and the prompt gate run before the manager and are untouched. A perf-journal sample per emitted frame records the run of drops it ended. Refs #746, #390 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…wire An alt-screen TUI has no scrollback, so recent equalled plain (and recentMarkdown equalled markdown) in 100% of recorded Claude frames, and Electron's structured clone serialises each string occurrence separately: every such frame crossed IPC at twice its size. The forwarder now omits the duplicate fields at the renderer edge and the preload expands them back, so no renderer type or consumer sees the optional form. The remote server keeps the full payload from the manager; recordings carry the wire form, which replay treats as a no-op tick and redaction caps only when present. Refs #746 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
…backend is superseded Review of #761: the slash picker is parsed from the terminal grid, so arrow navigation changes cell colours without changing a character of the plain text; before the gate the updated selection rode along on the next chrome tick, which the gate now drops. The picker state is part of the comparison key. A superseded backend under the same session id skips the generation-owned cleanup, so the gate is also reset at the site that already clears lastGateEvaluation for that reason. A manager-level test pins that a spinner tick never reaches listeners while the raw snapshot cache stays current; the spinner glyph set gains the ✺ the headless parser documents. Refs #746 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kk16MNVJtWAnCeGxGRuHqa
|
Review applied in the latest commit:
|
Juliusolsson05
left a comment
There was a problem hiding this comment.
Adversarial review (re-run after the pause; worktree node_modules present).
VERDICT: APPROVE-WITH-NITS
The gate's failure direction is safe (any diff outside the enumerated volatile shapes emits; a missing rule costs one extra frame, never a lost one), the rework items are complete (picker key screenFrameGate.ts:104-106 with tests; supersession reset sessionManager.ts:2852 after sessions.set plus the cleanup path at :862), and every wire/replay/redaction consumer traced handles the aliased form.
- SHOULD-FIX — Renderer reload/late-subscribe during a thinking phase gets no screen frame until the agent's next real change. Pre-gate, the next 6 Hz spinner tick repainted a reloaded pane within ~166 ms; post-gate,
runtime.screen/latestScreenRef(useIpcSubscriptions.ts:751,810) stay empty for minutes (DebugPanel/ReaderView fallback, TileLeaf Enter-baseline). Remote late-joiners are covered (RemoteServer.ts:200pushes the cached raw snapshot); the renderer is not. Fix: push the cached raw snapshot throughaliasScreenSnapshotForWireon window attach, mirroring the remote path. - NIT — trailing drop runs never journaled: a drop run only produces a
session.screen.gate.droppedsample when a later emit closes it (sessionManager.ts:2688-2696); long-thinking sessions that end while thinking lose their final run — biasing the metric away from exactly the sessions it measures. - NIT —
snapshotStats()documented "for the perf journal / tests" (screenFrameGate.ts:127-129) but nothing in production reads it; wire it to session-exit/quit journaling (would also fix #2) or drop the claim. - NIT — the picker key works only by runtime accident:
AgentScreenSnapshotdoesn't declarepicker(session.ts:202-207) andGatedScreenFrame.pickerisunknown(screenFrameGate.ts:74). Typing the realSlashPickerStatewould make drift a compile error. - NIT —
ELAPSED_TIMER/TOKEN_COUNTERrewrite those shapes anywhere in the frame (screenFrameGate.ts:49-51); an in-place-updating figure inside agent output would freeze. Theoretical for current TUIs; one line under limitations.
Defect hunts that came up clean: spinner-adjacent-to-new-output always emits (non-volatile byte breaks key equality; tested); markdown-only diffs dropped matches pre-existing renderer semantics (useIpcSubscriptions.ts:793-801 already excluded markdown from invalidation); string aliasing can't break the renderer's === fast path (value comparison of primitives); replay unaffected (reconstructSlices.ts:173,276 no-ops session:screen); redaction caps keys only when present; remote server takes the full raw payload.
Commands: screenFrameGate + screenGate + wire + coalescer suites 15/15 PASS; sessionFeed 4/4 PASS; rendering replay 20/20 PASS; tsc -b clean.
…creens Limit volatile comparisons to recognized chrome, protect multiline drafts and seed the requesting renderer from the latest raw screen after successful recovery. Preserve picker-key changes and avoid global gate resets. Twenty-six focused tests pass. Refs #746
Problem and behavior
Fixes #746. Reduce repeated screen IPC by comparing recognized complete spinner/status lines, while retaining raw cached snapshots and emitting real content and picker changes. Duplicate screen strings are aliased only on the renderer wire and restored in preload.
Review corrections
Global timer/token/whitespace substitutions are removed. Duration, token-count, /rc and whitespace edits in normal content remain meaningful; multiline composer regions are preserved even when pasted text resembles a status line. Unknown chrome formats take the full path.
Successful session recovery seeds only the requesting renderer from the latest raw screen cache. A reloaded or newly attached local renderer no longer waits for a non-spinner change or idle backend repaint. Failed/conflicting recovery and destroyed requesters receive no seed. The global gate is not reset. Headless composer/condition parsing remains independent; Claude package #52 retains attribute-aware parsing on every emitted frame.
Verification and limits
26 focused gate, real-manager, recovery IPC and wire tests pass. Current main is integrated; full CI runs on the final head. No live app restart or FPS claim. Spinner/timer text in structured mirrors remains frozen between meaningful frames; the raw PTY terminal and process working status are unaffected. This is signature-based chrome recognition, not a general terminal rendering-state parser.
Final verification: both full host CI gates pass on 2e561bf (run 33938037152). Ready for owner merge confirmation; no live app restart.