Skip to content

fix: strip model-emitted bili internal artifacts from prose streams - #1645

Closed
ranxianglei wants to merge 2 commits into
masterfrom
2026-09-29_bili-artifact-echo-strip
Closed

ranxianglei wants to merge 2 commits into
masterfrom
2026-09-29_bili-artifact-echo-strip

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #1635

Problem

Bili appends internal control data to outbound bodies (the chain checkpoint tag <bili-chain .../> on every processed request; summary handoff lines on the Responses wire). Once any of those strings enter the model's context — by injection, by history replay, or even by the user quoting them while debugging — the model can echo them back in its own output, and nothing stripped them on egress. In long sessions this becomes a self-reinforcing cascade: metadata floods the client TUI mid-answer and the conversation stalls.

Attribution (evidence-graded, per the thread agreement)

Shape Source Evidence grade
<bili-chain …/> per-request outbound stamp (stampOutbound, all wires; openai wire appends it as a trailing tag-only user message) source-proven + directly observed by reporter (tag-only user turn seen in-session). Primary trigger on the reporter's lane
[bili] context summary after compaction: generated only by replaceBiliCompactionItems (Responses wire); reporter's lane also has a second candidate origin — the model quoting the literal from dist bundle / debug report text it had read generation point source-proven; whether it reached the reporter's lane is undetermined (both candidates stand)
[Compressed conversation section] kernel SUMMARY_HEADER, visible in rendered blocks / summary carrier → imitation hypothesis (model-side behavior, not source-provable)
bare <summary> / <analysis> prose compress results replayed in the model's own history → imitation hypothesis

Machine-wide log counts from the report: 2304 /v1/responses requests coexisting with ~9400 /v1/chat/completions across two log files — multiple sessions share one log, and before this change no log line could attribute a forward to a session, so per-session attribution was impossible. That gap is closed below.

Changes

