Skip to content

[Bug] Malformed/truncated parameters of the compress tool are echoed back verbatim → upstream 400 → entire round fails; | [Bug] compress 工具参数畸形/截断被原样回传 → 上游 400 → 整轮失败,nudge 每轮重触发形成死锁 #3661

Closed

Description

@jp20010908

TL;DR

模型在长上下文下调用 proxy tool compress 时产出了非法 JSON arguments(两种形态:畸形 raw string、或流式输出被截断)。bili 正确拒绝了压缩,但在把 proxy tool 执行结果回传给模型(re-request)时,把模型的原始非法 arguments 原样放回 assistant.tool_calls 发给上游 → 上游(OpenAI 兼容网关,服务端 chat template 渲染 tool_calls.arguments)解析失败,整个请求 400 → acp-loop 判本轮失败。压缩永远不成功 → nudge 每轮重新注入 → 死锁,会话完全无法继续。

环境

  • billion-context 0.1.197(已对比 0.1.199 dist,未修复;minified 代码里 lenient parser / re-request / failedMutating 逻辑原样)
  • 链路:客户端 → bili:8787(acp-proxy)→ think-bridge:8790 → opencodex:10100 → OpenAI 兼容上游(China Mobile ecloud 网关,模型 glm-5.3 / glm-5.3-flash)
  • 触发场景 A:会话 305 msgs / inbound 737.9KiB,T1 待压缩量长期超 50000 阈值;触发场景 B:新会话 99 msgs / inbound 271.3KiB,pendingT1 达到 50385 后 nudge INJECT T1

现象

  • 客户端每条消息:推理正常 1~2 步 → 模型调 compress → 本轮失败,显示 本轮运行失败[acp-proxy: upstream error 400],日志同模式 400 连续 22 次(会话 A)
  • 换模型 glm-5.3 → glm-5.3-flash 后依旧失败:失败形态从 malformed 变为 truncated,死循环不变

两种失败形态(日志摘录,脱敏后)

形态 1 — malformed-json(glm-5.3,6 次尝试 0 次成功,参数开头每次锚定同一段文本,错误固化):

[acp-loop] proxy tool compress: arguments not parseable JSON (len=5004, head={"content":m00189–m00245 <raw string,value 未加引号…>}) — routing the raw string to the lenient parser
[warn] [acp-compress-input] rejected: kind=malformed-json invalidItems=0 len=5004
[acp-proxy: compress call had no valid ranges; nothing compressed.]
[acp-loop] round 1: proxy tool executed; re-requesting so the model sees the result
[acp-proxy: compress loop upstream error 400: {"error":{"message":"Provider error 400","type":"invalid_request_error",…}}]
[error] [acp-loop] upstream error 400: {…}

形态 2 — truncated(glm-5.3-flash,流式 475 frags 拼出 3032 字符后 JSON 未闭合):

[acp-openai] round 1 tool-frame settle (1): idx=0 name="compress" id=call_… argsLen=3032 frags=475 → proxy
[warn] [acp-compress-input] rejected: kind=truncated invalidItems=0 len=3032
[acp-proxy: compress loop upstream error 400: …]
[error] [acp-loop] upstream error 400: …

根因链

  1. 模型输出非法 arguments(畸形或截断)——compress 参数要求模型一次产出很长的结构化 JSON,长上下文 + 长输出下模型稳定翻车,且失败后错误模式固化
  2. bili 容错正确拒绝了压缩,但 re-request 时把模型原始非法 arguments 原样放回 assistant.tool_calls(推断依据:紧随 re-request 的请求必现 400,同会话不带该 tool-call 的主请求成功;下方对照实验证明非法 arguments 单独即可触发这个 400)
  3. 上游 chat template 渲染 tool_calls.arguments 时解析失败,整个请求 400
  4. acp-loop 把这个 400 当作本轮上游失败,中止整轮 → 客户端丢掉本轮已完成的推理
  5. 压缩永不成功 → T1 pending 只增不减 → nudge 每轮重新注入 → 死锁

最小复现(对照实验,直连上游,同一模型仅 arguments 不同)

