Skip to content

feat: read long conversations by window with shared history writes - #376

Merged
zxch3n merged 66 commits into
mainfrom
feat/conversation-view-range-rendering
Sep 14, 2026
Merged

zxch3n merged 66 commits into
mainfrom
feat/conversation-view-range-rendering

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Related issue

Same-repository maintainer work; no separate tracking issue.

Problem / pressure

Opening a long conversation should prepare the visible messages, not materialize every history body before the UI can use them. On main, DOM virtualization happens after createSessionMirror has already read the full history. This PR moves that boundary down to history reads.

Summary

One production ConversationView reads a shallow directory and hydrates requested windows. The existing shared HistoryWriter remains the only history writer. The renderer uses the history reader; CLI operations use synchronous in-process reads and the same write rules. This is a windowed-read change, not a new CRDT or a complete replacement-storage implementation.

Local 1000-round comparisons: on the 140 MB high-entropy sanitized workload, cold data preparation falls from 157.3 seconds to 317.7 ms (median, 3 fresh processes per revision, including import, initial bodies, message rows and outline). A smaller synthetic workload measures 121.4 ms → 33.2 ms for import + reader preparation. These are different workloads and endpoints, not browser first-paint measurements; see the details below.

Visual explanation

Before: virtualize the DOM after reading all bodies

flowchart LR
  S["Snapshot bytes"] --> D["LoroDoc import"]
  D --> M["createSessionMirror<br/>materialize ALL history bodies"]
  M --> H["Full JS history array"]
  H --> R["Build conversation rows"]
  R --> V["Virtua displays a window"]
  UI["UI / CLI writes"] --> W["Shared HistoryWriter"]
  W --> D
Loading

Now: separate session metadata, directory and requested bodies

flowchart LR
  S["Snapshot bytes"] --> D["LoroDoc import<br/>still whole-document"]
  D --> C["Control-plane Mirror<br/>history excluded"]
  D --> A["Loro history reader"]
  A --> I["Shallow directory<br/>IDs, status, input config, counts"]
  A --> B["Requested turn bodies"]
  I --> V["ConversationView<br/>directory + body cache"]
  B --> V
  C --> UI["React / Virtua"]
  V --> UI
  UI -->|"visible window / outline hover"| V
  UI --> P["History commands / planners"]
  CLI["CLI business operations"] --> P
  P --> W["One shared HistoryWriter"]
  W --> D
Loading

The directory remains O(total entries). Complete bodies are loaded for the visible/retained range, with an LRU target of 200 and a retained tail of 20; pinned windows can exceed the target. Outline counts do not require all summaries: opening a hover card reads that question and its replies, then releases the lease. There is no full-history outline-summary sweep.

Streaming: invalidate by identity, refresh the affected bodies

sequenceDiagram
  participant D as LoroDoc
  participant R as History reader
  participant V as ConversationView
  participant F as Shared fact table
  participant U as UI
  D->>R: Commit content edit
  R->>V: changed(turn IDs)
  V->>V: Invalidate pending reads for these IDs
  V->>R: Read affected cached / leased bodies
  R-->>V: Current bodies
  V->>F: Invalidate affected facts, even if body was evicted
  V->>U: Publish affected entries
  Note over R,V: Inserts/deletes use structure(from,to).<br/>Leases own IDs, not shifting positions.<br/>Late async results must pass membership/content epochs.
Loading

Repeated readTurn calls also reuse the identity index while a pending transaction is unchanged. A pending transaction alone no longer forces a full ID scan on every body read; new operations, identity/structure edits and checkout still invalidate it. Reads never commit the document.

Ownership and compatibility

Responsibility Current owner
Compose metadata Mirror, reader, view and writer create-conversation-session.ts
Shallow reads, target lookup and storage events session-data/loro.ts
Window leases, LRU, stale-read protection create-conversation-view-from-reader.ts
Shared goal/permission/diff facts derivation.ts + deriveSessionTurnFacts; consumers share one table per view
Validation and minimal writes Existing HistoryWriter + business planners; unchanged opaque/legacy data survives
Explicit export/replay/sharing Authoritative readAll, never stitched display windows

The old raw-doc view, test-only memory backend, storage capability registry and runtime window-mode fallback are gone. The array adapter remains for static shared pages, not as an app fallback. Detached fork snapshots reuse the writer's provenance and survive source unload. Stored data is not migrated. Import and editable-tail replacement retain commit-time checks and conditional compensation.

Before / after

Before After
Full history bodies allocated before the window is usable Shallow directory, then requested bodies
Pending transaction can cause an ID scan for each target read Reuse the index until a new operation or relevant invalidation
Outline summaries derived across offscreen history Question/reply preview acquired when the hover card opens
Display array and storage access intertwined Reader-backed display cache; writes retain one owner
A view cache can miss an evicted turn's changed fact Explicit changed IDs invalidate facts independently of hydration

Local benchmark

Compared main f3c399fe with PR 75ffc7bf, on macOS arm64 / Node 22.23.1, with each checkout's frozen lockfile. Five fresh processes per revision, alternating main/PR order; no simultaneous benchmark jobs.

Input: deterministic synthetic 1000 user rounds = 2000 entries, seed 376, generated by the committed replay worker through HistoryWriter. Each round includes a user message, thought, 4096-character tool output and assistant answer with high-entropy text. Both revisions import the same 12,687,300-byte snapshot, SHA-256 19794e069e956201613460c1844bcae4e298f5b2f6a159f974cffb6001292d98.

Measurement (median) Main PR Interpretation
Snapshot import 10.15 ms 10.29 ms No claimed decoding gain
Reader initialization through last-30 bodies available 110.73 ms 22.98 ms 4.82×, 79.2% less time
Import + reader/window preparation 121.37 ms 33.17 ms 3.66×
Complete entry objects materialized at that point 2000 30 Storage still contains all 2000

Reader/window raw samples (ms): main 110.50, 109.94, 120.69, 110.73, 112.58; PR 22.95, 23.73, 22.86, 22.98, 23.43.

