Repository navigation
[kernel][recommend] selector advertises spans the apply side structurally rejects — live-carrier screening missing (permanent advertise/validate disagreement) #2663
Description
来源: #2638 (#2638) 分析 preflight dead-span re-nomination loop 时发现
Mechanism (one failure domain: advertise/validate disagreement)
The kernel's range selector (buildCompressibleRanges, kernel/src/recommend.ts:159-361) and the apply side (applySingleRange, kernel/src/compress.ts) classify the same span differently when live checkpoint carriers are involved:
- A message carrying
summaryOfBlockIdpointing at an ACTIVE block (host-declared checkpoint carrier, fix: detect codex compaction requests before preflight; verbatim passthrough when not intercepted (#332) #335) is NOT covered by that block'seffectiveMessageIds(carriers are deliberately excluded from coverage so they stay visible). It therefore survives pruning and, becausebuildCompressibleRangeshas NO carrier awareness, falls through to "fully compressible" (recommend.ts:246-255). - In the pruned view, carriers separated by covered raws become ADJACENT — one contiguous group, one advertised range.
- At apply time, the positional span resolved in the ORIGINAL (full-log) array includes those covered raws, so the nested-block scan finds the covering block visible inside the span (
consumedBlockIds > 0) while every direct message is carved out (carriers excluded at compress.ts:1241-1263, covered ids minus coverage) →filteredIds = ∅→ the livelock guard throwsRange X..Y contains no new compressible messages — every message in it is already covered by active block(s) bN...— EVERY turn, forever.
Observed in production (#2638, v0.1.190, DSH plugin host, deepseek-v4.1-flash, window 1M, autoFold target 60K): spans like m00018..m00242 re-nominated and re-rejected every request across multiple sessions.
Minimal repro (geometry now pinned in tests)
Full-log host resends whole history each turn:
- fold m1..m6 → b_head (anchor m1); m5 user marker inside.
- A@7, B@17, M22@22 user CHECKPOINTS declaring
summaryOfBlockId: b_head; B/M22 sized ≥ minCompressRange combined (~3K chars). - fold m8..m21 → b_mid (carriers A/B/M22 excluded — kernel warns "Excluded N checkpoint message(s)").
- fold m23..m36 → b_tailblock (anchor nests inside the dead span).
- Every subsequent turn: recommended list includes
m00007..m00022(A..M22); its apply returns exactly the livelock-guard error naming b_mid; tail ranges fold normally.
Reproducible via the fixture in tests/preflight-dead-ranges.test.ts (PR #2660) — standalone kernel-level variant: .tmp/replay-2638.ts in that workdir.
Sibling facets of the same mechanism family (verify together, don't fragment)
mediaExcluded(compress.ts:1336-1344) warns "Excluded N message(s) carrying image/attachment payload(s)" but never actually removes them fromfilteredIds— recommend treats media as a gap (uncompressible), apply would fold them: same advertise/validate shape, opposite direction, plus a lying warning.- Integrity-withdrawal screening runs on the PRUNED view in recommend (recommend.ts:260-284) but on the ORIGINAL array in apply — withdrawal decisions can diverge with different adjacency (reasoning-run/call-result pairing changes after pruning). Same family: recommend must judge foldability on the same input apply judges.
Proposed fix direction
Screen live carriers in buildCompressibleRanges the way integrity withdrawals are already screened there (gap + skip, recommend.ts:260-284 precedent): a carrier of an active block is not plain-foldable, mark it as a boundary so groups don't bridge over it, and let the covering block's summary carry the context. Mirror the same input discipline for withdrawals/media so every advertised range is foldable.
Gating
Kernel Boundary applies: owner approval required before implementation, kernel-change: declaration in the PR, kernel/package.json version bump, two test layers (kernel unit suite + bili integration). Host-side defense meanwhile ships in PR #2660 (per-session structural-verdict memory) so affected sessions stop paying the cost regardless.
Activity
ranxianglei commentedon Oct 10, 2026
[bot] 🏷 Triage 结论:成立,已按三步审核对 master
7c993f9ed逐条验证完毕,按你给的修复方向开工。① 复现/查证(三处全部坐实)
buildCompressibleRanges(kernel/src/recommend.ts:206-258)的循环没有任何 carrier 判定——summaryOfBlockId指向 active block 的消息不在该 block 的effectiveMessageIds里(fix: detect codex compaction requests before preflight; verbatim passthrough when not intercepted (#332) #335 有意排除出 coverage 以保可见),prune 不删它,recommend 直接按「完全可压」分类(recommend.ts:246-255)。apply 侧(compress.ts:1241-1263)在原始数组上定位 span、把 live carrier 从rangeMessageIds剔除,而 span 内的 covered raws 只贡献 consumed blocks——于是出现「consumedBlockIds>0 ∧ filteredIds=∅」的死组合,livelock guard(compress.ts:1447-1480)每轮抛同一个错。[Bug][preflight] range selector re-nominates a fully-folded span every turn — permanent "no new compressible messages" rejection loop + nudge churn #2638 生产环境 m00018..m00242 每请求重提名重拒绝,机制与代码路径完全吻合。filteredIds——span 跨 media 时 apply 实际会折叠它们(这个插件压缩上下文,会导致deepseek识图出问题。 #1188 明确:media payload 折叠后不可恢复),警告与行为相反。computeIntegrityWithdrawals(recommend.ts:260-284),apply 在原始数组上跑(compress.ts:1410-1437)。turn-integrity 的分组/配对是数组依赖的(resultIdByCallId由传入数组构建),构造了一个具体分歧几何:原始[R1, A1, U(covered), A2(protected call), T2(protected result)],剪枝后四者相邻成一组 → recommend 固定点迭代把 R1/A1 也撤掉(under-advertise);原始视图里{R1,A1}独立成组,apply 本可正常折叠 R1/A1。方向安全的偏差,但确实是同一机制的第三张脸。② 层次判断:根因是选择器与执行器用不同输入判 foldability(剪枝视图 vs 原始数组)+ selector 缺 carrier 概念。#2638 是它的生产投影;#2660(host 侧死 range 记忆)止住成本但不修机制,两边正交、可并行合入。
③ 方案评估:你给的方向正确且最小——selector 侧给 live carrier 上 gap+skip(复用 withdrawal 筛查的同款纪律,recommend.ts:260-284 先例),stale carrier(block 已死)保持可折;withdrawal 筛查改为喂原始数组(prune 节点先把 pre-prune 数组暂存进 effects);mediaExcluded 真剔除(对齐既有 protected-tool 剔除范式 compress.ts:1329-1334,warning 变诚实)。不扩大范围:BLOCKED ref 不产生 gap 这个更宽的碎片化问题属另一失败域,本次不动,PR 里注明。
Gate 合规:owner 立项且给出明确修复方向 = Kernel Boundary 要求的批准前置已满足;PR 带
kernel-change:声明,kernel/package.json 0.0.112→0.0.113,kernel 套件 + bili 套件双层全绿后才开 PR。预期行为变更(会写进 PR):① 载体所在 span 不再被广告为可压区间(此前每轮广告一个必败区间);② span 跨 media 时 media 真被保留而非静默销毁;③ withdrawal 筛查换到原始数组后,部分区间的广告与否可能变化(偏差方向安全:宁可少广告)。无新增配置面。
开工中,完成后再按八问体例汇报。
ranxianglei commentedon Oct 10, 2026
[bot] 🏷 完成:PR #2669(#2669 本 issue。本地门禁:kernel 套件 1289/1289 绿、bili 套件绿(除 2 条 master 既有环境依赖失败,见文末 #2668)、typecheck/build 绿。
结构化八问:
buildCompressibleRanges)会 advertise 出 apply 侧结构性拒绝的 range——跨 live checkpoint carrier(fix: detect codex compaction requests before preflight; verbatim passthrough when not intercepted (#332) #335)的 span 在 apply 时解析为零可折叠消息,每轮必抛([Bug][preflight] range selector re-nominates a fully-folded span every turn — permanent "no new compressible messages" rejection loop + nudge churn #2638 生产 livelock)。同机制族另两个隐患:media payload 警告声称排除、实际照折;integrity withdrawal 两个 screen 用两种数组类判定。compress.tsapply 路径(live-carrier carve),recommend.ts完全没有 carrier 概念;且两个 screen 一个判 pruned view、一个判原始重发数组,没有任何东西保证两者一致。根因是 selector 与 applier 之间缺少共享判据,不是某条报错本身。state.ts抽出共享谓词isLiveCheckpointCarrier(selector 与 applier 同一份定义);selector 把 live carrier 当 gap+skip(与既有 integrity-withdrawal screening 同形,stale carrier 行为不变);prune node 把 pre-prune 重发数组 stash 进effects.originalMessages,selector 的 withdrawal screening 改在与 apply gate 相同的数组类上判定(新增可选参数integritySource,缺省为传入数组,外部调用方语义不变);applied span 内的 media 消息像 protected tool 一样从 filteredIds/effectiveMessageIds 中 carve 掉(原警告现在属实);acp_status 的 UNCOMPRESSED 行同样不跨 carrier。buildCompressibleRanges新参数可选且带默认值。顺带发现(已单独立项):验证 baseline 时发现
tests/responses-instructions-drift.test.ts两条 #2250 continuing-history 用例在本地 Node 22.23.2 确定性失败、同 commit CI(Node 24)全绿——clean master worktree 复现完全一致,与本改动无关,已立项 #2668(https://github.com/ranxianglei/billion-context/issues/2668),内含复现步骤与环境差异证据。收工判定:可合并(kernel 变更按 Kernel Boundary 需人工 review;PR 正文已带
kernel-change:声明,kernel-guard CI 会校验)。ranxianglei commentedon Oct 11, 2026
冲突解决一下
kernel: selector screens live carriers; withdrawal judged on original…
ranxianglei commentedon Oct 11, 2026
[bot] 🏷 冲突已解决,PR #2669 已 rebase 到当前 master(aac8bc3fe)并强推更新。
结构化八问(本轮 = 冲突解决 + rebase 适配):
compress.stripImages默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607(fold archivable media like text; pin only unrecoverable payloads)直接改写了我 draft 中 facet-2 carve 所在的代码区。compress.stripImages默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607,把媒体语义拆成「可归档(inline base64/data URL → 折叠如文本)/不可恢复(远程 URL、DeepSeek file ref、Google fileData/videoMetadata → 仍 BLOCKED)」;我的 blanket media carve 与新方向正面抵触。compress.stripImages默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 双向语义(可归档折叠、不可恢复 pinned)。filterProtectedToolMessages早已按hasMediaPayloadcarve 媒体并从 effectiveMessageIds 删除(Bug-39 块先于 warning 执行),媒体从未被静默折叠,warning 一直是真实的。因此该 carve 是冗余的,且在新语义下会把可归档媒体错误地重新钉死——丢弃而非移植。已在 PR 描述中披露此更正。全部门禁在 rebase 后的 head(6a50baf95)上重跑:kernel 1271/1271、bili 5329 pass / 0 fail、typecheck、build 全绿。顺带:[host][test] #2250 continuing-history drift tests fail deterministically on local Node 22 (CI Node 24 green) — session wrongly forks to |sub:<fp> #2668(drift 测试本地失败)已被上游 test(#2668): isolate responses-instructions-drift suite from machine-global XDG state #2673 修复,我在本次全量绿跑中验证后关闭了它。compress.stripImages默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 组合而非对抗(carrier screening 与 media 拆分正交)。判定:PR #2669 已就绪待复审(body 已同步更新 rebase 说明与 facet-2 更正),等待人工合并。
kernel: selector screens live carriers; withdrawal judged on original…
kernel: selector screens live carriers; withdrawal judged on original…
Merge pull request #2669 from ranxianglei/2026-10-10_kernel-live-carr…
kernel: bump acp-kernel to 0.0.117 (master took 0.0.116 via #2663)
merge master (took kernel 0.0.116 via #2663); keep 0.0.117
kernel: share one unit-projection judgment between recommend and apply (