Skip to content

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

Closed

Description

@Lin515

现象

在 Responses wire 上对接严格上游(ChatGPT Codex backend,
https://chatgpt.com/backend-api/codex/responses)时,第一次真正执行 ACP 压缩的那一轮会把会话
永久毒化:此后该会话的每个请求都失败于

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

该轮以硬错误结束(PI_AI_ERROR),且会话永不恢复——因为出问题的 item 一直留在客户端 rollout 里,
之后每个请求都会把它回放出去。

根因

生产者是 src/loop/adapter-responses.ts 的 emitMarker。它产出的是 message item
(buildMessageItemSequence → {type:"message", role:"assistant", ...}),但 id 铸造成

`marker-${Date.now()}-${outputIndex}`

而同文件产出同样 item 形状的 emitText 一直是 msg-proxy-${Date.now()}-${outputIndex}。
上游按前缀(msg)校验 message item 的 id,所以 marker- 前缀被拒。

它能到达上游,是因为这个 id 会往返:pi-ai 系客户端把 message item 的 id 编进响应文本块的
textSignature({"v":1,"id":"marker-…"}),并在之后每个请求里把它作为 input item 的 id 回放。
bili 知道这个往返并已做防御——sanitizeResponsesInputIds 会删掉 bili 自己命名空间下的
assistant-message id("Client-visible proxy ids are not upstream item identities; replay the full
message instead",5dc12ade、#1474)——但那份白名单只覆盖 msg-proxy-,于是 marker- id 原样
穿到上游,整请求 400。

只有 Responses wire 会产生这个问题:openai-chat / anthropic / google 三个 adapter 的 emitMarker
产出的内容不带任何 item id。

命中条件

四个条件同时成立才会触发——这也是它能潜伏下来的原因:

  1. Responses wire(Codex / Responses API 客户端);
  2. compress.visibilityMarkers 打开(这是默认值);
  3. 真的执行了一轮 ACP 压缩(需触及增长/质量阈值或 ≥75% 压力层),且会话继续下去;
  4. 上游强制校验 message id 前缀(真实的 ChatGPT Codex backend;宽容 / 自建上游会静默接受坏 id)
    且客户端会回放 item id。

把 compress.visibilityMarkers 设为 false 可以规避,代价是失去 marker 输出。

影响

会话不可恢复:之后每一轮都重发中毒 item 并拿到 400。fork 也救不了——fork-adoption 对被收养的
克隆跑的是同一个只认 msg-proxy- 的清洗器。对会持久化 textSignature 的客户端来说,用户侧唯一的
出路是放弃该会话。

环境

  • bili 0.1.187(proxy 模式,前置在 ChatGPT Codex backend 之前)
  • 客户端:pi-ai 1.0.4 via DSH;Node 24.x,Windows
  • 抓到的现场:input[59].id = marker-1791393957942-2(Date.now() = 1791393957942,
    outputIndex = 2),即第一轮压缩期间发出的 visibility marker item

建议修法

  1. bili 合成的每个 message item 都经由同一个 helper 铸造在 msg-proxy- 命名空间下
    (emitMarker 与 emitText 共用),使任何 emit 路径都不可能漂出上游的前缀规则之外。
  2. 把入口治愈放宽到历史的 marker- 命名空间,让已被已发布版本写坏的 rollout 能够自愈,
    而不是永远 400(sanitizeResponsesInputIds,与 [Bug] Responses ACP round 2 生成 66 字符 message id,导致 Codex 后续请求持续 400 #242/Preserve provider-issued reasoning IDs in Responses input sanitization #1474 同类做法)。
  3. 把该约束登记为 WC-015(台账条目 + 校验等价 fake 子句 + 闸门探针 + 定向测试)。

Activity

  1. Lin515 commented on Oct 7, 2026

    @Lin515
    Author

    已在 #2375 修复:#2375

    实现方式与本 issue 的「建议修法」一致:

    1. emitText / emitMarker 统一经 proxyMessageItemId() 铸造在 msg-proxy- 命名空间下,
      使任何 emit 路径都不可能再漂出上游的前缀规则;
    2. sanitizeResponsesInputIds 的 assistant-message 治愈分支放宽到历史的 marker- 前缀
      —— 只放宽删除分支,>64 字符的改写分支仍只认 msg-proxy-,Preserve provider-issued reasoning IDs in Responses input sanitization #1474 对 provider id 的保护不变;
    3. 新增 wire-contract 台账条目 WC-015(台账条目 + 校验等价 fake 子句 + 闸门探针 + 定向测试)。

    生产验证

    修复已在真实上游(ChatGPT Codex backend)上验证过治愈效果。一个被 0.1.187 毒化的会话
    (session-5f30b295…):

    时间 事件
    01:25:57 旧构建铸造毒 id marker-1791393957942-2
    01:26:12 客户端回放该 id → turn 1 以 PI_AI_ERROR 结束(上游 400)
    01:26–03:20 该会话闲置(代理日志零记录)
    03:15:32 换用含本修复的构建
    03:20:20 同一会话、同一 rollout 输入「继续」→ turn 2 正常运行
    03:25:36 持续正常(request: 99 msgs → forward POST → …/codex/responses,无 400)

    三条佐证排除了「碰巧好了」:

    • 毒 id 始终留在客户端持久化历史里(rollout 中 marker-1791393957942-2 出现 4 次,其中两次正是
      回放载体 "textSignature":"{\"v\":1,\"id\":\"marker-1791393957942-2\"}");
    • 期间未发生折叠(同一轮日志 nudge idle ... reason="max compressible 43822 < threshold 50000"),
      毒项确实仍在被原样回放的数组中;
    • 唯一变量就是构建 —— 修复前同一 rollout → 400,修复后同一 rollout → 正常。

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

  2. added a commit that references this issue on Oct 7, 2026
  3. ranxianglei commented on Oct 7, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    PR #2375 审核完毕——结论:可合并(message-id 逻辑属 §7.4 gate 8,人工合并;CI 正在跑新 head)。

    结构化七问

    1. 是什么问题:visibility marker 骑在 type:"message" item 上,而 bili 给这种 item 铸的 id 是 marker- 前缀——严格上游(Codex backend)要求 message item id 以 msg 开头;客户端把该 id 持久化进 textSignature 并逐轮回放后,会话每个请求必 400,永久毒化。
    2. 什么原因导致的:item 类型与铸造命名空间不匹配。emitText 对同样的 item 形状一直铸 msg-proxy-,emitMarker 却按 marker 的内容而非wire 类型命名 id;入口治愈白名单在 Preserve provider-issued reasoning IDs in Responses input sanitization #1474 收窄到只认 msg-proxy-(为保护 provider id),把 bili 自己铸造的另一命名空间(marker-)漏在了外面——产出侧出了界,治愈侧接不住。
    3. 怎么修复的:修在产出侧(疏不是堵)。emitText/emitMarker 共用一个 helper、统一铸在 msg-proxy- 下,任何 emit 路径不可能再漂出上游前缀规则;入口治愈放宽到删除历史 marker- assistant-message id,让已发布版本写坏的 rollout 自愈(不动客户端任何数据);约束登记为永久台账条目 WC-015。
    4. 是否有回归问题:无意外回归,既有行为逐项审过——
      • emitText id 形状字节不变(msg-proxy-\d+-\d+,既有正则断言钉住);
      • emitMarker id marker-…→msg-proxy-… = 本修复的有意变更(客户端可见流字节随之改变,PR 正文已披露);
      • 治愈只放宽删除分支;>64 改写分支仍只认 msg-proxy-,Preserve provider-issued reasoning IDs in Responses input sanitization #1474 的 provider-id 保护原样保留(其不变量测试照过);
      • fake 校验器新拒非 msg 前缀的 message item id → CI 收紧,两个夹具文件同步更新(理由 PR 正文已给);
      • 内部重请求路径无需改动(kernel coreToResponses 投影不带 item id,已核对);
      • 两种压缩模式都推理过:proxy 模式两半修复都生效,plugin 模式只有产出侧 + 明确的边界说明(plugin 中毒 rollout 的 id 留在客户端一侧、从不进入 bili 构造的请求体,无物可治——正确且诚实);
      • 无其他 open PR 触碰本 diff 的任何文件(70 个 open PR 标题全扫 + 最相关 4 个的文件清单核实);kernel/、update.ts、持久化格式、配置面均不涉及。
    5. 是否破坏缓存:不涉及——message id 是 per-round artifact,不是去重键/游标/缓存键;round>1 remap 表语义不变。
    6. 是否改了设计方向:没有——沿用 [Bug] Responses ACP round 2 生成 66 字符 message id,导致 Codex 后续请求持续 400 #242/Preserve provider-issued reasoning IDs in Responses input sanitization #1474 既有的「铸在规则之内 + 入口治愈」架构,只是把命名空间台账补完整。
    7. 是否引入新配置:没有。

    验证(本地定向范围执行)

    • PR head 上 typecheck 全绿 + 改动路径 10 个套件 80 项测试全过(responses-proxy-id-replay / responses-round2-lifecycle / visibility-markers / wire-contract / issue1733 / responses-empty-messages / responses-tool-images / store-marker-gate / marker-echo / e2e-responses-offhead-system);
    • WC-015 provenance 的外部佐证核过:CLI /review follow-up fails with review_rollout_user id: Expected an ID that begins with 'msg' openai/codex#27928(review_rollout_user)与 #20783(裸 UUID)均真实存在且错误形态与生产现场一致——前缀规则确属 Codex backend 自身硬校验,不是 bili 推断;
    • 测试有效性:产出侧回归走真实 compress loop round 并解析 SSE 输出的 message items(修复前必失败,断言有牙);治愈回归同时断言 marker- id 被删、provider id(msg_…/rs_…)字节原样;台账自检保证 WC-015 不可能存在而无执法子句。

    小修(已直接推上 PR 分支)

    判定:方向对(修根因)、无回归、diff 干净(7 文件 +87/−16,每行都在题内)、生产实证可信(同一 rollout 换构建即刻恢复,三条佐证排除了巧合)。等人工合并:#2375

  4. added a commit that references this issue on Oct 8, 2026
  5. added a commit that references this issue on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions