feat(rendering): ownership-ledger rendering pipeline + session recording + soak fixes — integration branch#442
Merged
Merged
Conversation
…aring tests First implementation slice of docs/rendering/rendering-rewrite-plan- 2026-07.md (Stage 1: spec-first build, zero app wiring — nothing imports this yet). model/types.ts — the ledger contract from plan §3: block-level RenderCandidate (D2) with plural identity fields (no single id spans providers; toolUseId reserved first-class for the subagent surface, rule 14), the fixture-gated RenderReason enum (rule 15), the ownership_decision debug schema (D5), and the UnknownBehavior structured-finding contract with #115 redaction rules. model/order.ts — the D4 ordering law: true chronological merge with phase ranks, the trust-hierarchy timestamp comparison (null sorts after timestamped, never as 'now'), and the five-way tiebreak. This is the #239 buried-prompt fix at the model layer. model/ownership.ts — committed-ownership suppression with the suppress-vs-reorder discriminator (D3): whole-turn suppression is a provider POLICY bit (claude true, codex/opencode false — an id- presence heuristic would wrongly whole-turn-suppress opencode, whose committed rows carry message ids; drift report item 12), exact/ normalized text keys (never prefix/fuzzy), itemId keys (#194), and tool-use vs tool-result ownership kept separate (dump invariant 10). model/ledger.ts — the composed pass with the D11 identity-stability contract: unchanged input references return the previous ledger BY REFERENCE (load-bearing for every Feed memo; the render-churn class). __tests__/ledger.test.ts — ten tests, each naming the incident it guards: #239 ordering, #170 resp_*/rollout text split, claude-vs-codex whole-turn asymmetry (#165/#191), tool-use/result separation, #344 decisions-for-every-candidate, D11 reference equality. All green (vitest unit project); tsc node+web 0 errors on main checkout. Test files here are per the plan's explicit exemption from the no-new- tests convention: src/renderer/src/rendering/ IS its tests (plan §4). Next slices: committed projection + candidate collectors (from runtime state), ghost predicate port, fixture corpus directories from the real bundles (buried-prompt-239, opencode-interleave-87f0eeef, ghost-matrix). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tage1 Rendering pipeline Stage 1: ownership ledger core + first ten tests
…manifest observations/committed.ts — the anti-corruption boundary from raw transcript entries to ledger candidates. Visibility decided here with decision records for HIDDEN entries too (meta-entry, not-conversation, synthetic-user-filtered) so bundles can answer 'hidden or never ingested'. Includes the #338 fix as the centralized predicate: Claude non-meta user rows carrying <command-name>/<local-command-stdout>/ <environment_context> scaffolding (no permissionMode, text starts '<') are filtered with reason — claude-scoped so a codex/opencode user message legitimately starting with '<' is untouched. Ingest-time stable ids (never visible-index); producer-clock timestamps only; exact + NFKC-normalized text keys on assistant rows. docs/rendering/legacy-deletion-manifest.md — the strip-out contract: every legacy rendering file enumerated with fate (DELETE/GUT/ABSORB/ KEEP), LOC, deletion point, and a grep-based completeness check the Stage 3 cutover PR must pass. The old system is not patched during the rewrite (the new pipeline imports nothing from it) and does not survive it. 17/17 tests green (vitest unit); tsc node+web 0 errors gated on main checkout with the full integration diff applied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lectors Rendering slice 2: committed collector (#338 fix) + legacy deletion manifest
model/ghostPredicate.ts — the full predicate ported with every scar intact: superseded / orphaned-TTL (the only guard in the turn_completed→JSONL race window) / semantic-owned (rule 3 carries its death sentence: deleted at cutover together with SemanticStreamingTurn, plan D6) / strictly-newer-than-tail on producer clocks with null-not- zero (rule 4) / sidecar shape backstop with the 200-char empirical anchor and tool_use exemption (rule 5). The rules-4-AND-5 pairing is documented in-code with the tail-sidecar counterexample that overturned the single-rule design. __tests__/ghostPredicate.test.ts — the canonical 10-case matrix plus the failed-fix regressions (686b94e stale-orphan class), the boundary cases, the documented accepted trade-off as an explicit test, and evidence-on-every-rejection (#344). 29/29 pipeline tests green; tsc node+web 0 errors on full integration diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rendering slice 3: ghost five-rule predicate as ledger decisions
…odel-level guards observations/semantic.ts — current + history turns into block candidates (plan D2). Guards that lived as React null-returns become decisions: compaction synthesis yields zero candidates regardless of how the flag was detected (#345's model-level kill), empty thinking / empty write_stdin rejected with reasons, history turn matching the current turn id dropped (dump invariant 14). Text ownership keys only on FINALIZED text so streaming blocks can't be suppressed mid-growth. History orders by endedAt, current by startedAt (the D4 choice that makes the ordering law work). 36/36 pipeline tests green; tsc node+web 0 errors on full integration diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…antic Rendering slice 4: semantic collector with model-level guards
…st end-to-end fixture observations/local.ts — optimistic-submit candidates (echo providers only; submit wall-clock so the stand-in sorts exactly where its committed twin will) and lifecycle candidates (work is a phase fact — always present when streamPhase non-idle; empty only when no content survived). ownership.ts — optimistic reconciliation: committed USER rows own their optimistic stand-ins by normalized text (marker+text, never tail position). Bias toward the optimistic row SURVIVING when ownership is unprovable: too-long is visible and diagnosable, too-early is the silent #339 class. committed.ts now stamps normalizedTextKey on user rows for exactly this (assistant-suppression sets remain assistant- only). fixtures.buriedPrompt239.test.ts — THE first end-to-end run: raw entries + history turn + optimistic prompt + work → collectors → ledger → asserted FINAL ORDER (the #172 comment-5 bar). Covers the core #239 shape, the D3 reorder-not-suppress discriminator, and the optimistic→committed handoff without re-burying (#172 comment 6). 39/39 green; tsc node+web 0 errors on full integration diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al-fixtures Rendering slice 5: local collectors + optimistic handoff + buried-prompt-239 fixture
The ledger's honesty when committed truth never arrives (#159 rollout fork / #290 Claude analog): uncommitted history stays visible in chronological order indefinitely (reordered, never suppressed — it is the only copy that will ever exist), the feed structurally cannot shrink, conservative text keys hold end-to-end (no fuzzy ownership), and the crash-recovery ghost variant renders. The legacy correctness model assumed JSONL eventually arrives; the ledger does not inherit that assumption. 42/42 green; tsc clean on full integration diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-channel Rendering slice 6: dead-committed-channel-159 fixture
model/unknowns.ts — the plan §3 contract as code: unknowns are structured findings (shape paths to depth 3, FNV-1a payload hash, first-seen + count), deduped by (plane, type, hash) so floods read as one counted finding. Redaction is structural (#115): payloads are never stored, previews are caller-redacted AND hard-capped, and auth-looking key names are stripped from shape paths as a second belt. queued_for_implementation is how future provider behavior is discovered from bundles instead of raw-event archaeology. 46/46 green; tsc node+web 0 errors via the clean two-step gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nowns Rendering slice 7: unknown-behavior registry
Concurrent assistant messages on one SSE stream (the render-spasm bundle): interleaved content renders exactly once in chronological order — the committed assembled message wins its slot with the bridge copy suppressed by exact text, the never-committed concurrent message survives in full, and the drift-report hazard is pinned: opencode committed rows carry message ids but the unit-level policy bit forbids claude-style whole-turn suppression from eating the other message. 50/50 green; tsc node+web 0 errors via clean two-step gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ncode-fixture Rendering slice 8: opencode-interleave-87f0eeef fixture
Stage 1 progress marker (slices 1-8, full suite green)
…idecar-tail e2e fixture The five-rule ghost predicate existed standalone; the ledger never consulted it. LedgerInput now carries an optional ghost plane (candidate + predicate-input pairs, plus GhostPredicateContext for rules 3/4), decided LAST so ghosts never influence committed/live ownership, with survivors competing in ordering via the existing ghost-fallback SOURCE_RANK. Identity-stability cache compares ghostContext FIELD-WISE (scalar tail + set reference) so adapters can mint the context object fresh each tick without busting the D11 contract. e2e fixture pins the sidecar-tail trap end-to-end: rules 1-4 pass for both a sidecar blurb and a genuinely stuck tool_use ghost; only the shape backstop separates them. 53/53 pipeline tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st-ledger feat(rendering): slice 9 — ghost plane in the ledger pass + sidecar-tail e2e
…m) + ghost collector createLedgerInputAdapter() is the single module that vouches real SessionRuntime shapes satisfy the collectors' structural types: record- keyed semantic blocks → blockOrder-ordered arrays, optimistic rows partitioned back out of entries (committed-grade trust would let them SUPPRESS the live stream), ghost _atp metadata → predicate facts. Reference stability is enforced per PLANE on the runtime slice references each plane reads, composing with the ledger's last-call cache into 'no real change ⇒ same RenderLedger object' end-to-end (D11). Merged live+optimistic array is memoized on both source refs. collectGhostCandidates gates opencode entirely (supersede-less ghosts would double every turn) and keeps the five rules provider-neutral. Type-only seam assertions make the tsc gate fail on runtime-type drift instead of it surfacing as a shadow-diff mystery. 56/56 pipeline tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…apter feat(rendering): slice 10 — runtime→collector adapter (the shadow seam)
…e-handoff e2e; Stage 1 complete policy/foldPolicy.ts encodes the two proxy-literal yield hatches from legacy foldEvent (isCompletedProxyTurnReadyToYield / isEmptyNonProxyShellTurn) as per-provider policy data (terminalYieldSources / shellClaimants) with policy-parameterized predicates — no provider-source literal in any logic path. Parity tests pin both 2026-05-16 bundle classes, the flicker defense, and the shared ended-turn rule. Legacy fold untouched until Stage 3 (plan drift item). fixtures.queueHandoff.test.ts pins the queue→optimistic→committed race: queue is a composer lane (D1, absence asserted), the committed twin takes exactly the optimistic row's position, and the overlap tick (both rows present) suppresses the optimistic copy by normalized text. Plan progress marker updated: Stage 1 COMPLETE, 67 pipeline tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ldpolicy-queue feat(rendering): slice 11 — fold-policy yield-hatch absorption + queue-handoff e2e (Stage 1 complete)
…DER_SHADOW shadowDiff.ts normalizes both renderers to the coarsest grain they share (committed/optimistic/ghost rows by stable id, semantic turns by turnId, work/empty flags) so representation differences — block-level vs turn-level, plane prefixes vs folded uuids — never read as divergence, while real ones (missing/extra/mis-ordered rows) always do. Set difference before order comparison so one missing row is one finding, not a cascade of order mismatches. useRenderShadow mounts in TileLeaf (the one component holding the full runtime): probes the flag once app-wide via the existing dev-debug config channel, recomputes the legacy model from the same pure functions Feed uses, runs the adapter+ledger per-session (refs, so the D11 caches are actually exercised on real ticks), and records divergences to a ring buffer on globalThis.__agentCodeRenderShadow + console.warn once per signature. Zero cost when the flag is off; DELETE-fated at Stage 3. 72 pipeline tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adow feat(rendering): slice 12 — Stage 2 shadow mode behind AGENT_CODE_RENDER_SHADOW
…ne in CI) Runs the exact comparison useRenderShadow performs live — legacy selectMergedEntries + deriveFeedRenderModel vs adapter + ledger, diffed by shadowDiff — over constructed runtimes, so divergence triage starts in CI with a minimal repro, not in a soak console. Seven scenarios from the incident trail: idle conversation, live turn + work, claude whole-turn suppression, empty session, stuck-tool ghost renders, sidecar ghost hidden, and the #239 buried-prompt class (both sides now agree at the unit grain — parity pinned, not just divergence tolerated). All seven pass with ZERO divergences on first run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rity test(rendering): slice 13 — shadow parity suite (legacy vs new pipeline in CI)
…derItems ledgerToFeedItems maps RenderLedger rows to the FeedRenderItem shape Feed already renders, so the Stage 3 flag swap replaces ONE call site (the decision core) while LazyEntry, memo structure, autoscroll tail-signature and TextProse/StreamingProse survive untouched. SemanticStreamingTurn keeps rendering semantic items until the final cutover slice deletes it with ghost rule 3 (D6) — decoupling the decision-core swap from a row- component rewrite. Order contract: items emit in LEDGER order with order fields engineered so legacy sortFeedRenderItems is a no-op (matching phase partition, null timeMs, constant source, index sequence). Payload references are the runtime's own objects (Entry / SemanticLiveTurn) so downstream memos hold. Unresolvable candidates surface in `dropped`, never silently omitted. 83 pipeline tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ewbridge feat(rendering): slice 14 — view bridge: ledger rows → legacy FeedRenderItems
New rendering/__tests__/recordingCorpus.test.ts (mirrors bundleCorpus.test.ts, Mode 1 golden replay per plan §6): for each redacted recording fixture, replay the SessionFeed channel stream through the REAL pipeline and assert the rows are STABLE against the checked-in golden. BLESS-gated by AGENT_CODE_RECORDING_BLESS; any un-tolerated change fails loudly. Unlike the bundle corpus there is no external legacy ground truth — the golden IS the pipeline's own last-blessed output, so bless rewrites `expected` and triage is normally empty (kept for symmetry / knowingly-tolerated divergence). The suite self-skips (describe.skipIf) when the fixtures dir is empty/absent so CI stays green until real recordings land, and is SEEDED with two hand-built fixtures (committed plane + semantic plane) so it actually runs today. Replay is INLINE for now: reconstructSlices folds recorded events straight into RuntimeLedgerSlices — a small local stand-in for slice 4's shared RecordedSessionFeed harness (built in parallel on another branch). A later integration commit swaps the reconstruction for the shared harness with no fixture changes, since fixtures speak the channel stream, not the reconstruction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replay a captured Session Recording (meta.json + events.jsonl, the 9
SessionFeed channels) tick-by-tick through the REAL rendering pipeline and
yield per-tick { ledger, rows, feedItems }.
- reconstructSlices.ts folds the recorded events back into RuntimeLedgerSlices
by REUSING the production leaf reducers (foldSemanticEvent, reduceStreamPhase,
ghostsFromSemanticTurn, reconcileUpstream, the provider transcript mappers) —
not a second fold. Only the thin orchestration glue is mirrored, cited to the
useIpcSubscriptions sites it mirrors.
- recordedSession.ts constructs the adapter + ledger ONCE and drives them each
tick, so the D11 identity caches are genuinely exercised. Because the running
state is held across ticks and the reducers are reference-stable on no-op,
unchanged planes hand back the SAME object reference and no-op ticks return
the cached ledger — the reference pattern is faithful by construction (no
content-pool needed).
- onBeforeFold clock seam lets a test pin Date.now() to the recorded wall clock
(via vi.setSystemTime) without adding a clock seam to runtime code.
Fidelity: reducer-faithful + orchestration-mirrored, driven from the node
`unit` project (the React fold hook is unreachable there). Documented in the
module header, including which fold-glue paths are out of scope.
Round-trip test: hand-built prompt→tool→committed-handoff→completion→exit
recording; asserts rows per tick, the D11 same-object contract on no-op ticks,
torn-tail tolerance, and __note/__truncated skipping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
assertInvariants(replayResult) checks all four ledger invariants at EVERY tick, with no hand-authored expected output — the permanent successor to the dev-only render shadow (plan §6 Mode 2): 1. single-owner / dual-render: no two selected rows own the same artifact (row id, toolUseId, turn+block, or exact textKey). 2. no-vanish-without-replacement: a row gone at N+1 must be explained — a rejection decision names it, a replacement covers its artifact, OR its live semantic turn was torn down (exit) and thus left the runtime entirely. 3. no-unexplained-shrink: aggregate signal when the selected row count drops with unexplained drops. 4. D11 reference stability: a tick whose adapter inputs are reference-identical MUST return the same RenderLedger object (adapter.test.ts:119-149 over a real stream). Lifecycle work/empty rows are excluded from single-owner and vanish checks, mirroring bundleCorpus scopeUnits (they toggle on every phase edge). Test: green over the realistic prompt→tool→completion→exit recording (with an assertion that the tool→committed handoff actually happened, so "no violations" isn't vacuous), plus one negative control per invariant proving each fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eat/session-recording-full
…eat/session-recording-full
Slices 4 and 7 were built in parallel; the golden corpus (slice 7) shipped a local reconstructSlices stand-in while slice 4 built the canonical reducer-faithful replayRecording. Integration swaps the corpus onto the shared harness (higher fidelity, one reconstruction path, no duplicate fold). The swap surfaced a real fixture bug: hand-semantic-turn.json jumped to block_completed with no block_started, which C's stand-in tolerated but the REAL foldEvent reducer correctly ignores (block_completed no-ops when the block was never opened - foldEvent.ts:668). Repaired the fixture into a valid semantic stream and re-blessed against the real reducers' output; both hand fixtures now paint their intended units (user row + semantic turn) with empty triage. Verified green without bless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(recording): Session Recording — full feature (slices 2–7, #467)
… honesty
A plan-vs-code audit found the shipped feature drifted from
docs/rendering/session-recording-plan-2026-07.md §7: recording was
auto-only (every session written from launch — a disk-burial trap), and
the record-session:start/stop handlers + Start/Stop command the plan
specified as the PRIMARY control were never built. Fixed here:
- Recording is now COMMAND-DRIVEN and opt-in. The manager records a
session only after startRecording() is called; observe() ignores
events for sessions the user never started. AGENT_CODE_SESSION_RECORD
is downgraded to an OPTIONAL auto-start power flag (off by default),
so a normal day never silently fills GBs.
- record-session:start / :stop / :is-recording ipcMain handlers (gated
on the capability = dev-debug), preload start/stop/isRecording, and a
"Toggle Session Recording" command that starts/stops the focused pane
and labels itself from live recorder state. Gate split: capability
(command available) = dev-debug; auto-start = the env flag.
- §3 header fields the audit flagged as missing: meta.json now carries
kind:'session-recording' + redaction:'none', and provider/cwd are
filled from session:started (providerSessionId stays best-effort).
- §4: the queue drop-oldest path now writes a __truncated{reason:
'queue-drop'} tombstone at the gap position (was counter-only; the
comment claiming an inline marker was inaccurate).
- Plan §0b "As-built reconciliation": records the intentional deviations
(folder-per-recording, command-driven) and the one real limitation
(replay is reducer-faithful, NOT the React fold — so the fold-glue bug
class #469 is capturable in the recording but not yet in replay; needs
the fold extracted from the React hook, converging with #394). No
silent drift between plan and code.
9 recorder tests (incl. on-demand + auto-record power-flag cases);
full unit suite 427; both gates clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…demand fix(recording): command-driven Start/Stop (§7) + header fields + plan honesty
, Track A) Subagent (Task/Agent) output was leaking into the parent's MAIN feed: Claude interleaves a subagent's OWN turns into the parent transcript tagged isSidechain:true, and the committed collector was structurally blind to the flag — RawCommittedEntry didn't even carry it, so a sidechain assistant turn (type:'assistant', not meta, text not '<') sailed through to `selected` and painted as a plain main-feed row, with minedToolIds even stamping its inner Read/Bash as committed-owned. Confirmed live and captured in a session recording (#467); diagnosis and plan in #477. TDD: the two committed tests + the e2e adapter fixture were written FIRST and FAILED (the sidechain turn was selected, its Read mined) — that failure IS the leak. Then the fix: - RawCommittedEntry gains isSidechain (the field already flows through the claude codec + history.ts:71; the feed just never honored it — only the session picker did, sessionList.ts:160). - collectCommittedCandidates rejects sidechain entries BEFORE mining tools, reason 'sidechain-filtered' (new RenderReason). Safe by construction: the same activity already renders inside the Task card via the watcher channel (SubAgentState), so nothing is lost. - e2e fixture proves the field flows adapter -> collector -> ledger and only the parent rows paint. This is the COMMITTED (persistent) half. The LIVE proxy-plane half (subagent turns multiplexed on the shared proxy stream, no isSidechain on the wire) is Track B in #477 — needs flow attribution, separate PR. Corpus unchanged (no fixture had sidechain entries); 170 rendering tests; full unit suite 431; both gates clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(rendering): filter subagent sidechain turns from the main feed (#477, Track A)
…bution (#477 Track B) Picks up Juliusolsson05/claude-code-headless#35: subagent API flows (cc_is_subagent=true billing-header flag) are routed OUT of the parent's visible turn stream instead of leaking into the main feed. The committed half of the leak landed in #479 (isSidechain filter); this is the live proxy-plane half. Subagent activity still renders in its own Task card via the watcher channel (Record<toolUseId, SubAgentState>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(subagents): subagent proxy-flow attribution — Track B (#477)
Repo-cleanliness pass (findings from a 3-lens audit): Deletions: - issues.md (stale, long gone) - CODEX.md (redundant — the codex CLI reads AGENTS.md) - testing/rendering/ (the whole render harness — a 2-2.5mo stale vendored fork of the real components, superseded by the ownership-ledger corpus + recording-replay test net; no longer used) - testing/regression-claude-tool-result.ts (dead orphan, zero references) - the stale testing/rendering includes + orphaned comment in tsconfig.node.json Fixes: - AGENTS.md synced with the `Directory conventions` section CLAUDE.md had drifted ahead on (AGENTS/CODEX were byte-identical and missing it) - MANIFESTO.md: removed a duplicated `## Structure` block (copy-paste bug) Docs restructure: - docs/archive/ created for superseded/closed material, each with a Status/ARCHIVED header: codex-rewrite-render (superseded notebook), issue-investigations (#147/#152/#154 closed), audit-plans (completed logs) - rec-research-* grouped under research-2026-07/session-recording/ (was mixed with the rewrite research); the 2 path refs in the recording plan updated - docs/README.md index added documenting the live-vs-archive convention Both tsc gates clean; unit suite 434 (unchanged — the deleted harness had no vitest tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore(repo): hygiene + docs restructure
…stream 3) src/renderer/src/rendering/__tests__/ was the ONLY __tests__ directory in the repo — everywhere else colocates test next to source. Standardized on colocation: - 1:1 tests moved next to their source: adapter → adapter/ collectLedgerInput.test.ts; committed/semantic → observations/; ledger/ghostPredicate/unknowns → model/; foldPolicy → policy/; ledgerFeedItems → view/; shadowDiff → shadow/; replay* → replay/ (invariants.test.ts, recordedSession.test.ts). - Cross-cutting tests with no single source (the corpus + e2e fixtures) moved to the rendering/ root: bundleCorpus, recordingCorpus, shadowParity, fixtures.*. - bundleCorpus/recordingCorpus fixture paths dropped one ../ (moved up a level). Pure move — vitest's src/**/*.test.ts glob already matches colocated files. 170 rendering tests unchanged; full unit suite 434; both gates clean. (Historical research docs keep their point-in-time __tests__ references by design.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests chore(rendering): colocate tests, remove the lone __tests__ dir
…l legacy renderer + flags (Stage 3) The ownership-ledger pipeline now decides Feed's entire item list for BOTH Feed consumers — the desktop (TileLeaf) and the phone (remote SessionView). The legacy in-Feed decision core is deleted, unconditionally, with no flag. WHY this landed as one PR after a scope discovery: the cutover was going to be a desktop-only flag flip, but the remote phone client turned out to be a SECOND <Feed> consumer still mounting the legacy deriveFeedRenderModel path (it passed no renderItemsOverride). Deleting the legacy renderer would have blanked the phone's feed. So the phone was migrated first, then legacy died. - useLedgerFeedItems is now UNCONDITIONAL — the AGENT_CODE_RENDER_PIPELINE flag, its async probe, and the whole dev-debug plumbing for it (+ the AGENT_CODE_RENDER_SHADOW flag) are gone. Safe because shadow parity was green over the incident corpus + recorded sessions: identical item lists ⇒ identical paint through the unchanged row components. - Phone migrated: remote SessionView builds a minimal SessionRuntime view (frozen empty ghost plane — the phone has no optimistic echo) and drives the same useLedgerFeedItems, passing renderItemsOverride like the desktop. - deriveFeedRenderModel + its sort/visibility helpers + FeedRenderModelInput deleted from renderModel.ts; Feed's legacy branch deleted (maps the ledger's items unconditionally). deriveFeedCommittedProjection + feedRenderModelFromItems stay — the former still feeds SemanticStreamingTurn. - Shadow subsystem deleted (useRenderShadow.ts + shadowParity.test.ts); the pure shadowDiff util stays (bundleCorpus/recordingCorpus assert through it). ledgerFeedItems.test's legacy oracle replaced with explicit expected shapes. NOT in scope — the block-level un-collapse: the view bridge still collapses the ledger's block-level semantic rows into turn-level items for SemanticStreamingTurn. Retiring that component (and selectMergedEntries / committedClaudeMessageTurnIds with it) needs the ledger to own live turns block-by-block through the shared registry dispatch + live visual verification the turn-grain corpus can't give. Tracked in the deletion manifest's status section. Gates: tsc node + web clean; unit 427, renderer 16, integration 30, rendering 163 — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(rendering): ledger is sole decision core; migrate phone, kill legacy renderer + flags (Stage 3)
This was referenced Jul 7, 2026
Semantic tool and edit rows can disappear without diagnostics explaining the suppression reason
#344
Closed
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.
DRAFT — review surface for the full
integration/rendering-pipelinebranch (~100 commits ahead of main). Merge is the "stop at main" gate.What this branch is
The ground-up rendering engine rewrite, plus the tooling and fixes that grew out of soak-testing it. Architecture: raw input → observation collectors → ownership decisions (selected AND rejected, each with a recorded reason + evidence) → chronologically ordered row objects → dumb React rows. One invariant: every visible feed artifact has exactly one owner; debug output is a serialization of the same decisions the paint used. Plan:
docs/rendering/rendering-rewrite-plan-2026-07.md.Landed on this branch
Rendering pipeline (Stages 1–3 built, cutover flag).
AGENT_CODE_RENDER_PIPELINE=1the feed paints from the new engine;AGENT_CODE_RENDER_SHADOW=1runs the divergence recorder alongside. Legacy renderer still present as the default + safety net (deleted only in the final cutover PR perdocs/rendering/legacy-deletion-manifest.md).Validation — three layers, ~430 tests.
Session Recording (#467). Continuous, debug-gated capture of a session's rendering-input stream to per-recording folders, replayable through the real pipeline; command-driven Start/Stop + Attach Recording Note; two-tier redaction; golden + invariant replay corpus. On its first real use it caught the subagent-feed leak below.
Soak & correctness fixes (found by running with the flag on): collapsed-running tail gate (#465), Claude priority-queue reconstruction (#469), subagent lifecycle #341 (wait-agent inference, stale status, pruning), committed tool-ownership + duplicate cards (#449), dangling tool chips (#451), JSON/MCP tool rows, task-notification rows, MCP-spawn cards, opencode dispatch, and the subagent-into-main-feed leak both halves — committed plane isSidechain filter (#479) and live proxy-plane
cc_is_subagentattribution (#480, via claude-code-headless#35). Plus the headless tailer scoped-unwatch fix (the dead-channel/queue root cause).Remaining before this merges to main
SemanticStreamingTurn+ ghost rule 3 atomically, remove legacy suppression guards, deletion-manifest grep check.🤖 Generated with Claude Code