Repository navigation
feat(renderer): live transcript window + memory instrumentation + raw-payload compaction - #511
Merged
Merged
Conversation
…-payload compaction Closes #375 — the renderer's per-session runtime grew without bound on the live path. Three parts, one PR: Part A — memory instrumentation (performance/memoryInstrumentation.ts): - 30s per-session gauges (renderer.session.memory.*): entries count + sampled byte estimate, semantic history/log byte estimates, feed-debug ring length, ghost map size, tool_use/tool_result index sizes, seen-uuid + trimmed-uuid set sizes. - Global gauges: code-block registry size, Monaco model count via a probe registry (lib/code/monacoModelProbe.ts) registered from inside getMonaco() so instrumentation can never force the Monaco lazy chunk (and the workspace hook's import graph never touches monaco-editor). - Byte estimates sample up to 64 evenly-spaced items and extrapolate — ingest-time accounting would tax the hot path and full stringify per tick is the workload being instrumented; a trend gauge only needs order-of-magnitude fidelity. Part B — the live entries window (session-runtime/liveEntryWindow.ts): - The jsonl burst handler trims entries above MAX_LIVE_ENTRIES (2000) down to TRIM_TO_LIVE_ENTRIES (1500). Hysteresis so the rebuild cost is paid ~once per 500 appends, not per burst. - totalEntries is NEVER decremented — it is the on-disk denominator and entries was always documented as a window over it. - Trim boundary stays strictly OLDER than: the oldest semantic.history turn startedAt, currentTurn startedAt, and every un-superseded ghost updatedAt — trimming a live turn's committed owner would defeat committed-ownership suppression and re-paint duplicates (the #165/#191/#344 regression class). Optimistic rows are never trimmed. - Any uuid-less entry anywhere freezes the window: uuid-less entries cannot reload dedupe-safely AND the ledger's entry:ingest-index fallback ids (committed.ts, comment updated) assume unshifted indices. - Pagination can RELOAD the trimmed region: every ingest site stamps the provider historyMarker on the entry as a non-enumerable symbol rider (dies with the entry — no new unbounded map); on trim, historyOldestMarker advances to the oldest retained marker and hasOlderHistory flips true. - Asymmetric dedupe: live ingest treats seen ∪ trimmed as already-seen (a resume bootstrapTail replay must not re-append old rows at the tail); the older-history path lets trimmed uuids back through and releases them from the set as they reload. Trimmed-uuid bookkeeping follows the seenUuids lifecycle at all six teardown/reset sites. - toolUseIndex/toolResultIndex are rebuilt in place from retained entries on trim (per-id deletion would break cross-entry pairings) with a toolIndexVersion bump; loadOlderHistory now also indexes paged-in entries so a reloaded trimmed region restores its pairings (previously a latent gap, load-bearing after a rebuild). - Scroll safety: no trim while loadingOlderHistory or within 30s of an older-history prepend (Feed has no shrink-anchoring). - Every trim leaves a feed-debug record (kind entries_trimmed, STATE) so debug bundles stay explainable. - The ledger pipeline needs no changes: adapter caches key on the entries array reference, so a trim is one clean recompute. Part C — raw-payload compaction: - semantic.log entries no longer retain the raw event by default (SemanticLogEntry.raw is optional); feed-debug SEM entries store { eventType } instead of the whole semantic event. Both capture raw only when semantic/rawCapture.ts is enabled, which rides the existing AGENT_CODE_DEV_DEBUG probe (App.tsx getDevDebugConfig) — the payloads are only readable through dev-debug surfaces, so capture and surface share one switch. - semantic.history block maps deliberately NOT compacted: they are paint inputs for the ledger's semantic-history plane; decision recorded in semantic/helpers.ts. Tests: extended session-runtime/entries.test.ts (trim planner constraint matrix + marker rider invisibility) and useIpcSubscriptions.renderer.test.tsx (burst-handler trim wiring + tail-replay dedupe). Full suite 476 passed; the agent-voice-dictation hotkeyBinding suite failure is pre-existing on main. tsc -b tsconfig.node.json and tsc --noEmit -p tsconfig.web.json both clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…straint, byte budget, gauge parity - Constraint 5 in planLiveEntryTrim: the trim boundary may not separate a cross-entry tool pair — any retained tool_result whose tool_use would be trimmed pulls the boundary older, so the post-trim index rebuild is pair-complete by construction (the review's phantom-degraded-rows class). - The window is now byte-driven as well as count-driven (#375's actual ask): MAX_LIVE_ENTRY_BYTES 32MiB -> trim toward 24MiB, estimates via a WeakMap-cached JSON.stringify length per entry object (no decrement bookkeeping; GC owns eviction). Safety constraints still always win. - Pagination tool-indexing decision documented as a deliberate fix of the latent scroll-back gap; feed-debug gauge now emits bytes alongside count. Finished from the review-fix agent's in-progress worktree after it was terminated by the spend limit; gates re-run green (tsc node+web clean, vitest 481/481 incl. new planner cases, remote-client build OK). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
App.tsx conflict: branch predates #505's thin composition root — took main's App.tsx and re-applied the rawCapture wiring into features/debug/devDebugConfig.ts's one-shot hydrate, where the dev-debug probe lives post-#505. Same behavior, new home. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #375.
The renderer's per-session runtime grew without bound on the live path: initial history loads 120 entries and older pages load 200, but every live jsonl burst appended forever (
[...baseEntries, ...appended]), and several side structures (tool indices, seen-uuid sets, raw semantic/debug payload rings) never shrank. This PR bounds the live transcript window by count, makes what's trimmed reloadable, instruments every structure the investigation flagged, and stops retaining raw event payloads by default. Hibernation of background agents is deliberately out of scope (deferred by design).Part A — memory instrumentation
performance/memoryInstrumentation.ts, driven by a 30s interval inuseIpcSubscriptions(separate from the 1 Hz ghost sweep — sweep cadence is a correctness knob, gauge cadence a cost knob).renderer.session.memory.*): entries count + byte estimate, semantic history/log byte estimates, feed-debug ring length, ghost map size, tool_use/tool_result index sizes, seen-uuid + trimmed-uuid set sizes.lib/code/monacoModelProbe.ts) registered from insidegetMonaco()— instrumentation can never force the Monaco lazy chunk, and the workspace hook's module graph never importsmonaco-editor(which the renderer vitest project cannot even resolve).Part B — the live entries window (the core)
New
session-runtime/liveEntryWindow.ts+ wiring in the jsonl burst handler:entries.length > MAX_LIVE_ENTRIES(2000, deliberately generous) down toTRIM_TO_LIVE_ENTRIES(1500) — hysteresis so the index rebuild + ledger recompute is paid ~once per 500 appends.Trim-safety constraints (each one load-bearing):
totalEntriesis never decremented — it is the on-disk denominator;entrieswas always documented as a window over it.semantic.historyturn'sstartedAt,currentTurn.startedAt, and every un-superseded ghost'supdatedAt. Trimming a live turn's committed owner would defeat committed-ownership suppression and re-paint duplicates — the fix(feed): keep semantic history visible while JSONL catches up #165/Fix stale semantic web-search rows sticking at bottom after rollout commit #191/Semantic tool and edit rows can disappear without diagnostics explaining the suppression reason #344 regression class. Superseded ghosts don't protect (they're already scheduled for GC). Optimistic rows are never trimmed.entry:ingest-${index}fallback ids (comment updated inobservations/committed.ts) assume unshifted indices — a front-trim would re-key a retained uuid-less entry into an id previously owned by a different entry (the phantom-duplicate React-key class). In practice every mapped entry carries a uuid, so this is a freeze-don't-corrupt failsafe.historyMarkeron each entry as a non-enumerable symbol rider — chosen over a parallel uuid→marker map because a rider dies with the entry (no new unbounded structure, no extra lifecycle plumbing) and is invisible to JSON.stringify (debug bundles, byte sampling). On trim,historyOldestMarkeradvances to the oldest retained entry's marker andhasOlderHistoryflips true; if no retained marker exists the trim aborts rather than strand the region.Asymmetric dedupe: live ingest treats seen ∪ trimmed as already-seen (a resume bootstrapTail replay must not re-append old rows at the tail); the older-history path lets trimmed uuids back through, releasing them from the set as they reload. Trimmed-uuid bookkeeping follows the seenUuids lifecycle at all six teardown/reset sites (pane close, kill, rename ×2, restart-in-place, tab close).
toolUseIndex/toolResultIndexare rebuilt in place from retained entries (per-id deletion breaks cross-entry pairings — a retained tool_result resolves its command via the map) with atoolIndexVersionbump.loadOlderHistorynow also indexes paged-in entries — previously a latent gap, load-bearing after a rebuild so a reloaded trimmed region restores its pairings.loadingOlderHistoryor within a 30s grace after an older-history prepend — Feed has no shrink-anchoring, and a user who just paged history in is reading exactly the rows a trim would remove. Skipping is always safe; the next quiet burst trims.kind: 'entries_trimmed', layer STATE) with trimmed/retained counts, the new marker, and the protection bound.Part C — raw-payload compaction
semantic.logentries no longer retainraw: evby default (SemanticLogEntry.rawis now optional); feed-debug SEM entries store{ eventType }instead of the entire semantic event (thesummaryfield already carries the digest). Both capture raw only whensemantic/rawCapture.tsis enabled, which rides the existingAGENT_CODE_DEV_DEBUGprobe (App.tsx→getDevDebugConfig) — raw payloads are only readable through dev-debug surfaces, so capture and surface share one switch. No consumer reads.rawoutside JSON-dumping debug exports, verified by grep.semantic.historyturn block maps deliberately NOT compacted: they are paint inputs for the ledger's semantic-history plane, and compacting safely requires committed-suppression knowledge that lives in the decide layer. Decision recorded next tosemanticHistoryRowinsemantic/helpers.ts.Verification
npx tsc -b tsconfig.node.json— clean.npx tsc --noEmit -p tsconfig.web.json— clean (no TS6305/TS6307 either).NODE_ENV=test npx vitest run— 476 passed (467 on main + 9 new); the full rendering corpus (bundleCorpus, recordingCorpus, replay invariants: 18 files / 151 tests) green. Theagent-voice-dictation hotkeyBindingsuite failure is pre-existing on main.session-runtime/entries.test.ts(trim planner constraint matrix + marker-rider invisibility) anduseIpcSubscriptions.renderer.test.tsx(burst-handler trim wiring: window applied, totalEntries untouched, pagination re-anchored, entries_trimmed record, tail-replay dedupe).Closes #375
🤖 Generated with Claude Code