Skip to content

fix: keep prose between paired render tags in tag-echo filter (#1720) - #1721

Merged
ranxianglei merged 1 commit into
masterfrom
2026-09-30_paired-prose-swallow
Sep 30, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-09-30_paired-prose-swallow

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #1720

What & why

PAIRED's inner group was ([^<>]{0,64}) — meant to hold a tag ref (m00123) — but [^<>] also matches newlines, so any up-to-64 non-angle-bracket characters (ordinary assistant prose included) between a leaked open tag and its close were deleted along with the tags by stripAcpTags(). Real-world symptom from the issue: a user pasted a leaked tag into the input box and the following answer text vanished.

The streaming filter (createTagEchoFilter) had the same hole on a different path: an attrs-bearing LONE_OPEN entered swallow-until-close mode and dropped everything up to the close within the swallow budget — prose included.

Changes (src/loop/tag-echo-filter.ts only)

  • Narrow PAIRED's inner group to the real ref shape: \s*m\d{4,}\s*. The kernel emits exactly one form between tags — <acp tokens="…" type="…">mNNNNN</acp> with refs of m + ≥5 zero-padded digits (acp-kernel REF_WIDTH = 5) — so \d{4,} is a safe lower bound. Per the issue's caveat, PAIRED keeps no g flag: the filter drives it with exec() on a sliding buffer, and a g flag would accumulate lastIndex across chunks.
  • Streaming: when the close tag arrives during a swallow, the span is now decided by the body's shape — ref-shaped body → drop the whole span (as before); non-ref body → release the prose and drop only the two tag fragments. Attested imitation swallowing (releases=false, odd-quote / wrapped-turn) still discards whatever the body is.
  • Supporting: looseCloseEnd split into looseCloseSpan(s) (+ thin wrapper) so the filter can see where the close tag starts.

Net effect on both paths: paired tags around prose keep the prose; paired tags around a ref are still stripped completely.

Behavior change (old → new)

  • Old: any ≤64 non-angle-bracket chars (newlines/CJK/prose included) between a paired acp-like tag were deleted together with the tags — whole-text path via PAIRED, streaming path via LONE_OPEN→swallow for spans ≤ ~80 chars.
  • New: only a ref-shaped body (m + ≥4 digits, optional surrounding whitespace) is deleted with the tags; any other content between paired tags survives with just the two tag fragments stripped. Strictly less deletion than before — no input that previously survived is now deleted. Genuine kernel echoes (≥5-digit refs, incl. whitespace-padded) are unaffected. Tool-call arguments remain byte-exact by design (invariant block atop the file).
  • Side effect: short non-ref text like m1 / m001 / ref between paired tags now survives as plain text instead of being eaten — intended, since the kernel never issues such refs.

Config surface

None — no config fields, env vars, or CLI flags added or changed.

Tests

Pre-flight

  • npm run typecheck — pass
  • npm test — 3283 pass / 0 fail / 17 skipped (e2e-gated skips)
  • npm run build — success

The PAIRED regex's inner group ([^<>]{0,64}) was meant to hold a tag ref
but matched any up-to-64 non-angle-bracket characters, including newlines
and ordinary assistant prose; stripAcpTags then deleted the whole match,
silently eating answer text that happened to sit between a leaked open and
close tag. The streaming filter had the same hole via its LONE_OPEN ->
swallow-until-close path for spans within the swallow budget.

Narrow PAIRED to a real ref shape (m + >=4 digits, optional surrounding
whitespace) and make the streaming swallow release a non-ref body at close
time, dropping only the tag fragments. Ref echoes are still stripped
completely on both paths; imitation swallowing is unchanged.
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-30_paired-prose-swallow (cc6391a)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.1674-pr.1721.3e83bfb

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

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr1721.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] 🏷 收到,开始 review。这是 #1720 的修复 PR——先拉分支核对代码与 CI,再按规范过回归审查(既有行为清点)、代码审查、测试有效性与 diff 整洁度,结论稍后回帖。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review 完成:可以合并(merge 仍由人执行)。按 PR Review 规范逐项如下。

① 原 issue 审核(方向关)

#1720 属实且方向正确:PAIRED 的 inner group ([^<>]{0,64}) 中 [^<>] 匹配换行,把「开标签 + ≤64 字符任意内容 + 闭标签」整体当作标签引用删除——修的是产生缺陷的正则本身(收窄为真实引用形状),不是在中途拦截症状;流式路径的 LONE_OPEN→swallow 分支是同一根因的另一条通路,一并处理。符合 root-cause-not-symptom。issue 的注意事项(PAIRED 不加 g,filter 用 exec() 驱动滑动缓冲区)已被遵守——PAIRED 保持非全局,g 只在整文路径的使用点(line 221)附加,与修复前一致。

