Skip to content

perf(sessions): drop spinner-only screen frames and alias duplicate strings on the wire - #761

Merged
Juliusolsson05 merged 6 commits into
mainfrom
perf/screen-frame-spinner-gate
Sep 5, 2026
Merged

Juliusolsson05 merged 6 commits into
mainfrom
perf/screen-frame-spinner-gate

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

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.

Juliusolsson05 and others added 3 commits September 3, 2026 16:28
… 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
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review applied in the latest commit:

  • Picker in the gate key: GatedScreenFrame.picker is compared (JSON of the small parsed state), so a selection move or close emits even on a spinner tick — test added.
  • Supersession reset: screenFrameGate.forget now also runs at the lastGateEvaluation clearing site after this.sessions.set(...).
  • Manager-level test (sessionManager.screenGate.test.ts): a spinner tick never reaches manager.on('screen') but updates getScreenSnapshot.
  • Glyph set gains ✺; timer-regex scope documented; plan and PR body corrected (no forwarder/preload suites claimed; remote-client frozen-spinner limitation stated).

@Juliusolsson05 Juliusolsson05 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. 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:200 pushes the cached raw snapshot); the renderer is not. Fix: push the cached raw snapshot through aliasScreenSnapshotForWire on window attach, mirroring the remote path.
  2. NIT — trailing drop runs never journaled: a drop run only produces a session.screen.gate.dropped sample 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.
  3. 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.
  4. NIT — the picker key works only by runtime accident: AgentScreenSnapshot doesn't declare picker (session.ts:202-207) and GatedScreenFrame.picker is unknown (screenFrameGate.ts:74). Typing the real SlashPickerState would make drift a compile error.
  5. NIT — ELAPSED_TIMER/TOKEN_COUNTER rewrite 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
@Juliusolsson05
Juliusolsson05 merged commit 58e6201 into main Sep 5, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the perf/screen-frame-spinner-gate branch September 5, 2026 03:05
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.

perf(screen): session:screen frames are 50% duplicate strings and 99% spinner-only diffs

1 participant