Skip to content

perf(ipc): forward session:screen only to a renderer that leased it - #1236

Merged
Juliusolsson05 merged 9 commits into
mainfrom
perf/screen-ipc-interest
Sep 25, 2026
Merged

Juliusolsson05 merged 9 commits into
mainfrom
perf/screen-ipc-interest

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Fixes #762

The plan is the first commit: docs/plans/2026-09-25-screen-ipc-interest.md.

Why

session:screen was 93% of recorded IPC bytes. Recording g00000190 has 16,728 frames of about 8.8 KB over 123 minutes, and the committed fixture worktree-live-attribution/codex-live-channel-gap.json has 2,358 screen frames among about 12,600 events. Every frame was structured-cloned and committed into the renderer runtime map.

A full consumer map (below) shows that nothing live needs these frames.

Consumer map (why each is safe)

Consumer Where Disposition
Prompt delivery, paste confirm, readiness main, session.snapshotScreen() (providers/*/runtime/promptDelivery.ts) Unaffected; reads main's screen
Raw snapshot cache and spinner gate sessionManager.ts lastScreenSnapshot, ScreenFrameGate Unchanged
Composer slash picker applyConditionSnapshot (claude.slash-picker) Already owned by conditions; the screen handler's second write is removed
latestScreenRef Enter baseline useComposerKeybinds.ts → getScreen Dead; no composerSubmit read it (#855). Deleted
claudePaste.ts helpers TileLeaf Dead; no callers. Deleted (CLAUDE_PASTE_THRESHOLD now imported from @shared/claude/pasteConfirm)
ReaderView No screen fallback since #855
DebugPanel; Conditions, HeadlessSnapshotProbe and ClaudePasteDetection dev modules debug Hold a lease while open (useScreenLease)
Debug bundles (manual and 60 s autosave) saveDebugBundle.ts Read session:get-screen-debug: main's latest raw screen plus the tail history
Screen-tail trace was renderTrace.recordScreenTailSnapshot Recorded in main from every frame (shared/debug/screenTail.ts); same bundle files
Phone RemoteServer taps main Unaffected
Recover and resync seeds ipc/session.ts, ipc/sessionRouting.ts Unchanged; still sent directly

Lease contract

  • Acquire: session:screen-lease acquires a lease and seeds the current screen as one ordinary session:screen event, so an idle backend still shows a correct panel.
  • Release: session:screen-release releases it. A release the owner does not hold is ignored.
  • Ownership: a lease belongs to the calling webContents. A main-frame navigation (reload) or destroyed drops all of that owner's leases, because a dying renderer never runs its cleanup. A leaked lease costs bytes, never correctness.

Evidence

  • Fail-first on the real path. Using the recorded Claude 2.1.278 screen (image-absorption/wrapped-image-pill-2026-09-21.json), the "unleased frame is not forwarded" assertion fails on a clean origin/main (expected [ Array(1) ] to deeply equal []) and passes here.
  • Mutations killed:
    • removing the gate;
    • recording the history only for leased frames;
    • dropping leases on a sub-frame navigation;
    • removing the acquire seed;
    • restoring the picker write from screen frames.
  • New tests: screenInterest.test.ts (counting across owners, foreign release, dropOwner), screenTail.test.ts, useScreenLease.renderer.test.tsx, the forwarder recorded-frame test, and the IPC lease seed plus reload/destroy test.
  • Updated tests:
    • The routing composition tests now hold a lease explicitly, because an unleased session would make "nothing sent" true for the wrong reason.
    • The dimension-ownership test mocks the lease bridge.
    • The screen renderer test pins "no picker from screen".
  • Suites: Node 24, tsc -b clean. Full suite: 6,740 of 6,742 pass. The one failure, workflows/control.system.test.ts (a 5 s timeout), fails identically on clean origin/main locally, while main's CI is green; it is local environment.

Not verified

The live app (house rule). The IPC byte reduction is inferred from the gate, not re-measured in a running app.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 25, 2026 01:51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#762. session:screen was 93% of recorded IPC bytes and nothing live
reads it: main does its own prompt delivery, paste confirmation and
readiness from the screen, the composer picker is owned by the
conditions channel, and the Enter-baseline ref and renderer paste
helpers were dead. See docs/plans/2026-09-25-screen-ipc-interest.md.

- Main: the forwarder sends a frame only while some renderer holds a
  lease (sessions/screenInterest.ts), and records every frame in a
  bounded screen-tail history (shared/debug/screenTail.ts). Main's raw
  snapshot processing and gate are unchanged.
- Leases: session:screen-lease seeds the current screen down the
  ordinary path; leases belong to the calling webContents and drop on a
  main-frame reload or destroy.
- Debug bundles read session:get-screen-debug (latest raw screen and
  history). DebugPanel and the three screen-reading dev modules hold a
  lease while open (useScreenLease).
- Renderer: the screen handler applies only the screen strings (no
  picker). latestScreenRef, claudePaste.ts and ComposerSubmitIo.getScreen
  are deleted as dead code.

Fail-first: the recorded Claude 2.1.278 frame is forwarded with no
lease on origin/main. Mutations killed: gate removed, history recorded
only for leased frames, lease dropped on a sub-frame navigation, seed
removed, picker write restored.

Fixes #762

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 2 commits September 25, 2026 02:55
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
…paste chain

Review of #1236:
- C2: a same-document (hash/history) main-frame navigation no longer
  drops a renderer's screen leases, matching workflows.ts's identical
  per-renderer lease contract.
- C1: the debug-bundle consumer seam is tested end to end: the real
  assembleAndSaveDebugBundle with window.api as the only mock saves
  main's tail history and screen; buildStateSnapshot prefers main's
  screen; the get-screen-debug handler answers snapshot plus history.
  M1, M2 and M20 now fail.
- C3/C4/C5: the history records `recent` (not `plain`); a recent-only
  frame applies; the lease seed carries the exact aliased payload.
  M18, M15 and M19 now fail.
- C6 plus cleanup: the whole awaitClaudePastePlaceholder chain (preload
  API, IPC handler, manager and session methods, AgentSession optional)
  had no caller, the same dead cluster as the deleted claudePaste.ts;
  removed, and the comments that pointed at claudePaste.ts corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 5 commits September 25, 2026 03:35
…n release

- Leases no longer drop on did-start-navigation. Chromium reports it
  before the throttle where the app window blocks every will-navigate,
  so a blocked stray link click dropped every lease of a page that never
  changed, and an open debug panel went stale (review B's suspicion).
  The preload mints one id per loaded document; a lease from a new id
  drops the old document's leases, order-independently. Only destroy
  drops otherwise.
- Pins round-1 survivors: a release for a session the owner does not
  hold cannot cancel another owner's lease (J), the release handler acts
  for its caller (A), and a lease on a session with no screen sends no
  empty frame (E).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lease

Steering q15: with leases keyed to the document, a reloaded page that never
opened a debug panel took no lease, so the old page's leases kept the
heaviest IPC stream forwarding until the window closed. The preload now
announces its document id on every load (session:screen-document), which
retires an earlier document's leases at once. Blocked navigations still
drop nothing, and a dead page's late release still cannot touch the new
page's lease. Messages from one renderer arrive in order, so the
announcement lands after the old page's last message and before the new
page's first lease.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition (2 rounds: Codex A, Claude B, Pi C; plus steering)

Finding Disposition
C1: debug-bundle consumer seam untested Fixed. Real assembleAndSaveDebugBundle with only window.api mocked; get-screen-debug and tail files tested. Mutants M1, M2 and M20 fail.
C2: same-document navigation dropped leases Superseded by document-keyed leases (below).
C3/C4/C5: history recorded plain; recent-only frames; loose seed Fixed. Mutants M18, M15 and M19 fail.
C6: dead awaitClaudePastePlaceholder chain Removed end to end.
A1/A2: picker colour-only selection never republishes Pre-existing. Filed #1253.
B round-1 suspicion: a blocked will-navigate still fires did-start-navigation and dropped every lease Fixed. Leases are keyed to a per-load document id minted in preload; no navigation event is used.
B round-2 survivors J, A, E Pinned. Cross-owner release, release acts for its caller, no empty seed frame.
q15 (steering): a reload that takes no new lease kept forwarding forever Fixed. The preload announces its document on every load (session:screen-document), retiring the old document's leases at once. Host-path test plus mutant.

All round-2 verdicts are MERGE. CI is green on c08201f2, which contains origin/main.

@Juliusolsson05
Juliusolsson05 merged commit b21ed71 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the perf/screen-ipc-interest branch September 25, 2026 11:42
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…nce rule

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…ocess restore size, view size carried to a successor

- A2: the attach count now outlives the process, so a renderer that
  reloads or crashes without its leaf cleanup pinned a count forever. The
  IPC layer keeps each renderer page's attaches and releases them when the
  renderer is destroyed or loads a new document (the #1236 screen-lease
  lifecycle); a late detach from a dead page cannot take the new page's
  reference. This also covers #1283 item 3.
- A1/B: the restore size belongs to the process that had it; it is dropped
  with the process and recaptured (the natural size) for a successor
  started under an attached view, so a detach no longer shrinks a live
  successor to the dead one's size.
- A3: a successor under an attached view starts at the view's last size
  instead of 120x40, since the mounted xterm's size cache never re-sent it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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(ipc): gate session:screen delivery on renderer interest and split the picker onto its own channel

1 participant