Skip to content

feat(dashboard): session index 分離 — 所有 recorded session 皆可 deep link 到達 #303

Description

@lis186

摘要

Dashboard deep link 到超出 MAX_ENTRIES(5000)視窗的 session 時顯示「Session not found」。根因:sessionsMapstore.entries 衍生,entries 有固定上限,早期 session 被截斷後完全不存在於前端。

實測數據(2026-07-19)

指標
index.ndjson 119,063 entries / 328MB
獨立 sessions 2,890
獨立 projects 100
日期範圍 2026-05 ~ 2026-07(~2 個月)
年化預測 ~70 萬 entries / ~17K sessions
MAX_ENTRIES 5,000(store.js:10)

重現:http://localhost:5577/?p=ccxray&s=e622e4d2(2026-07-15 session,280 筆 entries 在 index.ndjson 中,但不在 store.entries 的 5000 筆視窗內)。

設計(四位專家圓桌 + Fable 對抗審查收斂)

核心原則

Session index 跟 entry loading 分離。 Session index 全量、輕量、always loaded。Entry data 分熱區(即時)和冷區(on-demand)。

架構

startup:
1. 讀 sessions.json (物化視圖, <1MB)
   → sessionIndex: Map<sid, {firstId, lastId, count, model, project, cost, title}>
   sessions.json 不存在或損壞 → fallback 掃 index.ndjson 重建(慢但正確)

2. load recent MAX_ENTRIES entries → 熱區(現行行為不變)

API:
  /_api/sessions → 回傳 sessionIndex 全量(給 client 建 sessionsMap)
  /_api/session/:sid/entries → on-demand 從 ndjson 載入特定 session 的 entries

Client:
  sessionsMap 從 /_api/sessions 建立(全量 2890 sessions)
  session list 用 virtual scroll 渲染(全量可見不分頁)

deep link / click:
  _resolveSessionId() 查 sessionsMap → 一定查得到
  entries 在熱區 → 即時導航
  entries 不在熱區 → fetch /_api/session/:sid/entries → spinner → 渲染

三個必補的洞(Fable 審查)

洞 1:sessions.json 物化視圖

為什麼:startup 掃 328MB index.ndjson 的 readline 要 3-5 秒,加在啟動時間上使用者會感受到。

做法

  • store.addEntry() 時同時 append/update sessions.json(每 session 一行 JSON,只含彙總欄位)
  • startup 讀 sessions.json(<1MB,<100ms)建 sessionIndex
  • sessions.json 損壞或不存在 → 從 index.ndjson 全量重建(一次性,3-5 秒)
  • sessions.json 是 index.ndjson 的物化視圖,壞了隨時可重建

彙總欄位(每 session 一筆):

{
  "sid": "e622e4d2-977a-4f68-9d05-bf3fa526365e",
  "firstId": "2026-07-15T09-41-59-779",
  "lastId": "2026-07-15T11-23-45-123",
  "count": 280,
  "model": "claude-opus-4-6",
  "project": "ccxray",
  "cwd": "/Users/justinlee/dev/ccxray",
  "totalCost": 12.34,
  "title": "Plan restructure session",
  "lastReceivedAt": 1752578625000
}

洞 2:熱冷邊界 session merge

問題:一個 session 的 entries 可能橫跨熱區邊界——前 200 筆在冷區、後 80 筆在熱區。

做法

  • /_api/session/:sid/entries 回傳該 session 的全部 entries(不只冷區部分)
  • client 收到後替換(不是合併)該 session 在 allEntries 中的所有 entries
  • 或者:server 端先查熱區有哪些、只補冷區缺的,client 按 id 排序 merge
  • 選較簡單的方案(替換),因為 session 粒度的 entry 數通常 <500

洞 3:evict 策略 = LRU + pin current

問題:maxSessions cap evict「最舊的」但使用者可能跳回之前看過的 session。

做法

  • 維護一個 LRU list of loaded cold sessions
  • maxLoadedColdSessions(建議初始值 5)
  • 超過時 evict LRU 最久未存取的,但永遠 pin 當前 selectedSessionId
  • 熱區 entries 不參與 evict(它們走現行 MAX_ENTRIES 機制)

Acceptance Criteria

  1. deep link 到 store 視窗外的 session 能導航成功

    • ?s=e622e4d2 → spinner → session 載入 → turns 可瀏覽
    • gate: 用 CCXRAY_MAX_ENTRIES=100 啟動(強制幾乎所有 session 在冷區),deep link 到任意已記錄 session 皆成功
  2. startup 不退步

    • sessions.json 存在時 startup 時間不增加(<100ms 讀取 sessions.json)
    • sessions.json 不存在時 fallback 重建、下次 startup 正常
  3. session list 顯示全量

    • sessionsMap.size === index.ndjson 中的獨立 sessionId 數
    • session list virtual scroll 渲染,scrollbar 反映全量
  4. 記憶體穩定

    • 連續 deep link 到 10 個冷 session 後,loaded cold sessions 不超過 maxLoadedColdSessions
    • 瀏覽器 heap 不持續增長
  5. 現行行為不退步

    • 熱區 session 的導航速度不變
    • live SSE 串流不受影響
    • 既有測試全過

實作建議(非強制)

Phase 1:sessions.json + /_api/sessions(最小可用)

  • server/store.js: addEntry 時 append sessions.json
  • server/restore.js: startup 讀 sessions.json(fallback 掃 index.ndjson)
  • server/routes/api.js: 新增 /_api/sessions endpoint
  • public/miller-columns.js: sessionsMap 從 /_api/sessions 建立(不再只從 entries 衍生)
  • public/miller-columns.js: _resolveSessionId 查 sessionsMap(已覆蓋全量)

Phase 2:on-demand entry loading

  • server/routes/api.js: 新增 /_api/session/:sid/entries(從 index.ndjson grep sid 的 entries)
  • public/entry-rendering.js: cold session deep link → fetch → spinner → addEntry → navigate
  • public/miller-columns.js: LRU cold session evict

Phase 3(可選,效能):byte-offset index

  • 如果 Phase 2 的 grep 延遲 >3s,建 session → byte-offset 映射加速查找

不做

  • 不遷移到 SQLite 或其他 DB(ndjson 是現行架構,本 issue 不改)
  • 不做 session list 的搜尋/過濾(另開 issue)
  • 不改 MAX_ENTRIES 的預設值(熱區大小是獨立的調校問題)

Type

Type: perf

Metadata

Metadata

Assignees

No one assigned

    Labels

    pipeline:batch-23屬性(非狀態):test-infra 硬化批(CI flake 治本,非緊急)pipeline:ready狀態:已 triage、可派工。僅 owner 標定,pipeline 不代標

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions