Skip to content

fix: always protect the compress tool (and its result) - #7

Merged
ranxianglei merged 1 commit into
masterfrom
2026-08-01_always-protect-compress-tool
Aug 1, 2026
Merged

ranxianglei merged 1 commit into
masterfrom
2026-08-01_always-protect-compress-tool

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

问题

compress 工具的调用记录是 ACP 元数据(system prompt 依赖、decompress 依赖),但 `isMessageProtected` 只匹配 `contentType === "tool-call"` 且要求该工具列在 `protectedTools` 里 —— 而 pai-acp 默认 `protectedTools: []`。

后果:compress 的 tool-call 和 tool-result 都漏进推荐给模型的可压缩范围,模型可能被 nudge 去压缩掉自己的 compress 调用,破坏 decompress/search 和"summary 是历史元数据"的契约。

实测(修复前):
```
消息: u0, bash, tool, compress(call), tool(result), u1, bash, tool
可压缩块: m00001–m00005 ← compress 调用(m4) 和 result(m5) 都在里
```

修复

  1. `ALWAYS_PROTECTED_TOOLS = ["compress"]`:`isMessageProtected` 硬编码此列表,compress 无论用户配置如何都受保护。未来加更多元数据工具只需一行。

  2. tool-result 也纳入判断:contentType 守卫从 `tool-call` 扩展到 `tool-call | tool-result`。host 投影 tool-result 时带相同 toolName,有 toolName 时直接匹配。

  3. toolCallId 配对(应对 tool-result 缺 toolName 的常见情况):新增 `collectProtectedToolCallIds()` + `isMessageProtectedWithPairing()`。recommend 路径用配对版,compress 的 tool-result 即便没有 toolName 也通过其 call 的 id 被保护。

`applyCompression` 的 `filterProtectedToolMessages` 已有 toolCallId 配对逻辑,硬编码列表同时流经 recommend(buildCompressibleRanges)和执行(applySingleRange)两条路径。

修复后

```
可压缩块: m00001–m00003, m00006–m00008 ← compress 调用/result 都排除
protected: m00004–m00005 [compress]
```

测试

  • 2 个新测试:空 protectedTools 下 compress 仍被排除;无 toolName 的 compress result 通过配对被排除
  • typecheck ✅ · 191 tests pass ✅ · build ✅

+117/-3,3 文件。

… config

The compress tool's records are ACP metadata: the system prompt treats past
compress calls as load-bearing context, and decompress relies on them. Yet
isMessageProtected only matched contentType === "tool-call" AND required the
tool to be listed in protectedTools — which pai-acp leaves empty by default.
So both the compress tool-call AND its tool-result leaked into the
compressible ranges recommended to the model, meaning the model could be
nudged to compress away its own compress call, breaking decompress/search
and the "summary is historical" contract.

Fixes:

1. ALWAYS_PROTECTED_TOOLS = ["compress"]: isMessageProtected hard-codes this
   list, so compress is protected even with empty user config. Adding more
   metadata tools later is one-line.

2. tool-result now also evaluated: the contentType guard widened to
   "tool-call" | "tool-result". Hosts project tool-results with the same
   toolName as their call, so direct matching works when toolName is present.

3. toolCallId pairing for tool-results lacking toolName (common: hosts
   often project a result with only toolCallId). New
   collectProtectedToolCallIds() + isMessageProtectedWithPairing(); the
   recommend path uses the pairing variant so a compress result is protected
   via its call's id even when its own toolName is absent.

applyCompression's existing filterProtectedToolMessages already pairs by
toolCallId, so the hard-coded list flows through both the recommend
(buildCompressibleRanges) and execution (applySingleRange) paths.

2 new tests: compress always excluded with empty protectedTools; compress
result protected via pairing without toolName.

typecheck clean; 191 tests pass; build OK.
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.

1 participant