Skip to content

Detect host-rewritten tool-call ids in fold-drift diagnostics + host id-stability contract - #2400

Merged
ranxianglei merged 2 commits into
masterfrom
2026-10-08_tool-id-rewrite-detect-docs
Oct 8, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-10-08_tool-id-rewrite-detect-docs

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #2396

Problem

When a host projects one conversation across providers and rewrites the authoritative tool-call ids inside its projection, bili's fold-reconcile layer (#1921) loses coverage of every folded tool pair: both claim passes key on toolCallId, so covered ids stop matching and compressed tool outputs re-enter the wire unfolded. #2396 documents this in production: Prime switched one conversation from Codex to a custom Responses provider, and its foreign-provider converter re-sanitized stored composite ids (call_switch_seed|fc_0_0 — Codex lane serializes call_switch_seed; foreign lane sends call_switch_seed_fc_0_0) on both sides of every pair. Context billed at ~66,525 input tokens re-entered at 558,541/562,549 against a 500,000-token window.

The root cause belongs to the host (it broke the only protocol-stable pairing factor). Bili's response here is honest diagnostics + the documented contract — NOT repair-by-guessing: pairing a missing covered id with an inbound twin under a different id would be a guess, i.e. misattribution, the expensive failure mode per MESSAGE-IDENTITY.md.

Changes

fix(fold-reconcile) — detection only, never repaired:

docs — the host contract:

Tests (targeted suite green locally: 42/42)

Config surface

No new config field, env var, or CLI flag. Persisted metadata is additive: fold anchors may carry one extra optional short-hash field; old records read fine, missing m = pre-#2396 record = no evidence (never an error).

Behavior changes (disclosed)

  • Drift log lines (warn/error) can now contain an appended clause naming suspected host-rewritten tool-call ids. Wording only; no semantic change to claim/repair decisions, modes, thresholds, timing, or wire output.
  • Fold-anchor metadata records grow by one short hash field for anchored tool messages (persisted size only).

Known limitation

