apache / apache/maka

perf(desktop): make session data flow incremental and bounded

Open
#2,913 2 comments 0 reactions 1 assignee Claimed by @M4n5ter View on GitHub
enhancement
Dominant language
TypeScript
Stars
5.4k
Forks
502
Avg merge
1d 2h
Merged PRs (30d)
715

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:

```text
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

- [x] Add storage APIs for byte-bounded forward/backward pages of `{sequence, message}`.
- [x] Read the durable high-water mark and tail page in one SQLite read transaction.
- [x] Add explicit fragmentation for a single message larger than the page budget.
- [x] Include an optional tail bootstrap in `subscription.open`, so continuity plus the latest messages arrive in one RTT.
- [x] Add one bounded `session.transcript.page` operation for older/newer ranges.
- [x] Add a lightweight `subscription.transcript_advanced` watermark frame; clients pull the changed durable range instead of receiving large messages through the subscription queue.
- [x] Keep active Runtime Event projection as a message-ID-keyed overlay, separate from durable sequence.
- [x] 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

- [x] Keep `loadTranscript(): Promise` as a shared client convenience API implemented over the new pages.
- [x] Continue using full materialization in CLI/TUI for now.
- [x] Update CLI/TUI protocol, hydration, and recovery tests in the same atomic protocol change.
- [x] 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

- [x] Own loaded sequence ranges, message identity, active overlays, target watermark, Host epoch, and a session generation in one replica.
- [x] Coalesce durable advancement and fetch only missing ranges.
- [x] Reject late IPC/page results by generation.
- [x] Recover gaps/reconnects through explicit catch-up or resync.
- [x] Apply byte-bounded per-session and global LRU policies; cold pages remain reloadable.
- [x] Replace `sessions:readMessages -> Message[]` with window/page/patch IPC.
- [x] Replace settlement polling with message-complete plus durable-watermark barriers.
- [x] 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-visibility` only 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

- [x] Phase 0: formalize fixtures, metrics, artificial RTT, and prove bounded tail bootstrap.
- [x] Phase 1: atomically replace the transcript protocol and update shared client/CLI/TUI tests.
- [x] 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-visibility` as 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 改造成有界的增量客户端:

```text
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 完成

- [x] 增加按 byte budget 限制、支持前后方向的 `{sequence, message}` storage page API。
- [x] 在一个 SQLite read transaction 内读取 durable high-water mark 和 tail page。
- [x] 对超过 page budget 的单条大消息增加显式 fragmentation。
- [x] 在 `subscription.open` 中加入可选 tail bootstrap,让 continuity 与最新消息通过一次 RTT 返回。
- [x] 增加一个有界的 `session.transcript.page` operation,用于读取 older/newer ranges。
- [x] 增加轻量 `subscription.transcript_advanced` watermark frame;client 主动拉取变化的 durable range,不通过 subscription queue 推送大消息。
- [x] Active Runtime Event projection 继续作为以 message ID 为 key 的 overlay,与 durable sequence 分离。
- [x] 删除 `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 完成

- [x] 保留 `loadTranscript(): Promise` 作为共享 client 的 convenience API,内部使用新 page protocol。
- [x] CLI/TUI 目前继续完整 materialize transcript。
- [x] 在同一个原子 protocol 变更中更新 CLI/TUI 的 protocol、hydration 和 recovery tests。
- [x] 对现有 512-frame hydration buffer 建立基准;只有 page packing 后仍存在可复现问题时,才为 TUI 增加 tail-ready projector。

本轮 Desktop 优化不要求重写 CLI/TUI 界面。

### 3. 增加 Desktop Main sparse transcript replica — 已由 #2937 完成

- [x] 由一个 replica 统一拥有 loaded sequence ranges、message identity、active overlays、target watermark、Host epoch 和 session generation。
- [x] 合并 durable advancement,只请求缺失 ranges。
- [x] 通过 generation 拒绝迟到的 IPC/page 结果。
- [x] 对 gap/reconnect 执行显式 catch-up 或 resync。
- [x] 使用按 bytes 限制的 per-session/global LRU;cold pages 可以重新加载。
- [x] 用 window/page/patch IPC 替代 `sessions:readMessages -> Message[]`。
- [x] 用 message-complete + durable-watermark barrier 替代 settlement polling。
- [x] 删除 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) |

## 交付顺序

- [x] Phase 0:正式化 fixtures、metrics 和 artificial RTT,并证明 bounded tail bootstrap。
- [x] Phase 1:原子替换 transcript protocol,同时更新共享 client/CLI/TUI tests。
- [x] 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 的只读审计整理。最终技术决策与实现由维护者负责。

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.