Stripping (the fix):

  • src/loop/tag-echo-filter.ts: new createBiliChainTagFilter (stateful, chunk-safe; over-long or unclosed openings released as prose per the [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 rule) and createInternalArtifactLineFilter (swallows column-0 head lines only — indented or quoted occurrences are user content). composeStreamFilters is now variadic; stripAcpTags / containsMarkerLineText / mayStartMarkerLine cover the same shapes on the non-streaming and fast-skip gate paths.
  • All four loop adapters + plugin passthrough sites (chat/responses/arg-streams) compose the new filters with per-drop logging ([bili-chain-echo] / [artifact-echo]).
  • src/codex-compact.ts: exports COMPACT_ECHO_PREFIX so the filter set derives from the single source of truth instead of a second literal (guard and carrier can no longer drift apart).

Auditability (requested in-thread):

  • Forward log line now carries the session id: forward GET → url [sid] — the missing correlation key the reporter needed to attribute forwards to sessions post-hoc.
  • Both outbound-append sites emit structured audit lines: kind=chain-stamp wire=… role=user shape=… len=… (debug, per stamped egress) and kind=compaction-summary-replace … replaced=N dropped=N (info). Every injection bili makes is now attributable to a session.

Invariants preserved: tool-call arguments stay byte-exact verbatim (#1039 — pinned by test); <summary>/<analysis> prose is preserved (not bili-owned, would corrupt legitimate content); no request-body / wire-shape changes (response streams only); both compression modes covered (proxy mode via the adapters, plugin mode via the plugin-channel filters).

Behavior changes (old → new)

  1. New stripping: model-emitted <bili-chain .../> tags and column-0 [Compressed conversation section] / [bili] context summary after compaction: lines in prose streams (content/reasoning/summary text, streaming + non-streaming, all four wires) are now removed before reaching the client/agent renderer. Previously they passed through verbatim. This is the intended fix for 【bug】压缩元数据被当作正文输出:模型复述摘要而非回答用户,长会话随之中断 #1635.
  2. Forward log line: forward GET → https://… → forward GET → https://… [session-id] (non-session forwards log -). Log format change.
  3. Compaction-replace log line: [sid] replaced N echoed bili compaction item(s) with summary handoff message(s) (plus optional , dropped M legacy marker item(s) suffix) → [sid] [chain] outbound-append kind=compaction-summary-replace wire=responses role=user shape=message replaced=N dropped=M. Same level (info); structured format for grep-ability.
  4. New debug log line per stamped egress: [sid] [chain] outbound-append kind=chain-stamp wire=<wire> role=user shape=message|merged-part len=N. New output at debug level (this is why three chain-checkpoint tests' exact-count assertions were re-pinned — see Tests).

config surface

None changed. No new config fields, env vars, or CLI flags.

Tests

  • New tests/issue1635-bili-artifact-echo.test.ts: 22 regression tests — real renderChainCheckpoint tags stripped non-streaming and char-by-char/chunked streaming; over-long/unclosed openings released as prose; truncated echoes dropped at flush while a lone < survives; column-0 vs indented/mid-line head handling; JSON-escaped \u003cbili-chain gate detection; variadic composition stats + 2-filter backward compat; tool arguments untouched ([bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039).
  • Re-pinned three chain-checkpoint assertions that counted [chain] log lines exactly: plain-request tests now expect exactly the new debug kind=chain-stamp line; stale-unmatched expects verdict line + stamp line. Protective intent unchanged (any unexpected chain processing still fails the count).
  • Pre-flight on rebased head (master @ e8a15c4, v0.1.171): typecheck ✓ · full suite 3196 pass / 0 fail / 16 skip ✓ · build ✓

Related: #1641 (lenient isCodexClient UA substring fallback — separate trade-off-narrowing item, tracked there, not included here).

ework-agent added 2 commits September 29, 2026 12:09
…1635)

The kernel's chain checkpoint tag (<bili-chain .../>) and the internal summary artifact head lines ([Compressed conversation section] / [bili] context summary after compaction:) are injectable control data, but a model that saw them once could echo them back in its own output and nothing stripped them on egress. In long sessions this became an echo cascade: metadata flooded the client TUI mid-answer and the conversation stalled (#1635).

- tag-echo-filter: new createBiliChainTagFilter (stateful, chunk-safe; over-long or unclosed openings released as prose per #644) and createInternalArtifactLineFilter (column-0 head lines only — indented or quoted occurrences are user content); composeStreamFilters is now variadic; stripAcpTags / containsMarkerLineText / mayStartMarkerLine cover the same shapes on the non-streaming and gate paths
- all four loop adapters + plugin passthrough sites (chat/responses/arg-streams) compose the new filters with per-drop logging
- codex-compact: export COMPACT_ECHO_PREFIX so the filter set derives from the single source of truth instead of a second literal
- server: forward log line now carries [session-id]; both outbound append sites emit structured [chain] outbound-append audit lines (kind=chain-stamp debug per stamped egress; kind=compaction-summary-replace info), making every bili injection attributable to a session

Tool-call arguments stay byte-exact verbatim (#1039 invariant); <summary>/<analysis> prose is preserved.
22 tests: real renderChainCheckpoint tags stripped non-streaming and char-by-char/chunked streaming; over-long unclosed openings released as prose; truncated echoes dropped at flush (lone '<' kept); column-0 head line swallowing vs indented/mid-line preservation; JSON-escaped gate detection; variadic composition pipeline-end stats + 2-filter backward compat; tool arguments untouched (#1039).

Also re-pin three chain-checkpoint 'no [chain] log' assertions to expect exactly the new kind=chain-stamp egress audit line — their intent (no unexpected chain processing on plain/stale requests) is preserved.
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-09-29_bili-artifact-echo-strip (50a26a4)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.1592-pr.1645.d6a5b36

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-pr1645.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] 🏷 PR review — #1645 (fix #1635)

① 原 issue / 方向关 — 层次正确、范围正确。
#1635 在线程里拆成三层:stamp 注入(触发层,owner 决策区 —— 本 PR 未动 ✓)、egress 剥离缺口(放大器层 —— 本 PR 的目标 ✓)、分类翻转(良性 —— 不涉及 ✓)。按形态剥离、与来源无关(注入/历史回放/用户调试引用命中同一组形态门),符合线程共识;单一出处常量(COMPACT_ECHO_PREFIX、FORGED_SUMMARY_HEADER 现从 src/codex-compact.ts 导出)落实了报告人第 4 点;审计行 + forward 行会话 ID 落实第 5 点与关联键诉求。<summary>/<analysis> 正确排除(非 bili 自有内容,剥了会腐蚀合法正文 —— 有测试钉住)。

② 回归审查 — 触碰的既有行为清单及逐项结论:

  1. 四条 wire 的正文出站(streaming + non-streaming):新增剥离严格自闭合 <bili-chain …/>(名字派生自 CHAIN_TAG,跨度受解析器 512 上限约束)与列首 [Compressed conversation section] / [bili] context summary after compaction: 头行。有意变更,已披露(fix: keep system prompt byte-stable for prefix cache #1)。零误伤边界已核实并有测试钉住:缩进/行中保留、超长未闭合按 [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 规则放行、结尾孤 < 保留。非流式路径全部汇入同一收口(stripAcpTags,经各 wire 的 strip*Text 覆盖 stream.ts/stream-openai/stream-responses/stream-google),无遗漏 wire。
  2. 工具调用参数:不变。核实 pipePluginResponsesWithStrip 的 argStreams 只承载 response.reasoning_summary_text.delta;function_call_arguments.* 仍逐字转发(plugin.ts:2301 注释原样)。新测试用真实 REAL_TAG + <summary> 字面量喂 stripResponsesText 钉住 [bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 不变量。
  3. forward 日志行:追加 [session-id](passthrough 打 -)—— 已披露(fix: address all code review findings (condense, routing, session-id, dead code) #2),前缀兼容既有 grep。
  4. compaction-replace 日志行:改为结构化 kind=compaction-summary-replace …,级别不变(info)—— 已披露(Use kernel renderTags:"text-only" + forbid tag echoing in prompt #3);grep 旧措辞的脚本需更新模式。
  5. 每次 stamped egress 新增 debug 行(kind=chain-stamp … len=Δ)—— 已披露(feat: OpenAI Responses API support (/v1/responses) #4);连带三处 chain-checkpoint 重钉,已核对保护意图未削弱(精确计数 + level + kind 断言;stale-unmatched 仍要求 verdict+stamp 两行齐全)。
  6. fast-skip 门(mayStartMarkerLine 扩展):只会让更多 chunk 走过滤器而非 raw 直通,严格更安全。顺带修掉一个既有缺口:responses 插件通道 reasoning-summary 的门(plugin.ts:2425)原先完全没有 mayStartMarkerLine,reasoning_summary_text.delta 里的完整 marker/产物行可绕过过滤 raw 直通 —— 本次一并堵上。sawStrippedEcho 现在也计 chain/artifact 丢弃,degenerate-turn 检测能看见由新形态构成的纯回声轮次(与既有 tag/marker 处理一致)。
  7. composeStreamFilters 变参化:双过滤器向后兼容有测试钉住;handoff 消息文本字节不变(常量承载同一字面量)。
  8. wire 形状:请求体零改动;响应事件类型/id/顺序不变,仅文本字节被移除。两种压缩模式均覆盖(proxy 走 adapters,plugin 走 pipe 函数)。

③ 代码审查。 两个状态机均 chunk-safe:chain-tag 过滤器持有尾部有界(≤ CAP = 512 + open 长度,与 MAX_CHECKPOINT_CHARS 对齐),超长未闭合开标签流中放行、截断回显 flush 时丢弃、孤 < 保留;artifact-line 过滤器的持有量被最长头前缀分歧界死。stripAcpTags 内替换顺序(LONE_CLOSE→TRUNC→BILI_CHAIN→ARTIFACT_LINES→MARKER_LINE)交互安全:剥掉标签可能暴露出列首头行,后续 ARTIFACT_LINES 遍会接住,行为自洽。一处已接受的非对称:非流式正则排除属性值内的 >([^<>]),流式找第一个 /> —— 模型杜撰的属性内含 > 的病态标签会在流式被剥、非流式漏过;真实载体/回显形态的属性值(uuid/hex/数字)不含 >,两条路径对一切真实输入一致,最坏只是病态杜撰标签的视觉残留。不阻塞。

④ 测试有效性。 断言有牙:用真实 renderChainCheckpoint 输出而非手造串;逐字符 + 7 字节 chunk 边界;JSON 转义 \u003c 门检测;变参组合的丢弃顺序 + pipeline-end 统计;每条「不得剥」规则(缩进/行中头行、超长未闭合、<summary>/<analysis>、工具参数)都有显式钉子。重钉是精确计数 + level/kind 断言,不是放水。原 bug 回归时这套测试抓得住。

⑤ diff 整洁度。 11 文件 / +552 −35,两个聚焦 commit,无跑题文件、无锁文件/生成物噪音。

Pre-flight(本地在 head 50a26a47 重跑;base 即当前 master e8a15c4d,无需 rebase): typecheck ✓ · 全量 3196 pass / 0 fail / 16 skip ✓(与 PR 声明完全一致)· build ✓(dist-import-annotations OK,dist/index.js 保持零依赖)。CI:13 项检查 12 绿,含 e2e、hermetic registry、全部真客户端 lane。唯一红灯:test (ubuntu-latest, 22) —— tests/issue1086-chain-self-exemption.test.ts T3 的行为断言全部通过后,finally 里的 rmSync 清理抛 ENOTEMPTY … rmdir '/tmp/bili-chain-t3-dwxdci/openai';与本 diff 无关(PR 未触碰该文件),本地 Node 22 四连跑全绿,且同文件 663 行早已带 maxRetries 而 317 行是裸调用 —— 属既有的清理竞态类问题,已立 #1646(全树 91 处同类裸调用,单一根因)。建议重跑该 job。

结论:mergeable(待 ubuntu-22 job 重跑转绿)。合并操作按仓库规则留给人工。

一句话中文摘要:本 PR 修复模型把 bili 内部产物(<bili-chain> 戳与两条压缩摘要头行)复述进正文流的问题——按形态剥离(严格自闭合标签 + 仅列首头行)、常量单一出处、补齐出站审计与 forward 日志会话 ID,方向正确、回归面逐项清点且全部披露、测试有牙;唯一红灯是无关测试文件的清理竞态(已立 #1646),重跑该 job 绿后可合并。

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.

【bug】压缩元数据被当作正文输出:模型复述摘要而非回答用户,长会话随之中断

1 participant