现象
真实会话陷入压缩死循环:无任何新输入的情况下,模型逐字节相同地重发同一个 compress 调用 33 次 (约 50 秒一次,13:26:14 → 13:54:41,持续约 28 分钟)。每次工具都报告"成功",但上下文实际毫无变化,且模型看不到自己之前的重试,循环无法自终止,最终靠用户打断恢复。
根因链(v0.0.32 = 会话实际运行版本)
13:24:28 模型发起 3 区间批量压缩,成功创建 b15/b16/b17(回收 ~70K)。此后每轮迭代:
prune 注入合成消息 acp_summary_b15;模型(参考了过期的 nudge/状态,仍认为区间未压缩)再次发送 m00361..m00436。
这两个 ref 映射到的是夹住 summary 的两条幸存普通消息 (创建 b15 时处于保护区被排除、仍可见),区间解析成功,覆盖内容 = 2 条小消息 + acp_summary_b15。
v0.0.32 applySingleRange 不过滤合成 summary id(过滤在 compress cannot promote an active block after pruning replaces its raw messages #195 修复 8a4dc72 / v0.0.38 才加入)→ 产出 directMessageIds=["acp_summary_b15"] 的空壳包装块 ,报告 blocksCreated=1、回收 ~2.1K → 伪成功 。
扩展层 isCompressSuccessText() 只看 blocksCreated>0 → 判定成功,重试预算不消耗。
下一轮 syncBlocks 对比原始日志:合成 id 不存在 → 壳块被静默反激活;hide-consumed 又把该 compress 调用/结果对从模型视野隐藏。
模型看到的上下文与上一轮完全相同 → 相同 thinking → 相同调用。×33。
状态文件佐证:.acp.json 中 b18..b50(33 个)全部是 directMessageIds=["acp_summary_b15"]、compressedTokens 2075、active:false 的一次性壳块,与 33 次迭代一一对应。
v0.0.39(kernel master)仍会循环 —— 只是换了形态
用同一份真实数据(会话 jsonl 前 568 行 + 真实 .acp.json 裁剪到 b1..b17 + jsonl 第 569 行的原始 compress 参数)在 acp-kernel v0.0.39 上回放:
applyCompression →
{ blocksCreated: 1, tokensCompressed: 811, errors: [],
warnings: [
"Skipped range (m00509..m00542) — already compressed (messages consumed by existing block(s)); nothing to compress.",
"Skipped range (m00549..m00576) — already compressed (messages consumed by existing block(s)); nothing to compress." ] }
新建块: { id: "b54", directMessageIds: [], consumedBlockIds: ["b15"], tier: 1, active: true }
下一轮 processTurn: 壳块存活(active), b15 被反激活, 视图大小不变(120→120),
模型自己的 compress 工具结果仍不可见
#195 的两个修复(8a4dc72、95f7530)让 summary id 被过滤,但 nested-discovery 转而把 b15 本身识别为嵌套块,产出 directMessageIds=[] 的空 tier-1 重写块 并消费 b15 —— 依旧 errors:[]、blocksCreated=1,对扩展层仍是"成功";视图结构不变(summary 文本换壳)、模型自身调用仍被隐藏,第 3 轮起按同构机制无限继续。livelock 条件没有关闭。
为什么现有防护都拦不住
防护
为什么不适用
bcp tier3OnlyRewrite guard(PR #181 )
只拦 tier-3 重写;此处是 tier-1
bcp no-op 计失败 + retry cap(PR #182 )
只对 0-block no-op 生效;此处 blocksCreated=1 恒为成功,计数器永不增长
PR #194 (OPEN,terminal 拒绝不再重推 nudge)
处理的是被拒绝(0-block)后的 nudge 重推 ;此处工具"成功",nudge 逻辑不介入
kernel 空块守卫 compress.ts directMessageIds.length === 0 && consumedBlockCount === 0
只拦 direct 与 consumed 同时 为空;consumed=["b15"] 时放行
修复建议
kernel(核心) :message-ref 区间解析后,若实际覆盖内容只剩活跃块的渲染 summary(无 block-boundary ref、非显式 T2+ 提升路径),应按已压缩区间同样方式拒绝 :terminal 报错 + blocksCreated=0。
kernel :directMessageIds=[] 的 tier-1 块只应产生于显式提升路径(block-boundary refs 如 b2..b2,或 T2/T3 压缩),否则拒绝创建。
bcp :成功判定不应只看 blocksCreated>0,结合 warnings(本例每次都有 2 条 "Skipped range — already compressed")与空 direct 块。
复现
回放脚本基于真实会话数据,可在任意 kernel 版本验证(tmp/repro-iter2.mjs):用扩展自身的 entriesToCoreMessages 载入 jsonl → processTurn → resolveBoundaries → applyCompression → 再 processTurn,输出如上。
关联 #195 :其修复解决了 promote-after-prune,但未关闭本循环(反而是过滤引入后的新路径)。
现象
真实会话陷入压缩死循环:无任何新输入的情况下,模型逐字节相同地重发同一个 compress 调用 33 次(约 50 秒一次,13:26:14 → 13:54:41,持续约 28 分钟)。每次工具都报告"成功",但上下文实际毫无变化,且模型看不到自己之前的重试,循环无法自终止,最终靠用户打断恢复。
01a028fd-ed5f-7980-80c1-8267cfef4f19(2026-08-22 10:22 开始;该会话当时的任务正是处理compresscannot promote an active block after pruning replaces its raw messages #195)billion-context-pi@0.1.45(npm 最新已发布版,内置 acp-kernel 0.0.32);模型 qwen3.8-27b(vllm),每轮重发 ~130K tokens根因链(v0.0.32 = 会话实际运行版本)
13:24:28 模型发起 3 区间批量压缩,成功创建 b15/b16/b17(回收 ~70K)。此后每轮迭代:
acp_summary_b15;模型(参考了过期的 nudge/状态,仍认为区间未压缩)再次发送m00361..m00436。acp_summary_b15。applySingleRange不过滤合成 summary id(过滤在compresscannot promote an active block after pruning replaces its raw messages #195 修复 8a4dc72 / v0.0.38 才加入)→ 产出directMessageIds=["acp_summary_b15"]的空壳包装块,报告blocksCreated=1、回收 ~2.1K → 伪成功。isCompressSuccessText()只看blocksCreated>0→ 判定成功,重试预算不消耗。syncBlocks对比原始日志:合成 id 不存在 → 壳块被静默反激活;hide-consumed又把该 compress 调用/结果对从模型视野隐藏。状态文件佐证:
.acp.json中 b18..b50(33 个)全部是directMessageIds=["acp_summary_b15"]、compressedTokens 2075、active:false的一次性壳块,与 33 次迭代一一对应。v0.0.39(kernel master)仍会循环 —— 只是换了形态
用同一份真实数据(会话 jsonl 前 568 行 + 真实
.acp.json裁剪到 b1..b17 + jsonl 第 569 行的原始 compress 参数)在 acp-kernel v0.0.39 上回放:#195 的两个修复(8a4dc72、95f7530)让 summary id 被过滤,但 nested-discovery 转而把 b15 本身识别为嵌套块,产出
directMessageIds=[]的空 tier-1 重写块并消费 b15 —— 依旧errors:[]、blocksCreated=1,对扩展层仍是"成功";视图结构不变(summary 文本换壳)、模型自身调用仍被隐藏,第 3 轮起按同构机制无限继续。livelock 条件没有关闭。为什么现有防护都拦不住
tier3OnlyRewriteguard(PR #181)blocksCreated=1恒为成功,计数器永不增长compress.tsdirectMessageIds.length === 0 && consumedBlockCount === 0consumed=["b15"]时放行修复建议
blocksCreated=0。directMessageIds=[]的 tier-1 块只应产生于显式提升路径(block-boundary refs 如b2..b2,或 T2/T3 压缩),否则拒绝创建。blocksCreated>0,结合 warnings(本例每次都有 2 条 "Skipped range — already compressed")与空 direct 块。复现
回放脚本基于真实会话数据,可在任意 kernel 版本验证(
tmp/repro-iter2.mjs):用扩展自身的entriesToCoreMessages载入 jsonl →processTurn→resolveBoundaries→applyCompression→ 再processTurn,输出如上。关联 #195:其修复解决了 promote-after-prune,但未关闭本循环(反而是过滤引入后的新路径)。