Skip to content

[PR] fix(block-ops): acp_rewrite respects compress's summary min/max bounds and accepts numeric blockId | fix(block-ops): acp_rewrite 遵守 compress 的摘要上下限,并接受数字形式的 blockId - #6315

Open
BBleae wants to merge 2 commits into
ranxianglei:masterfrom
BBleae:2026-11-12_acp-rewrite-summary-limits

Conversation

@BBleae

@BBleae BBleae commented Nov 11, 2026 •

Copy link
Copy Markdown

Fixes #6319

这是 #5765 / PR #5774(压缩块生命周期工具,compress.blockOps,默认关闭)的跟进。只改宿主侧,没有动 kernel/**。

问题

acp_rewrite 替换的是块里存的摘要,而这份摘要每个请求都会随常驻视图发出去。compress 对写进这里的摘要有把关;acp_rewrite 只查「非空」和「与原来不同」,所以同一个块上 compress 会拒收的摘要,acp_rewrite 照样存进去。

在 master c26c31ace 上,默认配置(compress.maxSummaryLength 20000,minSummaryLength 50),折叠一段约 6000 字符的范围:

摘要 compress 对得到的块调 acp_rewrite
25000 字符 拒收(Summary too long) b1 summary replaced (… → 25000 chars)
9000 字符(没超上限,但比原范围大) 拒收(no net shrink) 替换成功
1 字符 拒收(Summary too short) 替换成功

工具说明写的是「replace … with shorter text you supply」,但没有任何检查让调用遵守这一点。一个块被改写成 25000 字符后,之后每个请求的开销都比它折叠掉的那些消息还大。

另一处:acp_retire / acp_restore 通过 collectBlockIds 接受数字形式的 blockId: 5,acp_rewrite 却只认字符串:{ blockId: 1, summary } 回的是 provide { blockId, summary } or items: …,就像什么都没传;items: [{ blockId: 1, summary }] 也一样。

改法

src/acp-block-ops.ts,在调内核之前:

  • 改写后的文字受解析后配置里的 compress.minSummaryLength / compress.maxSummaryLength 约束,含义和 compress 里一样(0 = 不限制),回执措辞也一样(Summary too short (N chars, min M) / Summary too long (N chars, max M))。过短的回执指向 acp_retire——把块移出常驻视图本来就该用它。
  • 净收缩:摘要不比块折叠掉的内容小(compressedTokens,和 src/stream.ts 里 gate:no-net-shrink 的检查一样用内核的 defaultCountTokens 计数)就拒收。留一个例外,保证这道检查永远不会挡住改进:只要比它替换掉的那份摘要小,改写就一律放行,即使存着的摘要本来就比折叠内容大。
  • 只判内核本来会执行的改写。未知、格式不对或非 active 的 id,以及空白或相同的文字,仍然走内核原有的理由。
  • 批量时,被拒的那一项写进回执,其余各项照常生效(和现有的逐项失败一样)。
  • blockId 在单条形式和 items 里都接受非负整数,范围和 collectBlockIds 一致。工具 schema 仍写 string;这里只是不再把数字当成「没传」。
  • executeProxyTool 把 ctx.config 传给 executeAcpRewrite。

文档:docs/configuration.md 和 docs/configuration.zh-CN.md 里 compress.blockOps 下 acp_rewrite 那一行,现在列出了这些拒收情形。

对现有调用方有变化的一点:默认配置下,不到 50 字符的改写现在会被拒收。如果你更希望短改写继续合法、只保留上限和净收缩,说一声我就把下限去掉,就是一个 if。

不在这个 PR 里:回滚位

每次改写都会写 previousSummary,回执、工具说明、prompt 段和文档也都说上一份摘要「kept for rollback」。但没有任何地方读这个字段:没有工具或参数能把它恢复回来,第二次改写还会把这个位覆盖掉。我不想在没人提的情况下加新动作。你倾向哪种?

  1. 加一条回滚路径,比如 acp_rewrite({ blockId, rollback: true }) 对调 summary 和 previousSummary;或者
  2. 把回执 / 说明 / prompt / 文档里「for rollback」的说法去掉(原文仍可通过 decompress 取回,回执里已经这么写了)。

哪种都可以作为后续 PR 补上。

测试

tests/issue5765-acp-block-ops.test.ts 新增两个用例,在 master 上都失败、改动后都通过:

  • maxSummaryLength 20000 / minSummaryLength 50,块折叠了约 6000 字符:25000 字符、9000 字符、1 字符各自按对应理由被拒,存着的摘要和 previousSummary 都不变;批量时被拒的那项写进回执,同批里合法的那项照常生效;
  • 数字 blockId 在单条形式和 items 里都能用;1.5 和 -1 仍不算 id。

