Skip to content

[PR] fix: tag-echo dead reference unit leaks across chunk boundaries — hold pending ambiguous tail (Fixes #4164) | fix: tag-echo 死引用单元跨分块边界泄漏 — hold pending ambiguous tail (Fixes #4164) - #4220

Closed
ranxianglei wants to merge 2 commits into
masterfrom
2026-10-21_tag-echo-dead-ref-hold

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #4164

What this fixes

The streaming tag-echo filter violates the #3198 chunk-split determinism contract on one shape: a DEAD_REF_CLOSE-shaped unit (m1234</ap) sitting ahead of an acplike opening tag that straddles an SSE boundary. Whole-push strips the dead unit (the later tag attests the echo); when the boundary lands between the unit and that tag, the unit is released as body text first and never re-examined — same logical output, different persisted bytes → prefix-cache seam + user-visible residue flicker.

Repro from the issue: input m1234</ap<acp> → whole-push , chunks [" m1234</ap<", "acp>"] → m1234</ap (unit leaks).

Mechanism (commit 1)

src/loop/tag-echo-filter.ts: while a PARTIAL_TAIL's fate is still open, DEAD_REF_CLOSE-shaped units in the settled prefix are held as pendingSettle pairs instead of being released. They are settled by the tail's outcome at every resolution site:

Invariants: pending implies droppedAny false, so pending can never coexist with a live swallow session; settled-prefix-only scanning is lookahead-equivalent to full-buffer scanning for every PARTIAL_TAIL/BROAD tail shape (argument in code comments).

Commit 2 — in-passing fix, labeled

While instrumenting the repro I found an adjacent defect in the same flush(): when an absorbInstructed filter flushes with its head still held (tcHold), the early returns handed back only the process() output and silently discarded whatever process had stashed in held or left in a live swallow session — <acp>hello world lost its prose, < lost the lt, and a truncated attrs-bearing open vanished without drop accounting. The non-orphan branches now accumulate into preOut and fall through to the normal EOF settle (content released where the rules release it, tag content dropped WITH accounting). The orphan-tool-call-tail branch keeps its drop-and-return.

Behavior changes (old → new)

  • dead-ref unit + later acplike tag across a boundary: leaked as body text on some splits → consistently stripped (determinism restored; the whole-push path is byte-identical to before).
  • armed-flush tail bytes: silently lost, no accounting → settle normally (prose/dangling-lt preserved; truncated opens dropped and counted).
  • No config surface change. No kernel change. No wire-shape change beyond the two fixes above.

Verification

awork added 2 commits October 22, 2026 02:34
…ilter (Fixes #4164)

A DEAD_REF_CLOSE-shaped unit (e.g. m1234</ap) sitting ahead of a PARTIAL_TAIL
whose fate is still open used to be released as body text before the tail
resolved: whole-push strips it (the later acplike tag attests the echo), but an
SSE boundary between the unit and that tag releases the unit first and never
re-examines it (#3198 determinism contract violation).

Such units are now held as pendingSettle pairs while the tail is undecidable
and settled by its outcome at every resolution site: acplike completion or a
broken open attests the echo (unit dropped, parity with whole-push); prose
resolution or EOF re-emits the unit verbatim (#1760 content preservation).
flush() settles any remainder at EOF.
…turning it (#4164 in-passing)

While working #4164 the probe showed a second, adjacent defect in the same
file: when an absorbInstructed filter flushes with its head still held
(tcHold), the early returns handed back only the process() output and
silently discarded whatever process had stashed in held or left in a live
swallow session. A field ending in ' <acp>hello world' lost the prose,
' <' lost the dangling lt, and a truncated attrs-bearing open vanished
without any drop accounting.

flush() now accumulates those process() outputs into preOut and falls
through to the normal EOF settle for every non-orphan branch, so stashed
bytes reach the truncate/residue rules (dropped, counted) or are released
as content. The orphan-tool-call-tail branch keeps its drop-and-return.
Found in passing while instrumenting the #4164 repro.
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-21_tag-echo-dead-ref-hold (4429a0d)

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title fix: tag-echo 死引用单元跨分块边界泄漏 — hold pending ambiguous tail (Fixes #4164) [PR] fix: tag-echo dead reference unit leaks across chunk boundaries — hold pending ambiguous tail (Fixes #4164) | fix: tag-echo 死引用单元跨分块边界泄漏 — hold pending ambiguous tail (Fixes #4164) Oct 21, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复 — 本条即 #4164 的修复 PR,检索 open+closed issue/PR 无同机制并行工作;同文件家族 #4162/#4163 是不同缺陷、不同机制,不构成重复。注意有 4 个同文件在途 PR(#4170/#4180/#4181/#4210,均改 src/loop/tag-echo-filter.ts,其中 #4210 还改本 PR 同名的 fuzz 测试文件)——见收工判定前的合并顺序提示。

标签: bug scope/wiring p2 — wire 改写面(SSE 流层)缺陷修复;触发需三条件同现,影响为偶发前缀缓存接缝+残影,非 p1 缓存泄露类。无客户端车道(宿主无关的 wire 层)。

独立核验(非复读描述,全部实跑代码)

A/B 实证(master vs PR head,逐一切分点 vs whole-push):

  • 原 repro m1234</ap<acp>:master 上 4 个切分点字节漂移(k=11..14 全部泄漏 m1234</ap);PR head 零漂移。修复成立。
  • 我另探的 prose 变体( m1234</ap x <acp>,单元与 tag 间隔散文):已被覆盖——PARTIAL_TAIL 第 5 arm(refs-run + \x3c[^<>]*)本来就把「单元+后续散文」整体当歧义尾巴 hold(cap 走 IMITATION_SWALLOW_CAP);边界落在散文末尾的 < 处时走新增 pendingSettle peel 路径(PR 修掉的正是 master 上该形状的 4 个漂移点)。PR head 全切分点与 whole-push 一致。
  • 负向形状(纯散文无 tag、<div> 类非 acplike tag):内容逐字保留([bug] 单行中文答案被误判为退化轮:marker 行首前缀按设计被扣住,终点释放却被计成 markup(chat-completions 线整轮报错) #1760),drop 记账方向正确(tail 解决前 dropped()==false,attest 后 true)。

发现一个残余形状(本 PR 未根治,请拍板,见 Q5/Q7):
m1234</ap <div x + 同响应后续完成 <acp> —— 边界落在非 acplike 带属性 tag 的属性区(buffer 以 <div x 之类结尾,匹配不到任何 tail arm)时,单元经 else 分支按散文释放、不可追回。实测:master 12 个切分点漂移 → PR head 剩 4 个(已减半)。要根治需把 else 分支也改成 hold-until-EOF(所有未决单元压到 EOF/attest),代价:含 dead-ref 形状 run 的纯散文响应,该 run 的字节延迟一个 push/flush 才流出——这是流式时序的行为取舍,我不擅自扩:推荐接受为残余(触发需「回显截断 ACP 闭合 tag + 同 buffer 内非 acplike attr-tag 恰好被切开 + 后续 acplike open」三重巧合),若要根治我可以直接补 commit(~15 行,复用现成 peel 逻辑)。

既有行为回归审查(逐项过):

测试有效性: 新测试断言「每一个单切分点 == whole-push」(比抽查强的硬门,#3617 双向:该死的死、该留的留),fuzz 语料 +6,armed-flush parity 测试齐。断言有牙(字节相等 + 记账方向),此类 bug 回归能抓住。本地定向:167/167 绿(含 verif subset guard),typecheck 干净。

CI: 除 test (windows-latest, 22, 1) 外全绿。该红腿是已知 flake:失败签名与 #4214(→#4140 instance 2,duplicate)逐字节一致(cancelledByParent @ tests/issue4079-egress-window-fuse.test.ts:3:2498/:3:3850,harness close() 无界等待);master 最近 3 次 push(含本 PR base c87185a)同一 job 同样红——与本 PR 无关,rerun 即可。

八问

  1. 是什么问题:死引用单元(m\d{4,}+</xx)坐在 acplike 开 tag 前面时,SSE 切块边界落进两者之间,单元先被放行为正文——同一段逻辑输出因切法不同持久化出不同字节,产生前缀缓存接缝和用户可见残影。
  2. 什么原因导致的:流式过滤必须增量决策,而 settled 前缀在尾巴命运未定时就被立即释放——决策点早于证据(后续是否出现完整 acplike tag)到达点。
  3. 影响范围与严重程度:仅「回显了截断 ACP 闭合 tag 且同响应后续出现 acplike 开 tag」的回显场景,且边界恰落其间;后果是缓存接缝成本+残影闪烁,非可用性缺陷 → p2。
  4. 怎么修复的:把未决单元从「立即释放」改为「挂起待判」——连同前导 gap 剥入 pendingSettle,由尾巴结局在每个 resolve 点裁决(完成/broken open → 丢弃并记账;散文/EOF → 原样放回),与 whole-push 语义完全对齐;顺手修掉 armed-flush early-return 静默丢字节。
  5. 是否有回归问题:审过的既有行为无回归(清单见上);遗留一个更窄的残余形状(非 acplike attr-tag 中段切开),先于本 PR 存在且被本 PR 缩小一半,是否根治待你拍板。
  6. 是否破坏缓存:本 PR 正是修缓存接缝;不影响既有缓存/重试/增量游标机制。
  7. 是否改了设计方向:无转向——延续 [bug] tag-echo 在 0.1.194 仍放行一条 #2190 形态残渣(mNNNNN</acp 内联在正文行首,#2229 治理后复现) #2862/A/B experiment: ACP_RENDER_NONE=1 (renderTags=none) eliminates dsh render seams — hit rate 85.6% -> 91.2% #3198 的增量判定框架,只是把决策点从「释放时」挪到「证据到达时」。残余根治(else 分支 hold-until-EOF)属时序语义的小扩展,未做,等你决定。
  8. 是否引入新配置:无。

收工判定:代码可合(人工合并)。两点提示:① windows-22 shard 1 红腿 rerun 即可(#4140 家族已知 flake);② 同文件在途 PR 有 4 个(#4170/+31、#4180/+11、#4181/+13、#4210/+148,后两者与本 PR 都动 PARTIAL_TAIL 邻域,#4210 还动同一个 fuzz 测试文件)——建议确定合并顺序,若任一先合,本 PR rebase 后再合,避免语义冲突。

@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