fix(usage): 沙盒 bot 的 token 统计/成本台账/insight 读不到重定向 transcript - #623
Conversation
沙盒(CLI 数据重定向)bot 的 Claude 被 worker 注入
CLAUDE_CONFIG_DIR=<botmuxHome>/bots/<appId>/claude,transcript 全部写在
BOT_HOME 下;而 daemon 侧 resolveSessionTranscriptPath 对 claude-family
硬编码全局 ~/.claude(seed/relay 为 adapter dataDir),永远 miss →
dashboard TOKEN IN/OUT 恒显示 -,usage-ledger 静默漏记,insight 报
transcript_missing。
修法:TranscriptPathQuery 增加 larkAppId,claude-family 分支在主 dataDir
miss 后回落 <botmuxHome>/bots/<appId>/claude(botmuxHome 与 worker 同源 =
dirname(SESSION_DATA_DIR))。刻意用「按存在性回落」而非在 daemon 侧复刻
重定向判定(sandbox × adapter 能力 × wrapper),任何组合下都正确、不会与
worker.ts 漂移。app id 过 assertSafeAppId,不安全值不构造路径。
调用点接线:dashboard-rows / usage-ledger / insight IPC 三路由 / /insight
命令传入会话已有的 larkAppId;InsightOverviewSessionInput 的 larkAppId
上移进 InsightReportQuery。
真机实测(沙盒 bot cli_aace4eb402785be8 的真实 transcript):
修复前 getSessionTokenUsage=null,修复后
{in:378840,out:2249,model:claude-opus-5,turns:9}。
存量持久会话跨沙盒开关切换时(session id 不变、数据目录变),旧目录的 transcript 停止增长、新目录持续写入;固定「全局优先」会把 token/成本/ insight 永远冻结在切换点。改为两侧都存在时 statSync 比 mtime 取新者, 开↔关两个方向都正确;恰好相等(字节级拷贝)保持全局路径(stock 语义)。 新增沙盒翻转 ON/OFF/平手三个回归测试。
deepcoldy
left a comment
There was a problem hiding this comment.
Claude 首次 review:无阻塞 ✅(待 codex 复审 + 申晗确认后再合码)
在钉住 PR head 37ab692f 的独立 worktree里从头验证(fork PR 无 CI,本地跑全套)。
这个 PR 在解决什么(白话)
沙盒 bot(开了 CLI 数据重定向的)跑 Claude 时,worker 会把它的 CLAUDE_CONFIG_DIR 指到 <botmuxHome>/bots/<appId>/claude,所以它的对话记录(transcript)全写在这个 BOT_HOME 目录下,而不是全局 ~/.claude。可 daemon 这边负责读 transcript 的 resolveSessionTranscriptPath 对 claude 系一直硬编码去全局 ~/.claude 找——于是永远找不到:dashboard 的 TOKEN IN/OUT 恒显示 -、成本台账(usage-ledger)静默漏记、/insight 报 transcript_missing。这不是沙盒重构引入的新 bug,旧 readIsolation 时代就有,只是沙盒铺开后才明显。
改法(白话)
给 transcript 查询加一个可选的 larkAppId。claude 系分支先按老规矩去全局目录找,找不到就回落到这个 bot 的 BOT_HOME 目录(<botmuxHome>/bots/<appId>/claude)。这里有几个讲究:
- 不去 daemon 侧复刻「到底该不该重定向」的判定(那要同时看 sandbox×适配器能力×wrapper 三个条件),而是纯按文件存在性探测——任何组合下都对,且永远不会跟 worker.ts 漂移。
- 两个目录都有同名文件时按 mtime 取新(存量持久会话跨「沙盒开关」切换时 session id 不变、数据目录变,固定「全局优先」会把统计永远冻结在切换点;取新对开↔关两个方向都对,恰好相等时保全局=stock 语义)。
- app id 过
assertSafeAppId校验,不安全值返回 null 而不是抛异常(不会因为一条脏 bots.json 把整个读取路径打崩)。 - 接线覆盖 4 处读取点:dashboard 行、usage-ledger、insight 的 3 条 IPC 路由(conversation/report/turn)+ overview +
/insight命令。类型上把larkAppId从InsightOverviewSessionInput上移进InsightReportQuery——这一步是 overview 能修好的关键(放在旧位置时 resolver 的TranscriptPathQuery根本读不到它)。
我核实过的点
- 载荷链成立:pm2 给 bot daemon 和 dashboard 都注了
SESSION_DATA_DIR=DATA_DIR(cli.ts:480/519),worker 也是从同一个 var 推 BOT_HOME(worker.ts:6776)——所以「写方(worker)」和「读方(daemon/dashboard)」两侧dirname(SESSION_DATA_DIR)得到的是同一个 botmuxHome,回落路径与 worker 逐字同源。 - 无跨 bot 泄漏:
larkAppId是DaemonSession的必填字段、也是sessionKey的组成部分=会话所属 bot 的身份,恰好就是 worker 当初重定向进去的那个 bot。 - both-exist 分支不会误触:
getClaudeSessionJsonlPath返回前有existsSync门控;newerFile的statSync有 try/catch,文件在 stat 前消失就退回全局路径。 - wiring 无遗漏:insight 4 处 builder 调用(conversation/report/turn/overview)全部带上了
larkAppId。
验证结果
| 项 | 结果 |
|---|---|
钉 SHA 独立 worktree pnpm build |
✅ 0 错误 |
| PR 新测 + 相关 7 套件(transcript-resolver ×2 / cost-calculator ×2 / dashboard-token-usage-row / usage-ledger / insight-report) | ✅ 127/127(与 PR 声明逐字一致) |
对齐当前 master(PR base 是 b30e8949,master 已前移到 fdb105a8,并入 #611/#588/#621,其中 command-handler / dashboard-ipc-server / dashboard-rows 三文件被 master 同改) |
✅ trial-merge 零冲突;合成树 pnpm build 绿;9 套件 423/423;逐条 grep 确认 4 处 wiring 在 merge 后仍在(dashboard-rows 的 sessionTokenUsage 未被 master 挪去 session-row-enrichment) |
非阻塞观察(供 codex / 申晗参考,不拦本 PR)
- 同类 bug 对 codex 尚未修:daemon 侧
codexSessionsRoot()→codexHome()读process.env.CODEX_HOME,而 daemon 进程本身没有 CODEX_HOME(只有 worker child env 有;worker.ts:6623只改 worker 自身的process.env,不影响 daemon)——所以沙盒 codex bot 的 token/insight 同样读不到<BOT_HOME>/codex。本 PR 只修 claude 系、范围合理,但 codex 的缺口仍在,建议后续补一个对称的 PR。 aiden分支是防御性死路径(aiden 适配器没有claudeDataDir→worker 永远不会把它重定向,isClaudeFam=false),无害。getSessionCost/getSessionJsonlPath(cost-calculator.ts:44)没传larkAppId——是 base 里就存在、仅被自身单测消费的死代码,不是本 PR 的 live 遗漏。
结论:逻辑正确、影响面清楚、测试充分、与当前 master 合并干净。无阻塞项。@codex 请复审;未经申晗确认不合码。
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Codex 复审:无阻塞 ✅
我独立钉住 PR head 37ab692f 复核了改动,并把它 trial-merge 到当前 origin/master@fdb105a8 验证;结论与首审一致,未发现需要阻塞本 PR 的问题。
重点核对
- 读写路径同源:worker 写侧与 daemon/dashboard 读侧均从
dirname(SESSION_DATA_DIR)推导<botmuxHome>/bots/<appId>/claude,自定义 data dir 下也一致。 - 归属与安全边界正确:live 调用链使用会话所属
larkAppId;路径构造复用botHomePath/assertSafeAppId,非法 app id fail-closed 返回null,未引入跨 bot 扫描或降级读取。 - 沙盒开关翻转可恢复:全局与 BOT_HOME 两份 transcript 并存时按 mtime 取新,平手保持全局;聚合缓存以实际 path 为键,目录切换不会复用旧文件缓存。
- 接线完整:dashboard token row、usage ledger、Insight conversation/report/turn/overview、
/insight命令均把larkAppId传到底层 resolver;master 新增的 dashboard/session-row 公共路径合并后也未截断该字段。 - 横向影响可控:改动只扩展 Claude-family transcript 查找;非沙盒 Claude 仍走原全局路径,seed/relay 保留各自 primary data dir,非 Claude CLI 分支逻辑未改。
实际验证
- PR head:
pnpm build✅ - PR head 相关 7 套件:
127/127✅ git diff --check✅- trial-merge
origin/master@fdb105a8:零冲突;pnpm build✅ - trial-merge 冲突敏感 9 套件(额外含 command-handler / dashboard-ipc):
423/423✅
非阻塞备注:沙盒 Codex 的 daemon 侧 transcript resolver 仍只看 daemon 自身的 CODEX_HOME/全局目录,存在对称缺口;这不是本 PR 引入,也不建议在本 PR 扩 scope,可后续单独修。
结论:本 PR 可进入申晗确认环节。此处仅提交复审记录,不合码。
✅ 已合并(申晗授权 admin-merge)
尚未做(各需申晗另行授权):发版(打 tag)、live 部署。本次仅合码,未动 daemon。 非阻塞后续(双审都独立点出):沙盒 Codex bot 有同类 daemon 侧路径缺口——daemon 进程没有 |
问题
沙盒(CLI 数据重定向)bot 的 dashboard TOKEN IN/OUT 恒显示
-,usage-ledger(成本台账)静默漏记,insight 报transcript_missing。根因:重定向 bot 的 Claude 被 worker 注入
CLAUDE_CONFIG_DIR=<botmuxHome>/bots/<appId>/claude(worker.ts的willRedirectCliData路径),transcript 全部写在 BOT_HOME 下;而 daemon 侧resolveSessionTranscriptPath对 claude-family 硬编码全局~/.claude(seed/relay 为 adapter dataDir),永远 miss →getSessionTokenUsage返回 null。这不是沙盒重构引入的回归——旧 readIsolation 时代同样重定向,该问题一直存在,只是沙盒铺开后才显眼。修法
TranscriptPathQuery增加可选larkAppId;claude-family 分支在主 dataDir miss 后回落<botmuxHome>/bots/<appId>/claude(botmuxHome 与 worker 同源 =dirname(SESSION_DATA_DIR);无 SESSION_DATA_DIR 意味着从未发生重定向,不回落)。assertSafeAppId校验,不安全值不构造路径(返回 null 而非 throw)。/insight命令传入会话已有的larkAppId;InsightOverviewSessionInput.larkAppId上移进InsightReportQuery(overview 自动流经)。影响面
/insight卡片)对沙盒 bot 生效。测试
test/transcript-resolver-bot-home.test.ts9 个用例:BOT_HOME 回落、仅全局、沙盒翻转 ON/OFF/mtime 平手、无 larkAppId/无 SESSION_DATA_DIR、不安全 app id(../evil、a/b、..、空串)不构造路径、aiden 同路径。tsc --noEmit0 错误。v3-distillation-runner×5、command-handler/status ×1)在干净 upstream/master worktree 上逐一复现相同失败,与本改动无关;codex-app-threads为并发 flake,隔离复跑通过。对照结论:零新增失败。真机实测(本机 live 数据)
沙盒 bot
cli_aace4eb402785be8的真实 transcript(写在~/.botmux/bots/cli_aace4eb402785be8/claude/projects/…):codex review
两轮:一轮抓出「固定全局优先在沙盒翻转后冻结统计」(高,已修 + 3 回归测试);二轮复验 PASS 可合并(安全无路径穿越、缓存以路径为键切换正确、statSync 异常仅单次回退、调用点无遗漏)。