Skip to content

[kernel][recommend] selector advertises spans the apply side structurally rejects — live-carrier screening missing (permanent advertise/validate disagreement) #2663

Closed

Description

@ranxianglei

来源: #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:

  1. A message carrying summaryOfBlockId pointing 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's effectiveMessageIds (carriers are deliberately excluded from coverage so they stay visible). It therefore survives pruning and, because buildCompressibleRanges has NO carrier awareness, falls through to "fully compressible" (recommend.ts:246-255).
  2. In the pruned view, carriers separated by covered raws become ADJACENT — one contiguous group, one advertised range.
  3. 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 throws Range 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 from filteredIds — 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

  1. ranxianglei commented on Oct 10, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 Triage 结论:成立,已按三步审核对 master 7c993f9ed 逐条验证完毕,按你给的修复方向开工。

    ① 复现/查证(三处全部坐实)

    1. 主缺陷(载体盲区):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 每请求重提名重拒绝,机制与代码路径完全吻合。
    2. mediaExcluded 说谎(compress.ts:1336-1344):只发 warning「Excluded N message(s) carrying image/attachment payload(s)」,从不把 media id 移出 filteredIds——span 跨 media 时 apply 实际会折叠它们(这个插件压缩上下文,会导致deepseek识图出问题。 #1188 明确:media payload 折叠后不可恢复),警告与行为相反。
    3. withdrawal 输入不一致:recommend 在剪枝后视图上跑 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 筛查换到原始数组后,部分区间的广告与否可能变化(偏差方向安全:宁可少广告)。无新增配置面。

    开工中,完成后再按八问体例汇报。

  2. ranxianglei commented on Oct 10, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 完成:PR #2669(#2669 本 issue。本地门禁:kernel 套件 1289/1289 绿、bili 套件绿(除 2 条 master 既有环境依赖失败,见文末 #2668)、typecheck/build 绿。

    结构化八问:

    1. 是什么问题:压缩选择器(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 用两种数组类判定。
    2. 什么原因导致的:「advertised ⇒ foldable」这条不变量在选择器侧没有落点——carrier 排除逻辑只存在于 compress.ts apply 路径(live-carrier carve),recommend.ts 完全没有 carrier 概念;且两个 screen 一个判 pruned view、一个判原始重发数组,没有任何东西保证两者一致。根因是 selector 与 applier 之间缺少共享判据,不是某条报错本身。
    3. 影响范围与严重程度:full-log 型 plugin host(DSH 等)上,带 checkpoint carrier 的深度会话每轮重复 advertise 死 span→apply 拒绝,永久循环、浪费 token 并干扰模型行为([Bug][preflight] range selector re-nominates a fully-folded span every turn — permanent "no new compressible messages" rejection loop + nudge churn #2638 生产实证,v0.1.190);media 静默折叠是数据丢失级(字节折叠后不可恢复,这个插件压缩上下文,会导致deepseek识图出问题。 #1188);数组类分歧在当前可达状态空间无可观察影响(prune 的 orphan stripping 使两视图都 pair-complete),属结构性漂移风险。
    4. 怎么修复的:统一判据——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。
    5. 是否有回归问题:逐项过过:无 carrier 会话的 advertise 输出不变(segmentation 规则未动);carrier-bridging span 不再 advertise(本修复目标);media carve 只改变含 media 的新块覆盖集(旧=折进去销毁,新=保留,有意变更已披露);仅含 media 的手动 range 现在报错而非造空块(安全方向);BLOCKED-ref 的 gap 语义刻意未动(碎片化爆炸半径大,属另一机制族,PR 中注明为 follow-up 候选);golden wire-contract pin 未触碰。
    6. 是否破坏缓存:不涉及。prefix cache、更新节流、会话持久化格式均未动;block 覆盖集语义未变(media 只是不进新块,与 protected-tool 先例一致)。
    7. 是否改了设计方向:不涉及。仍是「selector advertise、apply validate」架构;本次只在两者之间加了共享判据(single source of truth),数据流与边界未转向。
    8. 是否引入新配置:不涉及。无新 env/config/CLI flag;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 会校验)。

  3. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    OwnerAuthor

    冲突解决一下

  4. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 冲突已解决,PR #2669 已 rebase 到当前 master(aac8bc3fe)并强推更新。

    结构化八问(本轮 = 冲突解决 + rebase 适配):

    1. 是什么问题:PR kernel: selector screens live carriers; withdrawal judged on original array class (#2663) #2669 与 master 冲突——master 前进了约 40 个提交,其中 建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607(fold archivable media like text; pin only unrecoverable payloads)直接改写了我 draft 中 facet-2 carve 所在的代码区。
    2. 什么原因导致的:master 侧合入了 建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607,把媒体语义拆成「可归档(inline base64/data URL → 折叠如文本)/不可恢复(远程 URL、DeepSeek file ref、Google fileData/videoMetadata → 仍 BLOCKED)」;我的 blanket media carve 与新方向正面抵触。
    3. 影响范围与严重程度:仅本 PR 分支自身;无生产行为影响。
    4. 怎么修复的:rebase + 三处决议——① recommend.ts:carrier screening 保留在 master 的 unrecoverable-media check 之上(union);② compress.ts:移除我原有的 mediaExcluded carve(见第 5 条的发现),保留 pruneNode stash / recommendNode wiring / shared predicate;③ 版本 0.0.115→0.0.116。t3 测试重写为钉住 建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 双向语义(可归档折叠、不可恢复 pinned)。
    5. 是否有回归问题:无。重要更正:重读旧基线代码后确认 issue 的 facet 2 是误诊——旧版 filterProtectedToolMessages 早已按 hasMediaPayload carve 媒体并从 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 修复,我在本次全量绿跑中验证后关闭了它。
    6. 是否破坏缓存:不涉及。
    7. 是否改了设计方向:不变;本 PR 现在与 建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 组合而非对抗(carrier screening 与 media 拆分正交)。
    8. 是否引入了新的配置:无。

    判定:PR #2669 已就绪待复审(body 已同步更新 rebase 说明与 facet-2 更正),等待人工合并。

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