Skip to content

resolveBoundaries 静默交换反向引用,参数错误被伪装成内容错误 #310

Closed

Description

@ranxianglei

来源: ranxianglei/billion-context#847 (ranxianglei/billion-context#847) 分析 模型提交反向引用(m01101→m01100)被误报为内容不足 时发现

现象

#847 中模型提交 startId=m01101, endId=m01100(反向)。kernel 静默交换后按真实小范围做 gate 检查,返回 Total compressible content too small (1734 chars ...).

参数错误被伪装成内容错误:模型收到的是「内容不够」,于是按提示「Combine more messages」继续挣扎,而真因是它把引用写反了。这是 7 次重复循环的直接燃料之一。

代码位置(0.0.74 dist)

resolveBoundaries 内的静默交换,两处:

  • dist/index.js:494-495 — if (startIndex > endIndex) { [startIndex, endIndex] = [endIndex, startIndex]; }
  • dist/index.js:1853 — 相同逻辑的第二处

建议修复

检测到 startId > endId 时不要静默交换,直接返回明确错误,例如:

startId m01101 is after endId m01100 — they appear reversed. Normalized range m01100..m01101 contains 1734 chars (< min 5000).

一次告诉模型两件事:方向错了 + 规范化后的真实大小。模型下一轮就能改对,而不是继续在「内容不足」的错误因果上打转。

注:billion-context host 侧已为 proxy mode 补了反向提示(见 #847 对应 PR),但 plugin mode(compress 在客户端扩展内执行)只能靠 kernel 本修复覆盖。

Activity

  1. ranxianglei commented on Sep 16, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已按 Triage 流程审核完毕(复现/查证 → 层次定位 → 方案评估),确认成立,修复进行中。

    查证结果

    • 真正的静默交换只有一处:src/boundaries.ts:114-116,resolveBoundaries 内 if (startIndex > endIndex) { [startIndex, endIndex] = [endIndex, startIndex]; }。已对照已发布的 npm 0.0.74 dist(index.js:494-495)逐字核实,一致。
    • 你指的第二处不需要修:dist index.js:1853 实际是 adjustBoundariesForReasoningPairs(src/reasoning-pairs.ts:35)——那是防御性守卫(if (startIndex > endIndex) return { startIndex, endIndex };),原样返回、不做任何交换,不是静默纠正。
    • 误导性消息来自下游:swap 之后该 range 分类为 "ok" 进入 applyCompression 的 gate;当批次里所有 range 都无效(countedRanges=0)时,gateMessage 三元式落到最后一个分支,产出 Total compressible content too small (0 chars across 0 range(s)...)——一个凭空捏造的内容结论,与真实的 per-range 错误并列出现。这就是模型收到「内容不够」的直接来源。

    层次判断

    静默 swap 是根因(参数错误被伪装成合法 range),gate 消息只是放大器。只改 gate 不解决 swap 吞掉参数错误的问题;只去掉 swap、不改 gate,模型仍会看到误导性的 "too small"。两处都要动:

    1. resolveBoundaries 检测到 startIndex > endIndex 时抛新的 BoundaryReversedError,不再交换。消息一次给两个事实:方向写反了 + 规范化后区间的真实字符数(模型下一轮即可改对引用顺序)。
    2. gate 在 countedRanges === 0(没有任何 range 被计入)时不再输出 "too small" 结论,只返回 per-range 错误。

    行为变化说明(有意为之)

    之前大的反向区间(规范化跨度超过 minCompressRange)会被静默交换后成功压缩;修复后一律返回反向错误——参数错误必须显性化,不能被内核吸收。若有任何 host 侧代码主动构造反向引用需同步修正(已知唯一生产方是 LLM 自己,见 #847)。

    实现中,PR 开好即更新本 issue。

  2. ranxianglei commented on Sep 16, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 修复完成,PR 已开:**https://github.com/ranxianglei/acp-kernel/pull/314**(分支 2026-09-16_reversed-boundary-error,commit e7550a2,基于 origin/master 5d54c31)。Closes #310。

    实现(按 triage 方案落地,4 文件 +195/−14)

    1. src/boundaries.ts — 去掉静默交换,抛显式错误

    新增导出的 BoundaryReversedError(code = "BOUNDARY_REVERSED",携带原始 refs + 规范化字符数),resolveBoundaries 检测到 startIndex > endIndex 时抛出,消息一次给两个事实:

    startId="m01101" resolves after endId="m01100" — the refs appear reversed: swap startId and endId. Normalized range m01100..m01101 contains 1734 chars.
    

    规范化字符数按 gate 同款口径统计(区间内非 summary 消息的 text.length 之和)。host 侧可像 BoundaryNotFoundError 一样精确 catch。

    2. src/compress.ts — gate 不再捏造 "too small"

    当批次里 零个 range 被计入(全部分类失败,如全反向)时,gate 跳过 Total compressible content too small (0 chars across 0 range(s)) 这个凭空结论,只返回 per-range 错误。混合批次行为不变:合法小 range 仍报 "too small",反向 range 的错误并列出现(有测试覆盖)。consumed/unknown 两个既有分支未动。

    测试

    删掉了把 bug 当特性固化的旧测试 applyCompression auto-swaps reversed boundaries,换成 4 个:

    • resolveBoundaries 单测:错误类型/code/refs/规范化字符数/消息形状
    • #847 形态复现:反向小 range(min=5000)→ 恰好一个错误,含 "reversed" + 真实字符数,不含 "too small"
    • 大反向区间(修复前会被静默交换后成功压缩)→ 现在拒绝
    • 混合批次 → "too small" 与反向错误同时出现

    Pre-flight

    • npm run typecheck — clean
    • npm test — 792/792 pass
    • npm run build — success;并核实构建产物 dist 中错误类已就位、swap 模式([startIndex, endIndex] = [endIndex, startIndex])已不存在

    行为变化(有意为之,已在 PR 描述展开)

    1. 大的反向区间以前会被静默交换后压缩成功,现在一律报错——参数错误必须显性化。内核内部无任何调用方会产生反向 refs(recommend 按数组邻接分段,见 src/recommend.ts:169-175 注释),唯一生产方是 LLM 自己。
    2. rebuildCompressionState 重放历史 LLM compress 调用:历史上写反的调用在 fork 恢复时不再产出 block——安全降级(那些消息保持可见/可压缩),而不是替模型圆一个它没真正请求过的顺序。

    中文摘要:静默交换改为抛 BoundaryReversedError(方向错 + 规范化真实大小一条消息说清),gate 在全批次无效时不再捏造 "too small";typecheck/792 测试/build 全绿,PR #314 待合并。

  3. ranxianglei commented on Sep 16, 2026

    @ranxianglei
    OwnerAuthor

    这个是不是不应该错误,静默交换并不损失什么

  4. ranxianglei commented on Sep 16, 2026

    @ranxianglei
    OwnerAuthor

    方向调整(owner 在 billion-context#847 裁定):静默交换保留,不引入 BoundaryReversedError。

    理由:反向引用对没有歧义——恰好只有一种合理解读(该区间),意图明确、零数据风险;硬报错会把本可原地恢复的情况变成强制重发(多耗一轮,且正是 #847 要消灭的重试循环模式)。真正的缺陷不在交换本身,而在下游消息对交换零交代:模型不知道自己提交的输入被改写过,于是继续在「内容不够」的错误前提上推理。

    调整后的方案:

    1. 保留 resolveBoundaries 的规范化交换(src/boundaries.ts:114-116)。
    2. 删除 BoundaryReversedError 计划。
    3. 用透明化替代:凡输入范围端点写反,kernel 在结果中留痕——
      • gate 拒绝路径:gateMessage 追加一句 note: refs were given reversed (mXXXXX→mYYYYY), normalized to mYYYYY..mXXXXX(无 API 变更);
      • 成功路径:结果对象新增可选 notes?: string[](加法式、向后兼容),同样内容,供 proxy mode host 与 plugin mode 扩展各自渲染进 tool result。
    4. countedRanges=0 时 gate 不再输出捏造的 "too small"——这条不变,照做。

    billion-context host 侧(billion-context#849)已按「提示而非报错」实现,与本方向一致。

  5. ranxianglei commented on Sep 16, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 已按裁定重做:保留静默交换,用透明化替代报错。PR #314 分支已更新(commit 7e30477),PR 描述同步改写。

    逐条落实

    裁定 实现
    1. 保留规范化交换 src/boundaries.ts 的 swap 原样保留,不引入 BoundaryReversedError(上一版已删净,dist 验证无残留)
    2. gate 拒绝路径留痕 gateMessage 末尾追加 note: refs were given reversed (mXXXXX→mYYYYY), normalized to mYYYYY..mXXXXX,无 API 变更。一次告诉模型两件事:尺寸不足 + 引用被改写
    3. 成功路径留痕 ApplyCompressionResult.result 新增可选 notes?: string[](加法式;host 渲染进 tool result 即覆盖 proxy/plugin 两种模式)
    4. countedRanges=0 不再捏造 "too small" 照做:全部 spec 分类失败时只返回 per-range 错误(如 range foo..bar: Invalid boundary ref(s): ...),不再出现 too small (0 chars across 0 range(s)) 这类误导性判词

    实现时的几个边界决定(均已写进测试)

    • notes 为空时整个字段省略(...(notes.length > 0 ? { notes } : {}))——对现有消费者零可见变化。
    • overlap-skipped 的范围不出 note:被跳过的范围本来就没被解释,不算「输入被改写」。
    • note 里不带字符数:gate 消息本身已含真实尺寸(too small (N chars ...)),单范围场景下 N 就是该范围规范化后的大小,重复会冗余。
    • 正向引用行为完全不变;无新错误类型、无新依赖。

    一个诚实的备注

    #847 里的 m01101/m01100 是相邻引用——规范化后区间完全相同,反转在那里只是外观问题,"too small" 判词本身是对的。真正的循环燃料是「sub-min 跨度 + 零交代」的组合:模型不知道输入被改写过,就在错误前提上反复挣扎。本修复保证 plugin mode 下模型每轮都能看到改写事实;host 侧提示(billion-context#849)仍是打断重试循环的主力,kernel 这条是补上 plugin mode 的盲区。

    验证

    • npm run typecheck 干净;npm test 793/793(重写 4 个旧测试、新增 1 个 all-invalid guard);npm run build 干净
    • dist 抽查:note 字符串存在,BOUNDARY_REVERSED 无残留

    PR: https://github.com/ranxianglei/acp-kernel/pull/314(等你合并)

    中文摘要:按 billion-context#847 裁定把「硬报错」方案改为「保留交换+透明化」——反向引用不再报错,而是通过 gate 消息追加说明和新增可选 result.notes 告知模型其输入被规范化过,同时修掉了全失败批次被捏造成 "too small" 的误导判词;typecheck/793 测试/build 全绿,可以合并。

  6. ranxianglei commented on Sep 17, 2026

    @ranxianglei
    OwnerAuthor

    继续

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions