Repository navigation
Description
Environment
- billion-context:
0.1.209(npm) - Host: Pi
1.4.0, native Pi extension/plugin lane - Wire/model: OpenAI chat-completions; observed with
kimi-coding/k3,kimi-coding/k3-256k, and once withzai-coding-cn/glm-5.3-flash - OS: macOS
- Default render-tag behavior (
diagnostics.renderNonenot enabled)
Symptom
A normal assistant turn is followed by another assistant text block whose persisted content starts with the tail of a canonical ACP render tag, without the opening <acp tokens="..." prefix:
" type="text">m00106
修复完成,编译验证通过。
...
The full injected metadata shape is:
<acp tokens="196" type="text">m00106</acp>Thus the visible residue is the suffix from the opening tag through the message ref. m00106 is a valid ACP message ref in that session.
In the clearest reproduction, the preceding turn completed normally:
- assistant said work would be applied,
- two edits succeeded,
- targeted compile returned
EXIT:0, - a diff check succeeded,
- the next final assistant text began with
" type="text">m00106.
Pi persisted that value as message.content[].text; it was not merely a TUI rendering artifact. Pi's OpenAI stream parser accumulates upstream delta.content directly into text, so the text had already reached Pi as ordinary content.
Local census
A local scan of Pi assistant text fields found 93 matching type="text">\s*m\d+ occurrences across sessions from 2026-10-27 through 2026-10-30:
- 90 with
kimi-coding/k3/k3-256k(stopandtoolUseturns) - 3 with
zai-coding-cn/glm-5.3-flash
Spot checks confirmed visible client-facing leaks, not merely examples inside tool output. The scan was limited to persisted host history; raw SSE was not captured.
Expected
ACP render metadata injected into the request history should not reach client-visible assistant text in whole or in part.
Actual
The canonical opening-tag suffix plus ref reaches the client and is persisted as assistant prose:
" type="text">mNNNNN
Because the leading <acp tokens="..." is absent, existing tag-shaped matchers do not recognize the fragment.
Notes and uncertainty
I did not have a raw upstream SSE dump for these turns, so I cannot prove whether the model emitted only the suffix or emitted a fuller tag that an intermediate path partially stripped. What is established is:
- billion-context injects the canonical
<acp tokens="..." type="text">mNNNNN</acp>metadata; - the exact suffix of that internal metadata becomes persisted client-visible text;
- the default output filter does not remove this suffix.
A possible targeted direction is to recognize the exact residue type="text">\s*m\d+ as a high-confidence canonical signature, at least for audit/logging if unconditional removal is considered too risky. Another direction is to avoid making the full XML literal so copyable in the prompt.
Workaround
diagnostics.renderNone: true (or ACP_RENDER_NONE=1) prevents the injection and therefore the observed leak, but disables ref-based compression. It is a workaround, not a fix.
Related
- 偶尔重复输出 <acp tokens="177" type="text">m00155</acp> #206 — full render tag repeated in visible output
- tag-echo filter leaks bare
<ac/<ac>render-tag fragments to the client (bare truncated open tag survives both process() and flush()) #1755 — bare<ac/<ac>fragments - [bug] tag-echo 在 0.1.194 仍放行一条 #2190 形态残渣(mNNNNN</acp 内联在正文行首,#2229 治理后复现) #2862 —
mNNNNN</acpresidue - [Bug] Drifted open-tag names (acpt/acct/accent…) and mid-text draft envelopes reach the client verbatim — two thinking-leak shapes #5175 — drifted open-tag names and draft envelopes
- [DSH plugin mode] tag-echo strip leaves bare
</atem:invoke></atem:function_calls>fragment; assistant turn ends as text (0.1.209, refs #1755) #5071 — related post-strip residue in another host
Activity
ranxianglei commentedon Oct 30, 2026
(ai: vllm-qwen/qwen3.8-27b)
Follow-up from my triage reply above — I implemented the safe portion and opened PR #5216: #5216
What ships:
stripAcpTags(whole-text / prepare-layer normalizer) now strips the opener-less residue signature (type="text">mNNNNN/tokens=N>mNNNNN, optionally + close), and the echo-residue audit predicate logs any occurrence that escapes. Regression-pinned intests/issue5195-openerless-residue.test.ts. Rebased onto current master; targeted suites green (152 pass incl. the verif replay property judge),tscclean.What does NOT ship (and why): the live streaming filter (
createTagEchoFilter, the path your Pi/OpenAI lane actually renders) is deliberately untouched. The residue has no<head to latch onto, so a chunk boundary splitting inside the attr (t|okens="5">mNNNNN) is indistinguishable from prose, and holding partial anchor-words (type/token/to) would delay ordinary prose. A filter-level streaming fix is therefore either nondeterministic (violates chunk-split determinism — worse than the bug) or a prose-latency tradeoff. Both are owner decisions, consistent with your earlier caution on stripping here (#1755). As a side benefit, when Pi resends history containing a leaked past turn, this PR's prepare-layer scrub removes it before it reaches the model, which helps break the echo loop on later turns.Durable root cause is upstream of the filter: the tag is a copyable literal the model imitates/mangles. The real fix belongs at the injection/kernel side (your direction "b": make the shape less imitable) — separate, owner-gated.
结构化八问
<name头,残留type="text">mNNNNN(可带</acp>)这种"无头"片段,泄漏进客户端可见的助手文本并被 Pi 持久化。<name头);根因是渲染标签在 prompt/历史里是可照抄的字面量,模型模仿时偶发截断。renderNone),非数据丢失 → 非 p1。待你拍板的两点(不阻塞本 PR 合入):
合并判定:本 PR 为窄范围
fix:、带回归钉、rebase 到最新 master、CI 待跑绿;按仓库规则走人工合并。ranxianglei commentedon Oct 30, 2026
(ai: vllm-qwen/qwen3.8-27b)
标签:
bugpiscope/wiringp2—— 缺陷类、Pi/OpenAI 车道、属协议接线(出站 tag-echo 过滤层,非 kernel)、有绕行(renderNone)且非数据丢失/不可用故 p2。loyalchiiina commentedon Oct 31, 2026
+1 — reproduces here on
billion-context@0.1.209.Environment:
0.1.209(npmlatestis also0.1.209)Symptom: intermittently, an assistant reply is led by the full ACP render envelope as its own line — the whole open tag (carrying
tokens=N,type="text"), the message ref, and the closing tag — before the actual text. The token count / ref vary per turn (e.g. refm00312,tokens="612"); it is not every message but recurs across a session.No root-cause addition here — just confirming it is not limited to the Pi/OpenAI lane: the same leak shows up on dsh desktop as well, so the census in #5192 extends beyond a single host/lane. Thanks for tracking it.
ranxianglei commentedon Oct 31, 2026
(ai: vllm-qwen/qwen3.8-27b)
Thanks for the datapoint. Treating this as a reported reproduction (unverified build) rather than confirmed root cause, but two things stand out versus the original report:
<acp tokens=N type="text">mNNNNN</acp>) leading the reply, whereas 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's census was the opener-less suffix (type="text">mNNNNN, head dropped). Those are distinct facets of the same render-tag-leak family.Worth flagging for accuracy: a well-formed full envelope is normally handled by the existing paired-tag strip path (both whole-text and streaming), so if it is genuinely surfacing on dsh that points at a separate facet — e.g. a response path/version the filter doesn't cover — and would need verification against your exact build before attribution (the shape alone isn't enough to pin the mechanism).
Tracking notes:
If it helps pin the dsh facet, a redacted copy of one leaked line (just the envelope + first few chars of the real text) is enough; nothing else needed.
ranxianglei commentedon Nov 1, 2026
(ai: vllm-qwen/qwen3.8-27b)
子缺陷清单更新:第 3 个子形状「bare ref head」(由 #5259 报告,dsh/OpenAI 车道,
m01464b式裸引用行——无标签前缀、无属性文本,既有签名谓词无子串可匹配)已在 PR #5216 修复(新 head8ec002396,commit521f86874):流式createBareRefHeadFilter(每条文本通道链最外层)+ 整文stripAssistantProse;双向测试行 + 全切分点确定性属性测试,本地定向套件 159/159。PR 描述已补该形状的行为变更披露(旧值→新值+原因)。当前状态:opener-less residue ✅(本 PR)、bare ref head ✅(本 PR)、drift-name leak → #5178 在途、注入侧根治 → owner-gated out-of-scope。等 CI + 人工审阅后合入即收口。Punchact commentedon Nov 4, 2026
补充一条 0.1.212 + DSH plugin mode 的数据点:正文里出现的是完整 canonical 信封(不是残片),而 tag-echo 过滤器全程 0 次触发。
环境
0.1.212(本机日志[v=0.1.212];同时段 npm 检查为 latest,无更新)dsh.profile.bundles),Windows 11 x64deepseek-v4.1-flash(DSH 自带 adapter);renderTags=text-only,未开diagnostics.renderNoneacp_status:0 blocks)现象
assistant 正文首行出现完整信封(本会话两处,间隔约 15 分钟):
time=属性(本机 0.1.212 的渲染格式)<DATA_DIR>/session_projcache/sessions/session-<id>.json的response字段以该行开头;同文件lastUser也带同一行(全文 3 处命中)——不是 TUI 渲染问题日志对照(同一会话、同时段)
即:不是剥离后留下残片(#5071 那类),而是完全没被剥离;
createTagEchoFilter在该时段没有 warn。与现有 issue 的差异
type="text">mNNNNNinto assistant text #5192:canonical 开标签的后缀type="text">mNNNNN泄漏,不带time=想确认
time=属性是否落在识别器覆盖之外?复现与材料
renderTags=text-only、DSH plugin mode;偶发,本会话 2 次diagnostics.dumpReq,没有原始 SSE;需要的话我可以开 debug 复现一版并附脱敏日志ranxianglei commentedon Nov 4, 2026
(ai: vllm-qwen/qwen3.8-27b)
Useful datapoint — a few grounded notes before attributing it:
On the
time=attribute. The canonical kernel render tag is emitted byacpTag()(kernel/src/render-refs.ts) as exactly<acp tokens="N" type="T">ref</acp>— there is notime=attribute in the current render format. So an envelope carryingtime=is not a verbatim kernel tag. Two readings: (a) a model-implemented imitation (the model invents/mangles a copyable literal — the same root cause behind the suffix and name-drift facets), or (b) your build renders differently than current master. Either way it's a signal, not yet a confirmed mechanism.Your three questions, as far as I can ground them:
acpt/acctfamily); a canonical-name full envelope is a distinct shape/facet — but same family, so I'm keeping it under this thread rather than splitting it out.time=is not an attr-recognition gap for a full envelope: the paired-tag matcher accepts arbitrary attributes (<name …any-attrs…>body</name>), so a complete envelope should strip regardless of extra attrs. That points away from "identifier missestime=" and toward "the filter didn't run on this path."time=detail, your build appears to diverge from current master, so I'd rather confirm than guess.Efficient next step (you offered it): enable
diagnostics.dumpReqor capture raw SSE on one repro and share a redacted slice — the leaked first line, the surrounding SSE deltas for that turn, and the[v=…]startup line. That tells us definitively whether the envelope reaches the filter input at all (→ wiring/bypass) or arrives in a form the matchers miss (→ matcher gap), and pins the version. Nothing sensitive required.Holding it under this thread as another facet of the same leak family until the mechanism is confirmed.
tax-mux commentedon Nov 7, 2026
One concrete mechanism for this shape, with raw-SSE and log evidence: #5848 — on the OpenAI plugin lane a text-less, non-terminal frame (
delta:{}, e.g. MTPLX progress frames every ~15 tokens) triggersflushTails(), which drops the held<acp tokens="3head alone, so the rest (1" type="text" …>m00039) arrives as prose. Fix: #5849.Related, separate shape (unclosed opening + ref + answer: short answers dropped at EOF, longer ones keep the ref): proposal in #5850, waiting for an owner decision before a PR.
ranxianglei commentedon Nov 7, 2026
(ai: vllm-qwen/qwen3.8-27b)
Thanks — I checked the mechanism against the tree rather than taking it on faith, and the core claim holds up at the code level:
The vector exists. In the DSH plugin lane (
src/plugin.ts), a text-less frame releases held filter tails as prose — the OpenAI path hasif (!hadText && anyPending()) return flushTails() + rawEvent…(~line 3183), with analogues for the other block types (~3211/~3239) and a second lane (~3482). So a non-terminal, content-empty frame (e.g. a progressdelta:{}) does reachflushTails()and would emit whatever partial tag/ref the filter was holding. That's a real, coherent mechanism — and it's a different axis than the one I'd punted on: I deferred the streaming matcher over mid-attribute chunk-split ambiguity, but this is a flush-timing question (when held tails are released), which may be cleaner to fix without the prose-latency tradeoff. Worth treating seriously.What I could not verify: I don't have your raw SSE (it's behind the referenced thread), so I've confirmed the code path exists but not that it's the exact producer of your bytes. A redacted slice — the progress frames around one leaked line — would let me build a minimal local repro to pin it.
On the referenced threads: per the platform's numbering rules I'm not resolving bare cross-thread numbers against GitHub from here, and I can't confirm the status of the three you cite — so please confirm they're real and where they stand (reference info only; I'm not attributing any of it to the owner, and none of it counts as a decision). Two coordination notes:
Holding it under this thread as another facet of the same leak family until the mechanism is confirmed against actual SSE.
ranxianglei commentedon Nov 7, 2026
(ai: vllm-qwen/qwen3.8-27b)
Status for this task — no owner decision has been made yet; everything below is pending.
Shipped (open PR #5216): whole-text/prepare-path strip of the opener-less render-tag residue + extended echo-residue audit, regression-pinned. Through review rounds and a related follow-up the PR has grown to also cover a bare-ref-head facet, and is currently
behindmaster awaiting sync + CI.Corrected this pass: the PR had referenced the wrong issue number in its GitHub-facing text (it pointed at a different, closed issue). I patched the PR body so it now targets this issue correctly (
Closes …) — necessary so the auto-close lands on the right issue at merge.Datapoints triaged (unverified reports, answered with code-grounded findings only, nothing implemented on their basis):
time=-bearing envelope report — grounded finding: the kernel's canonical tag is `` (notime=attribute), so such an envelope is a model imitation or a build divergence, not a verbatim kernel tag.Waiting on owner: (A) how to handle the live streaming lane — keep-as-is vs prose-latency hold vs injection-side root fix (the flush-timing datapoint adds a "fixable at the flush site" option); (B) whether to file the injection-side root cause (make render tags less imitable) as separate gated work. Full-suite verdict = remote CI on the PR head.
Keeping all facets under this issue rather than splitting them out.
fix(plugin): don't drain held tags on inert mid-stream frames (#5848)