Skip to content

feat: open any-length conversations in O(window) via ConversationView - #376

Open
zxch3n wants to merge 11 commits into
session/8336142afrom
feat/conversation-view-range-rendering
Open

feat: open any-length conversations in O(window) via ConversationView#376
zxch3n wants to merge 11 commits into
session/8336142afrom
feat/conversation-view-range-rendering

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Opening a conversation no longer materializes its history. The session store builds a control-plane Mirror whose history is schema.Ignore(), and the renderer reads turns through a windowed ConversationView that hydrates only the viewport (plus two screens each side) and the streaming tail. History writes go through a HistoryWriter whose container shape is byte-identical to the Mirror writes it replaces. No data migration: the doc format is unchanged, and the old path stays behind a rollback flag for one release.

Base: session/8336142a. Built against today's loro-crdt APIs (getShallowValue, getContainerById().toJSON(), doc.subscribe), with the mapping to loro-mirror's upcoming LazyList documented in lib/conversation-view/AGENTS.md.

What changed

  • packages/components/src/lib/conversation-view/ (new)
    • createConversationViewFromDoc: index rows from one shallow read per turn (item/plan counts cost two more container crossings each and land with the summary instead), tail hydrated eagerly within an item budget, LRU (maxHydrated 200) exempting pinned ranges and the last 20 turns, summaries and shallow user config filled in idle chunks (ready), doc events applied incrementally (copy-on-write patch, full re-read fallback).
    • HistoryWriter (append / replace / respondPermission / read) over Loro container APIs, driven by the shared schema with loro-mirror's inference rules restated (history-materializer.ts).
    • createControlPlaneDoc + sessionControlPlaneSchema: the Mirror never sees history events (loro-mirror's event path applies ignored roots) and skips root enumeration (a ~35 ms lazy-snapshot walk on 2,000 turns).
    • createConversationViewFromHistory (rollback adapter), createProjectedConversationView (accepted-projection overlay with projectAcceptedSessionHistory placement), createConversationDerivation (background per-turn fact table for whole-history readers).
  • Store: SessionDocStore.history / historyWriter; SessionDocState has no history. createSessionStore picks the path from isConversationViewEnabled() (env LODY_CONVERSATION_VIEW=0 or the Developer-mode switch).
  • Renderer: SessionChatStream takes view. buildChatStreamItems(view, …) yields one item per turn (message or placeholder with the index row), rows/outline/scrollToIndex use absolute turn indexes, TurnPlaceholderRow renders non-hydrated turns sized from the row summary, and the viewport reports its turn range (findItemIndex) to drive hydration with ±2 screens prefetch. shift={false}, leadingContent as a real row, scrollRowToTop, and the outline correction loop are untouched.
  • Converted full-history readers (all of grep "getState().history" / sessionDoc.history under packages/components):
    • ai-gui/index.tsx last user message → index.
    • hooks/use-task-actions.ts, hooks/use-session-actions.ts (dispatch, steer, pending_apply promotion), hooks/use-session-doc.ts (updateHistoryEntry, hasLocalHistory) → historyWriter.read/replace.
    • hooks/use-remove-local-project.ts, sessions/managed-preview-surface.tsxresolveActiveAssistantTurnIdFromIndex.
    • providers/workspace-writer-impl.ts (startSession, appendSessionTurn, appendSessionHistory, updateSessionHistory, respondSessionPermission now takes an optional turnId) → historyWriter.
    • sessions/session-chat-interface.tsx: conversation config / source fence / runtime config, activity, active turn id, context compaction, unstarted dispatch, editable last user, capacity retry, pending permissions, app-store review → hydrated tail (useConversationTail, extended to the last user turn); billable turn count, end-timing analytics, search analytics → index rows; scheduled tasks, goal, latest proposed plan, permission funnel → useSessionTurnFacts; search → hydrate-while-open (useIncrementalSearchBlocks); export → hydrate on demand and release; pin → useTurn; scroll targets → indexOf.
    • sessions/draft-session-chat-interface.tsx (parent config), sessions/session-detail.tsx (fork-origin notice is the last entry → tail), sessions/use-session-diff-summary.ts (fallback diff inputs → derivation).
    • apps/cli: unchanged writes; SessionDocument.getHistory() documented as full materialization reserved for import hashing and dispatch scans.
  • Guards: tests/no-materialized-history-in-components.test.ts fails on any sessionDoc.history / getState().history read under src/components; ai-gui/AGENTS.md, providers/AGENTS.md, and the new lib/conversation-view/AGENTS.md record the invariants.
  • Benchmarks / story: bench:open gains open, open+idle, scroll, stream, stream(Mirror) and runs on the synthetic fixture by default (a desensitized real fixture is used locally only). Sessions/ConversationView story renders 3,000 turns through a doc-backed view.

Tests

  • conversation-view-from-doc.test.ts: index/hydration equal Mirror output, LRU, chunked hydration, streaming patches, appends/deletes, idle pass, dispose.
  • history-writer.test.ts: append produces the same ops, snapshot bytes, and container shape as Mirror.setState; replace and respondPermission match the Mirror draft mutation shape and the old full-Mirror read path.
  • control-plane-mirror.test.ts: new Mirror on a 2,000-turn doc < 30 ms (measured ~1 ms median) and never materializes history while still seeing other roots and unknown roots.
  • apply-turn-event.test.ts: deterministic random-op property test against toJSON().
  • Existing stream/outline/writer suites updated for turnIndex.

Numbers

M-series laptop, pnpm --filter @lody/history-import bench:open. Machine load was ~4–5 throughout (other work on the same box), so treat these as an upper bound rather than a clean-room figure.

Synthetic fixture (committed, benchmarks/fixture.ts)

--scale=1,10 --iterations=10 --baseline-iterations=3

task x1 (240 turns / 1,080 items) x10 (2,400 turns / 10,800 items, 10.8 MiB)
Mirror (today's open) 50.3 ms mean see note below
open (view + tail hydrate → renderable rows) 7.0 ms mean, 9.1 p99 48.1 ms mean, 57.0 p99
open+idle (…plus the whole background index pass) 19.2 ms 1.64 s
scroll (ensureRange of 30 turns, ×20) 1.7 ms mean, 3.2 p99 12.4 ms mean, 65.2 p99
stream (100 text deltas on the tail turn) 0.02 ms mean, 0.09 p99 0.01 ms mean, 0.04 p99
stream(Mirror) (same deltas, full Mirror subscribed) 1.5 ms mean, 2.3 p99 14.9 ms mean, 17.9 p99

Acceptance: open ≤ 50 ms at x10 → 48.1 ms mean (p99 57.0). stream p99 ≤ 4 ms → 0.04 ms, ~370× under.

The x10 Mirror row is reported as 93.5 s mean over 3 iterations, which is not a
usable "before" figure: building 2,400-turn Mirrors back to back thrashes the
heap and the number reflects GC, not the operation. Measured in isolation
(median of 5, fresh doc each time) the same construction is ~3.8 s — the
honest before/after pair at x10 is therefore ~3.8 s → 48 ms, and the
per-token cost is 14.9 ms → 0.01 ms.

Desensitized real fixture (local only, never committed)

171-turn / 5,087-item conversation captured with bench:capture, --scale=1:

task before after
open Mirror 3.50 s mean (5.47 p99) open 34.7 ms mean (40.3 p99)
stream, per event stream(Mirror) 6.7 ms mean, 7.2 p99 stream 0.02 ms mean, 0.11 p99
scroll (30 turns) 4.7 ms mean, 64.9 p99

Where the x10 open cost goes (median of 5, fresh doc each)

step cost
history.getShallowValue() (2,400 container ids) 0.7 ms
getContainerById per turn +12.6 ms
map.getShallowValue() per turn +17.3 ms
tail hydrate (20 turns toJSON()) 6.7 ms
items/plan length per turn (removed; idle pass fills counts) +16.7 ms

new Mirror with history: schema.Ignore() over the same 2,000-turn doc is ~1 ms (asserted < 30 ms in control-plane-mirror.test.ts).

Invariants I could not preserve as-is

  • Sticky Agent Role over old user turns: the resolver's scan of older user turns now reads the index row's shallow inputConfig, which the view fills in idle chunks after open. On a very long conversation a Role pinned only on an old turn can resolve as absent for a few hundred milliseconds after open, then appear. The latest user turn's full config is always available synchronously.
  • Whole-history facts (goal, scheduled tasks, latest proposed plan, permission funnel, fallback diff summary) come from a background fact table that fills tail-first; until the pass completes, a fact that lives far back can be missing (goals fall back to session meta as before). This replaces the synchronous full scan by design.
  • App-store review outcomes and capacity retry read the hydrated tail rather than the whole history; both only act on the latest turns, so older outcomes are not re-seeded on mount.
  • Assistant placeholder → hydrated rows switch Virtua keys (<id>assistant:<id>:…); user and system rows keep the same key. Virtua takes the same path as group expansion; shift={false}, leadingContent as a real row, scrollRowToTop, and the outline correction loop are unchanged.
  • new Mirror < 30 ms is asserted with wall-clock timing because the task asks for it; the measured median is ~1 ms so the bound is not near noise, but it is the one timing assertion in the suite.
  • Extreme turns: a turn with hundreds of items (real docs have them: 305 and 646 items in the captured fixture) costs ~40 ms to hydrate on its own, so first paint admits the newest turn and then only what fits the 320-item budget; the rest of the 20-turn tail arrives on the next idle tick as placeholders swap in.
  • An unresolved itemCount (before the idle pass reaches a turn) reads as unknown, not zero. isEmptyAssistantIndexRow treats unknown as "not empty" so a real turn is never dropped, which means an interrupted empty assistant turn far up the history renders a placeholder row for up to ~1 s after open. The conversation opens on the hydrated tail, so this only perturbs estimated scroll geometry above the viewport, never a visible row.
  • Rollback path (LODY_CONVERSATION_VIEW=0 / developer switch) keeps the old full Mirror cost, per-token immer cost included; it exists for one release only.

…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 👍 / 👎.

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