Repository navigation
perf(desktop): make session data flow incremental and bounded #2913
Description
English
Problem
Desktop session work currently scales with total history instead of the amount of change:
- Transcript hydration performs one sequential request per small message. A synthetic tool-rich 1,000-turn session requires about 5,000 requests, giving a 150 s transport lower bound at 30 ms RTT.
- Runtime Host materializes an in-memory transcript snapshot capped at 16 MiB, so larger sessions cannot be opened.
- Live frames are buffered while the full transcript loads; sufficiently active long sessions can repeatedly hit the pending-frame limit and restart hydration.
- Desktop reloads the complete transcript after durable message/tool/terminal changes.
- Progressive transcript mounting eventually inserts the complete history into the DOM.
- Every session catalog invalidation refreshes the full catalog and replaces unchanged row objects.
- Fine-grained assistant streaming repeatedly scans the accumulated text and approaches O(n²).
- Startup waits for the complete onboarding/session snapshot before mounting React.
- Thread search can read up to 200 complete transcripts sequentially across the Runtime Host connection.
The storage layer already has a durable append-only (session_id, sequence) primary key for session messages. We can use that sequence directly as the transcript watermark; no historical data migration or new transcript revision is required.
Desired outcome
Make Desktop a bounded incremental client:
SQLite durable message ledger
│ bounded pages + durable watermark
▼
Runtime Host subscription
├─ full materialization helper ──> CLI / TUI
└─ tail bootstrap + advancement ─> Desktop Main sparse replica
│
▼
incremental bounded IPC
│
▼
virtualized Renderer transcript
Non-negotiable properties:
- Work per update is proportional to changed bytes/items, not total history.
- The latest useful content does not wait for full transcript or catalog hydration.
- Renderer state and DOM remain bounded independently of history size.
- Durable message sequence, continuity projection revision, subscription frame sequence, and Host epoch remain distinct concepts.
- Gaps, resets, reconnects, and late responses are explicit and fail closed.
- Sensitive content is sanitized before entering Renderer state.
- Remote Runtime Host RTT is treated as a supported environment.
Proposed implementation
1. Replace the transcript snapshot protocol — completed in #2922
- Add storage APIs for byte-bounded forward/backward pages of
{sequence, message}. - Read the durable high-water mark and tail page in one SQLite read transaction.
- Add explicit fragmentation for a single message larger than the page budget.
- Include an optional tail bootstrap in
subscription.open, so continuity plus the latest messages arrive in one RTT. - Add one bounded
session.transcript.pageoperation for older/newer ranges. - Add a lightweight
subscription.transcript_advancedwatermark frame; clients pull the changed durable range instead of receiving large messages through the subscription queue. - Keep active Runtime Event projection as a message-ID-keyed overlay, separate from durable sequence.
- Delete
session.transcript.query,TranscriptSnapshotStore, the 16 MiB total snapshot limit, snapshot-expiry recovery, and the one-message-per-request assembler.
This is a direct protocol replacement. Desktop and its Runtime Host communication layer ship together, so no v1/v2 capability negotiation or fallback is needed.
2. Preserve a simple CLI/TUI API — completed in #2922
- Keep
loadTranscript(): Promise<Message[]>as a shared client convenience API implemented over the new pages. - Continue using full materialization in CLI/TUI for now.
- Update CLI/TUI protocol, hydration, and recovery tests in the same atomic protocol change.
- Benchmark the existing 512-frame hydration buffer; only introduce a tail-ready TUI projector if a reproducible case remains after page packing.
The CLI/TUI UI does not need to be rewritten as part of the Desktop work.
3. Add a Desktop Main sparse transcript replica — completed in #2937
- Own loaded sequence ranges, message identity, active overlays, target watermark, Host epoch, and a session generation in one replica.
- Coalesce durable advancement and fetch only missing ranges.
- Reject late IPC/page results by generation.
- Recover gaps/reconnects through explicit catch-up or resync.
- Apply byte-bounded per-session and global LRU policies; cold pages remain reloadable.
- Replace
sessions:readMessages -> Message[]with window/page/patch IPC. - Replace settlement polling with message-complete plus durable-watermark barriers.
- Remove full transcript refreshes from message/tool/terminal event handling.
4. Implement real transcript virtualization
- Mount a bounded bidirectional turn window with overscan and top/bottom spacers.
- Load older pages near the top and keep live-tail pinning independent from history hydration.
- Materialize only a bounded window around navigation/search targets.
- Cache measured geometry by session, layout width, and turn ID; correct estimates with scroll anchoring.
- Route transcript search through an index and
loadAround(sequence). - Generate copy-all/export output from the data model rather than mounted DOM.
- Preserve keyboard and screen-reader navigation.
- Delete progressive fill-to-all and full-history warm-up. Keep
content-visibilityonly as a window-local optimization.
5. Incremental session catalog, Sidebar, and startup
The existing session.catalog.changed frame already carries a session ID, so it can drive a targeted session.catalog.query({kind: "get"}).
- Add a Main catalog store keyed by session ID and durable item revision.
- Coalesce targeted get/upsert/remove operations.
- Use one stable full scan only for bootstrap/reconnect resync, replaying changed IDs after the scan.
- Preserve unchanged object identity in Renderer state.
- Virtualize the flattened Sidebar row/header model.
- Fix the nested interactive button in
SessionNavRow. - Mount React from a local shell/catalog cache (or an immediate skeleton) without waiting for the full remote onboarding snapshot.
- Schedule background startup refreshes through a priority/concurrency limiter.
6. Make streaming sanitization incremental
- Replace stateless accumulated-text redaction with a stateful sanitizer that keeps a bounded pending tail for cross-delta matches.
- Store safe chunks/rope instead of flattening the entire string on every token.
- Batch provider deltas for roughly 8–16 ms and publish at most one React update per frame.
- Avoid duplicate full-text redaction in Markdown after the trust boundary.
- Incrementally inspect newly revealed unsafe image content.
- Property/fuzz test every split point of the sensitive-string corpus.
Cross-delta redaction must not be weakened for performance.
7. Move thread search to the Host and gate inactive Workbar views
- Add a Host-local bounded session search operation returning redacted snippets and message sequence targets.
- Eliminate the remote per-session full-transcript N+1.
- Measure local scan p95 before deciding whether FTS5 is necessary.
- Separate background engines (terminal/running side chat) from view caches (artifact/files/review/inspector).
- Suspend inactive view subscriptions/refreshes and unmount heavyweight views after a grace period.
Validation
Cover at least:
- empty, small-message-heavy, 16 MiB, and 64 MiB transcripts;
- a single oversized message and UTF-8 characters crossing fragment boundaries;
- concurrent append during bootstrap/page reads;
- active-overlay to durable-message handoff;
- duplicate/late pages, sequence gaps, reconnect, Host epoch changes, reset, eviction/reload, and session deletion;
- CLI/TUI
loadTranscript()equivalence with durable storage; - session switching, upward history loading, bottom pinning, search jumps, copy/export, and accessibility;
- 100/500/1,000/5,000-session catalogs;
- 20/100/512/4,096-character streaming deltas;
- real Electron/Chromium traces in addition to Node microbenchmarks.
Suggested initial regression budgets:
| Scenario | Target |
|---|---|
| Local session open to visible tail | p95 <= 150 ms |
| 30 ms RTT long-session open to visible tail | p95 <= 500–800 ms |
| Initial tail transport | one subscription.open round trip |
| 64 MiB session | tail opens and history remains pageable |
| Mounted transcript DOM | normally <= 100 turns |
| Sidebar DOM | bounded by viewport, not catalog size |
| Single update with 1,000 sessions | Renderer <= 16 ms |
| Normal input/scroll | no >50 ms long task |
| 256 KiB output with 20-char deltas | sanitizer cumulative CPU <= 250 ms |
| Steady-state transcript sync | O(changed bytes), not O(history) |
Delivery order
- Phase 0: formalize fixtures, metrics, artificial RTT, and prove bounded tail bootstrap.
- Phase 1: atomically replace the transcript protocol and update shared client/CLI/TUI tests.
- Phase 2: land the Desktop Main replica and incremental IPC.
- Phase 3: land transcript virtualization and search-target loading.
- Phase 4: land catalog store, Sidebar virtualization, and non-blocking startup.
- Phase 5: land stateful streaming sanitization and frame batching.
- Phase 6: move thread search Host-side, gate inactive Workbar views, and enforce final Electron performance gates.
Non-goals
- Raising the 16 MiB limit without changing the data model.
- Merely enlarging chunks or pending-frame limits.
- Debouncing complete refreshes.
- Treating
content-visibilityas virtualization. - Optimizing the already-secondary pure transcript projection first.
- Keeping the old wire protocol or long-lived compatibility branches.
- Rewriting the TUI UI without evidence that it is needed.
- Introducing FTS5 before a Host-local bounded scan is measured.
Prepared by Codex under maintainer direction after a read-only audit of current main. The maintainer owns the final technical decisions and implementation.
中文
问题
当前 Desktop 的 session 工作量随完整历史增长,而不是随实际变化量增长:
- Transcript hydration 对每条小消息执行一次串行请求。合成的 1,000-turn tool-rich session 约需 5,000 次请求;在 30 ms RTT 下,仅传输往返的理论下限就达到 150 秒。
- Runtime Host 会物化一份上限为 16 MiB 的内存 transcript snapshot,超过上限的 session 无法打开。
- 完整 transcript 加载期间需要缓存 live frames;足够活跃的长 session 可能反复达到 pending-frame 上限并重新开始 hydration。
- durable message、tool 或 terminal 状态变化后,Desktop 会重新读取完整 transcript。
- 当前 progressive transcript mounting 最终仍会把完整历史插入 DOM。
- 任意 session catalog 失效都会重新读取完整 catalog,并替换所有未变化 row 的对象。
- 细粒度 assistant streaming 会反复扫描累计全文,复杂度接近 O(n²)。
- React mount 前会等待完整 onboarding/session snapshot。
- Thread search 最多会跨 Runtime Host 连接串行读取 200 个完整 transcript。
存储层已经为 session messages 提供持久、append-only 的 (session_id, sequence) 主键。可以直接使用该 sequence 作为 transcript watermark,不需要迁移历史数据,也不需要新增 transcript revision。
目标结果
将 Desktop 改造成有界的增量客户端:
SQLite durable message ledger
│ bounded pages + durable watermark
▼
Runtime Host subscription
├─ full materialization helper ──> CLI / TUI
└─ tail bootstrap + advancement ─> Desktop Main sparse replica
│
▼
incremental bounded IPC
│
▼
virtualized Renderer transcript
不可妥协的约束:
- 每次更新的工作量与变化的 bytes/items 成正比,而不是与完整历史成正比。
- 最新有用内容不等待完整 transcript 或 catalog hydration。
- Renderer state 和 DOM 数量不依赖完整历史大小。
- Durable message sequence、continuity projection revision、subscription frame sequence 和 Host epoch 必须保持为不同概念。
- Gap、reset、reconnect 和迟到响应必须显式处理并 fail closed。
- 敏感内容在进入 Renderer state 前完成 sanitization。
- 远程 Runtime Host 的 RTT 是正式支持的运行环境。
建议实现
1. 替换 transcript snapshot protocol — 已由 #2922 完成
- 增加按 byte budget 限制、支持前后方向的
{sequence, message}storage page API。 - 在一个 SQLite read transaction 内读取 durable high-water mark 和 tail page。
- 对超过 page budget 的单条大消息增加显式 fragmentation。
- 在
subscription.open中加入可选 tail bootstrap,让 continuity 与最新消息通过一次 RTT 返回。 - 增加一个有界的
session.transcript.pageoperation,用于读取 older/newer ranges。 - 增加轻量
subscription.transcript_advancedwatermark frame;client 主动拉取变化的 durable range,不通过 subscription queue 推送大消息。 - Active Runtime Event projection 继续作为以 message ID 为 key 的 overlay,与 durable sequence 分离。
- 删除
session.transcript.query、TranscriptSnapshotStore、16 MiB transcript 总量上限、snapshot-expiry recovery 和 one-message-per-request assembler。
这是一次直接 protocol replacement。Desktop 与其 Runtime Host 通信层同步发布,因此不需要 v1/v2 capability negotiation 或 fallback。
2. 保留简单的 CLI/TUI API — 已由 #2922 完成
- 保留
loadTranscript(): Promise<Message[]>作为共享 client 的 convenience API,内部使用新 page protocol。 - CLI/TUI 目前继续完整 materialize transcript。
- 在同一个原子 protocol 变更中更新 CLI/TUI 的 protocol、hydration 和 recovery tests。
- 对现有 512-frame hydration buffer 建立基准;只有 page packing 后仍存在可复现问题时,才为 TUI 增加 tail-ready projector。
本轮 Desktop 优化不要求重写 CLI/TUI 界面。
3. 增加 Desktop Main sparse transcript replica — 已由 #2937 完成
- 由一个 replica 统一拥有 loaded sequence ranges、message identity、active overlays、target watermark、Host epoch 和 session generation。
- 合并 durable advancement,只请求缺失 ranges。
- 通过 generation 拒绝迟到的 IPC/page 结果。
- 对 gap/reconnect 执行显式 catch-up 或 resync。
- 使用按 bytes 限制的 per-session/global LRU;cold pages 可以重新加载。
- 用 window/page/patch IPC 替代
sessions:readMessages -> Message[]。 - 用 message-complete + durable-watermark barrier 替代 settlement polling。
- 删除 message/tool/terminal 事件触发的完整 transcript refresh。
4. 实现真正的 transcript virtualization
- 使用 top/bottom spacers 和 overscan 挂载有界的双向 turn window。
- 接近顶部时加载 older pages,live-tail pinning 不依赖完整历史 hydration。
- 导航和搜索只 materialize 目标附近的有界窗口。
- 按 session、布局宽度和 turn ID 缓存 geometry,并通过 scroll anchoring 修正估值。
- Transcript search 通过 index 和
loadAround(sequence)跳转。 - Copy-all/export 从数据模型生成,而不是依赖已挂载 DOM。
- 保留键盘和 screen reader 导航。
- 删除 progressive fill-to-all 和 full-history warm-up;
content-visibility只作为窗口内优化。
5. 增量 session catalog、Sidebar 和 startup
现有 session.catalog.changed frame 已经携带 session ID,因此可以直接触发一次定点的 session.catalog.query({kind: "get"})。
- 增加按 session ID 和 durable item revision 建模的 Main catalog store。
- 合并定点 get/upsert/remove。
- 只在 bootstrap/reconnect resync 时进行 stable full scan,并在 scan 后重放期间变化的 IDs。
- 在 Renderer state 中保持未变化对象的 identity。
- Virtualize flatten 后的 Sidebar row/header model。
- 修复
SessionNavRow中嵌套的 interactive button。 - 从本地 shell/catalog cache(或即时 skeleton)直接 mount React,不等待完整远程 onboarding snapshot。
- 使用带优先级和并发限制的 scheduler 执行后台 startup refreshes。
6. 将 streaming sanitization 改为增量处理
- 将 stateless accumulated-text redaction 替换为 stateful sanitizer,并保留有界 pending tail 处理跨 delta 匹配。
- 使用 safe chunks/rope,避免每个 token flatten 完整字符串。
- 在约 8–16 ms 内批量处理 provider deltas,每帧最多发布一次 React update。
- 完成 trust boundary 后,Markdown 不再重复做全文 redaction。
- 增量检查新揭示的 unsafe image 内容。
- 对 sensitive-string corpus 的所有切分点做 property/fuzz tests。
不能为了性能削弱 cross-delta redaction。
7. 将 thread search 移到 Host,并限制 inactive Workbar views
- 增加 Host-local bounded session search operation,返回 redacted snippet 和 message sequence target。
- 消除远程 per-session full-transcript N+1。
- 先测量 local scan p95,再决定是否需要 FTS5。
- 区分 background engines(terminal/running side chat)和 view caches(artifact/files/review/inspector)。
- 暂停 inactive view 的 subscription/refresh,并在 grace period 后卸载重量组件。
验证
至少覆盖:
- 空 transcript、小消息密集 transcript、16 MiB 和 64 MiB transcript;
- 单条超大消息和跨 fragment 的 UTF-8 多字节字符;
- bootstrap/page read 期间并发 append;
- active overlay 到 durable message 的 handoff;
- duplicate/late page、sequence gap、reconnect、Host epoch change、reset、eviction/reload 和 session delete;
- CLI/TUI
loadTranscript()与 durable storage 的内容等价; - session switch、向上加载历史、bottom pinning、search jump、copy/export 和 accessibility;
- 100/500/1,000/5,000-session catalogs;
- 20/100/512/4,096-character streaming deltas;
- 除 Node microbench 外,还需真实 Electron/Chromium trace。
建议的初始 regression budgets:
| 场景 | 目标 |
|---|---|
| 本地打开 session 到 tail 可见 | p95 <= 150 ms |
| 30 ms RTT 下长 session 到 tail 可见 | p95 <= 500–800 ms |
| 初始 tail transport | 一次 subscription.open round trip |
| 64 MiB session | 可以打开 tail 并继续分页历史 |
| 已挂载 transcript DOM | 通常 <= 100 turns |
| Sidebar DOM | 由 viewport 决定,不由 catalog 大小决定 |
| 1,000 sessions 下单项更新 | Renderer <= 16 ms |
| 正常输入/滚动 | 无 >50 ms long task |
| 256 KiB 输出、20-char deltas | sanitizer 累计 CPU <= 250 ms |
| Steady-state transcript sync | O(changed bytes),不是 O(history) |
交付顺序
- Phase 0:正式化 fixtures、metrics 和 artificial RTT,并证明 bounded tail bootstrap。
- Phase 1:原子替换 transcript protocol,同时更新共享 client/CLI/TUI tests。
- Phase 2:落地 Desktop Main replica 和 incremental IPC。
- Phase 3:落地 transcript virtualization 和 search-target loading。
- Phase 4:落地 catalog store、Sidebar virtualization 和 non-blocking startup。
- Phase 5:落地 stateful streaming sanitization 和 frame batching。
- Phase 6:将 thread search 移到 Host、限制 inactive Workbar views,并执行最终 Electron performance gates。
非目标
- 只提高 16 MiB 限制而不改变数据模型。
- 只增大 chunks 或 pending-frame limits。
- 对完整 refresh 加 debounce。
- 用
content-visibility代替 virtualization。 - 优先优化目前次要的 pure transcript projection。
- 保留旧 wire protocol 或长期 compatibility branches。
- 在没有证据时重写 TUI UI。
- 在测量 Host-local bounded scan 前引入 FTS5。
本内容由 Codex 在维护者指示下、基于当前 main 的只读审计整理。最终技术决策与实现由维护者负责。
Activity
M4n5ter commentedon Aug 13, 2026
English
Progress update
The merged work has green CI and was smoke-tested in Electron with a long session.
Posted by Codex on behalf of the maintainer.
中文
进度更新
已合并内容的 CI 全绿,并已在长会话上完成 Electron 实机 smoke test。
由 Codex 代表维护者发布。
M4n5ter commentedon Aug 24, 2026
English
Current status (2026-08-24)
Fresh check against live
mainat3bb645e9958a25986cb4502e8fbced5659b9ce8e:use-turn-virtualizer.tsprovides a bounded viewport window, geometry tracking, overscan, spacers, and scroll anchoring;ChatViewrenders the mounted window and usesloadAroundfor targeted loading.The following work remains open:
sessions.list()refresh for catalog updates; session events can trigger that refresh; the Sidebar still maps the complete catalog without flattened-row virtualization; and React startup still waits for the onboarding/session snapshot.listSessions()→openSession()→loadTranscript()pattern; no Host-local bounded search operation is present. Inactive Workbar tabs remain mounted rather than being suspended/unmounted after a grace period.Evidence paths on the checked-out main:
use-turn-virtualizer.tsruntime-host-search-ipc-main.tsuse-app-shell-session-list.tsworkbar-surface.tsxRecommendation: keep #2913 open as the incremental Desktop umbrella. Update the Phase 3 checklist to distinguish the landed bounded transcript window from the remaining search/export/accessibility/performance work; keep Phases 4–6 open until their end-to-end gates pass.
中文
当前状态(2026-08-24)
基于 live
main的3bb645e9958a25986cb4502e8fbced5659b9ce8efresh check:use-turn-virtualizer.ts提供有界 viewport window、geometry tracking、overscan、spacer 和 scroll anchoring;ChatView渲染有界窗口,并通过loadAround定点加载。仍待完成:
sessions.list()刷新 catalog;session 事件仍可能触发该刷新;Sidebar 仍映射完整 catalog,未做 flattened-row virtualization;React 启动仍等待 onboarding/session snapshot。listSessions()→openSession()→loadTranscript()路径;尚无 Host-local bounded search operation。inactive Workbar tab 仍保持 mounted,没有在 grace period 后暂停/卸载。建议:保留 #2913 作为增量 Desktop umbrella issue。更新 Phase 3 checklist,区分已落地的有界 transcript window 与仍待完成的 search/export/accessibility/performance;Phase 4–6 在端到端 gate 通过前继续保持 open。
fix(runtime-host): page oversized transcript Turns (#4433)
fix(runtime-host): page oversized transcript Turns (apache#4433)
ci(perf): add an end-to-end hydration measurement for long Sessions (#…