Repository navigation
Description
中文标题:压缩/折叠会产生不存在的「用户原话」引用与跨世代 ref,并把摘要回流成用户输入
目标:billion-context(ACP / 上下文压缩内核,本机 0.1.212)
关联:DSH core 0.2.1-rc.5(提供会话与 ref 世代,但无请求体取证,见另一 issue)
起草:2026-11-07,C 线(只读包源码 + 只读会话日志;未改包、未重启、未向其它会话发消息)
摘要(3 行)
- 跨世代(切换模型 / 上游重启 / 恢复)后,模型上下文里出现本世代不存在的 ref 与非本会话用户消息,模型把它当作“用户最后一条消息”引用并写进
compress的summary;内核只对 summary 做长度检查与代理对清洗(scrubLoneSurrogates),不校验被引用消息是否真实存在。 - 同一用例中
compress对越界 ref 报... stale artifact from an earlier generation ...,但报错发生在模型已写出伪引用之后,且只在compress调用时触发——推理阶段无任何提示渠道。 - 折叠块注入模型侧时只有
[Compressed conversation section] — <topic>+ 摘要正文,不带 blockId / generation / ref-span,下游无法区分“原始用户输入”与“上一世代模型摘要”,伪引用因此二次传播。
Summary (EN)
- 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
compresssummary; the kernel only length-checks and surrogate-scrubs summaries (scrubLoneSurrogates) and never verifies that a quoted user message exists. - In the same run,
compressrejects 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. - 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-context0.1.212(D:\DeepSeek Harness\.dsh\profiles\core-021\node_modules\billion-context\package.json:3;dist/index.js5,863,132 bytes,2026-11-03 15:05)- DSH core 0.2.1-rc.5;Node
v24.19.0;Windows (win32/x64);profilecore-021 - 事故会话:
session-34d5b146-d21b-41ff-8385-c8a101975d74,首次异常记录seq 10436 @ 2026-11-07T12:57:19Z - 会话日志:
session.v4.jsonl.zstd(多帧 zstd;本次取证用副本解帧,原日志只读)
复现步骤
- 在一个已触发过 ACP 压缩的长会话中,进行“切换模型 / 上游重启 / 恢复会话”之一(ref 世代重置)。
- 继续对话,使模型在 reasoning 中引用早先消息;当其引用到越界 ref(大于本世代已分配上限)或不存在的 user 消息时,记下该 ref。
- 随即调用
compress,把该 ref 作为startRef/endRef或写进summary。 - 观察:
compress该批被拒并返回含stale artifact from an earlier generation的错误;但第 2 步的伪引用已经在模型上下文中,且可被后续摘要继承。 - 检查模型侧上下文中的折叠段形态:只有
[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,行号待核实)。
期望行为
- 折叠/压缩时,若 summary 中出现
user m\d+、“用户说/用户原话”类引用,校验该 ref 真实存在于本次被折叠源区间;不存在则①拒绝该 range,或②在摘要中显式改写为[unverified quote: <ref>]并从上下文剔除该句。 - 越界/跨世代 ref 在拼上下文阶段就告警(本 turn 的 notice/system 提示),而不是等 compress 报错;报错时附最近 N 个合法 ref(可直接用),而非只指向
acp_status。 - 折叠块注入时固定带
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,不靠试探 |
附件清单(本地证据,按需脱敏后附)
_交付\202610\2026-11-07-消息来源审计\⓪ 审计结论-S0.md§二/§三 ——seq 10436、窗口内 4 条 user/message、ref 越界范围。- 同目录
⑥ rc5兼容性与处置建议.md§4 —— 缺陷 B 证据与建议。 - 会话日志副本(
_dev\下解帧)seq 10436及其附近的相关记录(reasoning 引用、compress 调用、错误返回;具体 seq 待核实)。 billion-context/dist/index.js第 129、138 行截取;dist/agent/pi-native.js:250/259、dist/agent/opencode-native.js:128/137。billion-context/package.json(name/version 两行)。
说明:本 issue 只引用本机 0.1.212 的实际产物与已核实日志;未运行代码、未改包、未重启。
seq/ref数字以会话副本为准;标注「待核实」处为仅见二手叙述、未逐字复核的项。
Activity
ranxianglei commentedon Nov 7, 2026
(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)。
三个事实主张的逐点判定
「写入侧不校验 summary 里引用的 ref」→ 成立。 compress 入口(kernel/src/compress.ts:317)只做 lone-surrogate 清洗(:330)与长度校验(:687/:1901),全文无任何对正文引用 ref 的存在性校验。
「报错只指向 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):beyondFrontierNote(dsh内部问题 出现这个错误后,重启dsh可以发消息 但陷入无限压缩失败 #2146):请求 ref 全部高于会话上限时,明确指出「这些 ref 在本会话不存在……只能请求 ≤ mXXXXX」并引用活的上限值;frontierClampNote(compress calls rejected when the summary text rides incontentnext to top-level startId/endId — plus U+2212 dash silently folds the wrong range #4252):部分越界情形;rebaseNote([Bug] #2436 的宿主本地压缩检测只接在 OpenAI 车道,Anthropic 车道缺失——未宣告宿主压缩后 nudge 广播的 compress ref 永久不可解析 #2592):native compaction rebase 情形;currentRefsSnapshot(ACP 状态标记被拼接进用户消息文本块;跨会话代际 stale refs 压缩整批失败 #1029):所有零折叠失败都追加[Current context: N visible message(s), refs mXXXXX–mYYYYY, K active block(s)]。你引用的「会话上限 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 的「拼上下文阶段告警」软版本已在你的构建里。
「折叠块回注无 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 上限即其输出),该项无需修复。八问
收工判定:等 owner 对 A/B/C 三项拍板;已被现有机制覆盖的部分(E2b、E4)无需动作;本轮不开 PR(可交付路径全部被 kernel/wire 门禁挡住,强开即违规)。