Repository navigation
acp_status visibility markers: stripMarkerLines removes the head line but leaves the multi-line report body in incoming history (#1029 follow-up) #2789
Description
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: foracp_statusthe 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.stripMarkerLineshas 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 whystripAcpPanelResponsesInputcan drop those whole).
Suggested fix (any of these)
- Strip the marker block, not the line:
emitMarkerdelivers 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). - Or wrap the body in an explicit pair (e.g.
<acp-status>…</acp-status>) and strip it whole, like the existing panel/report pairs. - 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
- replayed histories keep paying tokens for old status reports;
- leftover bodies are unattributed — the same "user text vs proxy text are indistinguishable" symptom as ACP 状态标记被拼接进用户消息文本块;跨会话代际 stale refs 压缩整批失败 #1029, one level lower (my model reasoned over its own stale
Nudge:/BILLED INPUTlines as if they were part of its answer); - affects every lane whose client echoes assistant text back (not just mine).
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
ranxianglei commentedon Oct 11, 2026
[bot] 🏷 Triaged — bug confirmed in code, fix in progress.
Verification (current master source):
buildVisibilityMarker(src/loop/core.ts) emits theacp_statusmarker as a multi-line unit:`\n📊 [ACP] acp_status result:\n${result.trim()}\n`— head line + the full status report.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.Fix direction (implementing now): make the unit self-delimiting instead of guessing terminators — wrap the
acp_statusreport body in an explicit open/close pair, the same scheme/acp-cacheand/acp-rulealready use for variable-length kernel-owned reports (wrapCacheReport/wrapRuleReportinsrc/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 strippedacp_statushead, 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.
ranxianglei commentedon Oct 11, 2026
[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.
结构化结论(八问):
acp_status可见性 marker 天生是多行单元(head 行 + 完整状态报告),但 incoming-history 清理是逐行的(MARKER_LINE的[^\n]*吞不掉 body),于是持久化并回放 assistant 文本的客户端每回合在历史里留下 10–40 行孤儿报告体,持续烧 token,还会被模型当成自己答案的一部分去推理。visibilityMarkers默认 true);不是完全不可用,但长会话每回合累积 0.5–1.5K 残留(issue 里 95 blocks / 3618.4K original 的会话正是此形态),且模型可能基于陈旧Nudge:/BILLED INPUT行推理——中高严重度(token 成本 + 上下文污染)。[acp-status]…[/acp-status]sentinel 对(与既有/acp-cache、/acp-rule同一机制);incoming 清理新增stripAcpStatusBlocks精确整对移除;对修复前已持久化、无对的旧 body,在剥掉 head 后按报告语法白名单(section 头/缩进行/空行)连续消费,遇到第一个普通行文样式的行即停——宁留勿错删,最坏残留陈旧报告,绝不会吃用户/模型正文。sentinel 开而无闭(消息中途截断)则消费到消息尾。[ACP]散文(含 CJK 行首)不受影响;marker-only 消息仍降级为" "而非删除(ACP 状态标记被拼接进用户消息文本块;跨会话代际 stale refs 压缩整批失败 #1029 的 tool_calls 孤儿保护原样保留);typecheck/build/export-gate 干净,108 项定向测试全过。无语义漂移、无 fail-fast 变吞错、无阈值/时序漂移。acp_statusmarker 文本在报告体外多了两行括号(旧\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/工件不受影响。compress.visibilityMarkers表面不变。顺带发现(已单独立项):核查时发现 google(Gemini native)lane 在
prepare-google.ts:23import 了stripAcpPanelMessages/stripAcpStatusMarkers却从未调用——该 wire 上面板与 marker(含单行)从未被清理。git 历史证实这是既有缺口(#1440 抽取重构之前 server.ts 同样只有 openai/anthropic/responses 三个调用点)。修复需要 parts 形状适配 + live-lane 行为变更,不宜混入本 PR,已按来源标记单独立项:#2798 。判定:CI 绿即可合并(合并由人执行)。
fix(#2789): strip the whole acp_status marker block from incoming his…
feat(marker): declarative marker spec + raw-stage engine (#2811 plan B)
feat(marker): wire stripRawStage into all four prepares and fork adop…
fix(marker): id churn from marker-minted covered ids must not escalat…
Merge pull request #2797 from ranxianglei/2026-10-11_acp-status-marke…
Merge pull request #2815 from ranxianglei/2026-10-11_marker-declarative