原有的 #5765 用例没改,全部通过。

在 Windows、Node 22 上离线跑(出站网络被挡),tests/*.test.ts 用 node --test 跑,带的三个 --import 和 npm test 一样:

  • tests/issue5765-acp-block-ops.test.ts:改动前 8 过 / 2 败(就是新增的两个),改动后 10 / 10。
  • 这个分支上的宿主全套(--test-concurrency=4):11007 项(master 上 11005 项 + 新增 2 项),10922 过、9 败、9 取消、67 跳过。9 个取消(external-summary-autofold 7 个、fork-nudge-deadzone 2 个)和其中一个失败(preflight-target-stability)是机器负载高造成的:这几个文件单独跑都过。其余 8 个失败不经过这次改的代码,而且在这台机器上,未改动的 master c26c31ace 也是同样的失败:
  • tsc --noEmit 干净。
  • 内核测试没有重跑:kernel/ 下没有文件改动。

…ccept a numeric blockId

acp_rewrite only checked "non-empty" and "not identical", so it stored
summaries compress refuses for the same block: over
compress.maxSummaryLength, under compress.minSummaryLength, or larger than
the content the block folded. The stored summary rides in the resident view
on every request, so a block could end up costing more than the messages it
replaced.

executeAcpRewrite now judges each rewrite before the kernel call:
- compress.minSummaryLength / maxSummaryLength of the resolved config
  (0 or absent = unbounded, as in compress), with compress's wording;
- net shrink: a summary not smaller than the folded content
  (compressedTokens, defaultCountTokens) is refused, unless it shrinks the
  summary it replaces;
- unknown/inactive ids and blank or identical text keep the kernel's
  existing reasons; in a batch the other items still land.

blockId also accepts a non-negative integer (single form and items), the
way acp_retire / acp_restore already do; before, a number was read as a
missing argument.

Host-side only; kernel/** is untouched. Docs line for compress.blockOps
updated in both languages.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 23:48
@ranxianglei ranxianglei changed the title fix(block-ops): hold acp_rewrite to the compress summary bounds and accept a numeric blockId [PR] fix(block-ops): hold acp_rewrite to the compress summary bounds and accept a numeric blockId Nov 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟡 Changes recommended

There are unresolved review findings—including a moderate correctness issue where blockIdArg() returns an empty string for whitespace-only input (leading to inconsistent and harder-to-reason-about id handling), plus documentation mismatches about acp_rewrite’s intended exception behavior.

3 open issues
What changed in this PR

Tightens the host-side acp_rewrite block-op so rewritten summaries are constrained by the same bounds and net-shrink expectations as compress, and extends acp_rewrite to accept numeric blockId inputs (matching other block-ops), with updated docs and regression tests.

Changes:

  • Enforce compress.minSummaryLength / compress.maxSummaryLength and a “no net shrink” gate for acp_rewrite prior to the kernel mutation while preserving kernel-owned rejection reasons.
  • Accept non-negative integer numeric blockId values in both single and batch rewrite forms and thread resolved config into the rewrite executor.
  • Add tests for rejection reasons + partial success behavior in batches, and update EN/ZH configuration docs to describe the new rejection conditions.
File Description
tests/​issue5765-acp-block-ops.test.ts Adds regression tests covering acp_rewrite refusals for too-long/too-short/no-net-shrink summaries, batch partial success reporting, and numeric blockId handling (including rejecting non-integer/negative values).
src/​loop/​core.ts Updates the proxy tool execution path to pass the resolved ctx.config into executeAcpRewrite so rewrite validation uses the effective configuration.
src/​acp-block-ops.ts Implements host-side acp_rewrite validation aligned with compress (min/max summary length and net-shrink checks) and expands blockId parsing to accept numeric IDs in single and batch forms.
docs/​configuration.zh-CN.md Updates the Chinese configuration documentation for compress.blockOps to describe additional acp_rewrite rejection conditions and related behavior.
docs/​configuration.md Updates the English configuration documentation for compress.blockOps to describe additional acp_rewrite rejection conditions and related behavior.

Review effort: Balanced

Comment thread src/acp-block-ops.ts
Comment on lines +86 to +90
function blockIdArg(v: unknown): string | undefined {
if (typeof v === "string") return v.trim();
if (typeof v === "number" && Number.isInteger(v) && v >= 0) return String(v);
return undefined;
}
Comment thread docs/configuration.md
- `acp_retire` — move active compressed blocks out of the resident view (their summaries no longer ride on every request; `acp_restore` / `decompress` / `search_context` still reach them by id).
- `acp_restore` — put retired blocks back into the resident view.
- `acp_rewrite` — replace a block's summary in place (one-step rollback slot `previousSummary` kept; identical/blank rewrites rejected).
- `acp_rewrite` — replace a block's summary in place (one-step rollback slot `previousSummary` kept; identical/blank rewrites rejected, and so are the summaries `compress` would refuse for the block: outside `compress.minSummaryLength` / `compress.maxSummaryLength`, or not smaller than the content the block folded).
- `acp_retire` — 把 active 压缩块移出常驻视图(其摘要不再随每个请求发送;仍可按 id 用 `acp_restore` / `decompress` / `search_context` 取回)。
- `acp_restore` — 把已退场块放回常驻视图。
- `acp_rewrite` — 原地替换块摘要(保留一步回滚位 `previousSummary`;相同/空白摘要拒收)。
- `acp_rewrite` — 原地替换块摘要(保留一步回滚位 `previousSummary`;相同/空白摘要拒收;`compress` 对该块会拒收的摘要同样拒收:长度超出 `compress.minSummaryLength` / `compress.maxSummaryLength`,或不比该块折叠的内容小)。
@BBleae BBleae changed the title [PR] fix(block-ops): hold acp_rewrite to the compress summary bounds and accept a numeric blockId fix(block-ops): acp_rewrite 遵守 compress 的摘要上下限,并接受数字形式的 blockId Nov 12, 2026
@ranxianglei ranxianglei changed the title fix(block-ops): acp_rewrite 遵守 compress 的摘要上下限,并接受数字形式的 blockId [PR] fix(block-ops): acp_rewrite respects compress's summary min/max bounds and accepts numeric blockId | fix(block-ops): acp_rewrite 遵守 compress 的摘要上下限,并接受数字形式的 blockId Nov 12, 2026
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

进度同步(进程曾被强制重启一次,现在继续):

正在做的最后一步:本地定向跑 tests/issue5765-acp-block-ops.test.ts 确认改动生效 + 测试有牙,然后给出完整八问结论。稍等。

…vement' exception (ranxianglei#6319)

The net-shrink refusal in executeAcpRewrite has an exception: a rewrite that
shrinks the summary it replaces is always allowed, even when the stored summary
was already larger than the folded content. The PR's two new tests cover the
refusals (too long / no net shrink / too short) and the numeric blockId, but not
this exception branch. Add a case that pins it so a future tightening of the
check cannot silently start refusing bloat cleanup.
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

结论先行:可合。 方向对、实现正确、测试有牙;我顺手补了一个 PR 漏掉的测试(已 push 到分支,见下)。作者抛出的两个决策点附推荐,均不阻塞合并。

查重:不重复。本 PR 正是 #6319 的修复(症状/机制一一对应);#6311 是另一处 blockOps 缺陷(改写后常驻视图仍带旧摘要),症状与机制都不同,不在本 PR 范围;无其他在途 PR 触碰 src/acp-block-ops.ts。

我不止读 diff,还本地验证了(PR head worktree,npm install + kernel build 后定向跑该测试文件):

  • PR head 上 tests/issue5765-acp-block-ops.test.ts 10/10(8 旧 + 2 新)。
  • 换回改动前的 src/acp-block-ops.ts+src/loop/core.ts(PR head 的父提交)再跑:8 过 / 2 败 → 两个新测试有牙,真能抓到这个 bug。
  • 核实 parseBlockIdArg/defaultCountTokens 是 kernel 既有导出(kernel/src/index.ts:52,114)→ "host-only、未动 kernel" 成立。
  • 核实 compress.{min,max}SummaryLength 经 compress-settings.ts 解析后恒为数字(kernel defaultConfig 默认 20000/50),ctx.config 即解析后配置 → "默认配置下 <50 字符被拒"这条真的成立,不是只活在测试里。
  • 净收缩判定与 src/stream.ts 的 gate:no-net-shrink 同口径(defaultCountTokens),且留了"永不挡改进"例外;compressedTokens 在真实折叠块恒 >0,检查不会空转。

八问

  1. 是什么问题:acp_rewrite 只查"非空/与原摘要不同",把 compress 会拒收的摘要(超长/未净收缩/过短)照样写进块,而这份摘要每轮都随常驻视图发出——一个被改写成 25000 字符的块,之后每轮比它折叠掉的消息还贵。另:acp_rewrite 只认字符串 blockId,{blockId:1} 被当成"没传"。
  2. 什么原因:compress 的摘要把关在内核折叠路径里,acp_rewrite 走的是另一条 host 执行器(executeAcpRewrite),当初没把同一套 min/maxSummaryLength/净收缩门搬过来;blockId 解析也没对齐 acp_retire 用的 collectBlockIds。
  3. 影响范围与严重程度:仅影响显式开启 compress.blockOps.enabled(默认关)的会话,且需模型实际发起一次"坏改写"才触发;触发后是每请求成本/上下文膨胀,非数据丢失或不可用。窄触发 + 成本类 → 偏 p2。
  4. 怎么修的:调内核前加 host 侧预判 rewriteRefusal——套用解析后配置的 min/maxSummaryLength(0=不限,与 compress 同义)+ 净收缩(新摘要 ≥ 折叠内容 token 且 ≥ 被替换摘要 token 才拒;比被替换摘要小则一律放行,保证永不挡改进);只判内核本来会执行的改写,未知/非 active/空白/相同仍走内核原理由;批量里被拒项写回执、其余照落。blockId 用新 blockIdArg 对齐 collectBlockIds(接受非负整数)。executeProxyTool 把 ctx.config 传进来。
  5. 是否回归:审了既有行为——① 默认关时误调仍回"not enabled"、不动状态(未改);② 原 8 个 Request two capabilities: compressed block "retirement" and summary "external rewrite". | 请求两个能力:压缩块「退役」与摘要「外部重写」 #5765 用例全过(本地+CI);③ 数字 blockId 是纯增量(不改变字符串路径);④ 唯一行为变更是默认配置下 <50 字符改写从"放行"变"拒收"——有意(对齐 compress),PR 正文已披露,见下方决策点。无 fail-fast 变吞错、无阈值/时序/输出格式漂移。
  6. 是否破坏缓存:不涉及。被拒改写不落状态(markDirty 仅 appliedCount>0 时调),不触碰前缀/缓存。
  7. 是否改设计方向:没有。仍是"内核管状态、host 管解析+回执+脏标记"的既有分工,只在 host 侧补一道与 compress 对齐的输入校验;未动 kernel、未动 wire。
  8. 是否引入新配置:没有新增键。复用既有 compress.minSummaryLength/maxSummaryLength(默认 50/20000,0=不限)。

代码质量(⑤b):命名达意、注释讲"为什么"、错误信息带上下文(字符数/上限)、无死代码、风格一致、未引新依赖。唯一小点:blockIdArg 的字符串/整数解析与 collectBlockIds 的 push 有 ~2 行重复——可接受(两处对空白串处理略异,但都最终被内核拒掉,无行为差),不强求抽公共。

我补的测试(已 push,commit 59d1e989,test-only):PR 两个新测试覆盖三种拒收 + 数字 id,但没覆盖"永不挡改进"的例外分支(把一份已膨胀、比折叠内容大的旧摘要改小——即使新摘要仍 > 折叠预算也应放行)。我加了一个用例钉住它(本地 11/11 过),防止日后收紧这道检查时悄悄开始拒绝"清理膨胀"。CI 会在新 head 重跑。

两个作者已抛出的决策点(附推荐,均不阻塞合并)

  • 50 字符下限留不留:推荐留——与 compress 既有下限一致,正是本 issue 要补的"compress 拒收的 acp_rewrite 也拒收";想写短指针本就该用 acp_retire,且可设 compress.minSummaryLength: 0 放开。若要"只留上限+净收缩",删一个 if 即可,我可跟进。
  • 回滚位 previousSummary:作者说"kept for rollback"但没工具读它、第二次改写还会覆盖。推荐选项 2(去掉回执/说明/prompt/文档里"for rollback"的说法,原文仍可用 decompress 取回)——比新增 rollback:true 动作更轻、不扩工具面。作者已表示可作后续 PR。

合并判定:方向对、实现正确、测试有牙(本地已验证 fail-on-master / pass-on-fix)、CI 全绿(head 8cffeaf6 的 test/gate/kernel-guard/docs-sync 等全 success;补测试后新 head 59d1e989 在重跑)、diff 干净(5 文件 +119 −10,全与本 PR 目的相关)。可合。合并前建议:① rebase 到最新 master(当前 mergeable_state: behind);② 打 bug + 优先级标签(建议 p2,默认关 + 窄触发);③ 上面两个决策点由 owner 拍板(默认按我推荐:留下限、回滚位走选项 2)。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

补充:我补测试后的新 head 59d1e989 CI 已全绿(38 success / 14 skipped / 0 failure,全部 completed;e2e 车道为常规的零 token 跳过)。PR 现为 2 个 commit(原修复 + 我补的"永不挡改进"例外测试),mergeable。

结论不变:可合。合并前待办仍是那三条——rebase 到最新 master、打 bug+p2 标签、owner 拍板两个决策点(50 字符下限推荐留、回滚位推荐选项 2)。

This branch has not been deployed

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

Labels

None yet

Projects

None yet

3 participants