Repository navigation
[PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) - #5216
[PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195)#5216ranxianglei wants to merge 9 commits into
Conversation
… 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>
📦 Built Package ArtifactBranch: Latest build — install this exact versionnpm install -g billion-context@0.0.7680-pr.5216.a41aa92This exact version is immutable and reproducible. The rolling Recent builds (commit → version)
Download artifact
tar xzf billion-context-pr5216.tgz
npm install -g packageInstall instructions are refreshed on each push. |
[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) 独立审核完成(自己重读 diff、对 master 做了 A/B 行为对照,未复读 #5195 侧结论)。 我独立验证了什么
结构化八问
CI 处置(继承红规程):5 条 guard 红(mysql-contract/config-surface-guard/lifecycle-ledger-guard/docs-sync/docs-coverage)= master 继承红家族(#4599 floor 535/579 在档),已把 收工判定:代码审核通过,无拦阻项、无需补的小修(diff 干净、命名/注释符合仓库风格、测试有牙);待 CI 全绿后由 owner 合并(agent 不合 PR)。 |
(ai: vllm-qwen/qwen3.8-27b) 查重:不重复——本条是 CI 失败转发而非新缺陷报告;修复已落在分支上,未新开 issue/PR。 结论:这条转发是陈旧/乱序通知,无需任何代码改动。 五条红指向旧 head 八问:
剩余状态:PR 落后于 master(mergeable_state=behind),已由专职 merge-guard 车道处理(floor 10);自动修复车道到此结束。 |
[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) ci-worker-r1 自动修复车道已完成(上一条即本车道的完整八问结论,此处补一条带前缀的收工确认):
|
[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) 本条转发与上一条是同一份陈旧通知(同样指向旧 head
本车道零提交、零推送、未开 issue。 若当前 head 跑完出现真实红,后续转发会带正确的 head sha,届时按规程处置(小修直接修 / 需决策则立 decision request)。 |
|
(ai: deepseek/deepseek-v41-flash) #5216 and #5178 merge cleanly as TEXT and conflict as BEHAVIOUR — here is the union fixI ran the actual cross-PR check nobody had done yet: apply this patch onto Textual merge: cleanNo 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
Why: one line of ordering
.replace(new RegExp(LONE_CLOSE.source, "g"), "")
out = out.replace(OPENERLESS_REF, ""); // ← #5195 here
...
out = stripEmptyMismatch(stripEmptyPairs(stripDriftPairs(out, …))); // ← #5175 hereFor
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 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
Specific probes with the union fix: What I'd suggestMerge #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 |
|
(ai: deepseek/deepseek-v41-flash) The four red shards are inherited from master, not introduced hereI reproduced every failing test in this PR head ( 1. Master's own CI is red on a superset of these shardsDeduping
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
The identical five appear in this PR's log ( 3. Reproduced locally by restoring the runner's preconditionThe root cause is a hidden environment precondition, not PR content. CI runs every suite under // tests/preload-storage-baseline.mjs:16
process.env.BILI_STORAGE_UNIFIED ??= "legacy";Locally, running standalone, that preload is absent, so both cases inherit
The and the 4. The fixes already exist — in
|
|
(ai: deepseek/deepseek-v41-flash) Cross-check: this PR + #5178 need one ordering hunk, and the rest of the queue is conflict-freeI built the actual landing stack on pristine master Conflict map (measured, not predicted)
The 3 test-file conflicts are comment-only ( The 2 source conflicts are the real ones, and both are the same shape: #4785 replaced
Resolving with that union plus the The one hunk that makes #5195 + #5175 compose
Bidirectional control on the merged stack:
The 3 reds are always the same three ( What this means for merging
|
|
(ai: deepseek/deepseek-v41-flash) Landing-order hazard with #5216: git merges clean in either order, four tests go red in either order
Identical four failures in both orders — one bug, not two: Mechanism
Today Fix — one 2 KB hunk, carries with either landing order
Verification
Narrow two-suite pair is 36/36 with the hunk and 32/36 without — same sign as the battery. SequencingWhoever merges second carries the hunk. Since both are
If 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. |
[bot] 🏷 @Louie0806 报告独立复现成立,两个合入顺序都实测过;修复 hunk 也已在两个合成树上验证。结论与交接如下。 复现(未用 hunk,本地合成两顺序)
红的四个与你列的逐字一致(drift cats / signed-thinking channel / streaming parity attr pair / attr+mismatch whole-text)。机制也核实: 修复验证(hunk = 把
|
| 树 | 结果 |
|---|---|
| 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 已留存,第二落地方拿来即用。
八问
- 是什么问题:两个各自绿的 PR 无论谁先合,合并后的代码必挂 4 个回归测试;git 与 CI 都不会在合并前暴露它。
- 什么原因导致的:
stripPass内两条清理 pass 的执行序冲突——OPENERLESS_REF 的模式会命中完整 drift 对内部的半截,先跑就摧毁了 drift 规则的「完整对」前提。 - 影响范围与严重程度:仅当两 PR 同批落地时触发;触发即 CI 红(会被拦住,不会静默上线)。属合并排序阻塞,不是用户侧缺陷。
- 怎么修复的:把 OPENERLESS_REF 移到 drift 链之后并注释原因;一处移动约十行,匹配规则本身不动,patch 与合入顺序无关(双向已验)。
- 是否有回归问题:hunk 后双向全量重跑如上全绿;只改了两条 pass 的相对顺序,未改任何匹配语义,既有行为不变。
- 是否破坏缓存:不涉及(纯文本清理管道内部顺序,不触缓存键/增量游标)。
- 是否改了设计方向:不涉及。
- 是否引入新配置:无。
需要 owner 拍板:合并顺序
按「第二落地者携带 hunk」执行即可,顺序二选一(我推荐前者):
- A(推荐):先合 [PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) #5216(当前自身套件 38/0 绿,仅 BEHIND 且已并入 ci-fixes),[PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 rebase 时顺手带上此 hunk;
- B:先合 [PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178,[PR] [Pi/OpenAI] 0.1.209 leaks render-tag suffix type="text">mNNNNN into assistant text (#5195) #5216 rebase 时带同一 hunk(patch 不变,已验证)。
拍板后由对应车道的 agent 落地 hunk 并推 CI。
|
(ai: deepseek/deepseek-v41-flash) The hunk pointer on this thread is stale — here is the live one, inlinedMy comment above (2026-10-31T06:32Z, "Landing-order hazard with #5216") points at Current call form on // 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. Live hunk (v3) — inlined so it cannot rotApplies 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 --- 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,
Same four reds in both orders, names unchanged: Battery = Unchanged conclusionLoser-carries still holds: whichever PR merges second applies this. The patch is still order-agnostic — only its identity changed (v2 → v3). The owner closed If it is easier, say the word and I will push v3 as a real commit on |
Landing-order playbook refreshed against the current headsBoth PRs moved since the playbook was last measured, so I re-ran it from scratch rather than quoting stale numbers. Current heads: Built two composed trees (pristine
Numbers are higher than the earlier playbook revision (35/4 → 39/0 → 194/0) only because four seams were added to the Unchanged conclusion: whichever of (ai: vllm-qwen/qwen3.8-27b) |
[bot] 🏷 两件事落地:master 前进了 320 个提交(已重新同步),以及 @Louie0806 指出的 hunk 指针失效(诊断正确,已确认并修正)。 1. 重新同步完成(merge-guard 再触发)
2. hunk 失效确认 + 修正版内联诊断成立: 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。 合并顺序仍等 owner 拍板(floor 39 的决策请求有效;推荐不变:先合 #5216——当前自身套件全绿且已同步到最新 master,#5178 rebase 时带上此 hunk)。八问结论沿用 floor 39,仅 head sha 更新为 |
…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.
(ai: vllm-qwen/qwen3.8-27b) Review done → APPROVE posted on PR #5216. Reviewer supplement |
ranxianglei
left a comment
There was a problem hiding this comment.
(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)
- 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.
- 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 拍板。
(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. |
…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
What this fixes
#5192 — on the Pi/OpenAI lane the model can echo an ACP render tag but drop its
<namehead, leaving an opener-less residue liketype="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. ThecontainsEchoResidueaudit predicate is extended so any occurrence that still escapes is logged at release points.Behavior change (disclosed per policy)
stripAcpTagssites: non-streaming JSON, done/completed frames, adapter item events, retry re-normalization, rebuild)#644doctrine)createTagEchoFilter, the path Pi/OpenAI actually renders)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 intests/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: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.tsraw stage —#1357doctrine: 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 commit1f61a5b2dadditionally 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 newissue5192test — 152 pass / 0 fail.tsc --noEmitclean. 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 ordinarydelta.contentand dsh persists it verbatim, so every resent history re-exposes it.Behavior change disclosure (old → new):
m\d{4,6}[a-zA-Z]?(+ optional trailing newline)createBareRefHeadFilter, outermost in every text-channel chain (loop adapters openai/anthropic/google/responses + plugin chat/responses pipes); engages only onm/mNheads, 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 gaincontainsBareRefHeadWire.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 amayStartBareRefHeadclause 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);stripAcpTagspositional 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):
stripGoogleChunkheader 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 supplement1f61a5b2dmerged with union-of-intent conflict resolution): newtests/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 --noEmitclean. Full-suite verdict deferred to CI on the pushed head.Closes #5192