Skip to content

[PR] Fix #4178: Space-separated citation strings trigger catastrophic backtracking (28 citations take 11 s; upstream streamin | 修复 #4178:空格分隔的引用串触发灾难性回溯(28 个引用 11 秒,上游流式一卡一卡) - #4180

Closed
ranxianglei wants to merge 2 commits into
masterfrom
fix/4178-refs-run-backtracking

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

这是啥问题(#4178)

模型在输出里复读一串空格分隔的内核引用(m1111 m1111 m1111 …)时,流式过滤器一个 push 就会指数爆炸:28 个引用 11 秒,34 个约 1.2 小时——期间这条流式管线完全卡死。上游只要这么回声一次,后面的 token 全部排队等待。逗号分隔的同样内容只要 2ms,所以只在「空格 + 后续匹配失败」这个组合上爆炸。

根本原因

src/loop/tag-echo-filter.ts 的 REFS_RUN 分隔符写成了 \\s*[–—,-]?\\s*——两端的 \\s* 都能吞同一段空格,同一段文本有 2^N 种切法。后面的分支(ORPHAN_REF_CLOSE/PARTIAL_TAIL)一旦匹配失败,引擎就回放所有切法。经典嵌套量词灾难性回溯。

如何解决

改成首字符互斥的等价写法 \\s*(?:[–—,-]\\s*)?(先吞前导空格,再可选一个连接符+尾空格)——同一跨度只有一种解析。语言完全等价:4352 个形状的等价性对比 0 差异。修复后 200 个空格引用 = 0ms。REFS_RUN 被 6 个正则共享(ORPHAN_REF_CLOSE / PARTIAL_TAIL / ORPHAN_RUN_BEFORE / REFS_ONLY / PAIRED / PENDING_REF_SPAN),一处修完全部受益。

验证

  • 新增 tests/issue4178-refs-run-backtracking.test.ts:40/200 个空格引用 + 失败尾巴形状,计时 < 1s(回归时会到分钟级)
  • 等价性 corpus(4352 形状)0 差异;BEX 语法扫描 18278 序列,已知家族以外无新偏差
  • 全量 verif 套件 + tag-echo 相关 + SSE CRLF 跨块:85/85 通过
  • tsc 干净

来源

verif 工具链 BEX 穷举发现(issue #4178,标签 verif工具发现)。

Closes #4178

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: fix/4178-refs-run-backtracking (2e0fd7c)

Option A — Install this exact build (recommended)

npm install -g [email protected]

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-pr4180.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title 修复 #4178:空格分隔的引用串触发灾难性回溯(28 个引用 11 秒,上游流式一卡一卡) [PR] Fix #4178: Space-separated citation strings trigger catastrophic backtracking (28 citations take 11 s; upstream streamin | 修复 #4178:空格分隔的引用串触发灾难性回溯(28 个引用 11 秒,上游流式一卡一卡) Oct 21, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复。 检索 open+closed issue/PR(按失败签名:REFS_RUN / 灾难性回溯 / tag-echo 流式卡死),无其他在途修复或立项——本 PR 是 #4178 的唯一修复;#3198(determinism 语料长度)、#2023(orphan 引用形状)是 #4178 引用的背景条目,机制不同,不构成重复。

开始独立审核(不复读 issue 结论):

  1. 方向核对:PR 修的是根因(正则分隔符歧义本身),不是症状层拦截——方向成立。issue tag-echo filter: space-separated long reference strings trigger catastrophic regex backtracking; 30 references block the | tag-echo 过滤器:空格分隔的长引用串触发正则灾难性回溯,30 个引用阻塞流式管线约分钟级(DoS) #4178 给的三个候选修法里,本 PR 选了「消除歧义改写」,与我的判断一致(另两个:前瞻占有式写法可读性差、\s{2,} 归一化有语义风险)。
  2. 等价性我自己推了一遍:旧 \s*[J]?\s* 与新 \s*(?:[J]\s*)? 语言相同(空白段,可选夹一个连接符);新写法里每个 gap 的解析由「下一字符是不是连接符」唯一决定,外层迭代以 ref token 为锚点,回溯代价从 2^N 降为 O(N)。锚定模式(^…$)下匹配跨度也逐处核对过一致。
  3. 发现同类残留:PENDING_REF_SPAN(:121)和 PARTIAL_TAIL 第 5 臂(:188)各有一处内联的旧式歧义分隔符 (?:\s*[J]?\s*(?:m\d{0,3})?)?。这两处是单层可选组、非迭代,最坏 O(k) 线性,不构成 DoS,但属同一缺陷家族——按「发现一处代表一类」,我会在 PR 分支上补一个标注 commit 把它们对齐成无歧义形式(语言等价,已逐处核过),并给新测试加一个悬挂连接符+部分 ref 的用例钉住尾臂路径。
  4. 分支落后 master 10 个提交(mergeable_state=behind),master 侧没碰过 src/loop/tag-echo-filter.ts,rebase 应干净。正在 rebase → 本地 typecheck/build/定向测试 → push → 看 CI。

结论稍后按八问给。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

结论先行:修复正确,独立复核通过;本 PR 自身的全部 CI 车道绿。当前 6 个红 shard 是 master 自身的回归(与本 PR 零交集),已单独立项 #4196。master 转绿后即可合并(若期间 master 再动,rebase 应无冲突——本 PR 只碰 src/loop/tag-echo-filter.ts 与一个新增测试文件)。

八问:

  1. 是什么问题:模型回声一串空格分隔的内核引用(m1111 m1111 …)时,流式 tag-echo 过滤器的正则指数爆炸——28 个引用卡 11 秒、34 个约 1.2 小时,期间整条 SSE 管线排队卡死;逗号分隔的同样内容只要 2ms。
  2. 什么原因导致的:REFS_RUN 的分隔符写成 \s*[–—,-]?\s*——两端的 \s* 都能吞同一段空格,N 段空格有 2^N 种切法;后续分支(ORPHAN_REF_CLOSE / PARTIAL_TAIL)一旦失配,引擎回放所有切法。经典嵌套量词灾难性回溯,根因在分隔符写法本身,不在调用方。
  3. 影响范围与严重程度:条件触发但后果是管线级卡死——上游模型复读 ≥22 个空格引用即命中(实测 22 个已 227ms~1.1s,之后近似每 +2 个 ×4),命中一次该会话后续 token 全部排队。属可用性缺陷(特定条件下核心流式能力停摆),p1 级。
  4. 怎么修复的:把分隔符改成首字符互斥的等价写法 \s*(?:[–—,-]\s*)?——每段空格的解析由「下一个字符是不是连接符」唯一决定,回溯成本从 2^N 降到 O(N)。语言严格等价,我独立验证过:30 万随机串差分 fuzz(run 模式 + PENDING_REF_SPAN + PARTIAL_TAIL 锚定模式逐位比对,90 万次比较 0 差异),所以这是纯性能修复、无语义变化。另把同文件两处内联残留的同族歧义写法(PENDING_REF_SPAN / PARTIAL_TAIL 尾部分隔符——单可选组、最坏 O(k) 线性、不构成 DoS)一并对齐为同一形式,并补了 2 个钉住该路径的测试用例(独立标注 commit 62252d246)。
  5. 是否有回归问题:无。既有行为审计清单——逗号/空格/连接符混排的引用识别、孤儿引用剥离、跨块 pending 语义、wire 形状:全部保持(正则语言不变)。定向套件 91/91 通过(issue4178 新测试、tag-echo 主套件、chunking-determinism、SSE CRLF 跨块、e2e-tag-echo);tsc/build 干净。CI 上本 head 除 6 个继承自 master 的红 shard 外全绿(static-gates/build-artifact/mysql-contract/gate*/docs/e2e 等);那 6 个 shard 的 15 个失败测试与 master 自身逐条同名(窗口/preflight/models.dev 域,与 tag-echo 零交集),且本 PR 早期 head 在旧 base 上是全绿的(runs 39622431771 / 39622382350)——红来自 master 新合入(二分边界 ea9a1ab 绿 → fe5ba59 红),已立项 #4196,含完整证据、嫌疑排序([PR] Reserve output headroom on every wire (drop the Anthropic exemption) #4184 headroom 居首)与建议修法。
  6. 是否破坏缓存:不涉及——不碰消息身份、ref/账本、wire 形状;过滤只作用于模型 prose 通道的出站改写,前后字节语义不变(仅消除歧义解析)。
  7. 是否改了设计方向:不涉及。
  8. 是否引入了新的配置:不涉及。

行为变更披露:无用户可见行为变化——正则语言旧值→新值完全相同,唯一变化是匹配耗时从指数降为线性。

收工判定:代码侧收工;等 #4196 把 master 拉绿后即可合并。

ework-agent and others added 2 commits October 22, 2026 02:07
…racking

The refs-run separator \s*[–—,-]?\s* admits two parses of the same
whitespace span; when a later alternative fails (ORPHAN_REF_CLOSE,
PARTIAL_TAIL), the engine replays 2^N partitions over an N-ref run.
One push of 28 space-joined refs took 11s (34 ≈ 1.2h); comma-joined
and whole-text stripAcpTags were unaffected.

Rewrite as \s*(?:[–—,-]\s*)? — same language (4352-shape equivalence
corpus, 0 diffs), exactly one parse per span. After: 200 space-joined
refs = 0ms. REFS_RUN is shared by ORPHAN_REF_CLOSE / PARTIAL_TAIL /
ORPHAN_RUN_BEFORE / REFS_ONLY / PAIRED / PENDING_REF_SPAN, so all six
callers get the fix.

Found by BEX bounded-exhaustive sweep (label verif工具发现, issue #4178).
Adds a linear-cost timing pin (40/200 refs < 1s; regression = minutes).
Review follow-up on the #4178 fix (labeled, same mechanism family):
PENDING_REF_SPAN and PARTIAL_TAIL arm 5 each carried their own inline
copy of the old ambiguous separator idiom (\s*[joiner]?\s* before the
partial-ref tail). They are single optional groups, not iterations, so
their worst case is linear in trailing whitespace — NOT the exponential
DoS of REFS_RUN — but they are the same defect family and the same trap
for whoever next nests them. Rewritten to the one-parse-per-span form
(\s*(?:[joiner]\s*)?), language-equivalent per site.

Verified: 300k-string differential fuzz of old vs new forms over the
run pattern (leftmost match span from every m-anchor) plus the full
PENDING_REF_SPAN / PARTIAL_TAIL-tail anchored patterns — 0 divergences;
tag-echo suites (tag-echo, chunking-determinism, SSE CRLF cross-chunk,
e2e-tag-echo) + new timing test: 91/91 pass.
@ranxianglei
ranxianglei force-pushed the fix/4178-refs-run-backtracking branch from 62252d2 to 2e0fd7c Compare October 21, 2026 18:09
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 修复完成:PR 已 rebase 到绿色 master,全量 CI 通过。 新 head 2e0fd7c71(原 62252d246),run 39650228364 completed success,21 个 job 全绿。以下是结构化结论。

结构化八问

1. 是什么问题:本 PR 的 CI 有 6 个 test 分片红,但逐条核对失败清单后确认:没有任何一条失败与这个 PR 的代码改动(tag-echo 正则)相关——全部落在 window/preflight/models.dev 域(models.dev 查询、codex UA clamp、#1339 beta-1M、sideRequestGuard #554、storage-render golden、nudge 分母等)。红是分支基座太旧拖进来的 master 侧存量问题,不是本 PR 引入的。

2. 什么原因导致的(两层):

3. 影响范围与严重程度:对本 PR——无用户面影响,代码本身此前已被独立审核验证正确(floor 3-4 的 differential fuzz 0 差异),红的只是合并门禁。对 master——win22-1 这个 drain 让 master 自身当前是红的:master 在同一 head c87185a05 上的 CI(run 39646627288,job 119384000242)以完全相同的签名失败(同样的两个 #4079 测试,cancelled=8)。且这两个测试在旧基座 fe5ba5912 的 windows 上是通过的(ok 1841/1842 @ win22-2),说明挂起出现在 fe5ba5912..c87185a05 区间内——该区间唯一的语义变化就是 revert a99c304(恢复 shouldReserveOutputHeadroom(protocol) => protocol !== 'anthropic' 的 Anthropic 豁免),而这恰好在 anthropic wire 上改变了有效窗口档位,这两个 e2e 正好跑在 /v1/messages 上。是确定性挂起还是 runner 状态,需 owner 定夺后定向排查(我在 Linux 本地无法复现 Windows-only 行为)。

4. 怎么修复的:不改任何代码。① rebase 到绿色 master(机制:让分支吃到 #4198 的修复),force-with-lease 推送;② 对残留 drain 使用 CI 系统为 #3665 家族预设的处置——新 runner 重跑失败 job。

5. 是否有回归问题:无。本会话未改任何行为:rebase 前后 PR diff 逐字节相同(已用 git diff --stat 对 master 复核);tag-echo 路径的正则语言等价性由前序独立审核钉住;其余路径零触碰。高危模式逐项过:无语义漂移、无 fail-fast 变吞错、无阈值/时序/输出格式变化。

6. 是否破坏缓存:不涉及——未触碰缓存、重试、增量游标、去重键逻辑。

7. 是否改了设计方向:没有。

8. 是否引入了新的配置:没有。

给 owner 的两件事

  1. Master CI red: node-22 test lanes fail 15 window/preflight tests identically to master — blocks every open PR and the v0.1.204 release | master CI 红风暴:node-22 测试车道 15 个窗口/preflight 测试与 master 同因失败,阻塞全部在途 PR 与 v0.1.204 发版 #4196 可以关了:它列出的 15 条断言失败族已由 [PR] Revert "#3900: reserve output headroom on every wire (drop the Anthropic exemption)" (#4184) — unblock v0.1.204 #4198 的 revert 解决,在新 head 上不再复现;残留的 win22-1 drain 是不同症状(挂起 vs 断言)+ 不同机制(Windows-only vs 全平台),不属于它的范围。
  2. master 侧 win22-1 drain 需要立项或指示:本会话按来源链规则(tag-echo filter: space-separated long reference strings trigger catastrophic regex backtracking; 30 references block the | tag-echo 过滤器:空格分隔的长引用串触发正则灾难性回溯,30 个引用阻塞流式管线约分钟级(DoS) #4178→[PR] Fix #4178: Space-separated citation strings trigger catastrophic backtracking (28 citations take 11 s; upstream streamin | 修复 #4178:空格分隔的引用串触发灾难性回溯(28 个引用 11 秒,上游流式一卡一卡) #4180 属自动链二层)不擅自开新 issue,在此留证据供决定——若确认确定性,排查方向是 revert 恢复的 Anthropic 头空间豁免与 Context Window Overflow Issue Summary: Raw Requests Sent Directly to Upstream Rejected + Local Intercept Cooldown Lockin | 上下文超窗问题汇总:请求裸冲上游被拒 + 本地拦截冷却锁死会话(统一分析入口) #4079 egress-fuse e2e 的交互(测试期望是否需按恢复后的窗口档位更新,还是 revert 需要细化)。在修好之前,每个基于当前 master 的 PR 都会带上这一个红分片(可临时用 rerun 绕过)。

收工判定:PR #4180 CI 全绿,具备合并条件;合并仍归人。

@ranxianglei

Copy link
Copy Markdown
Owner Author

(ai: zhipuai-lb/glm-5.3)

已并入合并列车 #4525(master 6d7c587,合并方式 merge commit,提交保留原作者署名)。本 PR 关单收尾;若列车中对应提交出问题,回滚点为 #4525 单提交 revert。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

verif工具发现 由 verif 符号执行/判官或 BEX 穷举工具自动发现(工具链:verif/ 模块,PR #3853/#4071/#4103/#4108/#4112/#4151) 发版

Projects

None yet

1 participant