Boundary: file reading, TS/module loading and fixture generation are outside timing. The main endpoint is createSessionMirror(...).getState().history.slice(-30); the PR endpoint is directory readiness plus acquireRange(last30).ready. Both verify the returned IDs and item payloads against the stored tail. Idle callbacks are not drained and React/business derivation consumers are not mounted. This does not measure paint, Markdown, Virtua layout, scrolling, IPC, network, persistence, or device frame time.

Larger high-entropy workload: complete cold data preparation

The earlier 120-second timeout was followed up with a longer per-process deadline. All three main and three PR runs completed. Same revisions as above (f3c399fe / 75ffc7bf), macOS arm64, Node 22.23.1, --expose-gc --max-old-space-size=8192; both checkouts use loro-crdt 1.15.1 / loro-mirror 2.3.2. Fresh processes run sequentially in alternating order.

Input is the local high-entropy sanitized Storybook workload: 140,206,036 bytes, 1000 user rounds, 2671 history entries, reused unchanged on both revisions. The fixture and raw evidence remain local and are not committed or uploaded. These results describe this workload, not all 1000-round conversations.

Phase (median of 3 runs) Main PR
Snapshot import 86.5 ms 78.4 ms
Full Mirror / shallow directory initialization 157,161 ms 220.5 ms
Initial window hydration Included in full materialization 16.1 ms
Message-row construction 19.1 ms 1.0 ms
Left-outline construction 24.1 ms 0.9 ms
Total cold data preparation 157.3 s 317.7 ms
Complete bodies materialized 2671 42
Process RSS sampled after outline construction 2879 MiB 884 MiB

Total samples: main 142.0 / 157.3 / 167.6 seconds; PR 317.7 / 392.2 / 305.4 ms. Totals include the common session-ID setup (~2 ms); medians of phases need not sum to the median total.

The main path calls the actual full createSessionMirror, then the actual message-row and outline builders. The PR path calls createConversationSession, waits for the directory and the product's initial window (last 40 entries, extended to the preceding user question: 42 bodies for this fixture), then the same row/outline entrypoints. Both produce 1000 outline entries. Main has 2663 message rows and PR 2671 because offscreen placeholders differ from full empty-assistant filtering.

The measured main delay is inside full Mirror initialization, not the later outline builder. Outline construction consumes already-materialized JS data; it is not another independent CRDT toJSON() pass. No internal CPU profile was taken to attribute the Mirror delay to a narrower algorithm.

Measurement boundary: file I/O, module loading, fixture generation, React/Markdown mounting, browser layout/paint, network/IPC and background business-fact scans are excluded. Idle callbacks are disabled. RSS is an after-outline process sample, not peak memory, retained heap after GC or browser memory. This is a complete A/B of the stated data-preparation path, not end-to-end UI acceptance.

Reproduce the input and opening comparison

Generate the synthetic input once on this PR using the committed benchmarks/replay worker; reuse the resulting snapshot for both checkouts. The full replay also covers window jumps, append, streaming and hover preview, and can emit CPU profiles.

node packages/components/benchmarks/replay/run.mjs --rounds 1000 --runs 1 --mode committed --out /tmp/replay-1000
# Input for both revisions: /tmp/replay-1000/synthetic.snapshot

For the opening comparison, save the following as packages/components/open-compare-worker.ts in both checkouts. Run in a fresh Node 22.23.1 process with --expose-gc --max-old-space-size=8192 --import <the checkout's tsx loader>; arguments are main|pr and the snapshot path. Alternate the modes across five repetitions. The benchmark uses a committed session-id initialization in both modes.

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { performance } from 'node:perf_hooks';
import { LoroDoc } from 'loro-crdt';
import { createSessionMirror } from '@lody/shared';
const mode = process.argv[2];
const bytes = new Uint8Array(fs.readFileSync(process.argv[3]));
const sessionId = 'benchmark-open';
const makeWindowed = mode === 'pr' ? (await import('./src/lib/conversation-view/create-conversation-session')).createConversationSession : undefined;
global.gc?.();
const startMemory = process.memoryUsage();
const doc = new LoroDoc();
let t = performance.now();
doc.import(bytes);
const importMs = performance.now() - t;
doc.getMap('session').set('id', sessionId); doc.commit();
const expected = doc.getList('history').length;
const from = Math.max(0, expected - 30);
let rows: unknown[];
let count: number;
let dispose: () => void;
let release = () => {};
t = performance.now();
let directoryMs = 0;
if (makeWindowed) {
  const session = makeWindowed(doc, {sessionId: sessionId as never, scheduleIdle: () => () => {}, yieldToEventLoop: () => new Promise<void>(resolve => setImmediate(resolve))});
  for(let pass = 0; session.history.turnCount !== expected; pass++) {
    assert(pass < 1000, 'directory did not become ready');
    await new Promise<void>(resolve => setImmediate(resolve));
  }
  directoryMs = performance.now() - t;
  const lease = session.history.acquireRange(from, expected);
  await lease.ready;
  rows = Array.from({length: expected - from}, (_, i) => session.history.turn(from + i));
  const openMs = performance.now() - t;
  count = Array.from({length: expected}, (_, i) => session.history.isHydrated(i)).filter(Boolean).length;
  dispose = session.dispose; release = lease.release;
  finish(openMs);
} else {
  const mirror = createSessionMirror({doc, initialState:{session:{id:sessionId as never},history:[]}});
  directoryMs = performance.now() - t;
  const history = mirror.getState().history;
  rows = history.slice(from);
  const openMs = performance.now() - t;
  count = history.length; dispose = mirror.dispose;
  finish(openMs);
}
function finish(openMs:number) {
  const memory = process.memoryUsage();
  assert.equal(rows.length, Math.min(expected,30));
  for(let i=0;i<rows.length;i++) {
    const raw=doc.getList('history').get(from+i);
    const stored=raw && typeof raw === 'object' && 'toJSON' in raw ? raw.toJSON() : raw;
    assert.equal((rows[i] as {id:string}).id,(stored as {id:string}).id);
    assert.deepEqual(JSON.parse(JSON.stringify((rows[i] as {items:unknown}).items)),(stored as {items:unknown}).items);
  }
  console.log(JSON.stringify({mode,node:process.version,entries:expected,bytes:bytes.length,importMs,directoryMs,openMs,totalMs:importMs+openMs,materialized:count,heapDeltaMB:(memory.heapUsed-startMemory.heapUsed)/1048576,rssMB:memory.rss/1048576}));
  release(); dispose(); doc.free();
}