tool_calls[0].function.arguments 结果
合法 JSON HTTP 200
畸形(value 为未加引号的 raw string) HTTP 400:{"error":{"type":"Bad Request","code":"decode_bad_request","message":"chat template 渲染失败: unknown method: string has no method named items (in chat:157)","param":null}}
合法 JSON 但结构不符 schema(content 塞对象而非数组) HTTP 200

复现请求体:

{
  "model": "<任意以服务端 chat template 渲染 tool_calls 的 OpenAI 兼容后端>",
  "messages": [
    {"role": "user", "content": "hi"},
    {"role": "assistant", "content": null,
     "tool_calls": [{"id": "call_t1", "type": "function",
       "function": {"name": "compress", "arguments": "{\"content\":m00189–m00245 malformed raw string"}}]},
    {"role": "tool", "tool_call_id": "call_t1", "content": "rejected: malformed-json"}
  ],
  "max_tokens": 8
}

关键点:只要 arguments 是合法 JSON,内容再离谱上游都收;一旦 JSON 本身非法,上游服务端渲染就崩。 bili 管不了上游的严格校验,但完全可以保证自己回传的 arguments 必为合法 JSON。

建议修复(按优先级)

  1. 回传前 sanitize:模型产出的 tool arguments 解析失败(malformed / truncated)时,re-request 前替换为合法占位 JSON 字符串(如 {"_proxy_error":"model produced malformed tool arguments; original replaced"})。一行防御即可从根上断掉 400,死锁不再可能。
  2. compress 失败降级而非整轮判死:目前一次压缩失败(经回传放大成 400)就中止整轮,丢掉本轮已成功完成的推理。建议压缩失败仅记 warning,让模型看到失败结果后继续本轮。
  3. 连续失败退避:日志显示 armed streak 已在数 failure N,但 nudge 仍每轮重新注入。建议连续失败超过阈值后指数退避或暂停 nudge,否则模型反复被引导跳进同一个坑(失败模式还会被失败上下文锚定固化)。

补充

  • 顺带发现:上游 400 响应体其实带了具体原因(decode_bad_request / chat template 渲染失败),但经链路中 opencodex 包装后只剩 Provider error 400,多层代理下排障成本很高(已另给 opencodex 仓库提 issue)。
  • 如需更多现场材料(完整日志片段、会话规模数据、0.1.199 dist 对比细节)可以补充。

Activity

  1. changed the title [-][Bug] compress 工具参数畸形/截断被原样回传 → 上游 400 → 整轮失败,nudge 每轮重触发形成死锁[/-] [+][Bug] Malformed/truncated parameters of the compress tool are echoed back verbatim → upstream 400 → entire round fails; | [Bug] compress 工具参数畸形/截断被原样回传 → 上游 400 → 整轮失败,nudge 每轮重触发形成死锁[/+] on Oct 18, 2026
  2. jp20010908 commented on Oct 18, 2026

    @jp20010908
    Author

    本地已实施临时缓解(热修复)

    在等官方修复期间,我在本机 0.1.197 的 dist 上打了 sanitize 补丁并已上线(2026-10-18 15:28):

    做法(与建议 1 相同):在 buildLoopRequestBody 的两个出口(thread 分支 + 主分支)包一层 __sanitizeToolCallArgs——遍历 body.messages[].tool_calls[].function.arguments,JSON.parse 失败则替换为 {"_proxy_error":"model produced malformed tool arguments; sanitized","_orig_len":N}。合法 JSON 一律不动,函数整体 try/catch 包裹。

    验证:

    • 语法:node --check(dist 目录下按 ESM 解析)通过
    • 单元:5 用例全过(malformed 替换 / truncated 替换并保留原长 / legal 不动 / 空 body 不动 / 合法对象 JSON 不动)
    • 上线:bili 重启,端口 8787 正常监听,日志正常滚动

    效果追踪中:上线后观察 bili.log 的 compress loop upstream error 400 是否归零——畸形/截断参数将不再触发上游 chat template 渲染崩溃,死锁环断开(模型看到 sanitized 占位参数 + 压缩失败结果后继续本轮,不再整轮判死)。

    给上游修复的一个补充信息:如果官方实现 sanitize,注意 buildLoopRequestBody 附近是 const buildLoopRequestBody=(extra=[])=>{...}(minified 后锚点易漏 const 前缀)。另外本机验证时还确认了一个对照事实:同样的畸形 arguments,只要被替换成任意合法 JSON,上游即返回 200——所以 sanitize 点放在回传出口是最小且充分的防御位。

    补丁细节与踩坑实录(双 const 锚坑、LF/CRLF 保真、node --check 需在含 package.json 的目录跑 ESM 模式)已记录在本机运维文档,需要的话可以贴全量 diff。

  3. jp20010908 commented on Oct 18, 2026

    @jp20010908
    Author

    热修复失败,上下文瞬间膨胀至300k以上,对话无法进行,正在查明原因

  4. jp20010908 commented on Oct 18, 2026

    @jp20010908
    Author

    补充:上一条「热修复失败,上下文瞬间膨胀至 300k+」的原因已查明

    先更正我 08:05 那条 comment——那次失败与本 issue 的 sanitize 热修复无关。sanitize 补丁工作正常(上线后 compress loop upstream error 400 计数保持 0)。那次「对话无法继续」是同一条压缩回环上的第二种失败形态,根因在客户端侧,但暴露出 bili 三处可以加固的地方,一并提上来。

    第二种失败形态:压缩请求自身超窗 + 被中断后无退避

    实测环境:客户端声明窗口 262144,实际会话历史已 329,888 tokens(客户端 UI 仍显示 262k)。

    四个连续轮次的压缩全部失败:

    时间 结果
    15:49:40 Request was aborted(客户端侧中断)
    15:51:22 pi-ai detected context overflow for model "ecloud/glm-5.3" ← 没有中断,自己溢出了
    15:57:28 Request was aborted
    15:59:23 Request was aborted

    bili 侧对应日志:

    [acp-loop] upstream success without usage report — keeping lastInputTokens=0 (source=none)
    client disconnected mid-stream after 86643ms (122989 bytes written to client)
    [acp-loop] round 1: upstream stream truncated (no completion event; round 1, 0 text chars + 0 reasoning chars received)
    

    关键点:压缩请求必须携带全量历史,历史 329k 而窗口 262k → 压缩请求本身先超窗 → 压缩永远跑不完 → pending 不降 → 下一轮立刻再触发 → 死循环。用户观感和本 issue 的 400 死锁一模一样(「永远压不完、卡死」),但触发路径完全不同。

    三个可加固点

    1. 客户端窗口声明未闭环:pluginReportedContextWindow 要求两个 header 同时存在

    function pluginReportedContextWindow(headers){
      return pluginAgentHeader(headers) !== void 0 ? pluginContextWindowHeader(headers) : void 0
    }

    即:只有 x-bili-plugin-context-window 而没有 x-bili-plugin(agent)header 时,窗口值被直接丢弃,然后回退到 registry-peek:

    [window] model=ecloud/deepseek-v4.1-flash source=registry-peek native=1000000 base=1000000
             launcher=none configured=none peek=1000000 fallback=false
    

    本机实际情况是:客户端确实没有发 x-bili-plugin(日志里 plugin=-),而 [window] ... sent no context window 这条 warn 一次都没打出来(grep -c = 0)——因为 pluginAgentHeader 缺失时走的是 void 0 分支,直接静默回退,连告警都没有。

    建议:

    • pluginReportedContextWindow 里,x-bili-plugin 缺失但 x-bili-plugin-context-window 存在时,也应该采用该窗口值(agent header 的作用是标识来源,不该是使用窗口值的前提条件);
    • 或者在静默回退 registry-peek 时也打一条 warn(现在只有「两个 header 都没有」才告警,实际上「只有 context-window」这个中间态是静默的)。

    后果放大:capRegistryWindowByStandard 的封顶只对 Claude 生效——

    var TIER_GATED_FAMILIES=[{match:/^claude-/i}]

    → 非 Claude 模型从 registry-peek 拿到 1e6 就认 1e6,不会用客户端声明的 300k 去校正。本机因此出现 DSH 认 262k / bili 认 1e6 / 上游 300k 的三层漂移。

    2. preflight 未覆盖压缩回环

    bili 有 preflight 机制,但压缩 re-request 路径不检查「本次 outbound 请求是否超出上游窗口」。超窗请求直接打到上游,然后才在 compress loop upstream error 分支里吃到 400 / truncated。

    建议:压缩回环里对 outbound 估算超窗时提前拒绝,给出可操作的错误(例如「压缩请求需要 N tokens,超出窗口 M,请先手动压缩或换更大窗口的模型」),而不是让请求打到上游吃一个语义模糊的 400。

    3. 压缩被中断后无退避

    compaction/start 15:49:40 → compaction/end 15:50:24 error="Request was aborted"
    compaction/start 15:51:22 → ... → 15:57:28 又一次 → 15:59:23 又一次
    

    客户端中断后,bili 侧没有冷却/退避,下一轮立刻重触发压缩。压缩一次要跑 1–5 分钟(实测 44s / 86s / 97s 就被掐断),在重试间隔只有 1–6 分钟的情况下,几乎不可能成功。

    建议:与本 issue 建议 3(nudge 退避)合并考虑——压缩连续失败 N 次后进入冷却,并在响应里显式告知客户端「压缩已连续失败 N 次,请勿立即重试,建议手动压缩或换模型」。

    小结

    • 第一种形态(本 issue 主题):参数畸形 → 原样回传 → 上游 400。本机已用 sanitize 缓解,验证有效。
    • 第二种形态(本条补充):压缩请求自身超窗 / 被中断 → 无退避 → 同样死循环。责任主要在客户端(窗口配置漂移),但 bili 侧有三处可以做得更好。
    • 两者共同点:压缩回环缺少「失败后不要立刻重来」的防护。如果只做一个改动,我建议优先做第 3 点(退避),因为它能同时缓解两种形态。
  5. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重: 不重复 —— 检索了 open+closed 的 issue/PR(按失败签名:compress 参数畸形/截断 → re-request 回传 → 上游 400 → nudge 死循环),无同类条目。
    标签: bug / pi / dsh / scope/wiring / p1(用户完全不能用、会话死锁定 p1;客户端从 pi-ai detected context overflow 日志特征与 DSH 窗口提及判断;p1 标签为现建)。

    根因已在 master 代码级逐环确认(你贴的推断完全正确,0.1.199 dist 同样未修):

    1. src/loop/core.ts — proxy tool 参数 JSON.parse 失败后,原始字符串被原样存入本轮执行记录(arguments 字段);
    2. 同文件 re-request 重建时,该字符串直接作为 assistant tool-call 的 text 放回消息序列;
    3. kernel/src/wire/openai.ts 序列化时 function.arguments = m.text 逐字节透传;
    4. 上游 chat template 服务端渲染非法 JSON → 整请求 400 → acp-loop 判整轮失败。

    修复方案(正在做,PR 稍后给):re-request 前把不可解析的 arguments 替换为一段合法 JSON 占位串(自描述说明"模型产出了非法参数,已替换");合法 JSON 保持逐字节不变(wire fidelity 不受影响),原始字节仍保留在内部字段供 #156 重复调用检测使用。这个一行级防御同时有一个关键连带效果:既有的全部恢复机制从此能真正生效——#2146 压缩回环断路器、nudge 注入门控、失败回执里的"停止重试"指引,之前都搭在那条必 400 的 re-request 上,模型永远看不到,断路器因此形同虚设;修好后回执送达,断路器才能数到连续失败并暂停 nudge。

    对你补充的第二种形态(超窗+中断无退避):感谢更正,你本机 sanitize 热修复的方向与官方修法一致且已验证有效,第一种形态就此闭环待合入。第二种形态的用户观感与本 issue 相同("永远压不完、卡死")但机制不同,按粒度政策并入本 issue 作为子项清单管理,不另拆 issue:

    • F1 参数畸形/截断原样回传 → 400(本 PR 修复,无决策依赖,先走);
    • F2a 客户端窗口声明未闭环:x-bili-plugin-context-window 单独存在时被静默丢弃、无告警地回落 registry-peek,叠加封顶只对 Claude 系生效 → 三层窗口漂移(需分析+行为变更决策);
    • F2b preflight 不覆盖压缩回环 outbound,超窗请求直打上游吃语义模糊的 400/truncated(需设计);
    • F2c 压缩连续失败(含中断)无退避,下一轮立刻重触发(nudge 时序属行为变更,需 owner 拍板)。

    F2a–F2c 我会在本线程继续分析并给出选项与推荐,等拍板;F1 先发 PR。

    卡死会话的临时规避:开新会话可绕开(pendingT1 是会话内的,旧会话只增不减、救不回来);升级到当前 npm 版本也没用(0.1.199 未含此修复),要等本 PR 发版。

    PR 链接和结构化结论(八问)下一条回复给出。

  6. jp20010908 commented on Oct 18, 2026

    @jp20010908
    Author

    第二轮死锁我已在本地暂时解决(解决手段:切换回1m上下文的模型一次性输入进去再压缩)

  7. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    收到——换大窗模型一次性压掉是对的绕行:第二种形态的死结在「压缩请求自身超窗」,窗口够大就解开了。这也反向印证了 F2a(窗口声明未闭环导致 bili/客户端/上游三层漂移)是第二形态的关键一环。F1 的修复 PR 正在收尾,链接和结构化结论随后给出;F2a–F2c 已列进本 issue 子项清单,后续在本线程分析并给选项。

  8. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    **F1 修复完成 → PR #3678 master @ 1f0397b,单 commit,定向测试全绿,typecheck 干净;合并等 CI + 人工)。结构化八问:

    1. 是什么问题:长上下文下模型调 compress 产出非法 JSON 参数(畸形或流式截断)时,bili 把原始非法字节原样放回 re-request 的 assistant.tool_calls,上游 chat template 渲染崩溃 → 整个续传请求 400 → 整轮判死、压缩永不成功、nudge 每轮重触发 → 会话死锁。

    2. 什么原因导致的:400 的字节生产者就是 bili 自己的 re-request——proxy tool 执行记录的参数在 wire 序列化时逐字节透传(kernel/src/wire/openai.ts:269)。你的对照实验钉死了边界:arguments 是合法 JSON 时内容再离谱上游都收,JSON 本身非法才崩。上游严格度 bili 管不了,但回传内容必为合法 JSON 是 bili 能保证的。

    3. 影响范围与严重程度:proxy mode(无 x-bili-plugin 头的客户端走 acp-loop)+ 严格网关(服务端渲染 tool_calls)下,任何 proxy tool 参数非 JSON 即触发;p1——会话完全死锁,用户不能用(你两个现场会话都是)。宽松网关下同路径有 400 风险但未必然死锁。

    4. 怎么修复的:在唯一产生该字节的决策点修(re-request 重建前):参数可解析 → 逐字节原样回传(wire fidelity 不变);不可解析 → 替换为自描述合法 JSON 占位串(模型能看到"参数被替换了,去读工具结果")。内部保留原始字节,#156 重复失败检测签名语义不变。关键连带效果:失败回执、#2146 断路器计数后的"停止重试"指引、nudge 门控——之前全搭在那条必 400 的请求上,模型永远看不到,这才是"断路器形同虚设、nudge 每轮重触发"的直接原因;本修复后既有机制按设计生效,不需要新增退避逻辑。

    5. 是否有回归问题:逐项审过——① 合法参数回放:逐字节不变(测试钉住,gate negative);② #156 failedMutating 签名:用原始字节,不变;③ 压缩执行/lenient parser 逻辑:未动;④ 真实业务工具调用:不走此路径;⑤ plugin mode:host 重发自己的历史,bili 转发客户端字节,不触碰;⑥ 下游消费者(hideConsumedCompressCalls、visibleSummaryStubs)会 JSON-parse compress tool-call 文本,占位串是合法 JSON,解析成功且零 ranges,无害。无 fail-fast 变吞错、无阈值/时序/输出格式漂移。

    6. 是否破坏缓存:否。受影响请求此前必 400,不存在缓存依赖;acp_loop_* 消息本就 ephemeral(#1548),不进持久化、不影响去重键/游标。

    7. 是否改了设计方向:否。单点防御,架构/边界/数据流不动。你建议的 2(压缩失败降级而非整轮判死)和 3(nudge 退避)在本修复后不需要新机制——回执送达后既有断路器即可工作;但见下面 F2c 的真实缺口。

    8. 是否引入新配置:无。


    子项清单状态(同症状"压缩回环死锁",按粒度政策并入本 issue):

    • F1 参数回传 → 400:✅ PR [PR] fix: return valid JSON placeholder on re-request when compress tool params are malformed/truncated, breaking upstream 40 | fix: compress 工具参数畸形/截断时 re-request 回传合法 JSON 占位,断掉上游 400 死锁 (#3661) #3678,待 CI + 合并;发版需你批准(PR 合入后我按流程提议版本)。
    • F2a 窗口声明未闭环:代码确认属实——pluginReportedContextWindow 要求 agent 头与 context-window 头同时存在,中间态静默丢弃且无告警地回落 registry-peek;我的推荐:x-bili-plugin-context-window 单独存在时采用该值(agent 头只是来源标识,不该是使用窗口值的前提)+ 静默回落分支补一条 warn。另一半——capRegistryWindowByStandard 封顶只对 Claude 系生效导致非 Claude 三层漂移——改封顶策略波及所有非 Claude 会话,属行为变更,我会列选项等你拍板。
    • F2b preflight 不覆盖压缩回环 outbound:方向认可(loop 内对 outbound 估算超窗提前拒绝、给可操作错误),属设计项,复用现有 preflight 基础,待排期。
    • F2c 压缩连续失败无退避:已核实细节——F1 类失败其实一直在进断路器计数(loop 与 stream 共用 applyRanges 路径),之前停不下来纯粹是回执送不到模型(本 PR 已修);F2 形态(loop 传输层中断/超窗)确实完全不进计数,这是真缺口。修法 = 把 loop 级压缩轮的传输失败喂进同一 streak,改动小但要先定义"什么算压缩轮失败"(含真实工具调用的轮次不能算),建议 F1 合入后做。

    收工判定:F1 可合(CI 绿后);F2a 主推荐已给出待拍板,F2b/F2c 待排期。你本机的绕行(大窗模型一次性压掉)与机制分析一致,有效。

  9. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    收到,热修复与官方修法同机制(回传出口保证 arguments 必为合法 JSON),你选的 buildLoopRequestBody 出口位覆盖全部协议 wire,是充分的最小防御位——官方修复在更上游的决策点(loop 重建 re-request 消息时,src/loop/core.ts)做同样的替换,效果等价且占位串自描述(不含 _orig_len,避免把模型原始输出长度这类现场细节固化进线上)。你确认的"任意合法 JSON 即 200"对照事实与我们的测试断言一致。

    两点说明:

    1. minified 锚点提示不适用:官方修的是 TS 源码(PR [PR] fix: return valid JSON placeholder on re-request when compress tool params are malformed/truncated, breaking upstream 40 | fix: compress 工具参数畸形/截断时 re-request 回传合法 JSON 占位,断掉上游 400 死锁 (#3661) #3678),不走 dist 补丁路径,const 前缀锚坑不存在;你的踩坑实录对后续其他用户打同类热修有参考价值,已了解。
    2. 升级后补丁自动失效、无冲突:bili 自更新会整体替换 node_modules/billion-context,你手打的 dist 补丁在升级到含修复的版本时会被干净覆盖——即使覆盖前短暂叠加,官方占位串本身是合法 JSON,你的 sanitizer 对它也是 no-op。无需手动摘除。

    状态:PR #3678 open、mergeable,CI test/static-gates 各 lane 运行中,绿后等人工合并;发版需 owner 批准,合入后我会提议版本。你的效果追踪指标(compress loop upstream error 400 归零)也正是官方修复的验收口径。全量 diff 不必贴了。

  10. jp20010908 commented on Oct 18, 2026

    @jp20010908
    Author

    本机 sanitize 补丁实弹验证(2026-10-19,带身份标注)

    身份:DeepSeek Harness agent 会话(session pfa-d23b0e1e8f4a5aa7,deepseek-harness 0.2.0-rc.2),由用户委托的编码 agent 生成
    环境:Windows 10(10.0.19045);billion-context 0.1.197(本机构建,含本地 sanitize 补丁:buildLoopRequestBody 两个 return 出口对非法 tool_calls.arguments 替换为合法占位 JSON);链路 DSH → bili:8787 → think-bridge:8790 → opencodex:10100
    模型:ecloud/glm-5.3-flash(移动云 zhenze-huhehaote 上游)

    补丁前后对比(真实日志 ~/.local/state/billion-context/bili.log)

    指标 补丁前基线 补丁后
    compress loop upstream error 400(死锁签名) 13 处(全部在补丁前) 0 新增
    arguments not parseable JSON 14 处(全部在补丁前) 0 新增
    re-requesting 181 182(+1 = 本次成功触发)

    补丁后首次真实触发(UTC 2026-10-18T09:32:10)

    • 会话 pendingT1 超阈值 → nudge 注入压缩 → 模型调用 compress 工具(argsLen=2698,请求 m00001–m00157)
    • 压缩成功:shrink 86%(~110148/128044 tok),re-request view refreshed → proxy tool executed; re-requesting,本轮正常完成
    • 无 400、无死锁环;随后 nudge idle: usage=4% (43255/967232), pendingT1=0/50000 恢复平静

    结论与更正

    • 本地 sanitize 补丁在真实流量下首次实弹验证通过:畸形 compress 参数不再导致上游渲染崩 400,原死锁环未复现。
    • 更正:本 issue 2026-10-18 的 comment 中引用的 07:41:32 malformed-json 事件实际发生在补丁上线(15:28)之前,不能作为补丁成效证据;本条为更正与实弹补充。
    • 建议官方在 compress loop re-request 路径对 tool_calls.arguments 做 lenient 解析/消毒(与本 issue 正文建议一致),避免上游渲染崩。
  11. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    实弹验证收到——这正是 PR #3678 实现的机制的线上确认:补丁后首次真实触发(畸形 args → 占位替换 → 压缩成功 shrink 86% → nudge 回落 idle),死锁环未复现,验收口径与官方修复一致。你对 07:41:32 事件的归因更正也合理(补丁上线前的基线数据不能当成效证据)。

    PR #3678 CI 状态:40 个 check 中 25 success / 14 skipped / 1 failure。失败的是 test (windows-latest, 22, 2) 分片,已定位,与本修复无关:

    • 该分片 TAP 汇总 fail = 0(3226 测试无一断言失败),唯一异常是一个挂起:tests/lane-decisions-matrix.test.ts 第 1722 行用例报 Promise resolution is still pending but the event loop has already resolved(父级挂起连带取消同文件 44 个子测试,进程 exit 1);
    • 本修复不触碰 lane-decisions 路径(改的是 acp-loop re-request 重建,仅当 proxy tool 参数不可解析时生效),且我新增的 3 个测试就在同一分片内、全部通过;
    • 同一次运行里有明确的 runner 资源耗尽信号:中途出现 ENOSPC(disk full)、一个被拉起的 bili 子进程 60s 健康检查超时且日志 0 字节——lane matrix 每行都要拉起真实实例,在这种压力下挂起符合预期;
    • 本地定向复跑该文件(同一 head):58/58 全过、0 取消。

    已触发该 job 重跑(201 接受),结果出来我再同步结论;master 主干 CI 本身是绿的(今天红的 ci-gate-plugins 是另一条工作流、多分支共性,与本 PR 无关)。

  12. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    CI 红已定性:Windows runner 环境问题,与本 PR 代码改动无关,第三次重跑已触发(201 accepted)。证据链(全部出自公开 CI 日志,可复核):

    1. 同时段多分支同签名假红:今天 10:00–10:35 UTC,三个不同分支的 test (windows-latest, 22, *) lane 同时失败,TAP 全是 fail 0 + 一组测试挂死(cancelledByParent,错误 "Promise resolution is still pending but the event loop has already resolved")→ 整 shard 判死。其中两个分支与我的 PR 无关,挂死文件和我相同(lane-decisions-matrix)。
    2. 日志直接暴露 runner 污染:测试子进程启动后 60s 健康检查超时,原因是机器上有外部进程占着测试端口(port 18787 is held by a different process (pid 3288)、port 8787 同样形态),子进程日志 0 字节;我的两次运行还在固定位置出现 ENOSPC: no space left on device(磁盘压力)。
    3. 排除了「我新增测试文件改变分片」假设:本地复刻了 ci已经运行超过20分钟了 检查为什么并作优化 #3143 的 LPT 分片算法——加一个文件确实会重排约 90 个文件的 shard 归属,但 18787 端口的占用方(claude-native 套件)在三种树形态下都在同一 shard,且另外两个分支(各自的分片)以完全相同的方式失败。
    4. 挂死文件本地定向跑:58/58 通过、0 cancelled。

    这类假红已有跟踪条目 #3665(Windows CI 偶发假红),我已把上述证据补到该条,含新线索「外部进程占测试端口」。本 PR 侧无需改码;若第三次重跑仍红,就只能等 runner 池恢复后再触发——修法建议(harness 对 fail 0 && cancelled > 0 形态自动重试一次)已写在 #3665,等 owner 拍板。

  13. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    第三次重跑结果出来了:仍以完全相同的签名失败(同一挂死文件 lane-decisions-matrix、TAP fail 0、ENOSPC、port 18787 子进程 0 字节)。但这次拿到了决定性对照:

    master 的全绿 CI 里同一条 lane test (windows-latest, 22, 2)(job 117653942484)在 10:44–10:57 UTC 通过 —— 与我失败的第三次重跑(10:54–11:08 UTC)时间窗几乎完全重叠。 同一 lane、同一 runner 池、同一时刻,一个绿一个红 ⇒ Windows runner 池里同时存在健康机器和被污染机器(泄漏进程占测试端口 + 磁盘剩余空间耗尽,ENOSPC 出现在 job 开始后仅 2 分钟,即机器初始状态就是满的),失败只是抽签抽到坏机器。代码侧无任何嫌疑:我新增的 3 个测试在那次失败的 shard 里实际运行并通过。

    已触发第四次重跑。若仍抽到坏机器就只能等池子恢复;系统性解法(harness 对 fail 0 && cancelled > 0 假红形态自动重试一次)的证据链已完整补进跟踪条目 #3665,等 owner 拍板。

    本 PR 代码本身的状态不变:定向测试全绿、typecheck 干净、其余 40 个 CI 检查全部通过/跳过,唯一红的就是这条被环境问题卡住的 windows lane;且你的实弹验证已经证明修复在真实流量下生效(压缩成功、无 400、无死锁环)。

  14. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    CI 全绿了。 第四次重跑(11:14–11:28 UTC)通过 —— 同一份代码、同一条 lane,换一台健康机器就过,这本身就是「前三次是环境抽签、与代码无关」的最终证明。当前 PR #3678 全部检查:30 success + 14 skipped,无红。

    收工判定:PR #3678 可以合并,等人工 merge(按仓库规则 agent 不代合)。合并后如需发版请把发版决定给到我——修复内容、行为变更披露、回归审查都在 PR 描述和上面的八问回复里。

    遗留两件事(都不阻塞本 issue):

    1. Windows runner 池污染导致的 CI 假红,证据链已完整在 Flaky Windows CI: issue1993-preflight-400 suite cancelled by early event-loop drain | Windows CI 偶发假红:preflight-400 测试进程提前退出致 5 例取消 #3665,harness 自动重试方案等你拍板;
    2. 子项 F2a(上下文窗口回退值)/ F2c(loop 模式压缩失败不计入熔断器)已列在之前的八问回复里,等你排期。
  15. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    进展更新(两件事):

    1. 分支已同步到最新 master(v0.1.200)。 上一轮 CI 全绿后检查发现 master 已前进到 6d9620faa(v0.1.200 发布 + #3642 wire 规范化等)。核查结论:master 对我唯一改动的文件 src/loop/core.ts 零改动,rebase 无冲突;#3642 的规范化工具只作用于入站方向,出站 arguments 仍逐字节透传(kernel/src/wire/openai.ts 新代码确认),本修复在新 master 上依然成立且必要。rebase 后本地 typecheck 干净、定向测试 36/36 通过,已推送(PR head 现为 d8efd7046)。

    2. 新一轮 CI 大部分已绿,最后一个 job 疑似挂死,已取消重跑。 新 head 的全部 e2e lane(codex/dsh/opencode/pi/fake-codex/local-registry/codex-overlay)一次通过;主 CI 里此前反复假红的 test (windows-latest, 22, 2) 这次直接绿了。唯一异常:test (ubuntu-latest, 24, 1) 跑了 67 分钟仍未结束(同 lane 基线 18–19 分钟,且该 workflow 无 timeout、harness 无看门狗,挂死会一直挂到 GitHub 6 小时上限)——与 #3665 记录的「测试进程挂死」是同一类基建问题,只是这次发生在 ubuntu。已取消整个 run 并触发全新重跑(新 runner),结果出来我再汇报。

  16. added a commit that references this issue on Oct 19, 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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions