Skip to content

fix(usage): 沙盒 bot 的 token 统计/成本台账/insight 读不到重定向 transcript - #623

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
xu4wang:fix/sandbox-token-usage
Jul 28, 2026
Merged

fix(usage): 沙盒 bot 的 token 统计/成本台账/insight 读不到重定向 transcript#623
deepcoldy merged 2 commits into
deepcoldy:masterfrom
xu4wang:fix/sandbox-token-usage

Conversation

@xu4wang

@xu4wang xu4wang commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

问题

沙盒(CLI 数据重定向)bot 的 dashboard TOKEN IN/OUT 恒显示 -,usage-ledger(成本台账)静默漏记,insight 报 transcript_missing

根因:重定向 bot 的 Claude 被 worker 注入 CLAUDE_CONFIG_DIR=<botmuxHome>/bots/<appId>/claudeworker.tswillRedirectCliData 路径),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 意味着从未发生重定向,不回落)。
  • 刻意用「按存在性探测」而非在 daemon 侧复刻重定向判定(sandbox × adapter 能力 × wrapper 三元条件):任何组合下都正确,且不会与 worker.ts 漂移。
  • 双侧并存时按 mtime 取新(codex 一轮 review 抓出):存量持久会话跨沙盒开关切换时 session id 不变、数据目录变,固定「全局优先」会把统计永远冻结在切换点;mtime 取新对开↔关两个方向都正确,恰好相等(字节级拷贝)保持全局路径(stock 语义)。
  • app id 经 assertSafeAppId 校验,不安全值不构造路径(返回 null 而非 throw)。
  • 调用点接线:dashboard-rows、usage-ledger、insight 三条 IPC 路由、/insight 命令传入会话已有的 larkAppIdInsightOverviewSessionInput.larkAppId 上移进 InsightReportQuery(overview 自动流经)。

影响面

  • 修复:dashboard token 列、usage-ledger 成本记账、会话 insight(单会话/turn/overview//insight 卡片)对沙盒 bot 生效。
  • 非沙盒 bot 行为不变(bot home 路径不存在 → 走原全局路径;tie-break 保持全局优先)。

测试

  • 新增 test/transcript-resolver-bot-home.test.ts 9 个用例:BOT_HOME 回落、仅全局、沙盒翻转 ON/OFF/mtime 平手、无 larkAppId/无 SESSION_DATA_DIR、不安全 app id(../evila/b..、空串)不构造路径、aiden 同路径。
  • 存量相关套件全绿:transcript-resolver-symlink / cost-calculator / cost-calculator-cache / dashboard-token-usage-row / usage-ledger / insight-report 共 127 用例。
  • tsc --noEmit 0 错误。
  • 全量 vitest:11,038 过 / 23 失败——失败项全部为既有基线(feishu 浏览器/真 CLI 的 e2e 套件 + 6 个单测),其中 6 个可隔离复现的单测(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/…):

WITHOUT larkAppId (= 修复前行为): null
WITH larkAppId    (= 修复后行为): {"in":378840,"out":2249,"inputTokens":17,"outputTokens":2249,
  "cacheReadTokens":355128,"cacheCreateTokens":23695,"model":"claude-opus-5","turns":9}

codex review

两轮:一轮抓出「固定全局优先在沙盒翻转后冻结统计」(高,已修 + 3 回归测试);二轮复验 PASS 可合并(安全无路径穿越、缓存以路径为键切换正确、statSync 异常仅单次回退、调用点无遗漏)。

xu4wang added 2 commits July 27, 2026 21:32
沙盒(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/平手三个回归测试。
@xu4wang
xu4wang requested a review from deepcoldy as a code owner July 27, 2026 14:54

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)静默漏记、/insighttranscript_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 命令。类型上把 larkAppIdInsightOverviewSessionInput 上移进 InsightReportQuery——这一步是 overview 能修好的关键(放在旧位置时 resolver 的 TranscriptPathQuery 根本读不到它)。

我核实过的点

  • 载荷链成立:pm2 给 bot daemon 和 dashboard 注了 SESSION_DATA_DIR=DATA_DIRcli.ts:480/519),worker 也是从同一个 var 推 BOT_HOME(worker.ts:6776)——所以「写方(worker)」和「读方(daemon/dashboard)」两侧 dirname(SESSION_DATA_DIR) 得到的是同一个 botmuxHome,回落路径与 worker 逐字同源。
  • 无跨 bot 泄漏larkAppIdDaemonSession 的必填字段、也是 sessionKey 的组成部分=会话所属 bot 的身份,恰好就是 worker 当初重定向进去的那个 bot。
  • both-exist 分支不会误触getClaudeSessionJsonlPath 返回前有 existsSync 门控;newerFilestatSync 有 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-rowssessionTokenUsage 未被 master 挪去 session-row-enrichment

非阻塞观察(供 codex / 申晗参考,不拦本 PR)

  1. 同类 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。
  2. aiden 分支是防御性死路径(aiden 适配器没有 claudeDataDir→worker 永远不会把它重定向,isClaudeFam=false),无害。
  3. getSessionCost/getSessionJsonlPathcost-calculator.ts:44)没传 larkAppId——是 base 里就存在、仅被自身单测消费的死代码,不是本 PR 的 live 遗漏。

结论:逻辑正确、影响面清楚、测试充分、与当前 master 合并干净。无阻塞项。@codex 请复审;未经申晗确认不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 可进入申晗确认环节。此处仅提交复审记录,不合码。

@deepcoldy
deepcoldy merged commit d041bff into deepcoldy:master Jul 28, 2026
@deepcoldy

Copy link
Copy Markdown
Owner

✅ 已合并(申晗授权 admin-merge)

  • 合并方式:admin-merge(xu4wang 的 fork PR,无 CI,双审收敛无阻塞后由申晗拍板)
  • 合并 commit:d041bff1origin/master HEAD,parent = fdb105a8,正是双审 trial-merge 验证的 base,期间 master 无漂移)
  • 合并前最终核对:PR head 仍为 37ab692f(与双审逐字一致,无新提交)、mergeable: MERGEABLEBLOCKED 仅因 fork 无 CI + 分支保护(非冲突)

尚未做(各需申晗另行授权):发版(打 tag)、live 部署。本次仅合码,未动 daemon。

非阻塞后续(双审都独立点出):沙盒 Codex bot 有同类 daemon 侧路径缺口——daemon 进程没有 CODEX_HOMEcodexSessionsRoot() 读不到 <BOT_HOME>/codex,token/insight 同样 miss。本 PR 只修 Claude 系、范围合理,建议后续另开对称 PR 补 codex 侧。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants