摘要
Sidebar badge 的更新依賴 herdr 事件觸發讀取 index.ndjson,但 ccxray 的 appendIndex 是非同步的——broadcast SSE 和 herdr 的 idle 偵測都可能在 index commit 之前發生。目前用 250ms 固定 delay bridge,但這是時間猜測而非因果保證。
根因(三方獨立分析收斂)
Fable(對抗式)、Codex GPT 5.6 Sol(獨立提案)、Sonnet(實作式)各自讀了 codebase 後收斂在同一個診斷:
這是一個因果可見性問題(causal visibility problem),不是延遲選擇問題。
forward.js:780 的順序是:結束 client response → broadcast SSE → 非同步 appendIndex()。herdr 偵測到 idle 轉換時 appendIndex 可能還沒完成。badge 讀到的是上一個 turn 的 index snapshot。
Codex 額外發現:herdr 每個 prompt 觸發兩次事件(idle→working + working→idle),spawn 兩個獨立的 refresh-badges.js。增加 delay/retry 可能讓舊 process 在新的之後完成並覆寫更新的 badge。
設計(P7 visibility barrier + per-pane cursor)
Producer(server 端):
async function persistVisible(entry, canonical = entry) {
await config.storage.appendIndex(buildIndexLine(entry) + '\n');
sessionIdx.updateFromEntry(canonical);
visible.advance({ sessionId: entry.sessionId, agentId: entry.agentId });
}
Consumer(plugin 端):
GET /_api/telemetry/visible?sessionId=X&agentIds=Y&after=<cursor>&timeoutMs=1500
Plugin 只在 terminal 事件(idle/done/blocked)才 long-poll,non-terminal(working/detected)直接讀現有資料。每個 pane 記住上次成功寫入的 cursor,超時保留舊 badge 不覆寫。
影響範圍
改:
server/forward.js ×3 push sites(appendIndex → persistVisible)
server/ws-proxy.js ×1 push site
server/importer.js ×1 push site
server/routes/api.js +1 new endpoint
plugins/herdr/bin/refresh-badges.js(long-poll 取代 waitForTelemetry)
不改(零影響):
server/sse-broadcast.js — broadcast 順序不變
public/*.js — dashboard 完全不碰
server/restore.js — 啟動重建路徑不變
- 既有的 cold-load REST API 不變
估計 ~80-100 行改動。
被排除的方案
| 方案 |
排除理由 |
| SSE daemon(P2) |
herdr plugin 無 persistent process 原語;SSE broadcast 在 appendIndex 之前所以重現 race |
| Proxy push metadata(P1) |
proxy 不持有 pane ID(只有 launch token);badge 需要 session grouping/dedup/confidence fold,不是單一 entry 能算的 |
| Custom herdr event(P3) |
herdr 無此 API |
| Adaptive delay(P6) |
proxy 存在 ≠ 這個 pane 用了它 ≠ 這個 request 完成了 ≠ append 完成了 |
Importer 路徑的誠實答案
三方一致:badge 側無法讓 importer 路徑即時。正確做法是讓使用者永遠走 ccxray claude(proxy 路徑),importer 是 best-effort fallback。badge 的 stale 偵測 + requestImport 是既有的誠實降級。
驗證建議
- fail-on-old:舊碼的 250ms delay 在
waitForTelemetry 裡,新碼用 long-poll 取代它
- 測試:mock
/_api/telemetry/visible endpoint,斷言 terminal event 才等、non-terminal 直接讀
- live e2e:proxy-launched pane 的 badge 在 turn 完成後 <100ms 內更新(不再依賴 250ms 猜測)
🤖 Generated with Claude Code
摘要
Sidebar badge 的更新依賴 herdr 事件觸發讀取
index.ndjson,但 ccxray 的appendIndex是非同步的——broadcast SSE 和 herdr 的 idle 偵測都可能在 index commit 之前發生。目前用 250ms 固定 delay bridge,但這是時間猜測而非因果保證。根因(三方獨立分析收斂)
Fable(對抗式)、Codex GPT 5.6 Sol(獨立提案)、Sonnet(實作式)各自讀了 codebase 後收斂在同一個診斷:
這是一個因果可見性問題(causal visibility problem),不是延遲選擇問題。
forward.js:780的順序是:結束 client response → broadcast SSE → 非同步appendIndex()。herdr 偵測到 idle 轉換時appendIndex可能還沒完成。badge 讀到的是上一個 turn 的 index snapshot。Codex 額外發現:herdr 每個 prompt 觸發兩次事件(idle→working + working→idle),spawn 兩個獨立的 refresh-badges.js。增加 delay/retry 可能讓舊 process 在新的之後完成並覆寫更新的 badge。
設計(P7 visibility barrier + per-pane cursor)
Producer(server 端):
Consumer(plugin 端):
Plugin 只在 terminal 事件(idle/done/blocked)才 long-poll,non-terminal(working/detected)直接讀現有資料。每個 pane 記住上次成功寫入的 cursor,超時保留舊 badge 不覆寫。
影響範圍
改:
server/forward.js×3 push sites(appendIndex → persistVisible)server/ws-proxy.js×1 push siteserver/importer.js×1 push siteserver/routes/api.js+1 new endpointplugins/herdr/bin/refresh-badges.js(long-poll 取代 waitForTelemetry)不改(零影響):
server/sse-broadcast.js— broadcast 順序不變public/*.js— dashboard 完全不碰server/restore.js— 啟動重建路徑不變估計 ~80-100 行改動。
被排除的方案
Importer 路徑的誠實答案
三方一致:badge 側無法讓 importer 路徑即時。正確做法是讓使用者永遠走
ccxray claude(proxy 路徑),importer 是 best-effort fallback。badge 的stale偵測 +requestImport是既有的誠實降級。驗證建議
waitForTelemetry裡,新碼用 long-poll 取代它/_api/telemetry/visibleendpoint,斷言 terminal event 才等、non-terminal 直接讀🤖 Generated with Claude Code