Suspect evidence exists only in the FIRST drift pass of an episode: the order backbone rolls onto the rewritten ids afterwards, so later passes (and the escalation error) report the generic causes. Deliberate — persisting suspect state into the drift-episode machinery is a separate mechanism family, and widening the scan to all inbound messages would add O(N_tool) full-text norms per stuck pass (a #2334 regression). Pinned by test.

The real fix belongs to the host (canonicalize ids at its boundary; the #2396 workaround is the reference implementation). If an owner wants a proxy-side escape hatch, the right shape is an explicit authoritative alias map (management endpoint per session), not implicit guessing — left as a decision point in #2396, not built here.

awork added 2 commits October 8, 2026 16:57
…gnostics (#2396)

A host projecting one conversation across providers may rewrite the
authoritative tool-call ids inside its projection (#2396: Prime switched a
conversation from Codex to a custom Responses provider and its converter
sent stored composite ids re-sanitized on both sides of every pair). Both
claim passes key on toolCallId, so every folded tool pair stopped matching
and compressed outputs re-entered the wire unfolded (~66k billed tokens
re-sent at ~560k against a 500k window).

Detection only — never repaired: pairing a missing covered id with an
inbound twin under a different id would be a guess, i.e. misattribution
(MESSAGE-IDENTITY.md failure-mode axiom). A no-toolCallId normalized
identity (FoldAnchor.m, additive persisted field, backfilled while bytes
live) lets planReconciliation count rewrite suspects and name them in the
three drift log lines. The #2334 work-count pins move 8->16 and 12->16
(the no-tool norm rides along on tool-anchor rebuilds); steady-state cost
stays zero. Evidence window is the first drift pass per episode (the order
backbone rolls onto rewritten ids afterwards) — pinned by an e2e test that
also asserts the escalation error does NOT claim evidence its own pass
cannot see.
PLUGIN.md gains obligation 5: keep tool-call ids byte-stable within one
conversation, including across provider projections, and canonicalize at
the host boundary before the payload reaches the proxy. MESSAGE-IDENTITY.md
(en + zh) records the #2396 violation as the one external dependency of
the content-hash identity premise, why implicit aliasing is rejected
(misattribution > non-recognition), and points at the new detection in
src/fold-reconcile.ts.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-08_tool-id-rewrite-detect-docs (b3bef62)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.2589-pr.2400.4bd07b0

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr2400.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷

PR #2400 Review 结论:可合(mergeable) — detection-only + 契约文档,零行为回归,门禁全绿。已本地取证(非「应该能过」):定向套件 tests/fold-reconcile.test.ts 42/42、typecheck 干净、零依赖门禁(unused-exports/docs-sync/docs-coverage/lane-guard/kernel-guard/version-guard/pi-subagents-guard)全绿;分支 base == 当前 master(f2b84b6c2,不落后);CI 上 ubuntu(node 22 + 24)全量绿、全部 guard job 绿、build-artifact 绿,24 项 check 仅余 windows 两条全量腿 in_progress(其余 22 项均 success)。

① 方向关(审原 issue #2396):根因判定正确——宿主(Prime)把同一会话跨 provider 投影时改写了权威 tool-call id;这是唯一「按会话协议唯一且客户端重序列化后原样存活」的字段被打破,bili 两条 claim pass 以它为键必然失配。bili 选择只检测、绝不猜修复(猜测 = misattribution = 更贵的失败模式,MESSAGE-IDENTITY.md 公理),契约落文档——scope 切分正确;真正的修复属于宿主边界(#2396 workaround 即参考实现)。

② 回归审查(既有行为逐项清点):diff 只碰 src/fold-reconcile.ts(代码)+ 三个文档。

  • claim/repair 决策:不变。suspect 块(src/fold-reconcile.ts:389-407)纯读 plan.unmatched 计数,从不写 claims/claimedCandidates。
  • 三条漂移日志:仅当 idRewriteSuspects > 0 时追加条件子句;suspects=0 时字节级等同原文(现有断言不受影响)。
  • FoldAnchor 持久化:新增可选字段 m,旧记录正常读(缺失 = pre-[Bug][Prime/Responses] provider switch rewrites composite tool IDs and reintroduces compressed history #2396 = 无证据,永不报错),additive、无需 schema bump。
  • 稳态成本:anchorFrom 对 tool anchor 多算一次 no-tool norm,但仅在 anchor 重建时发生(稳态命中缓存 early-return,line 282 missing.length===0 早退);suspect 扫描只在漂移 pass、且限 churn region(perf: 长会话每次请求重复计算并丢弃超上限的压缩锚点 #2334 纪律保留)。work-count pin 8→16 / 12→16 已如实反映。
  • wire 输出 / 模式 / 阈值 / 时序 / 前缀缓存:零改动(测试钉住 effectiveMessageIds 从不被改写)。
    高危模式逐条过:无 fail-fast→吞错、无默认值/阈值/时序漂移、无输出格式语义变化(仅诊断日志条件追加)。backfill 用 anchorFrom(byId.get(id)) 重算 n 看似有 stale 风险,但 raw id 本身是含内容的哈希——消息仍在 wire 上 ⇒ 内容未变 ⇒ 精确重算;被改写的 id 其旧 raw id 已不在 wire 上(无法 backfill、保持 residual-pinned),且 full-norm 含 toolCallId,不会引入假 claim。

③ 代码审查:逻辑成立——suspect 定义 = 「缺失 covered id 的去 toolCallId 归一化身份,与某个携带不同 toolCallId 的入站孪生相同」。region-bounded(只用 churn region 候选,避开每卡住 pass O(N_tool) 全文哈希)。诚实性有测试钉住:升级 error 行不得声称其自身 pass 看不到的 suspect 证据。\u0000 hex escape 合规,无 as any / @ts-ignore / console.error,走 opts.log。

④ 测试有效性:真测试非恒真。正例(同内容不同 id → suspect=1、claims=0)+ 三负例(stable-id churn 仍走 byTool 认领 / 真编辑内容不算 suspect / v0 无 m 保持 residual-pinned)+ e2e provider-switch 全生命周期(work count、日志点名、escalate-once、effectiveMessageIds 不改写、error 行 doesNotMatch 假证据)+ v0 backfill。断言有牙(精确计数 / deepEqual claims / 正则子串),bug 回归时抓得住。

⑤ diff 整洁度:5 文件 +253/−13,两个聚焦 commit(fix(fold-reconcile) + docs),每一行都服务于 #2396,无跑题 / 空行重排 / lockfile churn。


八问:

  1. 是什么问题:宿主跨 provider 投影同一会话时改写权威 tool-call id,bili fold 覆盖整体失效,已压缩内容按全额重送(~66.5K → ~558K/563K tokens)。
  2. 什么原因导致的:根因在宿主——唯一协议稳定配对因子被宿主自己改写;bili 次要缺陷是漂移日志只会归因「mutation/deletion」,识别不出「id 被改写」这一形状。
  3. 影响范围与严重程度:任何「同一会话跨多 provider 复用且投影内重写 tool-call id」的插件宿主都会触发(本例 Prime);后果是上下文膨胀 / 重复计费、逼近窗口即不可用。不触发的宿主完全不受影响。特定条件下严重。
  4. 怎么修复的:两层,都不猜身份——① 检测-only:fold-reconcile 把「缺失 covered id 的 no-toolCallId 归一化身份匹配到不同 toolCallId 的入站孪生」计为 rewrite suspect,在三条漂移日志点名,让该失败模式可辨识而非误诊为截断;② 契约落文档:PLUGIN.md 义务 5 + MESSAGE-IDENTITY(en/zh)记录 [Bug][Prime/Responses] provider switch rewrites composite tool IDs and reintroduces compressed history #2396 为内容哈希身份前提的唯一外部依赖,隐式别名化列入被否决备选。
  5. 是否有回归问题:无。claim/repair/模式/阈值/时序/wire/前缀缓存全部不变;日志仅在 suspects>0 时追加子句(suspects=0 字节级等同原文);FoldAnchor 增可选字段 additive;稳态成本为零。已知限制(测试钉住):suspect 证据只在 episode 首个漂移 pass——之后 order backbone 滚到改写后 id,后续 pass 与升级 error 报通用成因;刻意不把 suspect 状态推进 drift-episode 机制(另一机制家族 + 扩扫描面会令 perf: 长会话每次请求重复计算并丢弃超上限的压缩锚点 #2334 回归)。此为刻意取舍,非缺陷;属同一失效域故不单独立 issue,owner 若想要跨 pass 保留 suspect 证据可作为 follow-up 决定。
  6. 是否破坏缓存:不涉及。前缀缓存 / 重试 / 增量游标 / 去重键零改动;持久化元数据仅多一个短哈希字段。
  7. 是否改了设计方向:没有。身份仍由内容哈希派生,mNNNNN 账本不动,标签仍是派生视图;本 PR 只是给该前提的一个外部依赖命名并钉住检测。
  8. 是否引入了新的配置:没有。无新 config 字段 / env var / CLI flag,无迁移。(别名 API 若将来要做属独立 config surface,等 owner 拍板,不在本 PR。)

附带:state-coherence(双 resident 实例 21 vs 27 blocks)已由前序会话单独立项 #2401(带来源标记),正确分离、不混入本 PR。review 未发现需顺手修的小问题(命名 / 注释 / 文档措辞均与代码事实一致)。

收工判定:可合。分支 base == 当前 master、本地定向 42/42 + typecheck 干净 + 全部门禁绿、CI ubuntu 双 node 全量绿(仅剩 windows 两条腿在跑)。真正的修复属于 Prime 宿主一侧(边界规范化 id);bili 交付的是可辨识的诊断 + 落地的契约。

@ranxianglei
ranxianglei merged commit c2dc81b into master Oct 8, 2026
23 of 24 checks passed
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.159-master.c2dc81b (rolling master tag).

npm install -g billion-context@0.0.159-master.c2dc81b  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

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.

[Bug][Prime/Responses] provider switch rewrites composite tool IDs and reintroduces compressed history

1 participant