session.getSessionStats()(pi SDK agent-session.d.ts:633)一次给全:
本轮只取 contextUsage.percent;累计 token 不用它(它是当前实例的,回收后归零), 用 ask.ts 已有的 turnTokens 累加进库。
| 时机 | 通路 | 带什么 |
|---|---|---|
| 一轮结束 | /agent/ask 的 done / error 收尾帧 | totalTokens + ctxPercent(实时更新) |
| 打开会话 | GET /agent/sessions/:id | totalTokens(库内)+ ctxPercent(会话恰在内存里才有) |
ctxPercent 取不到时不带这个字段,前端显示 -(与 cost 同一个占位语汇,不新造)。 取不到的两种情形都正常:会话不在内存里(还没提问过 / 已被回收)、pi 刚压缩过上下文(percent: null)。
recordUsage 是「尽力而为的资源闸,不是账单」(quota.ts),会话累计沿用同一口径:
落库失败时帧内数字与库内会差一轮,下次打开会话回到库内值 —— 这是「尽力而为」计数本来的性质, 与 daily_quota 一致,不为它新增补偿机制。
| 文件 | 改动 |
|---|---|
| docs/security.md | §2 追加 R-USAGE 补记(先于代码) |
| apps/api/agent/migrations/014_session_tokens.up.sql | sessions 加 total_tokens BIGINT NOT NULL DEFAULT 0 |
| apps/api/agent/store.ts | SessionRow.totalTokens;addSessionTokens(id, delta) |
| apps/api/agent/runtime.ts | RuntimeSession.totalTokens(重建时从库读初值) |
| apps/api/agent/ask.ts | 收尾帧带两个数;finally 里累加落库 |
| apps/api/agent/sessions.ts | SessionSummary.totalTokens;GetSessionResponse.ctxPercent |
| apps/web/lib/agent-api.ts | TurnSummary 扩两个可选字段 |
| apps/web/lib/stats-bar.ts(新) | 纯函数格式化 + 单元测试 |
| apps/web/components/workbench/Workbench.tsx | 统计条改用 state;样式零改动 |
| apps/web/lib/demo-data.ts | 删 statsBar 常量 |
| ROUNDS.md / rounds/BACKLOG.md | 登记本轮;划掉 BACKLOG 第 86 行那条 |
| # | 检查 | 命令 / 期望 |
|---|---|---|
| 1 | 编译与测试全绿 | dev.ps1 check;dev.ps1 test(api + web 两处) |
| 2 | 生成物 diff 只含新增字段 | dev.ps1 gen 后 api-client.ts 只多 totalTokens / ctxPercent |
| 3 | 连问两轮,tokens 单调增 | 顶栏数字每轮结束时增长,与服务端日志里的 turnTokens 累加一致 |
| 4 | F5 后不回退 | 刷新页面,tokens 与刷新前相同(走 GET /agent/sessions/:id) |
| 5 | ctx 有值且随轮次增长 | 每轮结束 ctx N% 更新;打开未提问的旧会话显示 ctx - |
| 6 | cost 固定 - | 任何状态下第二项都是 -,不随数据变化 |
| 7 | 帧里没有配置面 | 抓 done 帧:无 model / provider / baseUrl / cost / contextWindow 绝对值 |
| 8 | 样式零改动 | git diff Workbench.tsx 里没有 style / className / 布局改动 |
| 9 | 迁移可回滚 | migrate.sh --status 到 14;列有默认值,存量会话读回 0 |
| 10 | 空会话与新会话 | 未提问的新会话显示 0 tokens + ctx -,不显示 NaN / undefined |
审查方式:codex /codex:review(全量 branch diff against main —— 按 CLAUDE.md「只有前两轮用固定的全量范围」)。 启动走 PowerShell Start-Process 脱离 + Monitor 盯 stdout 文件(记忆 codex-review-detached-launch: 经 Claude 后台 Bash 起会随 launcher 假死)。耗时约 9 分钟,与基线一致。
findings 处理(4 条,全部采纳整改):
| # | 级别 | 问题 | 处置 |
|---|---|---|---|
| 1 | P1 | lib/agent-api.ts 的 getSession 返回类型是手写的,没跟着加 ctxPercent,Workbench 解构它 → TS2339,生产 next build 直接失败 | 采纳。返回类型加上该字段,并在函数上写明「改服务端响应形状后要跑 npx tsc --noEmit」 |
| 2 | P2 | 累加落库在 finally 里、收尾帧先发:成功路径上也有竞态窗口,访客看到顶栏更新后立刻 F5 会读到上一轮的库值、数字当着面回退 —— 正好打在本轮验收 #4 上 | 采纳。改为先落库、再发帧(移动既有语句,不新增机制);usageFrame 不再自己加本轮,只读已定妥的 rec.totalTokens;docs/security.md §2 R-USAGE 补记同步改口径 |
| 3 | P2 | openSession 只在 getSession 成功后才换 usage:切会话的加载期间顶栏留着上一个会话的数字,请求失败则永久留着 | 采纳。与 setItems([]) 并列加 setUsage(null),一行 |
| 4 | P2 | ctx 圆点无值时压灰违反规则 7(画板没画过这个态) | 采纳,整个撤回(见上方「与设计稿的偏离」);诉求记 BACKLOG |
P1 暴露的验收漏洞(值得记住):本轮 #1 写的是「编译与测试全绿」,而我只跑了 dev.ps1 check(只覆盖 api) 与 bun test lib(纯函数,不做类型检查);next dev 不阻塞类型错误,所以 10 项浏览器验收全过、生产构建却会炸。 凡改动服务端响应形状,web 侧必须另跑 npx tsc --noEmit。已写进 agent-api.ts 的函数注释。
第 2 轮复审(全量范围,第 1 轮的 4 条无复发):2 条 P2,零 high,全部采纳整改:
| # | 级别 | 问题 | 处置 |
|---|---|---|---|
| 5 | P2 | totalTokens 设为必填后,runtime.test.ts 的 fakeRec() 夹具缺这个字段 → TS2322 | 采纳,夹具补 totalTokens: 0 |
| 6 | P2 | typeof === "number" 拦不住 Infinity:自定义兼容端点报越界 JSON 数(1e400)会被解析成 Infinity,流进 rec.totalTokens 后是永久污染 —— BIGINT 更新失败、SSE 里序列化成 null、之后每轮都还是 Infinity,直到会话回收 | 采纳。三处都改 Number.isFinite:摄入 usage 的源头(ask.ts,token 与 cost 都改)、累加进长寿对象 rec 之前、公共函数 addSessionTokens 自己的边界。补一条回归测试 |
跨轮次发现、按流程不当场改:npx tsc --noEmit 还报 3 个本轮之外的错 (catalog.test.ts 的 socketPath、skill-runner.test.ts 的两处 string | Error),都是 R-WEBFETCH 的测试文件。 说明 api 侧测试文件从来没过严格类型检查(vitest 用 esbuild,transpile-only)。已记 rounds/BACKLOG.md。
结论:整改后 PASS(2 轮 / 6 条 / 零 high)。第 2 轮的两条整改都是「补一个字段」与「换一个判断」, 未新增机制、未改动接口面,按 CLAUDE.md 的缺陷门禁不再触发第 3 轮。
同一验收项针对性整改后连续 2 次仍不过 → 写 rounds/round-usage/BLOCKED.md,停下呼人。
环境:本机开发库 llm_config 为空,按 local-acceptance-faux-provider 的口径起假 OpenAI SSE provider (scratchpad,不入库;contextWindow: 32000,每轮报不同的 total_tokens:1500 / 2600 / 3800 / 4200 —— 报不同的数才验得出「累加」而不是「覆盖」)。验完 DELETE FROM llm_config WHERE provider='faux'。
| # | 结果 | 实测 |
|---|---|---|
| 1 | ✅ | check 绿;test = api 527 passed(含本轮新增 4 条 store 用例)+ web 21 passed(含新增 6 条 stats-bar)。另跑 npx tsc --noEmit —— 见下方审查段,dev.ps1 test/check 查不到 web 侧类型错 |
| 2 | ✅ | api-client.ts 的 diff 只有 SessionSummary.totalTokens 与 GetSessionResponse.ctxPercent |
| 3 | ✅ | 两轮:1.5k tokens → 4.1k tokens(1500 → 1500+2600);库内 total_tokens = 4100 |
| 4 | ✅ | F5 + 重开会话:4.1k tokens · - · ctx 8% · 38 events,与刷新前逐字符相同 |
| 5 | ✅ | ctx 随轮次真实增长 5% → 8% → 12%;重启后端清空注册表后重开会话显示 ctx - + 灰点 |
| 6 | ✅ | 第二项在所有状态下都是 - |
| 7 | ✅ | 抓到的收尾帧原文:{"sessionId":"…","modelRoundTrips":1,"turnMs":608,"totalTokens":4200,"ctxPercent":13.125} —— 无 model / provider / baseUrl / cost / contextWindow |
| 8 | ✅ | Workbench.tsx 的 diff 里 span 数量、嵌套、fontSize / gap / borderRadius 全部未动(唯一例外见下方偏离) |
| 9 | ✅ | \d sessions 显示 total_tokens | bigint | not null | 0;三个存量会话读回 0 |
| 10 | ✅ | 新会话第一轮 totalTokens: 4200,无 NaN / undefined |
最关键的一条(验收 #5 的后半段与 runtime 改动的证明):重启后端 → 注册表清空 → 打开会话显示 4.1k tokens · - · ctx - → 再问一轮 → 7.9k tokens · - · ctx 12%(4100 + 3800)。 若 createRuntimeSession 没有从库读初值,这里会显示 3.8k —— 这正是所有者裁定「取会话历史累计」要防的回退。
本轮一度让 ctx 圆点在无值时压成 var(--text-dim)(理由:ctx - 配绿点等于在「不知道」时声称「正常」), 并在任务卡里标为「待所有者确认」。codex 第 1 轮 P2 判定它违反规则 7 —— 画板 1a 只画过有值的常绿态, 那是个画板没有的态,而规则 7 明确禁止在接数据时改样式 / token。已采纳整改:圆点恒为 #16a34a, ctxDotColor 整个函数连同它的测试一并删掉(留一个不被调用的导出只会诱使下次再用), lib/stats-bar.ts 末尾留注释说明这段历史。诉求本身记进 rounds/BACKLOG.md 等所有者裁定。
现在的状态:统计条在任何数据状态下都与画板 1a 逐像素一致,只有文字内容随数据变。