Skip to content

bare-content compress args are salvaged on the Anthropic lane only, nothing on the tool-execution layer | [Bug][dsh] compress 裸文本参数只在 Anthropic 线能救:第三方 provider 插件链路无中间层可救,整轮仍中止 #4889

Open

Description

@koca2005

环境

  • billion-context 0.1.208(dsh 插件,profile desktop,入口 dist/agent/dsh-native.js)
  • DSH Desktop(Windows);宿主对每个 tool call 的参数只做一次严格 JSON.parse
  • provider = 第三方 provider 插件(本地 OpenAI 兼容网关 dsh-providers-hub / Jet Hub)→ buddy 渠道,模型 deepseek-v4.1-flash;本轮 84 个工具,compress 在列
  • 车道:不是 Anthropic/OpenAI 反代。上游 HTTP 由 provider 插件在进程内产出,bili 只是工具注册方,不接触原始 SSE 字节

现象:同一形态第 3 次,跨 3 天 / 3 个不同会话,每次都是整轮中止

# 时间 (UTC+8) 会话 tool_call id 参数 宿主侧报错
A 2026-10-24 10:35 session-b61246db — 4865 字符 被误报成 max-tokens(「回答被截断」)
B 2026-10-25 14:25 session-e2199832 — 4757 字符 同上
C 2026-10-26 23:41 session-c8c2f77c call_00_XpmeGJOhuu3yDxKVZZ0i4904 5713 字符 provider 插件侧已修正为 TOOL_ARGS_INVALID(不再谎报 token 上限)

A/B 的误报已在 provider 插件侧修掉,本条要的是参数本身别再写坏 / 写坏了能救回来。

取证(C,逐字)

{"content": m00186–m00289 正式程序编写与构建(阶段二)\nTASK AS OF THIS BLOCK(历史):探针全部结论已定,正在写正式程序并首次编译…
…(正文 5600+ 字符,真实换行)…
…⑤端到端验证(待做)⑥打包分发(待做)。\n}
  • "content": 之后直接跟裸文本、没有开引号,末尾却补了 } ⇒ 结构闭合、括号配平,不是流被掐断
  • 统计:42 个真实换行、0 个 \"、70 个裸 " ⇒ 整个值从未进入「字符串模式」,不是转发链路丢了某个字节
  • 同会话、同工具的 3 次成功调用对照:0 个真实换行,\n 与 \" 齐全(其中一次 65 个 \")⇒ 链路正常
  • 全库统计(202 会话 / 8418 次成功工具调用):compress 78 成功 / 1 失败;另有 3 次失败是 pwsh 的 Unterminated string(真截断,属另一类)
  • 体量不是判别因素:成功样本最长 13738 字符(> 失败的 5713),与 [Bug] compress 工具描述推荐「ONE plain string … no JSON escaping」,模型照做即写出非法 JSON,宿主整轮 MALFORMED_RESPONSE 中止 #2580 的「成功最大 14554」一致

与 #2580 的一处差异,值得单独记一笔:本次不在 nudge 之后。整个会话里 Compressible ranges / ACP TAGS 出现 0 次,这是第 90 步、上下文已经很长、模型自己决定折叠时写坏的 ⇒ 除了「nudge 窗口」之外还有第二个触发面:模型在写一大段摘要时直接切到「写文档」模式,忘了自己还在填 JSON。

根因 1:文案首选形态仍是「ONE plain string」(0.1.208 实抓)

content = ONE plain string holding ALL ranges — each block starts with its 'm00150–m00220 optional topic'
header line followed by that block's summary (a valid JSON string — internal quotes \", newlines \n; a
JSON-encoded array as that string is also accepted for gateways that stringify arrays).

相比 0.1.189 的 no JSON structure, no escaping(#2580 已修),0.1.208 已经补了「必须是合法 JSON 字符串」的括号说明,但首选仍然是被字符串化的 line-form。而 #2580 自己的数据就是「对象数组 12/12 成功、line-form 3/19 致命」。建议 PREFERRED 收敛到 content: [{startId, endId, summary, topic?}],字符串形态降级为「兼容」而不是「首选」。

根因 2:salvageCompressArgs 只挂在 Anthropic SSE 改写器上(0.1.208 dist 实测)

  • BARE_CONTENT_RE = /^\s*\{\s*["']content["']\s*:\s*(?!["\[{])([\s\S]*)$/、REF_TOKEN_RE = /\bm\d{1,7}\b/i、canonicalize() + salvageCompressArgs():定义在 dist/index.js @3896913/3897268
  • salvageCompressArgs 全库只有 3 处引用:定义 + 2 个调用点 @4026896 / 4027522,两处都在 content_block_start / content_block_delta / content_block_stop / message_stop 分支里;heldCompressBlocks 的 8 处引用全部落在同一 cluster 4015212–4028171
  • ⇒ 规则本身完全命中本次坏样本(BARE_CONTENT_RE 捕获 m00186–m00289 …,含 ref token,wrap 成 {"content":"…"} 后 parseCompressArgs 能解析出 1 个区间)。只要这段代码能跑到,这一次就不会失败。
  • 但它覆盖不到下面三种链路:
    1. OpenAI 线(choices[].delta.tool_calls[].function.arguments)——没有等价的「持有到 stop 再改写」;
    2. Responses 线;
    3. DSH 原生 + 第三方 provider 插件(本部署)—— bili 根本不在 HTTP 中间层,原始字节在 provider 插件手里,宿主的严格 JSON.parse 在工具执行之前 ⇒ 这段救援在本部署里是死代码,与 [Bug] compress 工具描述推荐「ONE plain string … no JSON escaping」,模型照做即写出非法 JSON,宿主整轮 MALFORMED_RESPONSE 中止 #2580 里「抢救阶梯是死代码」同族,只是换了车道。

建议(按性价比)

  1. 救援下沉到工具执行层(与线无关):parseCompressInput / executeCompress 拿到 args 时,若 content 是裸文本(BARE_CONTENT_RE 命中且含 ref token)→ JSON.stringify({content: bare}) 再走 parseCompressArgs。这一条同时覆盖 Anthropic / OpenAI / Responses / 原生宿主。
  2. 给 provider 插件一个复用入口:salvageCompressArgs 目前是 kernel 私有(dist/index.js 内部符号,未从包导出),我们只能自己照抄一份正则。建议从 kernel 导出(或提供 repairToolArguments(name, raw)),让 provider 插件在产出 tool_call 之前调用同一份实现 —— 我们是唯一比宿主更早拿到完整参数字节的一层。
  3. 文案:PREFERRED → 对象数组;字符串形态降级为兼容说明。
  4. 宿主硬失败:DSH 对 tool args 只 JSON.parse 一次,失败即 MALFORMED_RESPONSE / 整轮中止([Bug] compress 工具描述推荐「ONE plain string … no JSON escaping」,模型照做即写出非法 JSON,宿主整轮 MALFORMED_RESPONSE 中止 #2580 已记录)。只要第 1 或第 2 条不做,用户可见代价仍然是「整轮工作丢失」,而不是「压缩失败一次」。

为什么提给 bili 而不是 provider 插件

同一 provider、同一模型下 compress 78/79 成功,其余 8418 次工具调用里只有 3 次真截断 ⇒ 转发链路没有系统性破坏,坏的是模型这一次的输出;而 bili 是唯一知道 content 这层语义、并且已经把救援规则写好的一方。我们不希望在 gateway 里再维护一份 compress 专用的正则副本。

Activity

  1. changed the title [-][Bug][dsh] compress 裸文本参数只在 Anthropic 线能救:第三方 provider 插件链路无中间层可救,整轮仍中止 | bare-content compress args are salvaged on the Anthropic lane only, nothing on the tool-execution layer[/-] [+]bare-content compress args are salvaged on the Anthropic lane only, nothing on the tool-execution layer | [Bug][dsh] compress 裸文本参数只在 Anthropic 线能救:第三方 provider 插件链路无中间层可救,整轮仍中止[/+] on Oct 26, 2026
  2. ranxianglei commented on Oct 26, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    查重: 不重复 —— 与已关闭的 #2580(2026-10-10 DSH 整轮中止事故)同家族,但本单针对的是 #2591 落地后的残留缺口:#2591 只把抢救规则(salvageCompressArgs)接在 Anthropic 插件 relay 的 SSE 改写分支(src/plugin.ts content_block_* 两处);OpenAI/Responses 车道、proxy 模式(loop/JSON rewrite 全协议路径)、以及第三方 provider 插件链路(bili 只是工具注册方、不碰 SSE 字节)都没有任何救援层。检索了 open/closed issue 与近期 PR,没有条目覆盖「执行层下探 + 导出入口」这两个面。

    标签: bug, dsh, scope/kernel, p2(缺陷成立且可复现;仅在模型写出坏法 A {"content": <裸文本>} 时触发——插件链路用户整轮丢失、proxy 模式用户折一次失败后模型重试;非普适不可用,非缓存泄露类)

    审核结论(对当前 master 23158db 逐层核对过):

    1. 规则本身能救坏样本——BARE_CONTENT_RE 会捕获裸文本,包成 {"content":"…"} 后喂内核宽松解析器可恢复 ≥1 range。规则有效,问题纯粹是接线面太窄。
    2. 共享执行层解析器缺的正是这一步:parseCompressInput(src/compress-tool.ts:543)是所有车道 compress 参数的唯一汇点(loop/core.ts 的 rawArguments 路径 proxy 工具参数 JSON 解析失败被降级为空对象执行,原始字符串被丢弃、lenient 容错层被绕过 #1502、stream-openai/stream-responses、external-summary-compress、preflight 全都经过它),而内核阶梯对坏法 A 恢复 0 range——它的截断抢救要求字面量 "content": [,认不出裸值。症状(整轮中止/折叠失败)的机制层根因 = 救援层挂在某条车道的 wire 改写上,而不是挂在执行层。
    3. 措辞面(建议③)已有决策,本单不动:kernel 默认 compress 文案仍引导非法 JSON(#2579 的内核侧后续) #2587 走过 Kernel Boundary 审批,合入版(540f440)刻意选了最小改法——ONE plain string 保持 PREFERRED,只把 "no escaping" 类措辞换成转义提示。把 PREFERRED 翻成对象数组与该决策相抵触;若要翻,请单独拍板(那是 kernel 变更),不塞进本 PR。

    执行计划(无决策依赖部分,立即开修):

    PR 稍后给出,出来按八问回复。

  3. ranxianglei commented on Oct 27, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    **PR: #4891 #4889;单 commit 4c6d761e5,7 文件 +143/−12;无 kernel 变更、未动 version、无新依赖)

    结构化八问

    1. 是什么问题:模型把 compress 参数写成坏法 A({"content": <裸文本>,无开引号)时,只有 Anthropic 插件 relay 能救(fix(#2580): salvage invalid compress tool-call args on the plugin verbatim lane (#1518 stage 2) #2591 的 wire 改写);其余所有路径——proxy 模式四协议、第三方 provider 插件链路——都直接撞上宿主严格 JSON.parse:proxy 模式丢一次折叠加一次重试,插件链路整轮中止。
    2. 什么原因导致的:抢救规则本身(BARE_CONTENT_RE + 包装 + 内核宽松重解析)fix(#2580): salvage invalid compress tool-call args on the plugin verbatim lane (#1518 stage 2) #2591 就写好了,但只接在 Anthropic SSE 改写的一个分支上,没接进所有车道共用的执行层解析器 parseCompressInput;而内核阶梯的截断抢救要求字面量 "content": [,认不出裸值,恢复 0 range(已用本单坏样本逐字节验证:kind=malformed-json)。分层:症状=整轮中止/折叠失败;机制=接线面太窄;根因=救援层挂在某条车道的 wire 改写上而不是挂在执行层。
    3. 影响范围与严重程度:触发条件=模型写出坏法 A(本单数据:同模型 compress 78/79,偶发)。受影响面:proxy 模式非 Anthropic 车道(丢一次折叠+重试,体验降级)、DSH 原生/第三方 provider 插件链路(整轮丢失,严重但低频)。总体 p2:可用,特定条件下有明显问题。
    4. 怎么修复的:① 救援下沉到 parseCompressInput——仅当首次解析失败且形状匹配才触发,只在救到严格更多 range 时才采纳(compress: literal backslash-n in string content still fails as kind=content-not-array, hiding the real missing-summary reason #3780/compress failed may be caused by params #1494 同款纪律),覆盖全部车道;正则/候选逻辑从 salvageCompressArgs 抽成 tryBareContentWrap,两处调用共用一份实现,不复制第二份。② 新增 subpath export billion-context/salvage(repairCompressArgs(raw) → strict JSON 或 null=原样放行),provider 插件可在宿主 JSON.parse 之前调用同一实现。③ 措辞翻转不做(见第 7 问)。
    5. 是否有回归问题:逐项审过既有行为——(a) 合法 JSON:字节一致透传不变([bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 不变量,负向测试钉住);(b) 能正常解析的 line-form 字符串形态/对象数组形态:不受影响(救援仅在 kind≠ok 时触发);(c) Anthropic 插件 relay:salvageCompressArgs 重构为调用同一 helper,输入输出等价,[Bug] compress 工具描述推荐「ONE plain string … no JSON escaping」,模型照做即写出非法 JSON,宿主整轮 MALFORMED_RESPONSE 中止 #2580 全套既有用例通过;(d) 其余抢救档(compress: literal backslash-n in string content still fails as kind=content-not-array, hiding the real missing-summary reason #3780 双转义、lenient compress-arg parsing: salvage salvageable model output #603 单引号、flat-summary):顺序保持、各自独立触发。无语义漂移,无 fail-fast 变吞错(不可恢复的照旧拒绝,诊断日志保留)。
    6. 是否破坏缓存:不涉及——未触碰缓存/重试/增量游标/去重键路径;改变的只是「参数字节能否被解析」,解析成功后走完全相同的既有折叠流程。
    7. 是否改了设计方向:无方向变化,反而把救援层放回它本应在的位置(执行层),与 compress: literal backslash-n in string content still fails as kind=content-not-array, hiding the real missing-summary reason #3780 注释里「this salvage covers every lane now」的既定方向一致。措辞面(建议③)与 kernel 默认 compress 文案仍引导非法 JSON(#2579 的内核侧后续) #2587 已合入的决策(最小改法,ONE plain string 保持 PREFERRED)相抵触——刻意排除在本 PR 之外;若要翻 PREFERRED 到对象数组请单独拍板(kernel 变更,需 Kernel Boundary 审批)。
    8. 是否引入新的配置:无 env/config/CLI 变化。唯一新表面是包 subpath export billion-context/salvage(面向 provider 插件/gateway,不是终端用户配置)。

    验证证据:本地 typecheck 干净;定向测试 25/25 绿(新增 9 例含本单逐字坏样本 + 负向门 + #1039 钉;#2580/#1502 既有套件全过);全量 build + dist 门禁干净;构建产物 dist/salvage.js 冒烟通过(坏样本→strict JSON,合法→null)。全量回归以 CI(PR head)为准。

    注释说明:本次新增的三处注释(compress-tool.ts 救援步、tryBareContentWrap 头注、salvage.ts 公共入口模块注释)均为必要注释——记录跨车道救援语义、采纳纪律与对外 API 契约,风格对齐相邻的 #3780/#1494 先例注释。

    判定:待 CI 绿即可合。建议④(宿主硬失败上下文)无需 bili 侧动作——①②落地后,第三方链路的「整轮丢失」窗口由 gateway 接入导出入口关闭(gateway 侧集成不在本仓库范围)。

  4. ranxianglei commented on Oct 27, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    CI 全绿(head 13d45a5):47 项检查 —— 12/12 全量测试分片(ubuntu/windows × node 22/24)success、全部静态门禁 success(含此前红的 test-import-graph)、build/kernel-guard/version-guard 等 success;skipped 仅路径门控的 e2e 车道与 red-signal,与本 PR 无关。无失败项。

    PR #4891 可合并(合并权在你)。收工判定:开发完成、验证闭环,等 owner 合并。

  5. koca2005 commented on Oct 27, 2026

    @koca2005
    Author

    补充(报障方):gateway 侧接入确认与 ② 的实测写在 PR #4891 下 —— #4891 (comment) ,这里只留个索引,避免同一份内容贴两遍。

    摘要:

    • 我们用你们挂在 PR 里的预构建包 0.0.6349-pr.4891.a2dad66(临时目录解包,没装进 profile)把本单那条逐字坏样本(5713 字符)跑了 repairCompressArgs:返回 5895 字符严格 JSON,content 仍是字符串、区间 m00186–m00289 内部已解析 ⇒ 规则确实救得回来。
    • 负向门同样重要:同一份合法 JSON 从 30% / 60% / 95% 处截断,三次全返回 null;非 compress 的坏 JSON 也返回 null。我们 gateway 侧正是靠这个 null 把「流被截断」(继续报 max-tokens,让用户发"继续")和「模型写坏」(可救)分开,接入不会引入新回归。
    • 集成计划:只在 classify === 'invalid'(结构闭合、括号配平、JSON.parse 仍失败)且工具名 == compress 时调用;现装 0.1.208 没有 ./salvage 这个 subpath,所以会 try/catch 静默降级,不需要你们做任何兼容。
    • ③(PREFERRED 翻成对象数组)被拒的理由我们认可——那是 kernel 决策,应单独拍板。

    等 #4891 合并发版后我们接上,然后回来回报「整轮丢失是否真的归零」。

  6. ranxianglei commented on Oct 27, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    收到,谢谢实测反馈。确认一下你们依赖的两个契约点:

    1. null = 「别动」语义:已是合法 JSON、或救不回来(含你测的各种截断形态)一律返回 null,gateway 继续走既有路径(截断→max-tokens+「继续」);只有「结构闭合 + compress + 可救」才返回 strict JSON。这条边界有测试钉住([bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 合法输入字节不变 + 负向门)。
    2. ./salvage 只在 [PR] fix: salvage bad-shape-A compress args at the execution layer, all lanes (#4889) #4891 合入后的首个发版出现:你们对现装 0.1.208 的 try/catch 静默降级是对的,我们这边无需任何兼容处理。

    细节以 PR #4891 下的实测评论为准(已阅,与本线程摘要一致)。等 owner 合并发版后请按计划接入并回报结果——「整轮丢失是否归零」是这单真正的验收标准;若还有救不回来的样本,直接贴到本线程即可。

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions