Skip to content

[Bug] Compaction mints phantom user quotes and cross-generation refs, then re-injects the summary as user input #5911

Open

Description

@ReadAI001

中文标题:压缩/折叠会产生不存在的「用户原话」引用与跨世代 ref,并把摘要回流成用户输入
目标:billion-context(ACP / 上下文压缩内核,本机 0.1.212)
关联:DSH core 0.2.1-rc.5(提供会话与 ref 世代,但无请求体取证,见另一 issue)
起草:2026-11-07,C 线(只读包源码 + 只读会话日志;未改包、未重启、未向其它会话发消息)

摘要(3 行)

  1. 跨世代(切换模型 / 上游重启 / 恢复)后,模型上下文里出现本世代不存在的 ref 与非本会话用户消息,模型把它当作“用户最后一条消息”引用并写进 compress 的 summary;内核只对 summary 做长度检查与代理对清洗(scrubLoneSurrogates),不校验被引用消息是否真实存在。
  2. 同一用例中 compress 对越界 ref 报 ... stale artifact from an earlier generation ...,但报错发生在模型已写出伪引用之后,且只在 compress 调用时触发——推理阶段无任何提示渠道。
  3. 折叠块注入模型侧时只有 [Compressed conversation section] — <topic> + 摘要正文,不带 blockId / generation / ref-span,下游无法区分“原始用户输入”与“上一世代模型摘要”,伪引用因此二次传播。

Summary (EN)

  1. After a generation change (model switch / upstream restart / resume), the model context contains refs that do not exist in the current generation, and the model cites them as "the user's last message" inside a compress summary; the kernel only length-checks and surrogate-scrubs summaries (scrubLoneSurrogates) and never verifies that a quoted user message exists.
  2. In the same run, compress rejects the out-of-range refs with ... stale artifact from an earlier generation ..., but only after the model already wrote the phantom quote, and only at compress time — reasoning gets no warning channel.
  3. Folded blocks are injected as [Compressed conversation section] — <topic> + summary text with no blockId / generation / ref-span, so downstream cannot tell a summary from real user input — phantom quotes propagate.

环境

  • billion-context 0.1.212(D:\DeepSeek Harness\.dsh\profiles\core-021\node_modules\billion-context\package.json:3;dist/index.js 5,863,132 bytes,2026-11-03 15:05)
  • DSH core 0.2.1-rc.5;Node v24.19.0;Windows (win32/x64);profile core-021
  • 事故会话:session-34d5b146-d21b-41ff-8385-c8a101975d74,首次异常记录 seq 10436 @ 2026-11-07T12:57:19Z
  • 会话日志:session.v4.jsonl.zstd(多帧 zstd;本次取证用副本解帧,原日志只读)

复现步骤

  1. 在一个已触发过 ACP 压缩的长会话中,进行“切换模型 / 上游重启 / 恢复会话”之一(ref 世代重置)。
  2. 继续对话,使模型在 reasoning 中引用早先消息;当其引用到越界 ref(大于本世代已分配上限)或不存在的 user 消息时,记下该 ref。
  3. 随即调用 compress,把该 ref 作为 startRef/endRef 或写进 summary。
  4. 观察:compress 该批被拒并返回含 stale artifact from an earlier generation 的错误;但第 2 步的伪引用已经在模型上下文中,且可被后续摘要继承。
  5. 检查模型侧上下文中的折叠段形态:只有 [Compressed conversation section] — <topic> 与摘要正文,无来源块元数据(本会话观察)。

说明:受限于没有 provider 请求体 dump(见 DSH issue),无法给出 100% 稳定的最小用例;但触发判据与失败现象确定,且本会话已实际发生一次(seq 10436)。

证据(文件:行 + 日志片段)

(1) 伪用户消息已被引用 — 本会话 seq 10436(2026-11-07T12:57:19.416Z)assistant reasoning 中把 user m08516 当作“用户最后一条消息”引用。核实:该窗口内真实用户消息 4 条(role=user 且正文无 [会话信箱] 前缀),均不含此 ref/此句;m08516 在本世代不存在。

(2) 越界 ref 与报错原文 — 同批 compress 引用了 m08424–m08538,而本 session 已分配上限为 m08328。内核错误文案(billion-context/dist/index.js:129,逐字):

An unknown ref is usually a stale artifact from an earlier generation (switching model or upstream mid-conversation starts a fresh session whose refs restart at m00001, and a native-compaction rebase resets them the same way) or a typo — but a number at or below the highest allocated ref may also simply be an unallocated gap: the ref space is sparse.

同段(dist/index.js:138)还输出 refGateDiagnostics(...) 并给出:

Do not probe for valid numbers. Run acp_status, then call the compress tool again using only the refs it reports.

即:诊断已存在,但只在 compress 被拒时出现,不内联最近合法 ref,也不会在伪引用进入上下文前拦截。

(3) summary 不校验引用真实性 — 同一 applyCompression 入口(dist/index.js:138)对 summary 只做:

if (spec.summary) spec.summary = scrubLoneSurrogates(spec.summary)

紧接着的长度校验 validateSummaryLength 只检查空/最短/最长(同文件)。没有任何“摘要中引用的 user ref 是否存在于被折叠区间”的检查;summary 是模型自由文本,伪引用可原样入库。

(4) 文案在多份产物中重复 — stale artifact from an earlier generation 同时出现在 dist/index.js:129/138、dist/agent/pi-native.js:250/259、dist/agent/opencode-native.js:128/137(同一 kernel 打包三份)。修一处需同步三份产物,否则不同宿主行为不一致。

(5) 二次污染 — 据 ⓪ 审计结论-S0.md §三,该伪引用随后被写进折叠摘要;在本会话上下文中,折叠段呈现为 [Compressed conversation section] — 任务简报与首批取证 + 摘要正文,不带 blockId / generation / ref 范围,因此后续 turn 无法把它与原始用户消息区分(具体 block 与摘要原文见附件 3,行号待核实)。

期望行为

  1. 折叠/压缩时,若 summary 中出现 user m\d+、“用户说/用户原话”类引用,校验该 ref 真实存在于本次被折叠源区间;不存在则①拒绝该 range,或②在摘要中显式改写为 [unverified quote: <ref>] 并从上下文剔除该句。
  2. 越界/跨世代 ref 在拼上下文阶段就告警(本 turn 的 notice/system 提示),而不是等 compress 报错;报错时附最近 N 个合法 ref(可直接用),而非只指向 acp_status。
  3. 折叠块注入时固定带 source-block 元数据(blockId、generation、startRef..endRef、createdAt),渲染为“摘要”而非 user 消息,并禁止下游把摘要内文当原始用户输入。

影响面

  • 正确性:模型把上一世代的伪引用当成用户指令/事实(本会话已出现一次),后续回答与摘要被污染,形成“伪记忆”。
  • 可审计性:日志里没有这条 user 消息,但模型确实“见过”它;没有请求体 dump 就无法证明,只能靠 ref 越界间接推断。
  • 跨宿主:三份 native 产物文案重复,说明这是内核行为而非某个适配器 bug,影响所有使用 ACP 压缩的 host。
  • 与 DSH 缺陷叠加:信箱投递已能伪造 user 轮次(另一 issue),压缩摘要又能伪造 user 引用 → 两条链路都可让“非用户内容”变成“用户说的话”。

建议修法

# 动作 落点 判据
1 折叠时校验 summary 内引用的 user ref 是否落在源区间;不存在则标 [unverified quote] 或拒绝 range applyCompression 入口(dist/index.js:138 对应源码)+ 三份 native 产物 注入伪引用时 compress 返回可读错误/标记
2 建 ref 世代账本:记录 generationId、已分配上限、最近合法 ref;拼上下文前校验越界并给替代列表 内核 越界 ref 在进入模型上下文前被标记,报错内联 m0xxxx… 候选
3 摘要块渲染固定带 source-block 元数据,下游按类型分流 渲染/注入层 折叠段可机器识别为 summary 而非 user
4 在 acp_status/nudge 中暴露“当前世代合法 ref 区间” 工具层 模型可用一次调用取到合法 ref,不靠试探

