Skip to content

fix(#2374): visibility marker 的 message item id 改用 msg 命名空间,并治愈 marker- 中毒会话 (WC-015) - #2375

Merged
ranxianglei merged 3 commits into
ranxianglei:masterfrom
Lin515:2026-10-08_fix-marker-item-id
Oct 8, 2026
Merged

ranxianglei merged 3 commits into
ranxianglei:masterfrom
Lin515:2026-10-08_fix-marker-item-id

Conversation

@Lin515

@Lin515 Lin515 commented Oct 7, 2026

Copy link
Copy Markdown

问题

emitMarker(Responses adapter)产出的 message item,其 id 不以 msg 开头:

Invalid 'input[59].id': 'marker-1791393957942-2'. Expected an ID that begins with 'msg'.

这个 id 会穿过客户端往返(pi-ai 系客户端把 message item 的 id 存进响应文本块的 textSignature,
再作为 input item 的 id 回放),所以一旦某轮压缩发出过一个,该会话此后每个请求都 400。
sanitizeResponsesInputIds——bili 针对这个往返的入口治愈——只认 msg-proxy- 命名空间,
于是历史的 marker- id 穿了过去,会话无法自愈。

根因,且修在产出侧(疏,不是堵)

缺陷的本质是 item 类型与铸造它的 id 命名空间不匹配。emitText 对同样的 item 形状一直铸
msg-proxy-…;emitMarker 铸 marker-…,这个名字描述的是 marker 的内容而不是它的 wire 类型。
现在两者共用一个 helper:

function proxyMessageItemId(outputIndex: number): string {
    return `${PROXY_MESSAGE_ID_PREFIX}${Date.now()}-${outputIndex}`;
}

因此未来的 emit 路径不可能漂到上游不接受的命名空间之外,而入口治愈认识的分母恰好就是 bili 铸造的命名空间。

治愈另外放宽到历史的 marker- 前缀。这不是"堵症状":产出侧在同一个改动里已经修掉,治愈的存在是为了
修复已发布版本已经写到用户磁盘上的 rollout(与它在 #242 的超长 id、#1474 的收窄里扮演的角色相同)。
没有它,升级 bili 会把每一个已被毒化的会话永久留在坏状态。

改动

文件 改动
src/loop/adapter-responses.ts 新增 PROXY_MESSAGE_ID_PREFIX / LEGACY_MARKER_MESSAGE_ID_PREFIX 常量、emitText 与 emitMarker 共用的 proxyMessageItemId()、sanitizeResponsesInputIds 的 assistant-message 治愈分支改用 isBiliMessageId();顺带把该治愈的文档注释(本次改动本就要重写它)从 normalizeResponsesMessageItems 头上移到它真正描述的函数上
tests/visibility-markers.test.ts 回归:发出的 marker 其 message item id 必须以 msg 开头
tests/responses-round2-lifecycle.test.ts 回归:历史 marker- assistant-message id 在入口被删除,同时 provider id(msg_…、rs_…)保持字节原样
tests/wire-contract-fakes.ts 台账条目 WC-015 + validateResponsesBody 里对应的校验等价子句
tests/wire-contract.test.ts 台账自检里的 WC-015 闸门探针 + 一组定向正/负测试
tests/issue1733-config-update.test.ts 仅夹具:message id 改为 msg_ 前缀。这些测试考的是 WC-011 相邻性,而它们原先的 m1/a1 id 在 WC-015 下已属 wire 非法(fake 现在会拦)
tests/responses-proxy-id-replay.test.ts 仅夹具:marker-123-0 原本被放在"原生 id 必须原样保留"一组里,但 marker- 是 bili 自己铸造的历史命名空间、不是原生的。换成原生 id;该历史命名空间由上面的治愈回归覆盖

因此有两个测试文件需要改夹具。其中之一(responses-proxy-id-replay.test.ts)此前把
"marker- id 不可触碰"这个修复前的假设写死了——那个假设就是本 bug 的适用范围在测试里的形态,
这也正是这条台账子句值得加的原因:它让夹具的 wire 非法性显式暴露,而不是默认隐含。

内部重请求路径已核查,无需改动:kernel 的 coreToResponses 投影 message/function_call item 时
不带 item id,所以入口处由客户端回放的 id 是 bili 合成 message id 能到达上游请求的唯一途径。

wire-contract 台账(#1304)

按 AGENTS.md §7.3 新增的永久条目:

  • WIRE_RULES —— WC-015 / wire responses / "a supplied message input item id must begin with msg",
    provenance 指向生产拒绝现场、往返机制,以及 Codex backend 自身的同类报告;
  • 执法 —— validateResponsesBody 拒绝任何 message 形状(带 type 的,或 omp 的无 type 形态)且 id
    不以 msg 开头的 input item;所有经校验等价 fake 转发的泳道都会在 CI 拦住回归;
  • 闸门 —— 在 "wire-contract ledger: every rule has a live enforcement clause" 里加了探针,
    该条目不可能不被执法地存在;
  • golden 快照未动(无 schema/manifest 变化)。

两种压缩模式(§6)

  • proxy 模式 —— 产出侧修复与入口治愈都生效(HTTP 入口会跑 sanitizeResponsesInputIds),
    且 fork-adoption 对被收养的克隆继承放宽后的治愈。
  • plugin 模式 —— 产出侧修复生效(同一个 adapter,不再有任何路径铸出坏 id);plugin 直通没有入口
    id 清洗环节,本改动也不新增一个。边界说明:被修复前版本毒化的 plugin 模式 rollout,其 id 留在
    客户端一侧,in-plugin 无物可治——因为该 id 从不进入 bili 构造的请求体。

配置面

无。没有新增、改名或改语义任何配置字段、环境变量或 CLI 开关;既有的 compress.visibilityMarkers
语义不变(它仍然只决定是否发出 marker)。

相关工作(开 PR 前已核对 tracker)

没有任何现存 issue 或 PR 报告过本缺陷——已检索 open + closed 的 issue/PR,包括
emitMarker / visibilityMarkers / sanitizeResponsesInputIds 三个词以及上游错误原文。最接近的
条目及其为何不覆盖本问题:

msg 前缀规则是 Codex backend 自身的硬校验、并非 bili 的推断,这一点有独立佐证:同样的错误在
Codex CLI 本地合成的 id 上也被报告(openai/codex#27928 的 review_rollout_user;openai/codex#20783
的一个裸 UUID)。该佐证同样记录在 WC-015 的 provenance 里。

验证

  • tests/visibility-markers.test.ts —— 新回归在修复前的产出侧(marker-…)失败,修复后通过。
  • tests/responses-round2-lifecycle.test.ts —— 新增治愈回归(历史前缀被删、provider id 不受影响)。
  • tests/wire-contract.test.ts —— WC-015 子句 + 定向测试 + 台账自检。
  • 目标 5 套件 + npm run typecheck 在当前 master head 上全绿(见下)。
  • 全量测试交给 CI(本改动局限于 message id 铸造加一条校验子句;本地全量只会多出报告者机器上的
    代理环境噪声)。
  • 生产实证:治愈半边已在真实会话上得到验证(session-5f30b295…,真实 ChatGPT Codex backend):
本地时间 事件 证据
01:25:57 旧构建铸造毒 id id 内嵌的 Date.now() = 1791393957942
01:26:12 客户端回放该 id → turn 1 以硬错误结束 客户端 rollout:turn/end / PI_AI_ERROR / Invalid 'input[59].id';代理日志 ← upstream 400
01:26–03:20 该会话闲置(代理日志零记录) —
03:15:32 换用带本修复的构建 —
03:20:20 同一会话、同一 rollout 输入「继续」→ turn 2 正常运行 客户端 rollout:turn/start seq=110;代理日志该会话恢复活动
03:25:36 持续正常工作 代理日志:[acp-usage] round 1 input=79287、request: 99 msgs、forward POST → …/codex/responses,无 400

三条佐证排除了"碰巧好了":

  1. 毒 id 始终留在客户端持久化历史里——客户端 rollout 中 marker-1791393957942-2 出现 4 次,
    其中两次正是回放载体 "textSignature":"{\"v\":1,\"id\":\"marker-1791393957942-2\"}",
    且该文件在正常工作期间(03:25)仍在写入。客户端没有丢弃它。
  2. 期间没有发生折叠——同一轮日志明确 nudge idle ... reason="max compressible 43822 < threshold 50000",历史未被压缩改写,所以毒项确实仍在被原样回放的数组里。
  3. 唯一变量就是构建——修复前(01:26)同一 rollout → 400;修复后(03:20)同一 rollout → 正常。

即:毒 id 由已发布的旧版本写到了用户磁盘上,新版本在不改动客户端任何数据的前提下把它治好,
无需任何客户端侧会话手术。

是否可自动合并

本改动触及 message id 逻辑,属 AGENTS.md §7.4 列明的必须人工评审范围(gate 8)——请求人工评审,
不申请自动合并。

Closes #2374

…间,并治愈 marker- 中毒会话 (WC-015)

emitMarker 产出的是 type:"message" 的 item,却用 marker-<ts>-<index> 作 id;上游按 msg 前缀
校验 message item id,整请求 400。该 id 会被 pi-ai 系客户端经 textSignature 回放,而入口清洗
只认 msg-proxy-,于是会话此后每轮必 400、无法自愈。

- emitText / emitMarker 统一走 proxyMessageItemId(),铸造在 msg-proxy- 命名空间下
- sanitizeResponsesInputIds 增认历史 marker- 前缀,救治已被旧版本写坏的 rollout
  (只放宽 assistant-message 的删除分支;>64 字符改写分支仍只认 msg-proxy-,ranxianglei#1474 保护不变)
- 新增 wire-contract 台账条目 WC-015(台账 + 校验等价 fake 子句 + 闸门探针 + 定向测试)
- 两个夹具文件随契约收紧同步修正
- 顺带把 sanitizeResponsesInputIds 的文档注释从 normalizeResponsesMessageItems 移到它真正描述的函数上

生产实证:一个被 0.1.187 毒化的会话(session-5f30b295…),其 rollout 里的 marker- id 始终未被
客户端丢弃(textSignature 回放载体仍在),换用本构建后同一会话即刻恢复正常,无需任何客户端侧处理。
…h PROXY_MESSAGE_ID_PREFIX

Reviewer nit on top of the PR head: WC-015 single-sources the msg-proxy- namespace, but this third client-visible mint site (round>1 upstream message remap) still hand-wrote the literal. String is byte-identical - no behavior change.
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 PR #2375 Review(fix #2374)— 判定:可合(人工评审,CI 把关全量)

我做了什么:拉取 PR head 541ef79ec 核对代码;确认它 = 当前 master 2495ad25df + 单提交、master 无领先提交 → 无需 rebase。在 PR head 上实跑:npm --prefix kernel build + host tsc --noEmit(干净,exit 0)+ 定向 6 套件(wire-contract / visibility-markers / responses-round2-lifecycle / responses-proxy-id-replay / issue1733-config-update / e2e-responses-offhead-system)41/41 全绿。全量回归按惯例交给 CI。

七问:

  1. 是什么问题:Responses wire 上 visibility marker 走的是 message item,但 emitMarker 铸的 id 是 marker-<ts>-<idx>,不以上游要求的 msg 开头;该 id 被 pi-ai 系客户端编进 textSignature 每轮回放,首轮压缩后会话每个请求都 400 且无法自愈。
  2. 什么原因导致的:item 类型与铸造它的 id 命名空间不匹配——marker- 描述的是 marker 的内容而非它的 wire 类型,同形状的 emitText 一直铸 msg-proxy-;而入口治愈只认白名单里的 msg-proxy-,marker- 穿了过去(Preserve provider-issued reasoning IDs in Responses input sanitization #1474 那次收窄把同为 bili 铸造的 marker- 留在白名单外,正是成因)。
  3. 怎么修复的:疏不是堵,两条腿——①产出侧把 emitText/emitMarker 统一到 proxyMessageItemId(),全部铸在 msg-proxy- 下,未来任何 emit 路径都不可能漂出上游前缀规则;②入口治愈放宽到历史 marker-(仅 assistant-message 删除分支),让已发布版本写坏的 rollout 自愈。
  4. 是否有回归问题:逐条过触碰路径的既有行为——
    • emitText id:输出字节不变(改走 helper)。✅保持
    • emitMarker id:marker-→msg-proxy-。有意变更、已披露(旧值→新值+原因),修的就是它。✅
    • 治愈删除分支:从只删 msg-proxy- 扩到也删 marker-。isBiliMessageId() 只匹配这两个 bili 自有前缀,provider id(msg_*/rs_*/fc_*)不受影响;round2-lifecycle 的治愈回归钉住「provider id 字节原样」(Preserve provider-issued reasoning IDs in Responses input sanitization #1474 不变量)。✅
    • 64 改写分支:仍只认 msg-proxy-,未动。✅保持

    • 高危模式排查:无语义漂移修 A 坏 B、无 fail-fast 变吞错、无阈值/时序漂移;唯一输出格式变化即上面披露的 marker id 前缀。
    • WC-015 台账子句的影响面(唯一会收紧全量测试的点):fake 对每个入站 Responses body 校验、违规即 400。我把全测试树里所有驱动 validateResponsesBody 的路径清点过:只有 wire-contract(受控探针)、e2e-responses-offhead-system(OMP 无 type 且无 id 的 item,子句对 id===undefined 直接 return,命中不了)、issue1733(PR 已把夹具改成 msg_ 前缀)三处;copilot-schema-contract 用的是 openai-chat 校验器、responses-websocket 用 validateResponsesWsCreate,都不碰这条子句。没有任何其它夹具会以非 msg 前缀的 message id 穿过校验器,全量不会因 WC-015 变红。✅
  5. 是否破坏缓存:不涉及。marker id 每次响应现铸(内嵌 Date.now())本就非缓存稳定项;assistant-message 的这些 id 入站即被删除、不进上游请求体,前缀缓存不受影响。
  6. 是否改了设计方向:不涉及架构转向。「bili 在自有命名空间铸 id + 入站治愈」的设计原样保留并被扩展(多认一个历史前缀)。
  7. 是否引入新配置:不涉及。没有新增/改名/改语义任何 config 字段、env var、CLI 开关;compress.visibilityMarkers 语义不变(仍只决定发不发 marker)。

两种压缩模式:proxy 模式产出侧 + 入口治愈双生效;plugin 模式产出侧生效(同一 adapter,不再有任何路径铸坏 id),plugin 直通本就没有入站 id 清洗、本 PR 也不新增——PR 关于「修复前毒化的 plugin rollout 因 id 留在客户端侧而不可治」的边界说明准确。

测试有效性:三个新回归都有牙——visibility-markers 断言发出的 marker item id 以 msg 开头(修复前的 marker- 必挂);round2-lifecycle 断言历史 marker- 被删 + provider id 原样;wire-contract 有正/负双向断言 + 台账自检探针保证 WC-015 不可能「存在但无执法」。非恒真断言。

diff 整洁度:7 文件全部与本 PR 目的相关;两个测试文件是必要的夹具修正(WC-015 下原夹具已属 wire 非法),其中 responses-proxy-id-replay 把「marker- 不可触碰」这个修复前的错误假设显式纠正——正是这条台账子句值得加的原因。无跑题、无生成物/锁文件 churn。

自动合并判定:触及 message-id 逻辑 = §7.4 gate 8,必须人工评审、不可自动合并——PR 自己也是这么标的,正确。

结论:可合(待人工批准 + CI 全量绿)。方向、根因定位层次、修法层次、既有行为回归清单、测试有效性、diff 整洁度我都过了;未本地跑的是全量套件(交给 CI,与作者一致)。没有发现需要顺手小修的小问题。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 补充:PR head 541ef79ec 的 CI 全量已绿——25 个 check-run 全部 success,含完整测试矩阵(ubuntu/windows × node 22/24)、全部真实客户端 e2e 泳道、kernel-guard、version-guard、docs-sync、build-artifact。这实证了我上面静态清点的结论:WC-015 台账子句在全量套件里没有任何其它夹具会被误伤。

我 review 时留的唯一开放项(「待 CI 全量绿」)现已闭合。判定维持:可合,剩人工批准即可。

@ranxianglei

Copy link
Copy Markdown
Owner

ci失败

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 查了,CI 不是全挂,是单个 check-run 红:test (windows-latest, node 22)。其余 24 个(含 ubuntu-22/24、windows-24、全部真实客户端 e2e、kernel-guard / version-guard / lane-guard / docs / build-artifact)全绿。

**归因:几乎可以断定是这条腿的偶发 flake,不是本 PR 引入的回归。**依据:

  1. PR 现在有两个提交。第 2 个 8e3acacd(route round>1 remap through PROXY_MESSAGE_ID_PREFIX)是纯 DRY 重构——PROXY_MESSAGE_ID_PREFIX === "msg-proxy-",重构前后铸出的 id 字节完全相同,运行时零行为变化,逻辑上不可能翻转一个确定性的测试。
  2. 第 1 个提交(真正的修复)当初在 windows-latest,node22 上就是绿的(我 review 时 25/25 全绿)。同一腿先前绿、现在红,而其它三个矩阵腿此刻全绿——典型单 runner+单 runtime 组合的偶发特征,不是代码回归(回归会跨腿或稳定复现)。
  3. 仓库历史里一串 Windows-CI 专属 flake 修复记录可佐证这条腿本就爱偶发:NTFS 忽略 mode bits(stripImages 滑窗剥离的图片无恢复通道,且滑窗每轮击穿 prompt cache(billion-context-pi#593 同族缺口) #1995)、one-shot 读日志改成轮询(WS 通道拒绝 codex 0.160.0 的帧:Invalid or oversized response.create event(reason=invalid_request) #2126)、cmd.exe argv([Bug][Windows launcher] 经 codex.cmd 启动破坏 argv:嵌入引号拆分、%变量% 展开和 & 被解释 #2196/fix: route owner-managed update lanes through InstallBackoff (#2192) #2206)、platform-param host-independent([v0.1.186 review] 汇总:#2187 子进程累积 / #2090 升级影响 / 发布小修 / codex×2 / 杂项(合并自 #2252–#2257) #2260)。

我的限制(说清楚):我拉不到原始日志——check-run logs 端点返回 404(要 admin scope),annotations 只到 Process completed with exit code 1(log 第 37945 行,说明套件跑到后段才挂,符合单测偶发而非早期 setup 崩)。所以"flake"是我基于「字节等价 diff + 同腿先前绿 + 仅单腿红 + 历史 flake 模式」的推断,不是读了日志下的结论。

建议动作:

  • 你在 PR 页点 Re-run (failed) jobs 重跑那条腿。变绿 → 确认 flake,放心合;再红 → 把该 job 日志里 not ok / ✖ 那几行贴给我,我按具体 test 定位。
  • 顺带:master 已领先 PR base 38 个提交(PR 现处 behind)。真要合的话,作者先 rebase 到当前 master 再让 CI 全量跑一遍最稳,能顺带把这次偶发一并确认掉。

我这边判定不变:代码可合(两个提交都过审,第 2 个是零风险重构);当前唯一挡路的是这条 flaky 腿,重跑即可。

@ranxianglei
ranxianglei merged commit cc368fe into ranxianglei:master Oct 8, 2026
26 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.147-master.cc368fe (rolling master tag).

npm install -g billion-context@0.0.147-master.cc368fe  # 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.

Responses wire:visibility marker 的 message item 用了非 "msg" 前缀的 id,且中毒会话无法自愈

2 participants