-
Notifications
You must be signed in to change notification settings - Fork 28
Description
背景
OpenClaw gateway 在多處呼叫 loadSessionStore(),每次都需讀取並解析整個 sessions.json。
隨著 cron job 累積,sessions.json 持續膨脹(目前已達 628KB、96 個 entries),造成可觀的 I/O 與 CPU 解析開銷。
流程圖
每次收到訊息 / cron 執行時:
┌─────────────────────────────────────────────────────────┐
│ loadSessionStore(storePath) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ cache 是否有效?(TTL = 45 秒) │ │
│ └──────────────┬──────────────────────────────────┘ │
│ │ │
│ Yes ─────┤──── 直接返回記憶體 cache ✅ │
│ │ │
│ No ─────▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ readFileSync(sessions.json) ← SSD 極快,幾乎可忽略 │ │
│ │ JSON.parse(628KB) ← ⚠️ CPU bound,同步阻塞 event loop │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ⚠️ cron job 呼叫時帶 skipCache: true │
│ → 強制繞過 cache,每次都重新讀取 │
└─────────────────────────────────────────────────────────┘
關鍵程式碼
TTL cache 定義(45 秒)
cache miss 時同步讀取 + 解析
每次收到訊息時強制 skipCache: true
cron session-reaper 強制 skipCache: true
cron isolated-agent 每次執行都讀取
skillsSnapshot 型別定義
sessions.json 膨脹原因
每個 cron session entry 都帶有 skillsSnapshot(完整技能 prompt 文字),
導致 sessions.json 體積隨 cron 數量線性成長:
sessions.json
├── agent:main:cron:xxxx → { skillsSnapshot: { prompt: "...幾千字..." } }
├── agent:main:cron:yyyy → { skillsSnapshot: { prompt: "...幾千字..." } }
├── agent:main:cron:zzzz → { skillsSnapshot: { prompt: "...幾千字..." } }
└── ... × 90+ 個 cron entries
注意:skillsSnapshot 不會注入 LLM context,純粹是 sessions.json 的體積負擔。
瓶頸性質:CPU bound,非 I/O bound
現代 SSD 讀取 628KB 約 0.1–0.5ms,I/O 本身幾乎可忽略。
真正的問題是 JSON.parse(628KB) 為同步操作,會完全佔用 Node.js event loop,
期間所有其他請求(訊息、cron、heartbeat)全部排隊等待。
readFileSync(628KB) ← SSD 極快,~0.1ms,不是瓶頸
JSON.parse(628KB) ← 同步 CPU,阻塞 event loop
sessions.json 越大越慢
cron skipCache: true → 每次都重跑
↑ 這才是真正的累積開銷來源
影響
| 呼叫場景 | cache 行為 | 實際開銷 |
|---|---|---|
| 一般對話(45秒內) | 命中 cache | 幾乎無 |
| 45秒 cache 過期後 | cache miss | JSON.parse(628KB),CPU 同步阻塞 event loop |
| cron job 執行 | skipCache: true |
每次都重跑 JSON.parse,CPU bound |
| heartbeat / subagent | 視 TTL 而定 | 不定 |
自動清除機制(Purge)
每次 saveSessionStore() 時觸發(非 skipMaintenance 模式),有三層清除:
┌─────────────────────────────────────────────────────────┐
│ 1. pruneStaleEntries() │
│ └─ 刪除 updatedAt 超過 N 天的 entries │
│ 預設:30 天 │
│ 可設定:config.maintenance.pruneAfter = "7d" │
│ │
│ 2. capEntryCount() │
│ └─ 超過上限時刪除最舊的 entries │
│ 預設:500 條 │
│ │
│ 3. rotateSessionFile() │
│ └─ sessions.json 超過閾值時輪替備份 │
│ 預設:10MB │
└─────────────────────────────────────────────────────────┘
關鍵程式碼:預設值
問題:預設閾值過於寬鬆
目前狀態:
entries:96 條 → 遠低於 500 上限,capEntryCount 不觸發
大小:628KB → 遠低於 10MB,rotateSessionFile 不觸發
最舊 entry: → 若在 30 天內,pruneStaleEntries 不觸發
→ 短期內三層機制全部不會自動清理
→ sessions.json 持續膨脹,JSON.parse 開銷持續累積
最嚴重的場景:每條訊息都強制 parse
auto-reply/reply/session.ts 在每次收到訊息時強制 skipCache: true,
45 秒 TTL cache 對這條路徑完全無效:
原因(見 #17971):避免多 gateway process 或 Windows mtime 精度問題導致 sessionId 錯誤。
每條訊息的處理路徑:
收到訊息
│
▼
loadSessionStore(skipCache: true)
│
├─ readFileSync(sessions.json) ← 每次都讀
└─ JSON.parse(sessions.json) ← 每次都 parse(CPU bound)
499 entries ≈ 數 MB
event loop 阻塞時間隨檔案大小線性成長
45 秒 TTL cache → 對收訊息路徑無效
只對 heartbeat / subagent 等其他呼叫有效
結論:sessions.json 越大,每條訊息的延遲越高,無法靠 cache 緩解。
建議解法
問題核心:每條訊息都 JSON.parse 整個 sessions.json
┌─────────────────────────────────────────────────────────┐
│ 方案 A:只讀單一 entry(Partial Load) │
│ │
│ 不讀整個 sessions.json,改用 sqlite 或 │
│ 每個 session 獨立一個小 JSON 檔 │
│ → 讀取量從 O(全部) 降為 O(1) │
│ → 改動最大,需重構儲存層 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 B:移除 skillsSnapshot(最快見效) │
│ │
│ cron entry 不需要帶完整技能快照 │
│ → sessions.json 體積大幅縮小 │
│ → parse 時間直接下降 │
│ → 改動小,風險低 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 C:收訊息路徑改用 mtime 驗證取代 skipCache │
│ │
│ skipCache: true 的目的是確保 sessionId 正確 │
│ 改為:比對 sessions.json 的 mtime │
│ 若 mtime 未變 → 用 cache │
│ 若 mtime 改變 → 重新讀取 │
│ → 多 process 安全,又避免不必要的 parse │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 方案 D:調低 pruneAfter / maxEntries(治標) │
│ │
│ config.maintenance.pruneAfter = "7d" │
│ config.maintenance.maxEntries = 100 │
│ → 限制 sessions.json 體積上限 │
│ → 不解決根本問題,但可控制損害範圍 │
└─────────────────────────────────────────────────────────┘
優先順序建議:
- 短期 → D(調低閾值)+ B(移除 skillsSnapshot)
- 長期 → C(mtime 驗證)或 A(sqlite)
待深入調查的方向
-
skipCache: true在 cron 路徑是否必要?能否改為允許短暫 cache? -
skillsSnapshot是否應從 sessions.json 中移除或壓縮? - 是否可對 sessions.json 做 lazy partial load(只讀取當前 session entry)?
- 調低預設閾值:
pruneAfter: "7d"、maxEntries: 100? - 是否提供
openclaw sessions cleanup指令讓使用者手動清理舊 cron entries?
sessions.json vs .jsonl 的關係
sessions.json(索引)
└─ agent:main:main → sessionId: "abc123" ← 每個 channel/cron 一條 entry
└─ agent:main:cron:xxxx → sessionId: "def456"
└─ ...(共 96 條)
sessions/*.jsonl(對話內容)
└─ abc123.jsonl ← agent:main:main 的完整對話歷史
└─ def456.jsonl ← cron job 的完整執行歷史
└─ ...(共 321 個檔案,總計 41MB)
每個 .jsonl 的格式(每行一個事件):
{"type":"message","message":{"role":"user","content":"..."}}
{"type":"message","message":{"role":"assistant","content":"..."}}
{"type":"tool_use","name":"execute_bash","input":{...}}
{"type":"tool_result","content":"..."}
...
/new 指令的行為:
/new 觸發時:
sessions.json 的 agent:main:main
→ sessionId 更新為新值(舊值覆蓋,sessions.json 不變大)
磁碟上:
├── abc123.jsonl ← 舊對話,封存保留,不刪除
├── abc123.jsonl.reset.* ← 輪替備份
└── def456.jsonl ← 新 session,從空白開始
舊 .jsonl 靠 pruneStaleEntries 30 天後才清理
載入規則:只讀當前 active session 的 .jsonl,其餘 320 個全部跳過。
環境資訊
sessions.json大小:628KB- session entries 數量:96
.jsonl檔案數量:321 個(總計 41MB)DEFAULT_SESSION_STORE_TTL_MS:45,000ms