② 回归审查(既有行为清点)

diff 触碰 src/loop/tag-echo-filter.ts,该文件承担的既有行为逐项判定:

# 既有行为 结论
1 真实内核引用回显(m+≥5 位零填充数字,含前后空白形式)被完整剥除 保持 — \s*m\d{4,}\s* ⊇ 全部真实引用;新增测试钉住空白填充形式
2 LONE_OPEN / LONE_CLOSE / TRUNC_OPEN / TRUNC_CLOSE / MARKER_LINE / stripBiliArtifacts 各 pass 保持 — diff 未触碰
3 流式 plain-open swallow(#644:超 80 字符预算释放为正文) 保持 — 只改了「闭标签到达」分支,预算溢出逻辑未动
4 attested imitation swallow(BROKEN_ATTRS / wrapped odd-quote,releases=false,预算 4096,payload 绝不外泄) 保持 — 新条件为 REF_LIKE.test(inner) || !swallowReleases,!releases 时仍整段丢弃,与旧行为逐字节一致
5 flush() 对未闭合 swallow 的处理(丢弃残余) 保持 — 未动
6 stats()/dropped 记账与 degenerate-turn 触发 保持 — prose 情形现在 drop(close 片段)而非 drop(整段),droppedAny 仍为 true,语义不变
7 tool-call arguments byte-exact 不变量(#1039,文件顶部 invariant 块) 保持 — diff 只改作用于模型 prose 字段的两条路径,arguments 路由未动
8 wrappedSpan() 对 looseCloseEnd 的消费(-1/end 语义) 保持 — 拆成 looseCloseSpan(s) + 薄包装 looseCloseEnd(s),返回值逐一对应
9 两种压缩模式(plugin/proxy,§6) 对称 — filter 对出站模型 prose 在两种模式下走同一函数,无模式专属路径被触碰

严格单调性验证:new PAIRED 的匹配集 ⊆ old PAIRED 的匹配集(引用形 body 必然是 ≤64 个非尖括号字符),流式侧改动只把「整段丢弃」转为「释放正文 + 只删闭标签」且仅限非引用形 body 且 releases=true 时。没有任何原本保留的输入现在会被删——PR 披露的 old→new 成立。无配置面变更(diff 证实)。

③ 代码审查(主关)

  • 正则收窄正确;REF_LIKE = /^\s*m\d{4,}\s*$/ 锚定全匹配,无误伤面;\d{4,} 相对内核 REF_WIDTH=5 是安全下界,短非引用文本(m1/m001/ref)留下是有意副作用且已披露。
  • looseCloseSpan 拆分干净,唯一消费点迁移完整(grep 确认无遗漏调用方)。
  • 发现一个残留边角(非阻塞):prose body 内再嵌套一个带属性的 acplike 开标签、后跟单个闭标签时(如 <acp x="1">x<acp y="2">y</acp>),流式路径会把内层开标签片段作为正文放行(输出含裸 <acp y="2">),而整文路径经 LONE_OPEN/LONE_CLOSE 会清掉它——两路在该形状上不一致。查证了旧代码在同一输入上同样不一致(旧:流式删光 vs 整文留 x),属既有类别且仅外观噪声(无用户内容丢失,方向符合 [bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 的内容保全优先)。未顺手修的原因:直觉修法(对释放的 inner 再跑一遍整文剥离器)会在 inner 以截断标签头结尾时引入 TRUNC_OPEN 端锚交互的新不一致,风险大于这个极罕见角度的收益——留作流式状态机后续改进的边界注记,不在本 PR 扩面。

④ 测试有效性审查

⑤ diff 整洁度

4 文件 +70/−16,单 commit,base 即当前 master(已验证祖先关系,无需 rebase);每一行都与本 PR 目的相关,无锁文件/生成物/空行噪音。干净。

证据汇总

一句话中文摘要:修复了 tag-echo filter 中 PAIRED 正则(及流式吞咽状态机)把配对标签间最多 64 字符助手正文当标签引用一并删除的数据损坏问题——收窄为真实引用形状、流式按内容形状判定,既有行为逐项核对无回归、测试有牙且两路 parity 钉住,本地+CI 全绿,可以合并。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

缺陷:PAIRED 正则吞掉最多 64 个字符的助手正文

1 participant