附件清单(本地证据,按需脱敏后附)

  1. _交付\202610\2026-11-07-消息来源审计\⓪ 审计结论-S0.md §二/§三 —— seq 10436、窗口内 4 条 user/message、ref 越界范围。
  2. 同目录 ⑥ rc5兼容性与处置建议.md §4 —— 缺陷 B 证据与建议。
  3. 会话日志副本(_dev\ 下解帧)seq 10436 及其附近的相关记录(reasoning 引用、compress 调用、错误返回;具体 seq 待核实)。
  4. billion-context/dist/index.js 第 129、138 行截取;dist/agent/pi-native.js:250/259、dist/agent/opencode-native.js:128/137。
  5. billion-context/package.json(name/version 两行)。

说明:本 issue 只引用本机 0.1.212 的实际产物与已核实日志;未运行代码、未改包、未重启。seq/ref 数字以会话副本为准;标注「待核实」处为仅见二手叙述、未逐字复核的项。

Activity

  1. ranxianglei commented on Nov 7, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重:不重复。相关条目:① GitHub #5588(「用户指令能否干预压缩?折叠摘要有没有防提示注入措施?」,等 owner 拍板 A/B1/B2/B3)——本 issue 期望 3(折叠块来源元数据)与其 B1(回注摘要带块级 provenance 头)落在同一字节位置,建议合并裁决,见下面决策项 A;② GitHub #2146/#2147(stale-generation 诊断与熔断机制的来源,已关闭);③ PR #5774(块退役/外部摘要改写,相邻功能域、不同机制)。无在途 [PR] 针对本线程。

    标签:bug、dsh、scope/kernel、p2、needs-decision —— 缺陷报告、dsh 车道、剩余缺口集中在内核渲染/写入侧;条件触发且有绕行(acp_status 重锚),非数据丢失/缓存泄露类,故 p2 非 p1。

    版本优先核对:你的构建 0.1.212 在相关路径上与 master 一致(两版本间 kernel/src/compress.ts 的 diff 仅 media-refs 相关改动;下述各机制均已包含在 0.1.212)。

    三个事实主张的逐点判定

    1. 「写入侧不校验 summary 里引用的 ref」→ 成立。 compress 入口(kernel/src/compress.ts:317)只做 lone-surrogate 清洗(:330)与长度校验(:687/:1901),全文无任何对正文引用 ref 的存在性校验。

    2. 「报错只指向 acp_status、不内联可用 ref」→ 不成立。 0.1.212 已有成体系的反应式诊断,且 plugin 车道(POST /__bili/plugin/tool → src/loop/core.ts:389 → src/stream.ts:1038 applyRanges)与 proxy 模式共用同一失败诊断簇(src/stream.ts:1400-1406):

      你引用的「会话上限 m08328」很可能正是 beyondFrontierNote 的输出。此外还有主动层:Switching model mid-conversation restarts the ref ledger, but the rendered transcript still shows the old refs — the model keeps aiming compress at dead ranges until the circuit breaker fires | 切模型后消息编号整体换账本,但渲染历史仍显示旧编号,模型对着死区间反复压缩直至熔断 #4305 generation-boundary standing note——新世代第一轮起,可见历史中检出 ≥2 个不同的越界引用即在 system 块常驻一条静态警示(字节稳定、前缀缓存安全)。即期望 2 的「拼上下文阶段告警」软版本已在你的构建里。

    3. 「折叠块回注无 blockId/generation/ref-span」→ 成立。 wire 渲染(kernel/src/prune.ts:548-560 renderSummary)只输出 [Compressed conversation section] — <topic>\n<body>;带 [blockId | range | topic] 元数据脚注的形式只存在于 acp_status 视图(kernel/src/report.ts:608)。另需澄清一点:proxy 模式把摘要改声为 user 消息是有意设计(SGLang 类严格后端要求 index 0 唯一 system + 头部字节稳定),不是缺陷,但它确实放大了「摘要与用户输入不可区分」。

    层次判断(根因 vs 现象)

    因果链三层:① 会话世代重置后,旧世代的 ACP 产物(旧 tag、旧回执、含 ref 的旧工具调用)逐字穿过新会话进入模型上下文——新会话没有 fold 状态,prune 只丢弃 id 映射到本会话所建块的旧渲染副本,映射不到的旧世代产物原样保留(kernel/src/prune.ts:526-537);② 模型把死 ref 当活的,把不存在的 user 引用写进新 summary——写入侧无校验,缺口成立;③ 折叠块回注无 provenance,二次传播下游无法识别——缺口成立。「phantom user quotes」是第 ② 层的表面现象。具体触发事件(模型切换 / 上游重启 / resume 三者中的哪一个、DSH 侧行为如何参与)没有请求体 dump 无法钉死——这一点你也已承认。

    四条期望的评估


    🔶 需要决策

    背景:跨世代旧 ref/摘要漏进模型上下文引发 phantom quotes;诊断面大部分在 0.1.212 已存在,剩余缺口集中在写入侧校验策略与折叠块 provenance 两处,均属 kernel/wire 门禁,agent 停手在此。

    推荐:A = 并入 #5588-B1 合并裁决;B = B2'(warn-only)或 B1';C = C1(先要证据,无证据则默认 C3)。
    不决定的默认行为:什么都不发版(剩余缺口全在 kernel/wire 门禁之后);E2b/E4 视为已由现有机制满足、无需修复;线程保持打开(标签已打)。

    @ReadAI001 请协助提供(直接决定 C 的走向):① 事发时段 bili.log,尤其 [acp-gen-boundary] 行——判定 standing note 在本次事故是否 latch;② 如可能,seq 10436 前后的上游请求体 dump——定位 phantom quote 进入上下文的唯一途径。另请知悉:期望 2 要求的内联上限诊断 0.1.212 已具备(你看到的 m08328 上限即其输出),该项无需修复。

    八问

    1. 是什么问题:会话世代重置后,旧世代 ref/摘要残留在模型上下文里,模型把死 ref 当活的、把不存在的 user 引用写进新摘要,且折叠块回注无 provenance 导致二次传播难以识别。
    2. 什么原因导致的:三层因果链(旧产物逐字穿过新会话 → 写入侧无校验 → 回注无 provenance);具体触发事件无请求体 dump 无法钉死。
    3. 影响范围与严重程度(按选项):各选项都不改变触发条件面(只有携带跨世代残留产物的会话命中);A 降低二次传播的识别成本;B2' 增加写入侧审计痕迹;B3' 引入误伤 brick 风险;C 控制首次发生概率。现状严重度 p2:条件触发、有绕行(acp_status 重锚)、非数据丢失、非缓存泄露。
    4. 怎么修复的(推荐选项):A 并入 [Q] Can user instructions intervene in compression? Are there prompt injection defenses for collapsed summaries? (DSH la | [Q] 用户指令能否干预压缩?折叠摘要有没有防提示注入措施?(DSH 车道,两个策略问题) #5588-B1 单一 provenance 头设计;B 取 warn-only(记日志不拦截);C 先要证据,若证明 standing note 已 latch 仍失效再升级 owner 权衡 C2。
    5. 是否有回归问题(按选项):A = 每个存量块一次性前缀失效(确定性字节,此后稳定),wire golden 快照需带理由再生;B2' = 零行为变更;B3' = 可用性回归(误伤 brick 合法 compress),故不推荐;C2 = system 块中途变更使上游前缀缓存失效;C1/C3 = 无回归。
    6. 是否破坏缓存:A 一次性(每块一次);C2 反复(每次 relatch);B/E2/E4 不涉及。
    7. 是否改了设计方向:不涉及转向——折叠块渲染仍归 kernel 所有、宿主保持诊断层,两种压缩模式下渲染一致。
    8. 是否引入了新的配置:不涉及——所有选项复用现有配置面,无新 env/config/默认值。

    收工判定:等 owner 对 A/B/C 三项拍板;已被现有机制覆盖的部分(E2b、E4)无需动作;本轮不开 PR(可交付路径全部被 kernel/wire 门禁挡住,强开即违规)。

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions