Skip to content

[PR] fix(#4106): tag cleanup is now idempotent — one call reaches the fixed point - #4148

Closed
ranxianglei wants to merge 2 commits into
masterfrom
2026-10-21_fix-4106-strip-idempotency
Closed

ranxianglei wants to merge 2 commits into
masterfrom
2026-10-21_fix-4106-strip-idempotency

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #4106

What was broken

stripAcpTags was not idempotent: the DEAD_REF_CLOSE gate (out !== text) was evaluated mid-pipeline, BEFORE the marker-line / truncate / bili-artifact channels ran. So evidence produced by those later channels never counted in the same call — a forged marker line stripped at the end of call 1 left trailing dead-ref closes (m3117</cPa) standing, and a second cleanup call shrank them via the ungated end-anchored TRUNC passes. The verdict depended on what prior cleanup had already changed, not on the content. Retry/rebuild paths that re-normalize already-cleaned messages (the #3938 PR-D alignment judge uses normalizeEchoArtifacts) shipped different bytes per pass (#3837 family).

Fix (src/loop/tag-echo-filter.ts)

Two-part mechanism fix:

  1. Input-side gate attestation. The gate is now attested || out !== text, where attested is computed ONCE on the original text from the channels that run AFTER the gate: truncated open at EOF, forged marker line, bili chain carrier / header block. Close-side channels (TRUNC_CLOSE / TRUNC_REF_CLOSE) deliberately do NOT self-attest — they are the same close-only family the gate protects ([bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760/[bug] #361 residual, form 3: turn whose entire text is a render-tag echo strips to an EMPTY message — agent records a marker-only turn, stalls until manual nudge (v0.1.92, post-#365) #644), so unattested close-only shapes stay byte-identical to before.
  2. Bounded fixed-point walk. stripAcpTags applies the pipeline until stable (every non-stable pass strictly shortens the text, so it terminates; STRIP_PASS_CAP = 8 bounds adversarial cascade depth on attacker-shaped fields). This closes cross-stage exposure holes no single-pass rearrangement covers — e.g. a trailing-close strip exposing a truncated open or a marker line at the NEW boundary.

Behavior changes (old → new)

Tests

@ranxianglei ranxianglei changed the title fix(#4106): tag cleanup is now idempotent — one call reaches the fixed point [PR] fix(#4106): tag cleanup is now idempotent — one call reaches the fixed point Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-21_fix-4106-strip-idempotency (48cefe7)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.4944-pr.4148.d688bcb

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

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复。 #4106 是唯一的幂等性缺陷报告;#4107(流式分块边界依赖)是同家族但机制不同——那是流式状态机的 chunk-split 问题,本 PR 明确不碰、且其回归钉(C#4107/C#4107-2)在本分支上保持原样绿着。open+closed 里检索 idempoten/stripAcpTags 无其他并行修复。

标签: bug + p2 + scope/wiring(修的是协议接线层 wire 文本清理的确定性缺陷;严重度与原 issue #4106 一致——已发送字节不稳定但无数据丢失)。

独立审核结论(未照抄 PR 描述,以下均为本轮重新验证):

结构化八问

  1. 是什么问题:stripAcpTags 对同一文本清两次结果不同——第二次会把第一次留下的死引用闭合(如 m3117</cPa)再削一刀。判定取决于「之前清过什么」而不是内容本身;重试/重建路径对已清理消息再跑 normalizeEchoArtifacts 时,每次送出的字节都在缩水(OpenAI/Codex channel: Per-request body rewriting causes the prefix cache to periodically reset to zero (degenerate foldi | OpenAI/Codex 通道:每请求改写请求体导致前缀缓存周期性归零(退化折叠 foldPoint=m00001 + abort 后 stamped 漂移),单会话 66.7% 计费为全价输入 #3837 家族)。
  2. 什么原因导致的:DEAD_REF_CLOSE 通道的门控(out !== text)在管道中段求值,只能看到同一次调用里更早通道删过的东西;排在门控之后的通道(marker 行删除等)产生的证据永远赶不上本次判决。于是 call 1 死引用单元存活、call 2 文本尾部变形后被无门控的 TRUNC_CLOSE 吃掉(它的 \s 还会吞掉换行符,留下裸 ref)。是门控时机缺陷,不是某个通道正则写错。
  3. 影响范围与严重程度:所有对同一文本重复整文清理的路径(重试/重渲染/Unified Prefix Cache Fix: Transactional Folding Boundaries + Single Exit Arbiter (Global Tracking) | 前缀缓存统一修复:折叠边界事务化 + 出口单一判官(总跟踪) #3938 PR-D 入站-出站对齐判官)。触发条件 = 文本同时含死引用闭合 + 后置通道形状(marker 行等),即真实 tag-echo 场景。p2:字节不稳定、重清理每轮再瘦一刀,但不丢数据、不断会话,二次调用会收敛(无死循环)。
  4. 怎么修复的:两个机制配合——① 门控改输入侧认证:进函数时对原始文本一次性计算后置通道形状是否存在(EOF 截断开标签/伪造 marker 行/bili chain carrier/header block),有则本次调用视为 attested;close 侧通道(TRUNC_CLOSE/TRUNC_REF_CLOSE)刻意不自证——它们正是门控要保护的同一家族([bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760/[bug] #361 residual, form 3: turn whose entire text is a render-tag echo strips to an EMPTY message — agent records a marker-only turn, stalls until manual nudge (v0.1.92, post-#365) #644),未认证的 close-only 形状保持逐字节不变。② 有界不动点走查:管道跑到稳定为止(每 pass 只删字符必收敛;STRIP_PASS_CAP=8 限制对抗性输入的级联 CPU)。一次调用做完多次调用才会做完的事,判决回到内容上。
  5. 是否有回归问题:逐项审过既有行为——未认证 close-only 形状逐字节不变([bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760 钉 stripAcpTags("ref m00001 </acp") → "ref m00001 " 实测不变);[bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760/[bug] #361 residual, form 3: turn whose entire text is a render-tag echo strips to an EMPTY message — agent records a marker-only turn, stalls until manual nudge (v0.1.92, post-#365) #644/[bug] tag-echo 在 0.1.194 仍放行一条 #2190 形态残渣(mNNNNN</acp 内联在正文行首,#2229 治理后复现) #2862 全部 pin 绿;流式过滤器(createTagEchoFilter 一族)完全未动,Streaming tag filter output depends on chunk position: when the same text arrives in two chunks, internal residual m1255 | 流式标签过滤器输出依赖分块位置:同一文本分两块到达时内部残留 m1255</acPA 泄漏给客户端 #4107 仍按原状钉住;两种压缩模式走同一个纯函数、无模式相关新行为。发现一处并已当场修掉:docstring 声称无条件「returns a FIXED POINT」过强——实测级联深度超过 cap=8 时(9 层 $ 锚定截断闭合堆叠、每 pass 只剥一层且永不 attested),cap 处留残余、f(f(x))≠f(x)。这是有意为之的 CPU 上限取舍(严格优于 master 的「深度 2 起就不幂等」),但文档说过头了。已推 213a7e161:docstring 改为精确的有界保证 + 一个双向 pin 测试钉住该角落(改 cap 或改剥除速率都会响亮地红)。
  6. 是否破坏缓存:否。纯出站文本函数,不碰缓存键/前缀/会话状态;改动方向恰恰是让重复归一化送出的字节更稳定。
  7. 是否改了设计方向:否。不碰 kernel、不碰配置面、不碰流式状态机(Streaming tag filter output depends on chunk position: when the same text arrives in two chunks, internal residual m1255 | 流式标签过滤器输出依赖分块位置:同一文本分两块到达时内部残留 m1255</acPA 泄漏给客户端 #4107 照旧 open);只是整文路径的门控时机修正 + 收敛保证。
  8. 是否引入了新的配置:否。STRIP_PASS_CAP=8 是内部常量,非配置项。

行为变更披露(old→new,PR 已声明、我复核成立):

验证证据:

  • A/B 实测:master 上 issue 复现 s1="angle < not a tag\nm7385</CpA\nm3117</cPa\n"、s2="...m3117"(不幂等 ✓ bug 真实存在);本分支 s1=s2="angle < not a tag\n\n\n" ✓。
  • 定向套件本地绿:tag-echo 家族 + verif judge + invariant-judge e2e 共 7 文件 157 tests;typecheck 干净。
  • 独立 fuzz(种子不同于 PR 自带测试,24000 次 f(f(x))===f(x) 断言覆盖 stripAcpTags 与 normalizeEchoArtifacts):本分支 0 违例;master 上同样语料 1313 违例——随机域确实踩中此 bug,修复确实盖住它。
  • CI:新 head 213a7e161 静态门禁全绿(36 green/skipped,0 failed),test shards 运行中。

结论:方向正确,机制打到根因(门控时机而非症状遮蔽),回归审计通过,唯一审核发现(docstring 过强声明 + cap 角落缺 pin)已在 213a7e161 就地修掉。等新 head CI 全绿即可合并——合并动作请你来。

awork added 2 commits October 21, 2026 18:49
… + fixed-point walk

The DEAD_REF_CLOSE gate (`out !== text`) was evaluated mid-pipeline, before
the marker-line / truncate / bili-artifact channels ran, so evidence produced
by those later channels never counted in the same call: a forged marker line
stripped at the end of call 1 left trailing dead-ref closes standing, and a
second cleanup call shrank them via the end-anchored TRUNC passes. The verdict
depended on prior cleanup state, not on content — retry/rebuild paths that
re-normalize already-cleaned messages shipped different bytes each pass.

Two-part mechanism fix in src/loop/tag-echo-filter.ts:
1. Input-side attestation: the gate now also counts shapes produced by
   channels that run after it (truncated open at EOF, forged marker line,
   bili chain carrier / header block), computed once on the ORIGINAL text.
   Close-side channels (TRUNC_CLOSE / TRUNC_REF_CLOSE) deliberately do not
   self-attest — they are the same close-only family the gate protects
   (#1760/#644 content preservation, bare-ref pins unchanged).
2. Bounded fixed-point walk: stripAcpTags applies the pipeline until stable
   (every non-stable pass strictly shortens; cap bounds adversarial cascade
   depth), so one call does everything repeat calls would.
…-point guarantee in the docstring (review finding)

Review of the idempotence fix found the docstring overclaimed: STRIP_PASS_CAP=8 bounds adversarial cascade depth, so an input layering more than 8 cross-stage exposures (e.g. 9 stacked $-anchored truncated closes that peel one per pass without ever attesting) keeps its residue at the cap, and a repeated call peels further. Empirically verified: K<=8 converges in one call, K=9 leaves 'A</acpa\n' and f(f(x)) != f(x). The behavior is the intended CPU-bound tradeoff (strictly better than master, which was non-idempotent from depth 2); this commit only makes it honest: docstring now states the bounded guarantee, and a pin test documents the corner in both directions.
@ranxianglei
ranxianglei force-pushed the 2026-10-21_fix-4106-strip-idempotency branch from 213a7e1 to 48cefe7 Compare October 21, 2026 10:49
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Status update — CI now fully green, PR is merge-ready.

  1. Rebased head carries my review fix verbatim. You pushed 48cefe72 (rebase onto current master 337705b17) which re-applies my review-finding commit (test(#4106): pin the pass-cap residue corner…, was 213a7e161 pre-rebase). Verified byte-identical across all three touched files (src/loop/tag-echo-filter.ts, tests/issue4106-strip-idempotency.test.ts, tests/verif-tag-echo-filter.test.ts); only delta vs my tree is release-notes/package.json from [PR] release-notes: critical entry for 0.1.203 — notify pre-0.1.203 users of retry-400 / context-blowup / skill-cache-miss fixes #4145. Nothing to redo on my side.
  2. CI: first run on 48cefe72 had exactly one red shard — test (windows-latest, 22, 2) — with zero assertion failures (TAP tail: pass 4434 / fail 0 / cancelled 3). The 3 losses were cancelledByParent event-loop-drain cancellations (Promise resolution is still pending but the event loop has already resolved) in unrelated e2e files (issue4079-egress-window-fuse ×2, #1365 attach mode ×1) — the known Flaky Windows CI: issue1993-preflight-400 suite cancelled by early event-loop drain | Windows CI 偶发假红:preflight-400 测试进程提前退出致 5 例取消 #3665 runner-drain signature, not an assertion regression (master is fully green; the harness itself classifies it as such). I triggered a rerun of that job on a fresh runner: all 46 checks now green/skipped, 0 failed.
  3. Verdict unchanged from my review above: mechanism is sound (input-side gate attestation + bounded fixed-point walk, close-only family untouched per [bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760/[bug] #361 residual, form 3: turn whose entire text is a render-tag echo strips to an EMPTY message — agent records a marker-only turn, stalls until manual nudge (v0.1.92, post-#365) #644/[bug] tag-echo 在 0.1.194 仍放行一条 #2190 形态残渣(mNNNNN</acp 内联在正文行首,#2229 治理后复现) #2862 doctrine), behavior changes disclosed old→new, docstring now states the honest bounded guarantee with the cap corner pinned both directions, full-suite verdict = this green CI run. No open findings on my side.

Merge is yours to do — I can't merge PRs. #4148

@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

bug Something isn't working p2 scope/wiring

Projects

None yet

1 participant