Test plan

  • On the merged head: component typecheck passed; 71 sharing tests and 32 focused opening/preview/share-reader tests passed; scoped lint/format and public-boundary validation passed locally. Isolated dependency/submodule setup was required; the live Storybooks were not modified.
  • Before merging main, the optimization tree passed 1150 shared tests and 3558 component tests. These are prior package results, not a claim that those exact counts were rerun on the merged head.
  • CI at 75ffc7bf: Tests and Desktop E2E (smoke) passed. Static checks failed on three unawaited promises in packages/acp-extension-core/test/usage.test.mjs (lines 12, 18, 30). Main and this head pin the same core submodule 80205d81; do not read this as an all-green CI claim. Current run.
  • This turn's A/B run checks exact tail IDs/payloads and measures both real opening entrypoints. The committed replay asserts window membership, append and every streamed value; errors/timeouts fail the run.

Remaining costs: Loro import, the initial directory and explicit full reads are still O(total). Goal/permission/diff derivations still read history in the background when consumers mount; lazy outline previews do not remove that business work. One enormous turn can still be expensive. Pinned ranges can exceed the body-cache target. No 3000-round desktop/mobile memory or frame-time acceptance is claimed.

Context handoff

Instructions for reviewing agents

  • Review focus: Reader epochs and identity-owned leases; changed IDs reaching evicted facts; changed-only writes and import/tail commit guards.
  • Decisions to challenge: Shallow O(total) indexing and one shared writer remain; static shares adapt arrays while app history has one windowed implementation.
  • Plausible failures / evidence gaps: Mixed structural/content races, large-turn latency, background business scans and unmeasured device memory/frame time; Static checks currently fail as detailed above.

Authoring context

  • User goal / directives: Open long conversations by window and explain the current architecture with reproducible main/head measurements.
  • Constraints / non-goals: Preserve legacy/unknown stored data and one writer; no second CRDT, payload externalization or data migration.
  • Risk-bearing decisions: Stable-ID leases and epoch fencing protect delayed reads; explicit changed IDs invalidate evicted facts; export uses authoritative data.
  • Destructive or irreversible behavior: Opening does not rewrite history. Existing import/edit operations retain guarded writes and awaited conditional compensation.
  • Deliberately not done or tested: A replacement backend and full device-scale performance acceptance; device rendering and background business-fact scans are excluded from the benchmarks.
  • Unknowns / confidence: Small synthetic gains repeat across five fresh processes and the larger high-entropy comparison across three; UI frame time and other workloads still need separate verification.

Original user prompt

请清理一下这个 pr 中的意外复杂度。产品目标只有一个:打开长会话时按窗口读、不物化全量历史。

… open/import benchmarks

- Move buildHistoryReplayImport, materializeReplay, refresh/conflict
  decisions, hashing and catalog helpers out of apps/cli and
  packages/shared into packages/history-import. The package is pure:
  clocks (nowIso, lastSyncAt) are injected, no IO, no Loro, no logger.
- apps/cli local-project-history-sync-service keeps orchestration only;
  its pure-logic tests move with the code.
- Add benchmarks: synthetic replay fixture, desensitizer for real local
  session docs, tinybench open-conversation (snapshot -> Mirror) bench,
  import bench, and a CLI script that captures a real ACP replay.
- Root AGENTS.md project map lists the new package.

Model: claude-fable-5-1
…e through HistoryWriter

The session store no longer materializes `history`: the control-plane Mirror
uses `sessionControlPlaneSchema` (`history: schema.Ignore()`) over a doc
facade that drops history events and skips root enumeration, turns are read
through a windowed `ConversationView` (index rows from shallow reads, tail
hydrated eagerly within an item budget, LRU with pinned ranges, idle
summaries, incremental copy-on-write patches from doc events), and writes go
through `HistoryWriter`, whose container shape is op-for-op what
`Mirror.setState` produced. Hooks that scanned `getState().history` now use
the writer's `read`/`replace` or index queries. `LODY_CONVERSATION_VIEW=0`
or the Developer-mode switch restores the full Mirror for one release.

Model: claude-fable-5-1
…lder rows

`SessionChatStream` takes `view`; `buildChatStreamItems` yields a message or
an index-row placeholder per turn, rows/outline/`scrollToIndex` use absolute
turn indexes, `TurnPlaceholderRow` sizes non-hydrated turns from the row
summary, and the viewport reports its turn range to drive hydration with two
screens of prefetch. Every full-history reader in the session surfaces moves
to the hydrated tail, index rows, or the background per-turn fact table
(`useSessionTurnFacts`); search hydrates while open and export on demand. A
guard test fails on any `sessionDoc.history` read under `src/components`,
and a 3,000-turn doc-backed story exercises scroll, outline jumps, and
expansion.

Model: claude-fable-5-1
…sationView

`bench:open` runs on the synthetic replay by default (a desensitized real
fixture stays local) and adds `open`, `open+idle`, `scroll`, `stream`, and
`stream(Mirror)` next to the full-Mirror baseline.

Model: claude-fable-5-1
…Actions tests

Dispatch, steer, and pending_apply promotion now read and replace turns through
`store.historyWriter`, so the runtime stubs expose one over their history array.

Model: claude-fable-5-1
…t exhaust wasm memory

A `LoroDoc`'s memory lives in the wasm heap and is reclaimed only when V8
finalizes its JS wrapper, which it does lazily. The open/import tasks build one
doc per iteration, so at x10 (2,400 turns, ~108k containers) the heap filled
mid-run and the next string crossing trapped with `RuntimeError: unreachable`.

Model: claude-opus-5[1m]
…open stays under 50 ms

The eager pass resolved `itemCount` / `planCount` per turn, which costs a
`getContainerById` plus a `length` crossing each: measured on the x10 synthetic
fixture (2,400 turns) that is ~17 ms of a 77 ms first paint, for numbers nothing
on screen needs yet. Counts now arrive with the turn's summary from the idle
pass, exactly when a turn is hydrated, or on demand for the tail turns whose
hydration budget needs them.

An unresolved count reads as unknown rather than zero, so
`isEmptyAssistantIndexRow` and the permission scan treat it as "not empty": a
real turn is never dropped from the stream, and only an interrupted turn shows a
placeholder until its counts land.

x10 open: 77.3 ms -> 48.1 ms mean (p99 104 -> 57), stream p99 0.04 ms.

Model: claude-opus-5[1m]
…ve view

The 2,000-turn fixture appended every turn through `HistoryWriter` while a
`ConversationView` was subscribed to the same doc, so each append also ran a
full event pass — tail hydrate, LRU eviction, summary refresh. That is ~900 ms
of test-only work locally and pushed the test past vitest's 5 s budget on a
two-worker CI runner.

`append` never consults the view (it inserts at the tail), so the build uses an
unattached stub instead: 1232 ms -> 463 ms locally. The fixture stays rich on
purpose — a full Mirror over it costs ~272 ms against the 30 ms bound, while a
one-text-item fixture would materialize in ~45 ms and leave the assertion no
usable margin.

