fix: 恢复 ACP usage 传递(usage_update + PromptResponse.usage)

撤销审计 §4.1 的原修复(删除 usage_update 以求严格 v1 stable 合规)。
现实中的所有主流 ACP 客户端(Zed、Cursor 等)实现的是 unstable spec,
删除 usage_update 后客户端 context 使用量一律显示 0/0,严重破坏 UX。

SDK 已包含 UsageUpdate 类型(sessionUpdate: 'usage_update',字段 used + size
+ 可选 cost)和 PromptResponse.usage 根字段(UNSTABLE 但被广泛实现),这是
context 使用量报告的唯一标准化载体,故选择优先保证 interop。

变更:
- bridge/forwarding.ts: 收到 'result' 消息且 lastAssistantTotalUsage !== null
  时发送 usage_update
  - used = 最近一条 assistant 消息的 input + output + cache_read + cache_creation
    token 总和(≈ 当前上下文占用)
  - size = lastContextWindowSize(默认 200000,通过 modelUsage prefix-match 解析)
  - compact_boundary 时不发(不知道压缩后实际占用;下一轮 result 会自然修正)
- agent/promptFlow.ts: PromptResponse 根部添加 usage 字段,并镜像到
  _meta.claudeCode.usage 供消费者任选读取路径
- 测试更新:bridge.test.ts 三个相关 test 改为断言 usage_update 被发出且
  used/size 正确;agent.test.ts 改为断言 root usage 存在
- 审计文档 §4.1 标记为已撤销,添加决策回滚说明

验证:bun run precheck 全通过(typecheck + lint + 5851 tests)
ACP service tests: 176 pass / 0 fail

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-19 16:14:04 +08:00
parent 65f81de52b
commit cac23e62cc
5 changed files with 97 additions and 65 deletions

View File

@@ -92,12 +92,10 @@ export async function forwardSessionUpdates(
const subtype = msg.subtype
if (subtype === 'compact_boundary') {
// Reset assistant usage tracking after compaction
// Reset assistant usage tracking after compaction. We don't emit a
// usage_update here because we don't know the post-compaction context
// size — the next prompt's result will carry the corrected value.
lastAssistantTotalUsage = 0
// NOTE: usage_update is an UNSTABLE SessionUpdate discriminator (not in
// stable v1 schema). Token/cost info has no v1-stable carrier; we drop
// it from session/update and rely on PromptResponse._meta for clients
// that need it (see audit §4.1).
await conn.sessionUpdate({
sessionId,
update: {
@@ -132,10 +130,23 @@ export async function forwardSessionUpdates(
}
}
// NOTE: usage_update was removed — it is an UNSTABLE SessionUpdate
// discriminator not present in the stable v1 schema (audit §4.1). Token
// and cost information is returned via PromptResponse._meta.claudeCode.usage
// instead.
// Per session-usage.mdx RFD: emit usage_update so clients can display
// context window utilization (e.g. "53K / 200K"). Although usage_update
// is currently UNSTABLE in the v1 schema, it is the only standardized
// carrier for context-window state and is implemented by all major ACP
// clients (Zed, Cursor, etc.). Strict v1-stable compliance broke this
// UX (clients showed 0/0), so we emit it whenever we have usage data.
// See audit §4.1 for the prior strict-compliance rationale and revert.
if (lastAssistantTotalUsage !== null) {
await conn.sessionUpdate({
sessionId,
update: {
sessionUpdate: 'usage_update',
used: lastAssistantTotalUsage,
size: lastContextWindowSize,
},
})
}
// Determine stop reason
const subtype = msg.subtype
@@ -307,9 +318,9 @@ export async function forwardSessionUpdates(
// ── Compact boundary ───────────────────────────────────────
case 'compact_boundary': {
// Don't emit usage_update here — we don't know the post-compaction
// context size. The next prompt's result will carry the corrected value.
lastAssistantTotalUsage = 0
// NOTE: usage_update removed — UNSTABLE discriminator, not in v1 stable
// schema (audit §4.1). Token info flows through PromptResponse._meta.
await conn.sessionUpdate({
sessionId,
update: {