Repository navigation
[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
Conversation
…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.
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g [email protected]This exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr4220.tgz
npm install -g packageInstall instructions are refreshed on each push. |
(ai: vllm-qwen/qwen3.8-27b) 查重: 不重复 — 本条即 #4164 的修复 PR,检索 open+closed issue/PR 无同机制并行工作;同文件家族 #4162/#4163 是不同缺陷、不同机制,不构成重复。注意有 4 个同文件在途 PR(#4170/#4180/#4181/#4210,均改 标签: 独立核验(非复读描述,全部实跑代码)A/B 实证(master vs PR head,逐一切分点 vs whole-push):
发现一个残余形状(本 PR 未根治,请拍板,见 Q5/Q7): 既有行为回归审查(逐项过):
测试有效性: 新测试断言「每一个单切分点 == whole-push」(比抽查强的硬门,#3617 双向:该死的死、该留的留),fuzz 语料 +6,armed-flush parity 测试齐。断言有牙(字节相等 + 记账方向),此类 bug 回归能抓住。本地定向:167/167 绿(含 verif subset guard),typecheck 干净。 CI: 除 八问
收工判定:代码可合(人工合并)。两点提示:① windows-22 shard 1 红腿 rerun 即可(#4140 家族已知 flake);② 同文件在途 PR 有 4 个(#4170/+31、#4180/+11、#4181/+13、#4210/+148,后两者与本 PR 都动 PARTIAL_TAIL 邻域,#4210 还动同一个 fuzz 测试文件)——建议确定合并顺序,若任一先合,本 PR rebase 后再合,避免语义冲突。 |
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:$, so the TRUNC_REF_CLOSE rule is untouched).Invariants: pending implies
droppedAnyfalse, 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
heldor left in a live swallow session —<acp>hello worldlost 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)
Verification