Model: claude-opus-5[1m]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c2c41c384

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +85 to +89
if (change.kind === 'index') {
if (view.turnCount < lastTurnCount) changed = pruneRemoved() || changed;
lastTurnCount = view.turnCount;
// Appended turns land in the hydrated tail; derive whatever is there.
changed = deriveHydratedRange(Math.max(0, view.turnCount - 64), view.turnCount) || changed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Invalidate facts when a non-hydrated turn changes

After the background pass has derived an older turn, a later update to that non-hydrated turn emits an index change, but this branch only re-derives the last 64 turns and retains the existing fact for every older ID. For example, late file-diff evidence written to a finalized turn after more than 64 newer turns leaves useSessionDiffSummary permanently stale until the view is recreated; older permission, scheduling, and goal facts have the same problem. Invalidate the affected fact and schedule hydration/re-derivation rather than assuming every index event is an append.

AGENTS.md reference: packages/components/src/lib/conversation-view/AGENTS.md:L29-L32

Useful? React with 👍 / 👎.

…nges

Two defects in the per-turn fact table, both only reachable once a turn has
been evicted:

The view reported nothing when a change landed under a turn nothing holds
hydrated. A `fileDiff` or `modelInfo` write touches no index scalar and cannot
be patched into a turn object that does not exist, so `handleBatch` fell
through silently. Any consumer caching something derived from that turn kept
its stale value until the whole view was rebuilt — `useSessionDiffSummary`
would show a diff summary missing a later file diff on an older turn. The view
now reports the index for those turns too, so a cache can see it is stale.

`createConversationDerivation` then acts on that signal: a changed range is
re-derived when hydrated and DROPPED when not, with the background pass
restarted so the fact is derived again instead of disappearing. The drop is
driven only by explicit change events; the speculative tail window keeps
deriving without dropping, or every index event would discard the facts of
every turn past the hydrated tail.

The pass also leaked its hydration pin: `ensureRange` pins before its first
await, and disposing the derivation mid-chunk returned without releasing. The
view outlives the consumer in the warm store cache, so those turns became
permanently un-evictable and the hydrated set grew past `maxHydrated` — the
resident-memory regression this module exists to remove. The release now runs
in a `finally`.

Both regressions are covered in tests/conversation-derivation.test.ts, and
both fail without their fix (the leak leaves 10 turns hydrated against a cap
of 6).

Model: claude-opus-5[1m]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffa2915a82

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

});
return useMemo(() => {
if (!view) return { turns: EMPTY_TURNS, from: 0 };
const next = collectHydratedRange(view, from, to);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Collect the hydrated suffix of the tail

When the last 20 turns exceed the item budget, createConversationViewFromDoc hydrates them newest-first and can leave the slot at from unhydrated; this forward collector then stops at that first gap and returns an empty or stale prefix even though the newest turns are already available. session-chat-interface.tsx uses this result for active-turn, configuration, activity, and app-review logic, so a fast sync can baseline the review hook without the historical completion and later treat that completion as new, potentially showing StoreKit merely from reopening a completed session. Collect the contiguous hydrated suffix ending at to, or withhold readiness until the entire requested tail is hydrated.

AGENTS.md reference: packages/components/src/hooks/AGENTS.md:L74-L79

Useful? React with 👍 / 👎.

… the control-plane Mirror

Two contracts the `history: Ignore` schema and the doc facade change the
mechanics of, neither covered by a test until now.

An unknown root written by a peer on a newer schema is no longer discovered at
construction — the facade answers root enumeration with nothing, so a root that
never changes during the session stays out of Mirror state. The part
providers/AGENTS.md actually requires still holds and is now asserted: a
control-plane write neither deletes nor rewrites it.

A stray `setState` touching `history` is skipped on write, so the durable list
is untouched and the view still reports the real turns; the value lingers only
in that Mirror's memory, which is what `schema.Ignore` means and what
`SessionDocState` (omitting `history`) keeps anyone from reading back.

Model: claude-opus-5[1m]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 701a3ad916

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +118 to +122
if (change.kind === 'index') {
if (view.turnCount < lastTurnCount) changed = pruneRemoved() || changed;
lastTurnCount = view.turnCount;
// Appended turns land in the hydrated tail; derive whatever is there.
changed = deriveHydratedRange(Math.max(0, view.turnCount - 64), view.turnCount) || changed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restart the fact pass after bulk appends

The added range invalidation fixes edits to existing turns, but structural batches still take this index branch: when an open session receives more than 64 turns at once—such as the single bulk append in apps/cli/src/lib/local-project-history-sync-service.ts:828-829—only the final 64 positions are considered, and this path neither calls requestPass() nor clears complete. Any earlier newly appended turns therefore never receive facts, so whole-history consumers such as fallback diff summaries, goals, and scheduled tasks permanently omit them until the view is recreated. Restart the background pass whenever the turn set changes.

AGENTS.md reference: packages/components/src/lib/conversation-view/AGENTS.md:L29-L32

Useful? React with 👍 / 👎.

…ced invariant

`SessionDocState` omitting `history` and `SessionDocUpdater` taking a draft
without it already make every ordinary second path a compile error: reading
`getState().history` is TS2339, an unannotated `setState` callback is TS7006,
and the pre-change `(draft: SessionDocMeta)` annotation is TS2345. What types
cannot close is a deliberate escape, so the guard now covers those instead of
only the literal spellings, and covers all of `src` rather than
`src/components`:

- a cast that puts the key back — `(draft as { history }).history.push(...)`,
  `(getState() as SessionDocMeta).history`, `draft.history = [...]`
- reaching past the store into the raw document — `getList('history')`
- the `SessionDocMeta` annotation the updater no longer accepts

Block comments are stripped and unrelated `history` receivers (the browser's,
a local project's import catalog) are blanked, so prose and neighbouring
features do not trip it. `lib/conversation-view` and the rollback branch that
builds its adapter stay exempt, and that exemption list is asserted to be
exactly the set of modules that need it — a stale entry fails too.

The rule itself now lives in components/src/AGENTS.md, where hooks, providers,
atoms and components can all find it; ai-gui points at it.

Model: claude-opus-5[1m]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bec90e22a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (anyChanged || allBlocks.length !== lastBuiltLengthRef.current) {
setBlocks(allBlocks);
lastBuiltLengthRef.current = allBlocks.length;
const unsubscribe = view.subscribe(scheduleRebuild);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Hydrate newly inserted turns while search remains open

When history grows after the one-time hydration loop has finished, this subscription only rebuilds from turns that are already hydrated. A bulk append such as apps/cli/src/lib/local-project-history-sync-service.ts:828-829 can add more turns than the view's hydrated tail retains, so the earlier inserted turns remain absent from search results until the user closes and reopens search. Handle structural view changes by pinning and hydrating the newly added range before rebuilding the index.

Useful? React with 👍 / 👎.

…from the conversation view

Cleanup pass over the windowed-history change. No behaviour change; the whole
components suite and the guard test are unchanged and green.

Reuse:
- `summarizeTurn` now calls the outline's own `firstTextOf` / `proseLengthOf`
  and shares `SUMMARY_SOURCE_WINDOW` instead of redeclaring the 960-char window
  as `TURN_SUMMARY_HEAD_CHARS`. Two definitions of "prose" had to stay
  byte-identical or a round's title and tick weight would change the moment its
  turn was evicted.
- `resolveActiveAssistantTurnIdFromIndex` delegates to `@lody/shared`'s
  `resolveActiveAssistantTurnId`, so "active" keeps one definition.
- The rAF coalescer existed three times (view, derivation, search); one
  `subscribeOnFrame` now takes any `subscribe(listener)` source.
- `isRecord` existed five times; one `isPlainRecord`, with the materializer
  keeping its stricter loro-mirror-compatible variant.
- The chat-stream-items cache uses the shared `LRUCache`.
- `CONTROL_PLANE_IGNORED_ROOT_KEYS` is read off the schema's `Ignore` fields, so
  the doc facade cannot drift from the schema it fences.

Simplification:
- `deriveHydratedRange` and `refreshRange` were the same walk twice; one
  `deriveRange(from, to, dropStale)`.
- `runIdleChunk` re-inlined `withHydratedFacts` and `fillRowCounts`; it calls
  them, and its tail pre-pass is dropped because the backwards main loop reaches
  those rows first with the same result.
- The items/plan count branches differed only by key name.
- `TurnIndexRow` is derived from `INDEX_SCALAR_KEYS` rather than repeating the
  eleven keys in a third place.
- Dead surface removed: `useConversationView` (no callers),
  `summarizeTurnShallow`'s unused `doc` parameter, the `idleChunkSize` /
  `idleItemBudget` options nobody passed, seven barrel exports with no importer,
  and the placeholder row cache (its consumer memoizes on `item.row`, which is
  already stable).

Efficiency:
- `rebuildLookups` cleared and rebuilt the whole id map per list delta;
  appending to a 2,400-turn conversation now touches one entry.
- `resolveTurnIndex` trusts `event.path[1]` unless the same batch moved turns,
  which retires a descendant→turn cache that grew to 100k entries per session
  and was then cleared wholesale.
- `respondPermission` no longer allocates an N-element candidate list, and the
  permission card passes the turn id it already knows, so the common path is one
  lookup instead of a scan.

Model: claude-opus-5[1m]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c43b62a29

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +43 to +46
let turnBlocks = cache.get(turn);
if (!turnBlocks) {
turnBlocks = extractSearchBlocksForMessage(turn, i);
cache.set(turn, turnBlocks);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recompute cached search indices after structural edits

When a turn is inserted or deleted before an already indexed turn while search remains open, the surviving hydrated turn keeps the same object identity, so this cache reuses blocks whose embedded messageIndex still points to the old position. Search navigation passes that stale index to scrollToIndex, causing results after the structural edit to jump to and highlight the wrong turn; cache entries need to include or refresh the current index.

Useful? React with 👍 / 👎.

…nderer

`pnpm typecheck` failed in `@lody/history-import`, whose benchmark imports this
module by relative path. Two causes, both introduced by the cleanup pass:

- `turn-summary.ts` and `types.ts` reached the outline through the `@/` alias,
  which only exists in the components tsconfig. The module is consumed
  cross-package, so it now imports siblings relatively; the note in the module's
  AGENTS.md says why, and `projected-conversation-view.ts` is converted too
  rather than left as the next trap.
- `conversation-outline.ts` imported `TurnIndexRow` from the conversation-view
  BARREL, so `types.ts` -> outline -> barrel closed a cycle and dragged
  `feature-flag.ts` — which reads `import.meta.env` — into a compilation with no
  Vite types. It imports the module file instead.

Verified with the whole workspace typecheck this time, not one package: `pnpm
typecheck`, `pnpm format:check` and `pnpm check:quick` all pass, components
428 files / 3072 tests and history-import 33 tests green.

Model: claude-opus-5[1m]
@zxch3n zxch3n changed the title feat: open any-length conversations in O(window) via ConversationView feat: open long conversations with windowed ConversationView Sep 6, 2026
zxch3n added a commit that referenced this pull request Sep 7, 2026
Extract the schema-aware HistoryWriter from #376 and route renderer and CLI history changes through one shared path. Parse new and changed inputs before CRDT operations while preserving untouched incompatible history and existing containers. Keep the full Mirror read path, add compatibility and compile-time regressions, and document the contract.

Model: gpt-6
zxch3n added a commit that referenced this pull request Sep 9, 2026
* fix: preserve future history items when sending messages

Model: gpt-6

* docs: scope session history invariant to shared package

Move the history storage rule next to its schema and tests so the root AGENTS.md stays within the documentation CI size limit. Preserve the rule and add the required CLAUDE.md symlink.

Model: gpt-6

* docs: require agent notes for non-trivial work

Clarify note triggers for implementation, research, and design; preserve read-only task restrictions and existing note ownership. Record the policy in bilingual notes and link the repository map to keep root instructions within budget.

Model: gpt-6

* docs: consolidate documentation workflow in root instructions

Make reading, Spec approval, Agent Note updates, and completion checks explicit in root AGENTS.md. Move catalog explanations into a linked guide while preserving binding rules and update the bilingual process note.

Model: gpt-6

* fix: temporarily bypass session whole-state validation

Disable session Mirror update validation in the renderer and CLI while preserving external input parsing. Cover malformed historical items and both construction sites, and document the temporary tradeoff and replacement contract.

Model: gpt-6

* fix: centralize session history writes behind typed boundaries

Extract the schema-aware HistoryWriter from #376 and route renderer and CLI history changes through one shared path. Parse new and changed inputs before CRDT operations while preserving untouched incompatible history and existing containers. Keep the full Mirror read path, add compatibility and compile-time regressions, and document the contract.

Model: gpt-6

* fix: accept fork origin notices in history writes

Add the correlated fork-origin schema and bidirectional notice type coverage. Exercise regular and worktree forks through the real SessionDocument and HistoryWriter; retain malformed metadata rejection and document the prior test gap.

Model: gpt-6

* fix: preserve stored history across fork and rollback

Separate writer-authored input from writer-captured stored history. Preserve opaque content during forks and guarded edit rollback, with real Loro integration coverage.

Model: gpt-6

* fix: preserve history across initialization and read acknowledgements

Retain fork setup logs, allow replacement read acknowledgements during rollback, and validate tool state changes without reparsing untouched payloads.

Model: gpt-6

* chore: move documentation maintenance changes to PR 466

Keep HistoryWriter-specific contracts and repair notes here; the general documentation workflow is reviewed independently in #466. No runtime code changes.

Model: gpt-6

* fix: preserve steer provenance and permission metadata

Preserve steer delivery markers through history writes and reads, and validate permission metadata updates independently of legacy tool payloads. Add real-writer regression coverage and update the related specs and notes.

Model: gpt-6

* fix: track stored baselines for imported history

Keep source hashes and turn identities stable while validating refreshed history against a cursor-bound stored-content baseline. Cover legacy storage, concurrent edits, delayed cursors and conflict resolution with real writer regressions, and document compatibility limits.

Model: gpt-6

* fix: preserve peer edits when resolving task proposals

Resolve task decisions against the latest history item through HistoryWriter instead of replacing stale entries. Add deterministic replica regressions and document the remaining structural-edit limit.

Model: gpt-6

* fix: isolate invalid ACP history inputs and preserve extensions

Accept JSON protocol extensions without relaxing known message validation, preserve unchanged legacy tool blocks, and isolate deterministic write failures from subsequent ACP output. Add nested schema contracts, regressions, and implementation notes.

Model: gpt-6

* fix: scope history rollback to the changed range

Preserve concurrent edits to untouched history rows during edit-and-resend compensation. Add real replica and service regressions and update rollback contracts and notes.

Model: gpt-6

* perf: reduce history writer parsing and field update overhead

Index discriminated schemas, avoid redundant copies and unchanged-container reads, narrow field updates, and batch initial history writes. Add synthetic benchmarks and document the remaining full-Mirror performance gap.

Model: gpt-6

* perf: use target-local history updates for ACP streaming

Route targeted text and thought batches through turn-local writes, derive cached single-pass input parsers with refinements retained, and reduce item and stored-copy matching overhead. Add compatibility regressions and benchmarks documenting remaining seed and commit costs.

Model: gpt-6

* perf: copy only changed paths for Mirror text events

Patch the pinned Mirror reader for single existing text updates while preserving descriptors, snapshots, and notifications. Add real-replica regressions, isolated reader benchmarks, and documentation of remaining bulk-import limits.

Model: gpt-6

* fix: preserve queued messages and compatible history edits

Normalize legacy selectors on new writes, validate independent field edits locally, retain queue items until history acceptance, and capture only the rollback range while preserving later appends. Add regressions and document remaining recovery limits.

Model: gpt-5

* chore: consolidate history writer docs and remove shallow tests

Consolidate incremental notes into the bilingual owning record, repair references, remove source-string construction tests, and require behavior-focused tests and compact PR documentation in AGENTS.md.

Model: gpt-5

* fix: repair queued turn activation on promotion retry

Retain queued messages until history and activation publication succeed. Recover history-only commits without duplicating turns, replaying terminal messages, or replacing another pending activation. Cover metadata failures with a real SessionDocument regression and update the owning documentation.

Model: gpt-5

* fix: preserve history input filtering through Zod pipes

Traverse both sides of schema pipes while preserving preprocessing, transforms, and refinements. Cover nested config filtering, legacy normalization, stored-history preservation, and invalid-write atomicity with real writer tests.

Model: gpt-5

* chore: adopt loro-mirror 2.3.2

Use the released Mirror text-event optimization and remove the temporary 2.3.1 patch. Update the paired React/Jotai packages and lockfile, and record the upstreamed reader path in the owning notes.

Model: gpt-5

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfe826410a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (!list) return undefined;
for (let index = list.length - 1; index >= 0; index -= 1) {
const value = list.get(index);
if (!isContainer(value) || value.kind() !== 'Map') continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle inline rows when selecting the plan target

When a resumed/upgraded session's newest assistant turn uses the supported legacy plain-JSON representation, this separate assistant lookup skips it and either selects an older container-backed turn or returns no id. handleACPUpdateMessage sends every latest ACP plan snapshot through doc.setPlan, so recovery of an inline assistant can durably attach the live plan to the wrong turn or silently omit it; inspect plain record rows for role and id here as the reader and writer already do.

AGENTS.md reference: packages/shared/src/session-data/AGENTS.md:L90-L92

Useful? React with 👍 / 👎.

Remove the unused history-import workspace and its capture/dump tooling. Keep a focused synthetic benchmark over the production reader and window hydration.

Model: gpt-6
Delete the unused raw-doc view, migrate its useful regressions to the shipped reader, and invalidate derived facts with explicit changed turn IDs. Keep only a build-time rollback and use one authoritative full read for exports.

Model: gpt-6
…venance

Keep CLI execution methods with SessionDocument, reuse shared planners, remove the test-only memory backend and unused receipts/durability hooks, and let captured HistoryWriter snapshots survive source disposal. Private Mirror access replaces the bespoke AST guard. Preserve real storage, concurrency, compatibility and fork regressions.

Model: gpt-6
@zxch3n zxch3n changed the title feat: isolate session history behind windowed reads and domain commands feat: read long conversations by window with shared history writes Sep 13, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ae04afdf4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const findLatestPendingUserTurn = async (data: SessionData): Promise<SessionTurn | undefined> => {
const count = await data.history.count();
for (let position = count - 1; position >= 0; position -= 1) {
const read = await data.history.readAt(position);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid materializing assistant bodies in the auto-read scan

With auto-read attached, every streaming history commit invokes this scan, and readAt() materializes each trailing turn with LoroMap.toJSON() before the loop reaches the latest user row. During a long assistant response, token batches therefore repeatedly parse the ever-growing assistant body even when that user is already seen, creating quadratic work and potentially stalling ordinary sessions independently of session output --wait; locate and status-check the latest user through readDirectory() and materialize only that user when necessary.

Useful? React with 👍 / 👎.

@@ -0,0 +1,2 @@
/** Temporary build-time rollback. Remove with the array adapter after two releases. */
export const WINDOWED_CONVERSATIONS = import.meta.env.LODY_CONVERSATION_VIEW !== '0';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expose the rollback constant to the renderer build

In the checked OSS Electron build, apps/electron/electron.vite.config.ts sets envPrefix to __LodyPublicBuildOnlyPrefix__ and its explicit define helper admits only VITE_* keys, so setting LODY_CONVERSATION_VIEW=0 leaves this property undefined and the expression always selects the windowed reader. The documented two-release rollback therefore cannot be activated for the shipped desktop build; explicitly inject this constant into the renderer build or use an admitted build-only prefix.

AGENTS.md reference: packages/components/src/lib/conversation-view/AGENTS.md:L22-L24

Useful? React with 👍 / 👎.

Use one shipped window reader and identity change events. Read CLI history synchronously, keep auto-seen on shallow scalars, and let stored permission outcomes win same-stack auto-approval. Ordinary writes propagate errors; import and editable-tail replacement retain commit-phase outcomes. Share the goal and diff fact table, remove forwarding modules, and test the single Loro implementation directly.

Model: gpt-6
Preserve Stop/undelivered-steer coverage with the session history port fixture and retire tests for main’s removed auto-prompt flow.

Model: gpt-6
Restore scroll position before paint and follow the DOM bottom without retaining a virtual row index that hydration can invalidate.

Model: gpt-6
Add a deterministic Node replay and profiling harness for long-conversation performance checks. Keep local snapshots and profiling artifacts out of the repository.

Model: gpt-6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ede159a182

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (released) return;
released = true;
pinIds(capturedIds, -1);
evict();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Notify subscribers when release evicts hydrated turns

When a whole-history lease is released—for example, when search closes after hydrating a 3,000-turn conversation—evict() removes the unpinned bodies without bumping the view version or emitting a change. useConversationStreamItems therefore remains memoized on its fully hydrated items, and its BuildChatStreamItemsCache continues retaining each turn through rawEntry; a completed session can remain fully materialized indefinitely despite the view reporting those turns as evicted. Publish a cache change whenever release-triggered eviction removes bodies so consumers rebuild with placeholders.

AGENTS.md reference: packages/components/src/lib/conversation-view/AGENTS.md:L5-L10

Useful? React with 👍 / 👎.

Keep static share publication and route shared snapshots through the ConversationView adapter. Adapt publication reads and sharing regression fixtures to the session history API.

Model: gpt-6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75ffc7bf0b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

isActive: () => boolean = () => true
): Promise<{ turn: SessionHistoryInput | null; history: SessionHistoryInput[] }> {
const history = await sessionDoc.getHistory();
const history = readSessionHistory(sessionDoc.sessionData.history);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid rematerializing history for every watched update

While a session has pending or processing work, the active-watch subscription enqueues a session check for every streaming history commit, and this line now makes each check call readAll()LoroList.toJSON() over the entire transcript. Thus normal assistant output—not only session output --wait—does O(history size × update count) parsing and allocation; long conversations can stall the CLI and delay output persistence or dispatch. Use directory/targeted reads for reconciliation or ignore body-only events.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L42-L43

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c37ad98f14

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

let userId = latestAssistant?.userId;
if (!userId) {
const history = await sessionDoc.getHistory();
const history = readSessionHistory(sessionDoc.sessionData.history);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid full-history reads for each usage event

For providers that emit usage updates during a turn, this fallback materializes and clones the complete transcript for every event via readAll(). Normal assistant entries created by createAssistantTurn do not contain userId, so this is the usual path rather than an exceptional fallback; long conversations can repeatedly stall the CLI and delay the pending-usage drain during finalization. Resolve the linked user through latestAssistant.userTurnId and a targeted readTurn instead.

Useful? React with 👍 / 👎.

const legacyMeta = meta as SessionLegacyMetaFields | null | undefined;
const historyGoal = resolveLatestSessionGoalFromHistory(await sessionDoc.getHistory());
const historyGoal = resolveLatestSessionGoalFromHistory(
readSessionHistory(sessionDoc.sessionData.history)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep GC probes from materializing every candidate history

When the periodic or memory-pressure GC evaluates idle sessions, session-gc-manager.ts calls hasActiveGoal for every candidate and again immediately before cleanup. This full readAll() projection materializes and clones every turn body, so a sweep over several long cached sessions creates a large allocation spike precisely while the process is trying to recover from memory pressure, potentially preventing eviction or causing an OOM. Use an incremental goal index or a bounded one-row-at-a-time scan instead.

AGENTS.md reference: apps/cli/src/lib/AGENTS.md:L58-L64

Useful? React with 👍 / 👎.

Keep the initial window lease until ready and follow committed virtual row geometry before paint, preserving explicit scroll intent.

Model: gpt-6
Preserve the caught React component stack in custom fallbacks and expose a copyable error report without resetting the composer.

Model: gpt-6
Preserve windowed row indices and ordered share capture; adapt new main fixtures to the session reader.

Model: gpt-6
Condense existing scrolling and resource lifecycle rules without changing their requirements.

Model: gpt-6
Wait for the rendered scroll destination rather than a scrollTop assignment. Observe mounted row geometry before paint and preserve the follow lock during layout corrections. Cover cold open and remount with a gated browser measurement regression.

Model: gpt-6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c75e76eb0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +328 to +331
const id = readIdentity(list.get(i))?.turnId;
if (id !== undefined) ids.push(id);
}
listener({ kind: 'changed', ids });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat turn-ID edits as structural changes

When a synchronized older/raw peer renames or removes a turn's id in place, this branch emits only the post-change ID (or an empty list). The ConversationView is still indexed by the old ID, so it cannot resolve that notification to a position and never refreshes the directory row or cached body, leaving the transcript permanently stale until the view is recreated. Classify identity edits as structural changes so the position is re-keyed.

AGENTS.md reference: packages/shared/src/session-data/AGENTS.md:L13-L17

Useful? React with 👍 / 👎.

@zxch3n
zxch3n merged commit 020c664 into main Sep 14, 2026
6 checks passed
zxch3n added a commit that referenced this pull request Sep 14, 2026
…ns (#695)

* test(components): reproduce the conversation-open blank flash

Doc-backed stories that mount `SessionChatStreamView` over a warm
`ConversationView`: the LoroDoc, the view and every `HistoryWriter.append`
happen while the stream is unmounted, so opening or switching costs exactly
what the sidebar costs on an already-loaded session. `OpenLongConversation`
toggles one conversation; `SwitchBetweenLongConversations` holds two warm
3,000-turn conversations under distinct session ids, because the reading
position and stream-item caches are per session.

`e2e/scripts/capture-conversation-open-flicker.mjs` drives them under
Playwright and samples the pane every animation frame — viewport presence,
computed visibility, committed row count, scroll offset — plus a screen
recording. CPU throttling stretches the flash past the recording's frame
period.

Model: claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(components): restore Virtua row measurements when a session reopens

Opening a long conversation showed an empty pane for ~55 ms on a production
build, longer on a slower machine. The cause is not #376's visibility gate:
ablating it kept the pane visible and the row set still emptied before it
refilled, now also exposing the uncorrected scroll position.

The cost is the cold virtualizer. With no measured row heights it lays a
3,000-turn conversation out at an estimated total height, writes the restore
offset into that wrong coordinate space, and only corrects once the first
rows are measured; the viewport stays hidden across those commits.

Virtua's measurements are now cached per session next to the reading position
and handed back through `Virtualizer.cache`, so a reopen lays out at the real
height and reveals a commit earlier. Two guards: the snapshot is positional,
so it is only restored when the row count is unchanged, and it is stored when
the initial layout settles and after scrolling stops, never at unmount —
React detaches the virtualizer ref before cleanup effects run.

Switching between two warm 3,000-turn conversations: 54 ms blank before,
33 ms after. A first, uncached open is unchanged, and the remaining blank is
the one commit Virtua needs before it knows its viewport size.

Model: claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(components): wait for real rows before consuming the row snapshot

The stored measurements were read on the first render with a positive item
count, which on the shipped session path is the wrong render. A session whose
document is still being acquired returns from the empty-state branch before
`Virtualizer` mounts, yet every hook above that return has already run, and
the session page's always non-null leading fragment counts as one row. The
lookup therefore answered for a one-row list, latched, and never asked again
for the real conversation — the blank flash stayed on the path the previous
commit set out to fix.

`useStickyScroll` now takes `hasVirtualizedRows` and reads the snapshot only
when the caller is about to mount the virtualizer.

The story missed this because it passed no `leadingContent`, so its empty
phase reported zero items and the latch never fired early. It now renders the
empty state on every switch and supplies the same non-null fragment the
session page does.

Switching between two warm 3,000-turn conversations on that faithful path:
54 ms blank on main, 57 ms with the read ungated, 17-35 ms with this fix.

Reported by the automated reviewer on #695.

Model: claude-opus-5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant