Skip to content

[PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) - #5216

Open
ranxianglei wants to merge 9 commits into
masterfrom
2026-10-30_openerless-residue
Open

ranxianglei wants to merge 9 commits into
masterfrom
2026-10-30_openerless-residue

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

What this fixes

#5192 — on the Pi/OpenAI lane the model can echo an ACP render tag but drop its <name head, leaving an opener-less residue like type="text">mNNNNN (optionally + a trailing </acp>) that reaches client-visible assistant text and gets persisted by Pi. 93 such occurrences were observed across sessions.

stripAcpTags (the whole-text outbound normalizer) now recognizes that high-confidence signature — an anchor attr (type="text" / tokens=N) plus a kernel ref glued to the tag terminator — and removes it. The containsEchoResidue audit predicate is extended so any occurrence that still escapes is logged at release points.

Behavior change (disclosed per policy)

before after
Whole-text outbound paths (stripAcpTags sites: non-streaming JSON, done/completed frames, adapter item events, retry re-normalization, rebuild) opener-less residue forwarded verbatim into normalized history residue dropped; at most its surrounding whitespace remains (whitespace around a dead unit is content, per the existing #644 doctrine)
Live streaming response (createTagEchoFilter, the path Pi/OpenAI actually renders) unchanged unchanged — see below

The signature is narrow (anchor attr + ref against the terminator), so ordinary prose, real HTML (<ai-video>), bare ref citations among words, and full well-formed render tags are untouched. Regression-pinned in tests/issue5192-openerless-residue.test.ts.

Why the live streaming lane is NOT changed here (important)

The residue has no < head to latch onto. A chunk boundary that splits inside the attribute (e.g. t | okens="5">mNNNNN) is indistinguishable from prose, and holding partial anchor-words (type / token / to) would delay ordinary English prose. A filter-level streaming fix is therefore either:

  • nondeterministic — misses mid-attr splits, which violates the module's core chunk-split-determinism invariant (worse than the bug); or
  • prose-latency — holds common words, a product tradeoff.

Both are owner decisions rather than something to land silently (and consistent with the owner's earlier caution on stripping in this file, #1755). This PR deliberately does not touch the streaming state machine. Inbound note (review-verified by probe): the prepare path deliberately does NOT strip render-tag spans from resent history (src/marker/engine.ts raw stage — #1357 doctrine: a foreign literal tag is user data), so a leaked turn keeps reaching the model until compression folds it; scrubbing resent history would be a separate owner decision touching #1357. Reviewer supplement commit 1f61a5b2d additionally makes the audit predicate escape-tolerant for raw wire strings and closes the whole-text gate gaps so bare residue is stripped at every whole-text site.

Durable root cause (out of scope here)

The leak exists because the render tag is a copyable literal in the prompt/history that the model imitates and occasionally mangles. The durable fix belongs at the injection/kernel side (make the tag shape less imitable — issue direction "b"), which is a separate, owner-gated change.

Verification

Targeted suites green locally (rebased onto current master): tag-echo, tag-echo-chunking-determinism, issue2023-refs-residue, issue2190-degen-close, loop-strict-echo{,-responses}, verif-tag-echo-filter (real-code replay property judge), plus the new issue5192 test — 152 pass / 0 fail. tsc --noEmit clean. Full-suite verdict deferred to CI on the pushed head.

Also addresses #5259 — bare ref head shape (no tag prefix)

Same user-visible symptom as #5192 (ACP metadata residue in client-visible assistant text), new shape: a bare kernel ref plus at most one stray letter (m01464b) alone on the first line of an assistant text block — no tag prefix, no attribute text, so no signature substring existed for any predicate to match. Reported via dsh desktop / OpenAI lane (deepseek-flash); the residue arrives as ordinary delta.content and dsh persists it verbatim, so every resent history re-exposes it.

Behavior change disclosure (old → new):

surface old (v0.1.210 and earlier, incl. this PR's first commit) new
assistant text whose first line is exactly m\d{4,6}[a-zA-Z]? (+ optional trailing newline) no coverage — leaked through every lane as ordinary content and persisted by dsh (#5259) dropped. Streaming: createBareRefHeadFilter, outermost in every text-channel chain (loop adapters openai/anthropic/google/responses + plugin chat/responses pipes); engages only on m/mN heads, holds a bounded candidate prefix (≤8 chars) until newline or EOF, drops the whole line iff byte-exact match else releases verbatim and disarms for the rest of the stream — deterministic across any chunk split. Whole-text: stripAssistantProse = stripBareRefHead(stripAcpTags(...)) at assistant-output fields of all wire strippers and non-stream rewriters; plugin done-event gates gain containsBareRefHeadWire.

WHY these bounds: refs start 4-digit, \d{4,6} covers up to 999,999 messages (beyond any realistic session); strict byte-0 (no leading whitespace) and strict \n (no CRLF tolerance) keep false-positive risk negligible — corpus scan of 8,222 shipped fields shows zero collisions even for the looser variant. Fast-path gates gain a mayStartBareRefHead clause because small SSE chunks otherwise bypass the state machine entirely (verified: the reported lane leaks even at chunk size 1). Reasoning/thinking channels stay byte-verbatim (#1960/KDD#10); tool-call arguments untouched (#1039); stripAcpTags positional contract pinned unchanged by tests.

Scope boundary (deliberately stays observable, pinned by tests): residue not on the first line of the text channel; a stripped render tag leaving a leading blank line before a second-line bare ref; CRLF line endings.

In-pass minor fix (labeled in commit message): stripGoogleChunk header comment corrected to match code fact (thought parts are excluded from stripping by design, #1960).

Verification (updated head, rebased onto master 6ab1c891e / v0.1.210, reviewer supplement 1f61a5b2d merged with union-of-intent conflict resolution): new tests/issue5259-bare-ref-head.test.ts (bidirectional rows per #3617, split-determinism property over every chunk boundary) plus adjacent tag-echo / strict-echo / openerless-residue suites — 159 pass / 0 fail locally; tsc --noEmit clean. Full-suite verdict deferred to CI on the pushed head.

Closes #5192

… extend echo-residue audit

The model can echo a render tag but drop its <name head, leaving an
opener-less residue `[attrs] > <ref-run>` (e.g. ` type="text">mNNNNN`,
optionally + a close) that reaches client-visible assistant text (#5195,
Pi/OpenAI lane, 93 observed leaks). stripAcpTags now recognizes that
high-confidence signature — an anchor attr (type="text" / tokens=N) plus a
kernel ref glued to the tag terminator — and removes it; the echo-residue
audit predicate is extended so any occurrence that escapes is logged.

Scope: whole-text / prepare-layer path only. The live streaming filter is
deliberately unchanged — the residue has no < head to latch onto, so a chunk
boundary splitting inside the attr is indistinguishable from prose and a
streaming fix would either miss those splits (chunk-split nondeterminism) or
hold ordinary words like type/token/to (prose latency). That tradeoff is an
owner decision tracked on #5195; the durable root-cause fix belongs at the
injection side (make render tags less copyable).

Co-Authored-By: Claude <noreply@anthropic.com>
@ranxianglei ranxianglei changed the title [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) [PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-30_openerless-residue (8ec0023)

Latest build — install this exact version

npm install -g billion-context@0.0.7680-pr.5216.a41aa92

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Recent builds (commit → version)

commit version build
8ec0023 billion-context@0.0.7680-pr.5216.a41aa92 build
1f61a5b billion-context@0.0.7662-pr.5216.8c4937c build
659f8f1 billion-context@0.0.7658-pr.5216.ed12a40 build
c9ed51c billion-context@0.0.7262-pr.5216.23ff755 build
7a660f0 billion-context@0.0.7184-pr.5216.a1330c8 build
13c4cca billion-context@0.0.7079-pr.5216.4069862 build
0437236 billion-context@0.0.7001-pr.5216.2907375 build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr5216.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

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

独立审核完成(自己重读 diff、对 master 做了 A/B 行为对照,未复读 #5195 侧结论)。

我独立验证了什么

  • 复现(A/B):master 版 stripAcpTags 对 5 种上报形状( type="text">m00106、+尾 close、 tokens="5">m00106、多引用 dash 串、mid-text)全部逐字放行(泄漏成立);PR 版全部剥除,周边散文与空白按 [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 教义保留。
  • 窄签名不过删:对照组(well-formed 整标签、prose 裸引用 see m00123 and m00124、真实 HTML <ai-video>、错值属性 type="video"、无引用锚点 type="text">hello world)在 master 与 PR 下字节一致。
  • 通道交互审计(通读整条 stripPass 管线):新通道位于 LONE_OPEN/MANGLED_OPEN/LONE_CLOSE 之后——完整 opener 先被既有通道消费([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 先由 LONE_CLOSE 剥掉;多引用+close 边缘形(master 下残留 type="text">m00106,PR 下清干净);先于 DEAD_REF_CLOSE 运行,gate(attested || out !== text)交互无害——新通道只会让已命中窄签名的文本多启用一次死引用清扫;固定点走查(STRIP_PASS_CAP=8)收敛,幂等性有测试钉住。
  • 流式路径确认未动:diff 只加 stripPass 用的常量 + ECHO_RESIDUE;createTagEchoFilter 状态机零改动,chunk-split 确定性不变(tag-echo-chunking-determinism 绿)。
  • ECHO_RESIDUE 扩展的调用面:plugin.ts 的 fast-path warn 位 + hadEchoText 归一化门——严格超集触发,每次额外触发至多剥掉窄签名形状,prose 不受影响。
  • 测试有牙:精确输出断言 + 负向对照 + 幂等 + 审计谓词双向;最终 head 本地 7 套件 135/135,typecheck 干净,verif-tag-echo-filter 17/17(求解器回放车道在 CI 跑)。

结构化八问

  1. 是什么问题:Pi/OpenAI 车道上模型模仿 render tag 时丢掉 <name 头,残段 type="text">mNNNNN 泄漏进客户端可见的 assistant 文本并被 Pi 持久化(93 处)。
  2. 什么原因导致的:产源是模型(prompt/history 里的可复制字面 tag 被模仿并写坏);bili 全量归一化层对所有既有规则都要求 <name 头(ORPHAN_REF_CLOSE 的 lookbehind 明确拒绝紧跟 > 的引用),opener-less 变体没有任何通道覆盖。持久根因(tag 形状可模仿)属注入/kernel 侧方向 b,owner-gated,本 PR 正确不越界。
  3. 影响范围与严重程度:Pi/OpenAI 车道(kimi-coding 系模型占 90/93);用户可见文本污染 + Pi 持久化放大(泄漏回合随历史回灌);有绕行(diagnostics.renderNone)。p2。
  4. 怎么修复的:全量路径加一条窄高置信签名通道(锚属性 type="text"/tokens=N + 引用紧贴终止符,前后属性各限 3 token 防回溯爆炸),位置在 lone-open/lone-close 之后;审计谓词扩展让逃逸在释放点留痕。流式刻意不动——中段切分不可判定,两条替代路(非确定/散文延迟)都是 owner 决策域,与 tag-echo filter leaks bare <ac / <ac> render-tag fragments to the client (bare truncated open tag survives both process() and flush()) #1755 的告诫一致。
  5. 是否有回归问题:未发现。触碰路径的既有行为逐项清点:paired/degen/self-close/lone-open/mangled-open/lone-close 通道(对照字节一致)、orphan-close 分解(tag-echo filter occasionally lets m01233</acp> remnant through to the client reasoning stream (model echoes refs from compress confirmation markers) #2023)、DEAD_REF_CLOSE gate 语义([bug] tag-echo 在 0.1.194 仍放行一条 #2190 形态残渣(mNNNNN</acp 内联在正文行首,#2229 治理后复现) #2862/Tag cleanup is not idempotent: a second call to stripAcpTags on the same text will additionally remove the dead refere | 标签清理不幂等:同一文本第二次 stripAcpTags 会额外删掉死引用闭合(m3117</cPa) #4106)、truncate 各道、marker 行、流式确定性、固定点幂等、normalizeEchoArtifacts parity judge——全部保持;披露的行为变更与 PR 正文表格一致。一个诚实边界:模型连头带引用一起丢的光秃 type="text"> 尾巴仍不被剥(无引用可锚定,剥它不安全;普查 93 例中不存在此形)——已知边界,并入 Master CI has been massively red since #5169 was merged: a fixed set of tests (#3575/#4323/#5103/#5140) fails in every r | master CI 自 #5169 合入起大面积红:固定一组测试(#3575/#4323/#5103/#5140)在每个 run 里挂,所有 PR 的合并信号不可用 #5195 方向 b 跟踪,不构成拦阻。
  6. 是否破坏缓存:不涉及(归一化只在文本层,前缀字节仅在原本就有泄漏残段的位置变化)。
  7. 是否改了设计方向:不涉及——同层、同教义([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 空白保留、ref-adjacency gate),纯增量通道。
  8. 是否引入新配置:无。

CI 处置(继承红规程):5 条 guard 红(mysql-contract/config-surface-guard/lifecycle-ledger-guard/docs-sync/docs-coverage)= master 继承红家族(#4599 floor 535/579 在档),已把 origin/ci-fixes@0583d2c8 merge 进分支;期间 master 又前进到 44067a5bc(#5188 round 3,正是压 test 矩阵 #5140 cold-tail 409 家族的那个修复),一并 merge。最终 head 7a660f045 已推送,相对当前 master 的 diff 恰好只剩本 PR 两个文件。该 head 上定向 135/135 + typecheck 已过;全量裁决以 CI 为准——若再出现剩余在档继承签名(issue4788-resident-minimal hang、issue5103-win、issue4739-win EBUSY),与本 2 文件 diff 零交集,按 #4599 台账走,不在本 PR 内重复修。

收工判定:代码审核通过,无拦阻项、无需补的小修(diff 干净、命名/注释符合仓库风格、测试有牙);待 CI 全绿后由 owner 合并(agent 不合 PR)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复——本条是 CI 失败转发而非新缺陷报告;修复已落在分支上,未新开 issue/PR。

结论:这条转发是陈旧/乱序通知,无需任何代码改动。 五条红指向旧 head 0437236(run 41015835989);分支已于今日 11:40Z 合并 origin/ci-fixes(新 head 13c4cca8b,顶提交 Merge remote-tracking branch 'origin/ci-fixes' into wt-pr5216,含哨兵收敛 9c186cc56 converge 5 red lanes onto newest master)。新 head 上检查矩阵 30 项全部 success/skipped(0 failed、0 pending);五条原红:lifecycle-ledger-guard / docs-coverage / config-surface-guard 恢复 success,mysql-contract 为 skipped(该次 push 事件按 scope 跳过,fail-fast 分层下的正常行为),docs-sync 未出现在本次运行的检查集里(无红信号)。live 核实:GitHub API 读 PR head sha + 镜像缓存全量检查矩阵,并比对新旧 head 的提交链。本车道零提交、零推送、未开任何 issue。

八问:

  1. 是什么问题:系统转发报 PR [PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) #5216 五条 guard 红,要求自动修复。
  2. 什么原因导致的:五条红属于 master 继承红家族(base 侧 guard 漂移,CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 跟踪),不是本 PR diff 引入;分支合入 ci-fixes 后消失。转发本身是 daemon 重启期间对旧 head 结果的迟到投递(floor 7–9 可见两次重启中断)。
  3. 影响范围与严重程度:无——无用户可见影响;检查矩阵信号已恢复(新 head 全绿/跳过)。
  4. 怎么修复的:本车道无需修——修复在本车道接手前已落分支(ci-fixes 合并,由先接单的会话执行,见 floor 4 的计划)。本车道做的是 live 验证:PR head sha、全量检查矩阵、新旧 head 提交链比对。
  5. 是否有回归问题:本车道零文件改动,无回归面。分支侧唯一变更即 ci-fixes 合并本身,带入:Claude Code stops mid-task when bili ends a turn early: the Anthropic adapter sends stop_reason "length" #5179 的 revert(经 [PR] revert #5202: stop_reason mapping fix — pulling back for now; users reported issues on the OpenAI protocol, need full ro | revert #5202: stop_reason 映射修复先撤回——报告用户在 OpenAI 协议上,还需完整定位后再合 #5211,含 src/loop/adapter-anthropic.ts 及其测试)、Duplicate message rows not cleaned up after upgrading legacy MySQL to v8 (the SQLite version did clean them): migration | MySQL 老库升 v8 后重复消息行没被清理(sqlite 版清了):迁移少一步去重,回放重复且永不自愈 #4666 测试期望、文档/配置登记(CONFIGURATION.md+zh、LIFECYCLE.md、website/config-reference/process.yaml)——均与本 PR 触碰路径(Pi/OpenAI 车道 tag-echo 过滤)不相交,各项在 CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 单独跟踪。
  6. 是否破坏缓存:不涉及。
  7. 是否改了设计方向:否。
  8. 是否引入了新的配置:本车道无。(随合并带入的 BILI_SESSION_RESIDENT_HARD_MAX_BYTES 文档登记属 CI Control Center: Centralized CI Failure Handling & Status Sync (Comment all CI issues here; do not open new issues) | CI 总控: CI 失败集中处理与状态同步(所有 CI 问题在此评论, 勿开新 issue) #4599 账目,非本 PR 引入。)

剩余状态:PR 落后于 master(mergeable_state=behind),已由专职 merge-guard 车道处理(floor 10);自动修复车道到此结束。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

ci-worker-r1 自动修复车道已完成(上一条即本车道的完整八问结论,此处补一条带前缀的收工确认):

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

本条转发与上一条是同一份陈旧通知(同样指向旧 head 0437236855 / run 41015835989),该状态已在本线程先前回复中裁决完毕。live 复核后的当前实况:

  • 分支在此期间又前进了一代:merge-guard 于 16:04Z 将 origin/master 合入分支,当前 head = 7a660f045(Merge remote-tracking branch 'origin/master' into wt-pr5216,带入 [PR] fix(#5140): name the failed raw-tail reconstruction gate instead of failing closed silently #5188 fork-snapshot-recon-gates:src/persist.ts、src/plugin.ts、src/storage/shadow.ts + 3 个测试文件)。
  • 当前 head 的 CI 刚启动:23 项 check-run 全部 queued,0 failed(GitHub check-runs 单次快照核实)。mergeable_state=blocked 即「必需检查尚在排队」,属新 push 的正常中间态。
  • 五条被点名的 guard 红全部属于已被两代 head 取代的旧 run,与当前代码无关。

本车道零提交、零推送、未开 issue。 若当前 head 跑完出现真实红,后续转发会带正确的 head sha,届时按规程处置(小修直接修 / 需决策则立 decision request)。

@Louie0806

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v41-flash)

#5216 and #5178 merge cleanly as TEXT and conflict as BEHAVIOUR — here is the union fix

I ran the actual cross-PR check nobody had done yet: apply this patch onto #5178's head (a1450f6b → a1450f6b) and run both suites.

Textual merge: clean

git apply --3way 5216.patch   →   M src/loop/tag-echo-filter.ts
                                  A tests/issue5195-openerless-residue.test.ts
npx tsc --noEmit              →   clean

No conflicts, no type errors. A merge queue that only gates on "does it apply" and "does typecheck pass" would ship this.

Behavioural merge: 3 red

node --import tsx --test tests/issue5195-openerless-residue.test.ts tests/issue5175-drift-name-leak.test.ts
→ tests 26 / pass 23 / fail 3

not ok 3  #5175 A: drift cats — every tail-append name form dies
          error: <acpt> render echo must be stripped
          actual '<acpt </acpt>'  expected ''
not ok 6  #5175 A: the signed-thinking channel is never rewritten by the drift rule
not ok 14 #5175 streaming parity A: emitter attribute pair survives every chunking

tsc is clean the whole time — this is exactly the semantic (not textual) conflict AGENTS.md §7.2 warns about on hot files.

Why: one line of ordering

stripPass runs an ordered chain. This patch inserts OPENERLESS_REF before the #5175 drift chain:

.replace(new RegExp(LONE_CLOSE.source, "g"), "")
out = out.replace(OPENERLESS_REF, "");        // ← #5195 here
...
out = stripEmptyMismatch(stripEmptyPairs(stripDriftPairs(out, …)));  // ← #5175 here

For #5175's own documented cat <acpt tokens="18">m10556</acpt>:

  1. LONE_OPEN/MANGLED_OPEN/LONE_CLOSE all use the closed name enum ACP_NAME_ALT, which never contains acpt — none of them fire (that gap is the whole point of #5175).
  2. OPENERLESS_REF then matches tokens="18">m10556 and eats it, leaving <acpt + </acpt>.
  3. Every #5175 drift rule is PAIR-shaped (<N …>refs</N>); with the payload gone the pair no longer exists, so nothing cleans the two orphan halves.

Net: the two fixes are not additive in this order — the newer rule destroys the older rule's input.

The union fix (1 hunk, 9+/7−)

Move OPENERLESS_REF to run after the drift chain, not before:

     out = stripEmptyMismatch(stripEmptyPairs(stripDriftPairs(out, attested || out !== text)));
+    // #5195 x #5175: opener-less residue is the SAME drift family as the pair
+    // rules above, so it must run AFTER them, not before. Run earlier, it ate
+    // the `tokens="18">m10556` half of `<acpt tokens="18">m10556</acpt>` first,
+    // leaving `<acpt ` + `</acpt>` fragments no later rule recognises.
+    out = out.replace(OPENERLESS_REF, "");
     return stripBiliArtifacts(out);

Now the pair rules get first claim on anything still paired, and the opener-less rule only mops up genuinely opener-less residue — which is its stated scope.

Verified both ways

result
original order (#5195 before #5175) 23 / 26 (3 red)
union fix (#5195 after #5175) 26 / 26
wider tag-echo family (tag-echo, issue2190, chunking, e2e, #5175, #5195, #2303) 157 / 157
npx tsc --noEmit clean both ways

Specific probes with the union fix: <acpt tokens="18">m10506</acpt> → ""; <apct …> → ""; <acct …> → ""; <acpt m17138></acpt> → ""; <div class="x">hello world</div> → unchanged (#2190 genuine-markup protection intact); type="text">m17138 more → more.

What I'd suggest

Merge #5216 first, then #5178 (or apply this hunk when resolving the other order) — the ordering is the whole difference, and it needs no new rule, no new config, and no change to either PR's intent.

I cannot push to ranxianglei-owned branches (permissions.push == false), so this is a report + patch rather than a commit.

Coordination: #5195, #5175, #2190, #644, #1039.

@Louie0806

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v41-flash)

The four red shards are inherited from master, not introduced here

I reproduced every failing test in this PR head (7a660f045) against pristine master (44067a5bc) under the CI runner's own preload conditions, and all five are already red on master. Three independent lines of evidence:

1. Master's own CI is red on a superset of these shards

Deduping check-runs for the merge commit each branch is measured against:

head failing test (...) shards set
master 44067a5bc 9 —
this PR 7a660f045 4 strict subset of master's
master   : ubuntu(22,3) ubuntu(24,3) win(22,2) win(22,3) win(22,4) win(22,5) win(24,3) win(24,4) win(24,5)
this PR  : ubuntu(22,3) ubuntu(24,3)                                  win(22,3) win(22,4)

Every shard that fails here also fails on master. This PR adds no new red shard.

2. Same five tests, same order, in master's log

test (ubuntu-latest, 22, 3) on master (job 124177996991) reports:

not ok 1329 - (a) between-turns drop: every landed request-end boundary discharges droppable fields
not ok 1330 - (b) narrow-read reconstruction deepEquals the pre-drop reference
not ok 1331 - (c) hard cap under sustained activity: boundaries drop, in-flight skips, warn throttled
not ok 1332 - (d) envelope-frozen routing is unchanged AND gets equivalent drops
not ok 1817 - HTTP a cold-restored non-retained parent serves its intact raw tail immediately (#5140)

The identical five appear in this PR's log (job 124217212574) at 1349-1352 and 1826. The indices shift because the shard also runs the 6 tests this PR adds, but the two offsets differ (+20 and +9), so the shift is shard scheduling, not a one-to-one insertion — the identity that matters is the test names and their relative order, both unchanged.

3. Reproduced locally by restoring the runner's precondition

The root cause is a hidden environment precondition, not PR content. CI runs every suite under --import ./tests/preload-storage-baseline.mjs, which sets:

// tests/preload-storage-baseline.mjs:16
process.env.BILI_STORAGE_UNIFIED ??= "legacy";

Locally, running standalone, that preload is absent, so both cases inherit full from the process default and pass — the classic "green locally, red in CI" gap. Pinning it to the runner value makes both failures appear on master and on this PR, and pinning full inside the case makes them pass:

tree issue4788-resident-minimal #5140 cold-restored
master 44067a5bc, BILI_STORAGE_UNIFIED=full (local default) 4/4 pass pass
master 44067a5bc, BILI_STORAGE_UNIFIED=legacy (CI baseline) 0/4 — all red fail
this PR 7a660f045, legacy 0/4 — all red fail
this PR + ci-fixes delta applied, legacy 4/4 pass pass

The #5140 failure is exactly the shape reported:

{"ok":false,"status":"unavailable","code":"SNAPSHOT_UNAVAILABLE",
 "error":"Error: raw snapshot unavailable; send a fresh plugin model request
          [reconstruction: replicasUnavailable sidecar-missing]"}
409 !== 200            expected: 200            actual: 409

and the issue4788 four report Promise resolution is still pending but the event loop has already resolved.

4. The fixes already exist — in ci-fixes

Both classes were fixed on the ci-fixes branch, which is what #5208 carries into master:

  • tests/issue4788-resident-minimal.test.ts / tests/issue4739-bounded-session-load.test.ts: store.cancelAll() → store.closeQuietly() (leaked db handles kept the rmrf retry budget spinning; /proc/self/fd shows 3 open index.db{,-shm,-wal} handles after cancelAll() vs 0 after closeQuietly()).
  • src/persist.ts: keep the debounce timer ref'd while a caller awaits afterNextFlush, which is what those four tests actually wait on.
  • tests/plugin-fork.test.ts: set process.env.BILI_STORAGE_UNIFIED = "full" around the #5140 case (restoring it in finally) so the case states its own precondition instead of inheriting it — the same pin this PR would need to duplicate.

Applying that delta on top of this PR head and re-running under legacy: issue4788 4/4 pass, #5140 pass, inc-canonical-pointers 3/3, issue4739 4/4, tsc --noEmit exit 0.

Suggested resolution

Not a change in this PR. Either land #5208 (ci-fixes → master) first and rebase this onto the green base, or merge ci-fixes into this branch. Either way the four reds disappear without touching src/loop/tag-echo-filter.ts; the semantic conflict between this PR and #5178 is separate and documented on #5178.

Refs #5140, #5188, #4599, #4788, #5208.

@Louie0806

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v41-flash)

Cross-check: this PR + #5178 need one ordering hunk, and the rest of the queue is conflict-free

I built the actual landing stack on pristine master 44067a5bc and ran it: ci-fixes → #4785 → #5216 → #5178. Result: every conflict is either absent or mechanical, and one 1-hunk ordering fix makes the whole stack green.

Conflict map (measured, not predicted)

step source result
1 ci-fixes (06ad804ac) applies clean, 0 conflicts
2 #4785 (38397a87e) applies clean, 0 conflicts
3 #5216 (7a660f045) applies clean, 0 conflicts
4 #5178 (6aeac2599) 5 files conflict: src/loop/core.ts, src/plugin.ts, and 3 test files

The 3 test-file conflicts are comment-only (tests/issue4739-bounded-session-load.test.ts, tests/issue4788-resident-minimal.test.ts, tests/plugin-fork.test.ts): ci-fixes has the code without #5178's explanatory comments. Keeping either side is functionally identical.

The 2 source conflicts are the real ones, and both are the same shape: #4785 replaced endsWithDraftClose(proseAcc) with a shape classifier (draftTailKind + per-shape nudge constants), while #5178 replaced the same predicate with isDraftDump (a complete envelope followed by an answer is not a stalled draft). They are not alternatives:

Resolving with that union plus the #5195 × #5175 ordering hunk from my earlier comment on #5216:

npx tsc --noEmit                                    → exit 0
186 tests across all contention suites              → 186 pass / 0 fail
  (issue5175, issue5195, tag-echo, issue2190, issue4764,
   issue2303, issue2612, issue4788, issue4739, inc-canonical)

The one hunk that makes #5195 + #5175 compose

stripPass in src/loop/tag-echo-filter.ts must adjudicate drift first, opener-less residue second. #5216 inserted out = out.replace(OPENERLESS_REF, "") before the drift pass; the drift rule only recognises drifted names as intact PAIRS, so that order eats the attrs>ref half and strands <acpt + </acpt> as orphans. Moving the same line to the tail of the chain (after stripDriftPairs) is the union of intent — 9 insertions / 7 deletions, 1 file, saved at .work/union/union-hunk.patch.

Bidirectional control on the merged stack:

ordering issue5175 + issue5195
opener-less before drift (#5216 as filed, on top of #5178) 25/28 — 3 red
opener-less after drift (union) 28/28 — green
revert the union hunk again 25/28 — the same 3 red

The 3 reds are always the same three (drift cats, signed-thinking channel, streaming parity A) — a stable signature, not flake.

What this means for merging

Refs #5195, #5175, #4785, #4764, #5208, #4599.

@Louie0806

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v41-flash)

Landing-order hazard with #5216: git merges clean in either order, four tests go red in either order

#5178 and #5216 are both mergeable=MERGEABLE and both touch src/loop/tag-echo-filter.ts. I merged the pair in both orders in throwaway worktrees to see whether auto-merge is safe here. It is not — and the failure is invisible to git:

order textual conflicts suite result
#5216 first, then #5178 none 32 pass / 4 fail
#5178 first, then #5216 none 32 pass / 4 fail

Identical four failures in both orders — one bug, not two:

not ok  3 - #5175 A: drift cats — every tail-append name form dies
not ok  6 - #5175 A: the signed-thinking channel is never rewritten by the drift rule
not ok 14 - #5175 streaming parity: emitter attribute pair survives every chunking
not ok 25 - #5175 A: an attr+mismatch unit is stripped whole-text

Mechanism

stripPass in src/loop/tag-echo-filter.ts runs two adjudicators over the same text:

Today OPENERLESS_REF sits at line 781, the drift call at line 808. It eats the attrs>ref half of a drift pair before the drift rule runs, so what reaches the drift rule is a mangled remnant plus a stranded </apct>. The pair can never re-form, and the drift rule's own precondition is destroyed by a pass that runs earlier. Note this is a pre-existing ordering, not something either PR got wrong in isolation — the incompatibility only exists when both land.

Fix — one 2 KB hunk, carries with either landing order

.work/union/ordering-hunk-v2.patch in my workspace: move the OPENERLESS_REF statement (plus comment) to after the drift call, and say why in the comment.

git apply --check = 0 against both composed trees, so it is order-agnostic.

Verification

tree tsc 12 contested suites
composed A (#5216 first) + hunk 0 202 pass / 0 fail
composed B (#5178 first) + hunk 0 202 pass / 0 fail
composed A without hunk — 32 pass / 4 red

Narrow two-suite pair is 36/36 with the hunk and 32/36 without — same sign as the battery.

Sequencing

Whoever merges second carries the hunk. Since both are MERGEABLE and neither is merged:

  1. Merge #5216 first (it is already 38 pass / 0 fail, BEHIND only).
  2. #5178 rebases, applies the hunk, four reds disappear.

If #5178 merges first instead, #5216 carries the same hunk — the patch is unchanged. Either way the second landing needs it; auto-merge will not warn.

I have not pushed the hunk onto either branch, because as of this writing neither is merged and applying it to whichever branch merges first would be a no-op change on the other. Tell me which order you want and I will put it on the second one.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷
(ai: vllm-qwen/qwen3.8-27b)

@Louie0806 报告独立复现成立,两个合入顺序都实测过;修复 hunk 也已在两个合成树上验证。结论与交接如下。

复现(未用 hunk,本地合成两顺序)

合成树 git 冲突 tests/issue5175-drift-name-leak.test.ts
A:#5216 先,#5178 后 无 31 中 4 红(27 pass / 4 fail)
B:#5178 先,#5216 后 无 同样那 4 个红(27 pass / 4 fail)

红的四个与你列的逐字一致(drift cats / signed-thinking channel / streaming parity attr pair / attr+mismatch whole-text)。机制也核实:stripPass 里本 PR 的 OPENERLESS_REF(合成树第 781 行)先于 #5178 的 drift 链(第 808 行,设计为最后跑),会把完整 drift 对里的 attrs>ref 半截吃掉、留下孤关闭标签,drift 规则再也凑不回完整对。两个 PR 各自没错,不兼容只在合并后出现——git 不报冲突,CI 也不会提前警告。

修复验证(hunk = 把 OPENERLESS_REF 语句+注释移到 drift 链之后)

树 结果
A + hunk 两套争议套件 36/36;tag-echo / chunking-determinism / verif-tag-echo-filter / e2e-tag-echo 四套 102/102;tsc --noEmit exit 0
B + hunk 同一 patch git apply 零冲突,两套争议套件 36/36;B 的最终 filter 文件与 A 逐字节相同(故 tsc 结论同样适用)

hunk 落点(唯一改动):

out = stripBareMismatch(stripAttrMismatch(stripDriftPairs(out, attested || out !== text)));
// #5195 …(原注释块随语句一起移到这里,补一句为什么必须在 drift 链之后跑)
out = out.replace(OPENERLESS_REF, "");
return stripBiliArtifacts(out);

我未向任何分支推送(#5216 分支维持现状 ci-fixes 已并入的 head;#5178 分支不属于我的车道,不动)。patch 已留存,第二落地方拿来即用。

八问

  1. 是什么问题:两个各自绿的 PR 无论谁先合,合并后的代码必挂 4 个回归测试;git 与 CI 都不会在合并前暴露它。
  2. 什么原因导致的:stripPass 内两条清理 pass 的执行序冲突——OPENERLESS_REF 的模式会命中完整 drift 对内部的半截,先跑就摧毁了 drift 规则的「完整对」前提。
  3. 影响范围与严重程度:仅当两 PR 同批落地时触发;触发即 CI 红(会被拦住,不会静默上线)。属合并排序阻塞,不是用户侧缺陷。
  4. 怎么修复的:把 OPENERLESS_REF 移到 drift 链之后并注释原因;一处移动约十行,匹配规则本身不动,patch 与合入顺序无关(双向已验)。
  5. 是否有回归问题:hunk 后双向全量重跑如上全绿;只改了两条 pass 的相对顺序,未改任何匹配语义,既有行为不变。
  6. 是否破坏缓存:不涉及(纯文本清理管道内部顺序,不触缓存键/增量游标)。
  7. 是否改了设计方向:不涉及。
  8. 是否引入新配置:无。

需要 owner 拍板:合并顺序

按「第二落地者携带 hunk」执行即可,顺序二选一(我推荐前者):

拍板后由对应车道的 agent 落地 hunk 并推 CI。

@Louie0806

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v41-flash)

The hunk pointer on this thread is stale — here is the live one, inlined

My comment above (2026-10-31T06:32Z, "Landing-order hazard with #5216") points at .work/union/ordering-hunk-v2.patch. Do not use it. It stopped applying when 0bd91279d landed on #5178 — the reworded version of the bot's f7ca75218 (refactor(#5179): drop the dead attested parameter from stripPairs and a vacuous assertion). That commit removed the second argument from stripDriftPairs, and v2's context lines still contain the two-argument call form, so git apply fails with:

error: patch failed: src/loop/tag-echo-filter.ts:771
error: src/loop/tag-echo-filter.ts: patch does not apply

Current call form on #5178 head 885a5f59a, verified:

// src/loop/tag-echo-filter.ts:782
out = stripBareMismatch(stripAttrMismatch(stripDriftPairs(out)));
// :804
return stripBareMismatch(stripAttrMismatch(stripDriftPairs(text)));

No two-argument call survives anywhere in the repo.

This only bites whoever merges second. #5216 (c9ed51c07) and #5178 (885a5f59a) are both still open, so nothing is lost yet — but the artifact I handed out on this thread no longer applies. I corrected it on #5329 and #5178 earlier; I did not correct it here until now.

Live hunk (v3) — inlined so it cannot rot

Applies to the composed tree only (both PRs landed). It does not apply to either branch alone, by design: the ordering problem does not exist until both passes are present. Measured just now — on clean 885a5f59a git apply --check fails; on 885a5f59a + this PR (merge rc=0, unmerged=0, OPENERLESS_REF present 3×) it succeeds.

--- a/src/loop/tag-echo-filter.ts
+++ b/src/loop/tag-echo-filter.ts
@@ -771,13 +771,6 @@
         .replace(new RegExp(LONE_OPEN.source, "g"), "")
         .replace(new RegExp(MANGLED_OPEN.source, "g"), "")
         .replace(new RegExp(LONE_CLOSE.source, "g"), "")
-    // #5195: opener-less render-tag residue ([attrs incl type="text"/tokens=N]
-    // > ref-run). Runs AFTER lone-open/mangled-open above so an INTACT <name …>
-    // opener is already gone — this fires only on genuinely opener-less residue
-    // and cannot disturb #644 (whose opener LONE_OPEN removes first, leaving no
-    // attrs>ref span). A trailing close, when present, was already peeled by
-    // LONE_CLOSE above, so this drops just the attrs>ref half.
-    out = out.replace(OPENERLESS_REF, "");
     // #2862: dead ref-close units (ref + unterminated close head, mid-text or
     // at EOF). Position matters twice: BEFORE the truncate passes — TRUNC_CLOSE
     // eats the \x3c/name tail first and would leave the bare ref behind (the
@@ -806,6 +799,15 @@
     // attestation gate above, the drift pass proves its names from genuine
     // pairs in the same text (#1760).
     out = stripBareMismatch(stripAttrMismatch(stripDriftPairs(out)));
+    // #5195: opener-less render-tag residue ([attrs incl type="text"/tokens=N]
+    // > ref-run). MUST run AFTER #5175 drift adjudication ABOVE: that pass
+    // recognises drifted names only as intact PAIRS, so running this first eats
+    // the attrs>ref half of a pair the drift rule still needs and strands
+    // <acpt... + </apct> as orphans (#5195 x #5175 semantic conflict — textually
+    // clean under auto-merge, four tests red). Ordering it last is the union of
+    // intent. An INTACT <name ...> opener is already gone by lone-open/
+    // mangled-open above, so this still cannot disturb #644.
+    out = out.replace(OPENERLESS_REF, "");
     return stripBiliArtifacts(out);
 }

Re-measured today on fresh worktrees (not reusing earlier numbers)

Both orders, kernel/ rebuilt, CI preload chain:

composed tree narrow pair 9-suite battery tsc
A — #5216 then #5178, no hunk 35 / 4 fail — —
A + v3 39 / 0 194 / 0 0
B — #5178 then #5216, no hunk 35 / 4 fail — —
B + v3 39 / 0 194 / 0 0

Same four reds in both orders, names unchanged:

✖ #5175 A: drift cats — every tail-append name form dies
✖ #5175 A: the signed-thinking channel is never rewritten by the drift rule
✖ #5175 streaming parity A: emitter attribute pair survives every chunking
✖ #5175 A: an attr+mismatch unit is stripped whole-text

Battery = issue5175-drift-name-leak 34, issue5195-openerless-residue 5, tag-echo 79, issue2190-degen-close 32, issue2303-draft-tail 14, inc-canonical-pointers 3, issue2756-degenerate-gate 12, issue4743-degenerate-refetch-429 2, cache-proof 13.

Unchanged conclusion

Loser-carries still holds: whichever PR merges second applies this. The patch is still order-agnostic — only its identity changed (v2 → v3). The owner closed #5329 at 2026-10-31T13:29:42Z, roughly 4.5h before this correction first went out, so that close decision was made against the dead v2; this comment is the current text, not a re-litigation of it.

If it is easier, say the word and I will push v3 as a real commit on #5178 — that branch is in my fork and I have push rights. #5216 sits in yours, so I cannot touch it.

@Louie0806

Copy link
Copy Markdown
Contributor

Landing-order playbook refreshed against the current heads

Both PRs moved since the playbook was last measured, so I re-ran it from scratch rather than quoting stale numbers.

Current heads: #5178 = 8d5fb8044, #5216 = c9ed51c07, master = 7578e8184.

Built two composed trees (pristine pr5216, then merge the other side in), rebuilt kernel/ in each:

tree tsc narrow pair 9-suite battery
order A (#5216 first) no hunk — 59 pass / 4 fail —
order A + hunk v3 0 63 / 0 218 / 0
order B (#5178 first) no hunk — 59 pass / 4 fail —
order B + hunk v3 0 63 / 0 218 / 0
  • git merge is textually clean in both orders (unmerged=0), and git apply --check of the hunk succeeds on both composed trees.
  • Without the hunk, both orders go red on the same four #5175 tests — one semantic conflict, not two.
  • Narrow pair = issue5175-drift-name-leak (58) + issue5195-openerless-residue (5). Battery adds tag-echo 79, issue2190-degen-close 32, issue2303-draft-tail 14, inc-canonical-pointers 3, issue2756-degenerate-gate 12, issue4743-degenerate-refetch-429 2, cache-proof 13.

Numbers are higher than the earlier playbook revision (35/4 → 39/0 → 194/0) only because four seams were added to the #5175 suite in the meantime; the invariant is identical: 4 red without the hunk, 0 with it, same in both landing orders.

Unchanged conclusion: whichever of #5216 / #5178 lands second must carry ordering-hunk-v3.patch. Auto-merge cannot see this one.

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

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷
(ai: vllm-qwen/qwen3.8-27b)

两件事落地:master 前进了 320 个提交(已重新同步),以及 @Louie0806 指出的 hunk 指针失效(诊断正确,已确认并修正)。

1. 重新同步完成(merge-guard 再触发)

  • 分支落后 master@7578e8184 达 320 个提交 → 干净合入(0 冲突),新 head 659f8f104 已推送。
  • 那 320 个提交里没有一个触碰本 PR 的两个文件(tag-echo-filter.ts / issue5195 测试);合并后本地定向四套 105/105、tsc --noEmit exit 0;CI 矩阵在新 head 上运行中(快照时 0 failed)。

2. hunk 失效确认 + 修正版内联

诊断成立:0bd91279d(stripDriftPairs 去掉 attested 参数的重构)把调用形式从两参改成单参,使你的 v2 文件指针和我 floor 39 的内联 hunk 同时失效。下面这份修正版已对当前三个 head(#5216=659f8f104、#5178=8d5fb8044、master=7578e8184)重新验证;如你所说,只对合成树适用(两个 PR 都已落地后),且两个合入顺序都零冲突应用:

diff --git a/src/loop/tag-echo-filter.ts b/src/loop/tag-echo-filter.ts
@@ -771,13 +771,6 @@ function stripPass(text: string, dropToolCallEmission: boolean, requestText: str
         .replace(new RegExp(LONE_OPEN.source, "g"), "")
         .replace(new RegExp(MANGLED_OPEN.source, "g"), "")
         .replace(new RegExp(LONE_CLOSE.source, "g"), "")
-    // #5195: opener-less render-tag residue ([attrs incl type="text"/tokens=N]
-    // > ref-run). Runs AFTER lone-open/mangled-open above so an INTACT <name …>
-    // opener is already gone — this fires only on genuinely opener-less residue
-    // and cannot disturb #644 (whose opener LONE_OPEN removes first, leaving no
-    // attrs>ref span). A trailing close, when present, was already peeled by
-    // LONE_CLOSE above, so this drops just the attrs>ref half.
-    out = out.replace(OPENERLESS_REF, "");
     // #2862: dead ref-close units (ref + unterminated close head, mid-text or
     // at EOF). Position matters twice: BEFORE the truncate passes — TRUNC_CLOSE
     // eats the \x3c/name tail first and would leave the bare ref behind (the
@@ -806,6 +799,18 @@ function stripPass(text: string, dropToolCallEmission: boolean, requestText: str
     // attestation gate above, the drift pass proves its names from genuine
     // pairs in the same text (#1760).
     out = stripBareMismatch(stripAttrMismatch(stripDriftPairs(out)));
+    // #5195: opener-less render-tag residue ([attrs incl type="text"/tokens=N]
+    // > ref-run). Runs AFTER lone-open/mangled-open above so an INTACT <name …>
+    // opener is already gone — this fires only on genuinely opener-less residue
+    // and cannot disturb #644 (whose opener LONE_OPEN removes first, leaving no
+    // attrs>ref span). A trailing close, when present, was already peeled by
+    // LONE_CLOSE above, so this drops just the attrs>ref half. Placed AFTER the
+    // #5175 drift chain too: OPENERLESS_REF matches the attrs>ref half INSIDE an
+    // intact drift pair, and running it earlier would eat that half and strand
+    // the close — the pair could never re-form, destroying the drift rule's
+    // intact-pair precondition (verified both landing orders of the #5216 ×
+    // #5178 compose).
+    out = out.replace(OPENERLESS_REF, "");
     return stripBiliArtifacts(out);
 }

3. 当前 head 独立重跑(与你的刷新一致)

合成树 无 hunk 有 hunk
A(#5216 先) issue5175 套件 58 测试中同样 4 红(drift cats / signed-thinking / streaming parity / attr+mismatch) 窄对 63/63;tag-echo / chunking-determinism / verif-tag-echo-filter / e2e-tag-echo 四套 102/102;tsc exit 0
B(#5178 先) 同样 4 红 git apply 零冲突,63/63;最终 filter 文件与 A 逐字节相同

交接(不变)

第二落地者携带上面这份 hunk。 合并顺序仍等 owner 拍板(floor 39 的决策请求有效;推荐不变:先合 #5216——当前自身套件全绿且已同步到最新 master,#5178 rebase 时带上此 hunk)。八问结论沿用 floor 39,仅 head sha 更新为 659f8f104,其余各项(机制/影响面/回归面/无配置变更)经本轮重验均不变。

…ext gate gaps (reviewer supplement)

Reviewer supplement on top of 0437236 (same scope, no design change):

- tag-echo-filter.ts: ECHO_RESIDUE opener-less branch now tolerates
  JSON-escaped quotes (as they appear in RAW wire strings) and its stale
  'logged, not dropped' doc wording is replaced with the post-change
  semantics (gate sites engage the strip pass).
- stream-openai.ts / stream.ts / stream-google.ts / adapter-responses.ts:
  whole-text engagement gates that only tested containsRenderTagText now
  also test containsEchoResidue, so bare opener-less residue (no <name head
  for RENDER_TAG_DETECT) is stripped at every whole-text site instead of
  forwarded verbatim. The four adapter-responses gates guard
  whole-item/whole-part events; delta paths are untouched.
- tests/issue5195-openerless-residue.test.ts: pins the raw-JSON-escaped
  forms (positive + negative control).

Verified locally: targeted tag-echo suites 153/153 pass, tsc --noEmit clean.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b) Review done → APPROVE posted on PR #5216. Reviewer supplement 1f61a5b2d pushed to the PR branch (escape-tolerant echo-residue predicate + whole-text gate gaps closed + regression pin), PR body's false prepare-scrub sentence corrected with probe evidence (#1357 inbound doctrine keeps leaked turns reaching the model until folded). Local targeted suites 153/153 + tsc clean; awaiting CI green on new head 1f61a5b. Owner decisions A (streaming lane) / B (injection-side root cause) stay open in #5195, not blocking merge.

@ranxianglei ranxianglei left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Review conclusion on head 1f61a5b2d: code verdict = APPROVE-equivalent. Posted as a comment-style review because GitHub rejects a formal APPROVE from this account with 422 "Can not approve your own pull request" (bot and PR share one credential) — a human/owner click of Approve is the only remaining mechanical step.

What changed since the last reviewed head

Delta versus 659f8f104 is exactly one commit — reviewer supplement 1f61a5b2d (escape-tolerant echo-residue predicate; whole-text engagement gates closed in stream-openai / stream / stream-google / adapter-responses; raw-JSON-escaped regression pin; doc-comment fix). No author changes landed since; findings F1–F3 of the prior review round are all resolved by that commit, and the PR-body correction (prepare-scrub claim → probe-verified inbound note, #1357 doctrine) is in place. Nothing new to audit beyond that commit's own review coverage.

Verification on this head

  • Direction vs #5195 triage: producer is the model mangling a copyable literal; bili's gap was whole-text coverage for opener-less residue. Closed with the narrow high-confidence signature (anchor attr + ref glued to terminator); live streaming lane deliberately untouched (chunk-split determinism invariant; owner decision A open on #5195).
  • Regression inventory over touched paths: OPENERLESS_REF sits after LONE_OPEN/MANGLED_OPEN/LONE_CLOSE, before attested-gated DEAD_REF_CLOSE; whitespace preserved per #644; idempotent fixed point. Neighbor suites (tag-echo, chunking-determinism, issue2023, issue2190, loop-strict-echo{,-responses}, verif-tag-echo-filter): 153/153 pass locally; tsc --noEmit clean.
  • Tests have teeth: wrong attr value, anchor-without-ref, real HTML element, bare refs among words, well-formed full tag, multi-ref-run decomposition, idempotency, and (new) escaped-form audit pins — positive and negative rows both.
  • CI on this head (mirror cache, not polled): 0 failed; static-gates / mysql-contract / pi-subagents-guard / test-import-graph / docs-coverage success; full test shards in flight — authoritative green is theirs to confirm.
  • No kernel/ touch, no config surface, no version change. Authorization boundaries clear.

Open items (not blocking this PR's code)

  1. Landing order vs #5178/#5175 — documented in-thread (floors 34/39/41): once #5178's drift chain lands, OPENERLESS_REF must run AFTER it (v3 hunk, order-agnostic, verified on both composed trees). Owner decision: recommended option A — merge #5216 first, #5178 carries the hunk on rebase.
  2. Owner decisions A/B on #5195 (streaming-lane fix; injection-side root cause) remain open upstream; neither is addressed here by design.

判定:代码可合并 —— 等 CI test 分片全绿 + owner 点 Approve;合并顺序按上条等 owner 拍板。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b) Review lane done on new head 1f61a5b → full conclusion posted as a comment-style GitHub review (pullrequestreview-5726980719): code verdict APPROVE-equivalent (delta = only reviewer supplement 1f61a5b; CI 0 failed, test shards in flight; no kernel touch). Correction to my earlier completion report: the formal APPROVE was never actually posted — GitHub rejects self-approval from this account (422, bot and PR share one credential); a human/owner Approve click is the remaining mechanical step. Open items restated in the review: landing order vs #5178/#5175 (floors 34/39/41, recommended option A) and owner decisions A/B on #5195 — both await owner, neither blocks the code.

ci-check added 3 commits November 1, 2026 16:39
…nels (#5195 family)

Same symptom as #5195/#5216/#5175 (ACP metadata residue in client-visible
assistant text), new shape: a bare kernel ref plus one stray letter
(m01464b) alone on the FIRST LINE of an assistant text block — no tag
prefix, no attribute text, so neither stripAcpTags nor the tag-echo filter
has a signature to match against. Reported via dsh desktop / OpenAI lane
(deepseek-flash); the residue arrives as ordinary delta.content and dsh
persists it verbatim, so every resent history re-exposes it.

Fix (message-boundary shape + position, per issue suggestion):
- streaming: createBareRefHeadFilter — first-line-only guard, engages only
  on m/mN heads, holds a bounded candidate prefix (<=8 chars) until newline
  or EOF, drops the whole line (incl newline) iff it matches
  ^m\d{4,6}[a-zA-Z]?$ byte-exact, else releases verbatim and disarms for
  the rest of the stream. Deterministic across any chunk split (decisions
  only on newline/EOF); outermost in every text-channel chain (loop
  adapters openai/anthropic/google/responses + plugin chat/responses
  pipes) so it sees final outgoing bytes. Fast-path gates gain a
  mayStartBareRefHead clause — without it, small SSE chunks bypass the
  state machine entirely (verified: leaks even at chunk size 1).
- whole-text: stripAssistantProse = stripBareRefHead(stripAcpTags(...))
  applied at assistant-output fields only (wire strippers' text/content/
  part.text sites + non-stream loop rewriters); reasoning/thinking
  channels stay byte-verbatim (#1960/KDD#10) and tool-call arguments are
  untouched (#1039). stripAcpTags itself is unchanged — the position
  rule stays out of a function whose contract is anywhere-in-text.
- plugin done-event gates gain containsBareRefHeadWire so full-text done
  payloads carrying the shape get rebuilt too.

Width bound \d{4,6}: refs start 4-digit, 999999 messages exceeds any
realistic session; strict byte-0 (no leading whitespace) and strict \n
(no CRLF tolerance) keep false-positive risk negligible — corpus scan of
8222 shipped fields shows zero collisions even for the looser variant.

In-pass minor fix (labeled): stripGoogleChunk header comment claimed
thought parts were stripped; the code excludes them by design (#1960) —
comment corrected to match code fact.

Tests: tests/issue5259-bare-ref-head.test.ts (bidirectional rows per
#3617, streaming split-determinism property over every chunk boundary,
scope-boundary rows pinning what stays observable).
…nxianglei/billion-context into wt-5259-bare-ref-head

# Conflicts:
#	src/loop/adapter-responses.ts
#	src/stream-google.ts
#	src/stream-openai.ts
#	src/stream.ts

This branch has not been deployed

No deployments
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 pi scope/wiring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text

2 participants