Skip to content

acp_status visibility markers: stripMarkerLines removes the head line but leaves the multi-line report body in incoming history (#1029 follow-up) #2789

Description

@iferon44

bili version: 0.1.192 (state dir /root/.local/state/billion-context, proxy via the pi plugin)
Wire: openai chat-completions, proxy mode, no plugin handshake for this lane — a custom provider inside pi (pi-cline extension → upstream https://<private-host>/api/v1/chat/completions), so requests arrive anonymous (prefix-affinity, sess=pfa-…) and compress.visibilityMarkers is at its default (true).

Summary

stripAcpStatusMarkers / stripMarkerLines remove only the marker line (MARKER_LINE). For acp_status the emitted marker is a head plus a multi-line body — buildVisibilityMarker returns \n${icon} [ACP] acp_status result:\n${result.trim()}\n — so the whole renderOverview report (10–40 lines: ACTIVE SURFACE, CONTEXT BREAKDOWN, COMPRESSED BLOCKS, BILLED INPUT, Nudge:, Tip: …) survives the strip in incoming history. A client that persists and replays assistant text (mine does) accumulates those bodies turn over turn.

Evidence (real persisted assistant turn, 5283 chars)

Applied MARKER_LINE + stripMarkerLines from 0.1.192 to the stored text:

  • marker lines matched: 9, removed: 1691 chars
  • left after the strip: 3592 chars
  • still present afterwards: ACTIVE SURFACE, CONTEXT BREAKDOWN, COMPRESSED BLOCKS, BILLED INPUT, Nudge:, Tip: buildStatusReport(...)
  • the log line reports success for the part it handles: stripped N ACP status marker line(s) from incoming history (ephemeral proxy status, issue #1029)

What the client is left with is an orphaned blob with no head:

I'll reassess the current state of the task. Based on the conversation history, the work involved patching the `pi-cline` plugin …
ACTIVE SURFACE: pack=default | host=billion-context 0.1.192 (…)

CONTEXT BREAKDOWN
  0 tool (0%) | 0 text (0%) | 27.1K summaries (100%)

COMPRESSED BLOCKS — 95 active (27.1K summary, 3618.4K original)

  b11 (T1)  474.4K→169  3 msgs  "preflight overflow compress"
  …
BILLED INPUT (upstream usage): 912625 tok · est-view total 516609 tok · ratio 1.8×
Nudge: ACTIVE — EMERGENCY T2 distill: max pending 27093 …, usage 122%

Scale in one session: 95 active blocks / 3618.4K original tokens, and every turn adds another 0.5–1.5K of report body to the replayed history.

Root cause

  • buildVisibilityMarker: for acp_status the marker is multi-line by design (head + \n + result.trim()).
  • MARKER_LINE = new RegExp("^\\p{So}(?:[ \\t])?\\[ACP\\][^\\n]*\\n?", "gmu") — [^\n]* cannot consume the body.
  • stripMarkerLines has no notion of a marker block, and the report body is not wrapped in a sentinel pair (box/cache/rule panels do have OPEN/CLOSE pairs, which is why stripAcpPanelResponsesInput can drop those whole).

Suggested fix (any of these)

  1. Strip the marker block, not the line: emitMarker delivers head + body as one unit, so consume the contiguous report body after the [ACP] … head line (stop at the first line that is not part of the report / at the next marker).
  2. Or wrap the body in an explicit pair (e.g. <acp-status>…</acp-status>) and strip it whole, like the existing panel/report pairs.
  3. Or apply issue ACP 状态标记被拼接进用户消息文本块;跨会话代际 stale refs 压缩整批失败 #1029's original suggestion and deliver the status as a separate text part/event, so a client that replays assistant text never inherits it.

Impact

Workaround

Fixed client-side for this lane (drop the marker chunk while streaming, plus sanitize on replay), so the proxy-side fix is only about other clients/lanes.

Activity

  1. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Triaged — bug confirmed in code, fix in progress.

    Verification (current master source):

    • buildVisibilityMarker (src/loop/core.ts) emits the acp_status marker as a multi-line unit: `\n📊 [ACP] acp_status result:\n${result.trim()}\n` — head line + the full status report.
    • The incoming-history strip (stripAcpStatusMarkers → stripMarkerLines, MARKER_LINE = /^\p{So}(?:[ \t])?\[ACP\][^\n]*\n?/gmu) is line-granular: [^\n]* cannot consume the body, so every replayed turn keeps the orphaned report — your 3592-char residue is exactly what that regex leaves behind.
    • The line-only behavior was a deliberate choice for the outgoing forged-marker stream filter (模型把 "📦 [ACP] Compressed" 确认标记伪造为普通 assistant 文本——17 次假压缩、0 次真实执行(自我回执幻觉) #717: "no reliable terminator exists to swallow it safely"), but the same primitive was reused on the incoming-history path where the unit IS known and trusted — that reuse is the gap.

    Fix direction (implementing now): make the unit self-delimiting instead of guessing terminators — wrap the acp_status report body in an explicit open/close pair, the same scheme /acp-cache and /acp-rule already use for variable-length kernel-owned reports (wrapCacheReport/wrapRuleReport in src/acp-panel.ts), and strip the whole pair from incoming history. On top of that, a best-effort pass for pre-fix persisted bodies (which have no sentinels yet): after a stripped acp_status head, consume contiguous report-grammar lines (section headers / indented rows / blanks) and stop at the first line that looks like ordinary prose — so marathon sessions started before the upgrade stop accumulating residue too.

    Single-line markers (compress/decompress/search_context/absorb) and the outgoing forged-marker contract are untouched. Will follow up here with the PR link once it's up.

  2. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Done — PR up: #2797 (Closes #2789). Local typecheck + build + unused-export gate clean; targeted suites green (marker-echo, issue1029-acp-marker-stale-refs, tool-pair-order, acp-status, search-context, acp-panel, loop-compress + new file — 108 tests). Full regression left to CI.

    结构化结论(八问):

    1. 是什么问题:acp_status 可见性 marker 天生是多行单元(head 行 + 完整状态报告),但 incoming-history 清理是逐行的(MARKER_LINE 的 [^\n]* 吞不掉 body),于是持久化并回放 assistant 文本的客户端每回合在历史里留下 10–40 行孤儿报告体,持续烧 token,还会被模型当成自己答案的一部分去推理。
    2. 什么原因导致的:根因是「marker 单元是多行的,清理原语却是行粒度,且 body 无定界符」。行粒度本身是对 outgoing forged-marker 流过滤的刻意保守选择(模型把 "📦 [ACP] Compressed" 确认标记伪造为普通 assistant 文本——17 次假压缩、0 次真实执行(自我回执幻觉) #717:伪造 body 没有可靠终止符),把同一原语复用到可信的 incoming-history 路径上,就是缺口所在。
    3. 影响范围与严重程度:所有客户端会把 assistant 文本 echo 回去的 lane(proxy mode + visibilityMarkers 默认 true);不是完全不可用,但长会话每回合累积 0.5–1.5K 残留(issue 里 95 blocks / 3618.4K original 的会话正是此形态),且模型可能基于陈旧 Nudge:/BILLED INPUT 行推理——中高严重度(token 成本 + 上下文污染)。
    4. 怎么修复的:让单元自定界,而不是猜终止符。发射端把报告 body 包进 [acp-status]…[/acp-status] sentinel 对(与既有 /acp-cache、/acp-rule 同一机制);incoming 清理新增 stripAcpStatusBlocks 精确整对移除;对修复前已持久化、无对的旧 body,在剥掉 head 后按报告语法白名单(section 头/缩进行/空行)连续消费,遇到第一个普通行文样式的行即停——宁留勿错删,最坏残留陈旧报告,绝不会吃用户/模型正文。sentinel 开而无闭(消息中途截断)则消费到消息尾。
    5. 是否有回归问题:逐项审过——单行 marker(compress/decompress/search_context/absorb)字节不变;outgoing forged-marker 过滤保持 head-only(模型把 "📦 [ACP] Compressed" 确认标记伪造为普通 assistant 文本——17 次假压缩、0 次真实执行(自我回执幻觉) #717 契约有测试钉住);非 marker 的 [ACP] 散文(含 CJK 行首)不受影响;marker-only 消息仍降级为 " " 而非删除(ACP 状态标记被拼接进用户消息文本块;跨会话代际 stale refs 压缩整批失败 #1029 的 tool_calls 孤儿保护原样保留);typecheck/build/export-gate 干净,108 项定向测试全过。无语义漂移、无 fail-fast 变吞错、无阈值/时序漂移。
    6. 是否破坏缓存:否。前缀缓存锚点(system prompt/tools)未动;新增两行括号只出现在调用 acp_status 那一轮的 assistant 文本里(该文本本就是不可缓存的瞬态内容);无 marker 的健康输入仍是同引用 no-op,字节稳定保持。
    7. 是否改了设计方向:无架构转向;有一处有意披露的行为变更:客户端可见的 acp_status marker 文本在报告体外多了两行括号(旧 \n📊 [ACP] acp_status result:\n<report>\n → 新 \n📊 [ACP] acp_status result:\n[acp-status]\n<report>\n[/acp-status]\n)。这正是让回放清理精确的载体,与既有 cache/rule 报告的呈现方式一致;其余 lane/工件不受影响。
    8. 是否引入了新的配置:无。没有新 env/config/默认值,compress.visibilityMarkers 表面不变。

    顺带发现(已单独立项):核查时发现 google(Gemini native)lane 在 prepare-google.ts:23 import 了 stripAcpPanelMessages/stripAcpStatusMarkers 却从未调用——该 wire 上面板与 marker(含单行)从未被清理。git 历史证实这是既有缺口(#1440 抽取重构之前 server.ts 同样只有 openai/anthropic/responses 三个调用点)。修复需要 parts 形状适配 + live-lane 行为变更,不宜混入本 PR,已按来源标记单独立项:#2798 。

    判定:CI 绿即可合并(合并由人执行)。

  3. added a commit that references this issue on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions