Skip to content

fix: recognize mcp__bili__compress in reasoning drop and prune orphan exemption (#3222) - #3225

Merged
ranxianglei merged 3 commits into
masterfrom
2026-10-15_fix-3222-mcp-compress-stages
Oct 15, 2026
Merged

ranxianglei merged 3 commits into
masterfrom
2026-10-15_fix-3222-mcp-compress-stages

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #3222

kernel-change: #3222 mandatory fix 2 requires the kernel's prune pass to recognize MCP-form compress calls when exempting pending calls from orphan stripping; the host cannot solve this because stripOrphanedToolCalls runs inside the kernel pipeline (compress.ts pruneNode) where only ctx.config is available. The change reuses the existing Config.isCompressTool predicate family (already wired by src/config.ts loadOptions since #2970) as an optional PruneOptions field — unset stays bare-name only, byte-identical to before. acp-kernel bumped 0.0.124 -> 0.0.125.

What changes

Mandatory fix 1 — MCP reasoning cleanup (name + request order)

  1. dropCompressReasoning selected only toolName === "compress", so MCP-lane hosts (mcp__bili__compress) never received Protected-message exemption inflates the incompressible context floor: per-compression reasoning is never reclaimable (~83.5% of measured residual) #651's reasoning cleanup even with compress.reasoning.drop enabled. The selector now uses isMcpBiliCompress (src/config.ts).
  2. Ordering: the drop ran AFTER core.processTurn, whose hide-compress-calls node removes inactive-block call+result pairs INSIDE the pipeline. Once the anchor pair is gone, the pre-call reasoning can no longer be located — this leak applies to the BARE name too, not just MCP. Fix: planReasoningDrop computes the id set on the INBOUND history before processTurn (anchors always present); applyReasoningDrop filters any derived view by that set afterwards (steady post-turn view in all three prepare lanes, plus relay round-2 re-render via Prepared.dropReasoning). Kernel ids are never reused within a session, so the plan stays valid across views. withReasoningDrop is kept for standalone callers (tests) as plan+apply.

Mandatory fix 2 — prune keeps pending MCP compress calls

stripOrphanedToolCalls hardcoded toolName === "compress", so a pending mcp__bili__compress call (no result yet) was deleted as an orphan under active coverage while its bare-name twin survived. prune() now takes an optional isCompressTool predicate; pruneNode passes ctx.config.isCompressTool; renderHandoff keeps the default (no config on that path, view-only export).

Behavior changes (old -> new)

Path Old New Why
Reasoning before closed mcp__bili__compress round never dropped dropped per existing compress.reasoning.* config the reported defect
Reasoning before a bare/MCP compress call whose pair is hidden mid-turn (inactive block) leaked forever once anchors gone dropped (planned pre-turn) ordering half of the same defect; affects bare name too
Pending (no-result) mcp__bili__compress call under active coverage deleted as orphan kept, like bare compress the reported defect
Kernel consumers without isCompressTool set bare-name only unchanged (byte-identical default) discipline of the Config.isCompressTool contract (types.ts)

No threshold/default/timing drift elsewhere: all protection gates (drop:false, threshold strictly-greater, no-result, result-is-last, strictEcho, thinkingSignature) are unchanged and re-pinned by tests for BOTH names.

Config surface

None. Reuses existing compress.reasoning.* config and the host-injected Config.isCompressTool predicate already present in loadOptions().kernelConfig since #2970. No new env var / CLI flag / config field.

Verification

  • Function-level parity repro (issue tables): both defects confirmed pre-fix, parity confirmed post-fix; default-option baseline byte-identical.
  • New regression tests:
    • tests/issue3222-mcp-reasoning-prune.test.ts (host): name parity across every protection gate + request-order scenario driving the REAL core.processTurn (superseded block's pair hidden mid-turn; pins that old post-hoc computation leaks the reasoning while the planned drop does not).
    • kernel/tests/prune-orphan-mcp.test.ts: pending MCP call kept with predicate; bare-name pinned; bash control still stripped; other MCP tools not misidentified; default options byte-identical.
  • Local targeted green: host suites (issue3222/reasoning-drop/reasoning-echo/issue2970/basic = 126 tests) + kernel prune/hide-consumed suites (49 tests); typecheck clean; build OK.
  • Codex real-client e2e: local run blocked — the local upstream at 127.0.0.1:8199 refused connections during the run (ECONNREFUSED in proxy log; pipeline itself ran clean through processTurn). This diff touches src/server.ts + src/server/** so CI's ci-e2e.yml auto-runs it against the hosted upstream.

Residual boundary (noted, out of scope)

Id-based apply matches exact message ids. If a drop-target reasoning message is simultaneously reminted by instance-reid (genuinely-new instance whose id a folded copy claims, absent from the previous pass — the #2695/#462 churn corner), the plan misses that one message and it survives (pre-fix behavior for that case, conservative direction: leak, never corruption). Not part of the reported defect; left as a documented boundary rather than widening matching without traffic evidence.

awork added 2 commits October 15, 2026 10:53
…mpressTool

#3222 mandatory fix 2. stripOrphanedToolCalls hardcoded toolName === "compress", so a pending mcp__bili__compress call (no result yet) was deleted as an orphan under active coverage while its bare-name twin survived. prune() now takes an optional isCompressTool predicate (same family as Config.isCompressTool / hideConsumedCompressCalls); unset stays bare-name only, byte-identical to before. pruneNode passes ctx.config.isCompressTool through; renderHandoff keeps the default (no config on that path). Bumps acp-kernel to 0.0.125.
…re processTurn

#3222 mandatory fix 1. Two defects, one mechanism family (compress-call name
recognition + request ordering):

1. dropCompressReasoning selected only toolName === "compress", so MCP-lane
   hosts (mcp__bili__compress) never got #651's reasoning cleanup even with
   compress.reasoning.drop enabled. The selector now uses isMcpBiliCompress
   (src/config.ts, wired since #2970). No new config surface.

2. The drop ran AFTER core.processTurn, whose hide-compress-calls node removes
   inactive-block call+result pairs inside the pipeline. Once the anchor pair
   is gone, the pre-call reasoning can no longer be located — the leak applies
   to the bare name too, not just MCP. Fix: planReasoningDrop computes the id
   set on the INBOUND history before processTurn (anchors always present),
   applyReasoningDrop filters any derived view by that set afterwards (steady
   post-turn view AND relay round-2 re-render via Prepared.dropReasoning).
   Kernel ids are never reused within a session, so the plan stays valid.
   withReasoningDrop stays for standalone callers (tests) as plan+apply.

Regression tests: tests/issue3222-mcp-reasoning-prune.test.ts pins name
parity for every protection gate, plus the request-order scenario driving the
real kernel pipeline (superseded block's pair hidden mid-turn; old post-hoc
computation leaks the reasoning, planned drop does not).
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-15_fix-3222-mcp-compress-stages (5926407)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.3723-pr.3225.6255f47

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr3225.tgz
npm install -g package

Install instructions are refreshed on each push.

Review follow-up on #3225: the kernel version bump (0.0.124 -> 0.0.125) was
not regenerated into package-lock.json, so the lock-sync gate failed and
cascaded into skipping the entire test + static-gates matrix. Regenerated via
the exact CI command (npm install --package-lock-only); single-line change.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

查重:不重复。#3225 是 #3222 两条必改的唯一实现 PR(Closes #3222);open issue/PR 按缺陷签名检索无并行工作。

独立审核(不复读 issue 侧结论):我重读了 diff、行为面与两个新测试,并对关键链路做了代码级验证。方向正确、命中根因、无回归、测试有牙。发现并当场修掉一个 CI 阻塞项(lock-sync 红),已推到 PR 分支;其余 gate 此前均绿。

结构化八问:

  1. 是什么问题:MCP-lane 宿主把 bili 压缩工具以 mcp__bili__compress 形态重放,但两个只认裸名 compress 的阶段没接上——(a) reasoning 清理(Protected-message exemption inflates the incompressible context floor: per-compression reasoning is never reclaimable (~83.5% of measured residual) #651)选择器漏掉它;(b) prune 孤儿豁免漏掉它,pending 的 MCP 压缩调用被当孤儿删掉。

  2. 什么原因导致的:两处都硬编码 toolName === "compress",没走既有的 isCompressTool/isMcpBiliCompress 谓词族。更隐蔽的一半是顺序缺陷:reasoning drop 跑在 core.processTurn 之后,而 processTurn 内部的 hide-compress-calls 节点会先删掉 inactive block 的 call+result 对——锚点没了,前置 reasoning 再也定位不到(这个泄漏对裸名同样成立)。根因是「识别谓词没统一 + 计算时机晚于锚点消失」,不是表面过滤。

  3. 影响范围与严重程度:仅 MCP-lane 宿主(把工具以 mcp__bili__ 前缀暴露的场景)。触发条件:compress.reasoning.drop 开启 + 出现 closed/pending 的 mcp__bili__compress 轮次。严重度 = 特定条件下有问题:① reasoning 不被清理(多占 token,成本/体验瑕疵);② pending MCP 压缩调用在 active coverage 下被误删为孤儿(丢一次进行中的压缩意图)。非全量崩溃;裸名路径不受影响。

  4. 怎么修复的:识别谓词统一到现成的 isMcpBiliCompress(同时匹配裸名+MCP,src/config.ts:1293),host 与 kernel 用同一谓词族(config.ts:1409 注入 Config.isCompressTool)。顺序改为 plan/apply 两段:plan 在 processTurn 之前对入站历史算出待删 id 集(锚点必然在场),apply 按 id 过滤 post-turn 视图与 relay round-2 重渲染(id 会话内不复用,跨视图稳定)。kernel prune() 新增可选 isCompressTool,unset 默认裸名、字节级不变;pruneNode 透传 ctx.config.isCompressTool;renderHandoff(view-only)保持默认。

  5. 是否有回归问题(逐项过既有行为):裸名 reasoning drop 不变(谓词含裸名);全部保护门(drop:false / threshold 严格大于 / 无 result / result 是最后一条 / strictEcho / thinkingSignature)两名字都被测试重新钉住;无谓词的外部 kernel 消费者字节级不变(kernel 测试显式钉死);其它工具(bash / 外部 MCP / acp_rule)不误判(测试钉死);tool-call arguments 字节保真不受影响(本改动只决定删哪些消息,不改写任何 payload 字节);高危模式(语义漂移/fail-fast→吞错/阈值时序输出格式)未见;残留 remint 边界是保守方向的泄漏而非损坏,已在 PR 正文记录。→ 无回归。

  6. 是否破坏缓存:不涉及。改动只在请求重建阶段过滤消息集合,不触碰 prefix-cache 前缀稳定性、增量游标或去重键。

  7. 是否改了设计方向:不涉及架构转向。只是把两处硬编码裸名收敛到既有谓词族,并把 reasoning-drop 计算时机提前到锚点消失之前(plan/apply 拆分)。google lane 有意不适用 Protected-message exemption inflates the incompressible context floor: per-compression reasoning is never reclaimable (~83.5% of measured residual) #651(src/server/prepare-google.ts:55 注释),不是遗漏。

  8. 是否引入新配置:无。复用现有 compress.reasoning.* 与 host 已注入的 Config.isCompressTool,无新 env/CLI/config 字段。kernel 版本 0.0.124→0.0.125(Kernel Boundary 要求的人工审核层,合并仍 human-only)。

行为变更清单(old→new):

路径 旧 新
closed mcp__bili__compress 轮的前置 reasoning 从不清理 按现有 compress.reasoning.* 清理
中途被隐藏的 compress 调用(裸/MCP)的前置 reasoning 锚点消失后永久泄漏 plan 提前算好,正常清理(裸名也受益)
active coverage 下 pending mcp__bili__compress(无 result) 当孤儿删 保留(与裸名一致)
未设 isCompressTool 的 kernel 消费者 仅裸名 不变(字节级一致)

我当场修掉的 CI 阻塞(小问题直接修):原 PR 10 文件 +315/-38,diff 干净、逐行与本 PR 目的相关。但 CI 的 lock-sync 红——kernel 版本 bump 到 0.0.125 没有同步进根 package-lock.json(仍停在 0.0.124,即 lockfile packages["kernel"].version)。由于 test/static-gates 两个 job needs: [.., lock-sync] 且 if: !failure(),这一处红级联跳过了整个单测矩阵 + typecheck + kernel suite + build——全量套件当时根本没跑。我用 CI 同款命令(npm install --package-lock-only)重新生成,只有 1 行(0.0.124→0.0.125),已作为独立标注 commit 推到 PR 分支(2026-10-15_fix-3222-mcp-compress-stages,commit 592640731)。这会重新触发 CI,预期 lock-sync 转绿并放行完整 test matrix。其余 gate(kernel-guard / version-guard / config-surface-guard / lane-guard / docs-sync / 各 e2e lane / build-artifact / theorem)此前均已绿。

判定:可合,前提两条——① 我刚推的 lockfile 修复让 CI 全量 test matrix 转绿(目前因级联跳过尚未真正运行,需看新一轮 CI);② kernel 改动按 Kernel Boundary 走人工审核(human merge)。两个新回归测试均有牙:host 侧用真实 core.processTurn 复现原 bug 并断言旧 post-hoc 逻辑泄漏 r1、新 plan 逻辑清除它;kernel 侧强制走 active-coverage 路径让 stripOrphanedToolCalls 真执行并钉死默认路径字节一致。我不代合并。

@kuviki

kuviki commented Oct 15, 2026

Copy link
Copy Markdown

检查 CI

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b)

检查完 PR #3225 的 CI:全绿通过(43/43 checks 全部 success)。

1. lock-sync 修复已生效,整条测试矩阵解锁。 我上一轮在分支上补的 package-lock.json 重新生成(commit 592640731)把 lock-sync 从红转绿。之前它红导致 test 与 static-gates 被级联跳过、全量单测根本没跑;现在都跑了且全过:

  • test 全矩阵 12 leg(ubuntu/windows × node 22/24 × shard 1/2/3)✅
  • static-gates 4 leg(typecheck + kernel 套件 + pi-subagents + build)✅
  • kernel-guard / version-guard / build-artifact / e2e 等门禁 ✅

2. 唯一一次失败 = 抖动(flake),不是本 PR 的回归。 real opencode native lane vs deterministic fake upstream 首跑挂在断言 proxy must persist the session(tests/e2e/e2e-native-opencode.test.ts:592)。判定为「SIGTERM→flush→落盘」的懒持久化时序竞态,证据链四条:

  • 我的提交只动了 package-lock.json(版本号字符串,零运行时影响);
  • 同一 lane 在上一个 head(5f3be1f5e)是绿的;
  • 同一次运行里 test 3(同样断言 graceful stop 后会话已持久化)是过的 —— 说明持久化本身正常,只是那次 one-shot teardown 的 flush 没赶上;
  • 本 PR 完全没碰持久化路径(无 persist.ts / session-id.ts 改动)。

我已触发 rerun-failed-jobs,重跑通过,坐实是 flake。这条属于既有的 e2e teardown 时序问题、不在本 PR diff 内,先在此记录留痕(单次抖动、重试即绿,未达立项阈值;若后续复现再单独跟进)。

结论:CI 全绿。代码审核结论维持上一轮不变(根因正确、无 bare-name 回归、保护门钉死、两个新回归测试有牙)。剩下就是 kernel 变更按 Kernel Boundary 走人工 merge —— PR 可合并。

@ranxianglei
ranxianglei merged commit c43bdf0 into master Oct 15, 2026
44 of 45 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.446-master.c43bdf0 (rolling master tag).

npm install -g billion-context@0.0.446-master.c43bdf0  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] MCP compress 未接入 reasoning 清理及未完成调用保护

2 participants