Skip to content

Commit 2e2140a

Browse files
authored
Merge pull request #393 from ranxianglei/2026-09-12_candidates-review-fixes
fix(candidates): review follow-ups — OFF-mode byte-exact + overridable surface
2 parents d0c3875 + c26e201 commit 2e2140a

9 files changed

Lines changed: 118 additions & 17 deletions

File tree

‎CONFIGURATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ Core compression behavior.
242242
- **Default:** `undefined`
243243
- **Status:** ACTIVE
244244
- **Description:** Nested per-provider / per-model overrides for **every tunable compress field**, resolved field-by-field with the cascade **model > provider > global** (mirrors the sibling project billion-context-pi, issue #344). Deeper levels only override when the field is explicitly set — unset fields never clear shallower values. `0` / `false` are explicit values, not "unset". Unknown provider/model ids fall back to the global value. Percentages and `"X%"` limits resolve against the active model's context window. Across the three config file layers (global → config dir → project) the maps deep-merge per provider/model key — a project layer can narrow one provider without wiping others configured in lower layers.
245-
- **Overridable fields:** `maxContextLimit`, `emergencyThresholdPercent`, `minNudgeContextPercent`, `nudgeFrequency`, `iterationNudgeThreshold`, `toolOutputNudgeThreshold`, `nudgeGrowthTokens`, `minNudgeGrowthRatio`, `minNudgeGrowthFloor`, `nudgeForce`, `protectedTools`, `showCompression`, `summaryBuffer`, `candidates`, `protectTags`, `protectUserMessages`, `maxSummaryLengthHard`, `minCompressRange`, `maxVisibleSegments`, `keepEmbedMaxChars`, `lastSegmentSoftBlock`, `preserveRecentMessages`, `preserveRecentTokens`, `preserveLastUserMessage`, `reasoning` (nested, field-wise).
245+
- **Overridable fields:** `maxContextLimit`, `emergencyThresholdPercent`, `minNudgeContextPercent`, `nudgeFrequency`, `iterationNudgeThreshold`, `toolOutputNudgeThreshold`, `nudgeGrowthTokens`, `minNudgeGrowthRatio`, `minNudgeGrowthFloor`, `nudgeForce`, `protectedTools`, `showCompression`, `summaryBuffer`, `protectTags`, `protectUserMessages`, `maxSummaryLengthHard`, `minCompressRange`, `maxVisibleSegments`, `keepEmbedMaxChars`, `lastSegmentSoftBlock`, `preserveRecentMessages`, `preserveRecentTokens`, `preserveLastUserMessage`, `reasoning` (nested, field-wise).
246246
- **Not overridable:** `permission` (session-level, fixed before model info is known), the deprecated `minContextLimit` / `modelMinLimits` family, the flat `modelMaxLimits` / `modelMinLimits` maps themselves, and `providers` itself. `modelMaxLimits` itself is **not** deprecated — it remains fully supported (only outranked). For `maxContextLimit` the precedence when set nested is **nested override > `modelMaxLimits` flat map > global**. `protectedTools` set here affects the compress tool and nudge-side logic; the system-prompt protected-tools listing (shown at prompt build time, before model info is available) always reflects the global value.
247247

248248
```jsonc

‎CONFIGURATION.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ ACP 从最多三层配置文件中读取(后加载的覆盖先加载的):
242242
- **默认值:** `undefined`
243243
- **状态:** ACTIVE
244244
- **说明:** 对**所有可调 compress 字段**的嵌套按 provider / 按模型覆盖,逐字段按 **模型 > provider > 全局** 级联解析(与姊妹项目 billion-context-pi 一致,issue #344)。深层仅在该字段被显式设置时才覆盖——未设置的字段不会清空浅层取值;`0` / `false` 是显式值,而非“未设置”。未知的 provider/model id 回退到全局值。百分比与 `"X%"` 限额按当前激活模型的上下文窗口换算。在三个配置文件层(全局 → 配置目录 → 项目)之间,该映射按 provider/model 键深度合并——项目层可以只细化某个 provider 而不清掉低层配置的其他 provider。
245-
- **可覆盖字段:** `maxContextLimit`、`emergencyThresholdPercent`、`minNudgeContextPercent`、`nudgeFrequency`、`iterationNudgeThreshold`、`toolOutputNudgeThreshold`、`nudgeGrowthTokens`、`minNudgeGrowthRatio`、`minNudgeGrowthFloor`、`nudgeForce`、`protectedTools`、`showCompression`、`summaryBuffer`、`candidates`、`protectTags`、`protectUserMessages`、`maxSummaryLengthHard`、`minCompressRange`、`maxVisibleSegments`、`keepEmbedMaxChars`、`lastSegmentSoftBlock`、`preserveRecentMessages`、`preserveRecentTokens`、`preserveLastUserMessage`、`reasoning`(嵌套对象,字段级)。
245+
- **可覆盖字段:** `maxContextLimit`、`emergencyThresholdPercent`、`minNudgeContextPercent`、`nudgeFrequency`、`iterationNudgeThreshold`、`toolOutputNudgeThreshold`、`nudgeGrowthTokens`、`minNudgeGrowthRatio`、`minNudgeGrowthFloor`、`nudgeForce`、`protectedTools`、`showCompression`、`summaryBuffer`、`protectTags`、`protectUserMessages`、`maxSummaryLengthHard`、`minCompressRange`、`maxVisibleSegments`、`keepEmbedMaxChars`、`lastSegmentSoftBlock`、`preserveRecentMessages`、`preserveRecentTokens`、`preserveLastUserMessage`、`reasoning`(嵌套对象,字段级)。
246246
- **不可覆盖:** `permission`(会话级,在得知模型信息前已固定)、已废弃的 `minContextLimit` / `modelMinLimits` 系列、扁平 `modelMaxLimits` / `modelMinLimits` 映射自身、以及 `providers` 本身。`modelMaxLimits` 本身**未废弃** —— 仍完全支持(仅优先级被超越)。`maxContextLimit` 在嵌套层设置时的优先级为 **嵌套覆盖 > `modelMaxLimits` 扁平映射 > 全局**。在此设置的 `protectedTools` 影响压缩工具与 nudge 侧逻辑;系统提示词中的受保护工具列表(在提示词构建时生成,早于模型信息可用)始终反映全局值。
247247

248248
```jsonc

‎devlog/2026-09-12_candidate-switch/WORKLOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@
2929
(`getCurrentTokenUsage` 倒序查找),不是消息求和;且 inject 会向
3030
messages 追加合成 suffix —— 测试须按引用捕获 tail。
3131

32+
## 评审跟进(独立 reviewer,双评审之一)
33+
34+
Reviewer 结论 "fix first",处理如下:
35+
36+
| 发现 | 处置 |
37+
|---|---|
38+
| C(Medium,开关提交引入):docs 称 `candidates` 可按模型覆盖,但 `OVERRIDE_FIELD_TYPES`/嵌套 schema/运行时均不支持 —— 四向不一致 | ✅ 已修:`CompressOverridableConfig` Omit 列表排除 `candidates`,CONFIGURATION.md/.zh-CN 可覆盖字段列表移除(全局/项目层配置不受影响) |
39+
| A(Low):OFF 模式 efficiencyNote 措辞多了 "when content is no longer needed" | ✅ 已修:OFF 时恢复 master 原文 |
40+
| D(Low):OFF 模式 acp_status 无参概览输出 renderUncompressedRanges 块(含表头/提示)而非 master 的裸范围列表 | ✅ 已修:OFF 恢复 master 裸 `formatCompressibleRanges` + 原 Tip 行 |
41+
| E(Info):OFF 模式 debug 日志报 "0 candidate(s)" | ✅ 已修:OFF 恢复 master 的 range 日志行 |
42+
| F(Info):每次 transform 无条件 `messages.slice()` | ✅ 已修:仅 candidates 开启时拷贝 |
43+
| B(Medium,PR 既有):hooks.ts 管道重排(truncateLargeToolOutputs/enforceContextBudget 移到 nudge 之后)未门控 | ⛔ 保留:属 PR #341 既有设计,四轮 ework 评审通过;e2e 场景 01–12 全部在 OFF 模式下通过验证重排无害。已在 PR 评论中明示 |
44+
3245
## 验证
3346

3447
- typecheck 0 错误;**1261/1261** 测试通过;build OK。
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# REQ — PR #341 候选开关的评审修复(A/C/D/E/F)
2+
3+
## 背景
4+
5+
PR #341(`compress.candidates` 开关)合并后,独立 reviewer 评审结论
6+
"fix first"。其中 finding C(Medium)由开关提交引入,其余为 PR 分支带入
7+
master 的 OFF 模式偏差。本 PR 落地全部代码级修复;finding B(hooks 管道
8+
重排)按评审建议保留并在 PR 描述明示。
9+
10+
## 需求(与 reviewer 报告对应)
11+
12+
| # | 发现 | 修复 |
13+
|---|---|---|
14+
| C | `candidates` 被类型/文档标为可按模型覆盖,但 `OVERRIDE_FIELD_TYPES`/嵌套 schema/运行时均不支持 | `CompressOverridableConfig` Omit 排除 + CONFIGURATION.md/.zh-CN 可覆盖字段列表移除 |
15+
| A | OFF 模式 efficiencyNote 措辞 ≠ master("when content is no longer needed" 多出) | OFF 分支恢复 master 原文 |
16+
| D | OFF 模式 acp_status 无参概览输出 renderUncompressedRanges 块(表头/提示)而非 master 裸范围列表;Tip 行多了 view:"ranges" 提示 | OFF 恢复 master 裸 `formatCompressibleRanges` + 原 Tip 行 |
17+
| E | OFF 模式 debug 日志报 "0 candidate(s)" | OFF 恢复 master 的 `range(s)` 日志行 |
18+
| F | 每次 transform 无条件 `messages.slice()` | 仅 `candidates === true` 时拷贝 |
19+
20+
## 不修(有意保留)
21+
22+
- **B**:`truncateLargeToolOutputs`/`enforceContextBudget` 移到 nudge 注入
23+
之后 —— PR #341 既有设计,e2e 场景 01–12 全部在 OFF 模式通过,两种模式
24+
统一管道,不做按开关分叉。
25+
26+
## 验收
27+
28+
- OFF(`candidates` 缺省/false)模式下 nudge/breakdown/acp_status/debug
29+
输出与 v1.17.1 逐字节一致(开关提交触及的所有字符串)。
30+
- typecheck / 全量测试 / build 通过。
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# WORKLOG — PR #341 候选开关的评审修复
2+
3+
## 变更
4+
5+
从 pr341-merge-test 工作区(stash pop 到基于 github/master 的本分支,
6+
无冲突)提取 reviewer 要求的代码级修复,7 个文件:
7+
8+
- `lib/config.ts`:`CompressOverridableConfig` Omit 列表加 `candidates`
9+
- `lib/messages/inject/inject.ts`:efficiencyNote 与 debug
10+
Recommendation-filter 日志按 `candidatesEnabled` 分叉,OFF 恢复 master 原文
11+
- `lib/compress/status.ts`:无参概览 OFF 时走 master 裸
12+
`formatCompressibleRanges` 路径(computeProtectedRefs +
13+
buildCompressibleRanges),Tip 行 OFF 恢复原样
14+
- `lib/hooks.ts`:`candidateMessages` 仅在 `candidates === true` 时 slice
15+
- `CONFIGURATION.md` / `CONFIGURATION.zh-CN.md`:可覆盖字段列表移除
16+
`candidates`
17+
- `devlog/2026-09-12_candidate-switch/WORKLOG.md`:评审跟进表(B 保留理由)
18+
19+
## 验证
20+
21+
- typecheck 0 错误;1263/1263 测试;build OK(在 release 分支 cc868b3
22+
上验证,与本分支内容相同;本分支另跑 typecheck+全量)。
23+
- check-pr.sh:分支名/规范通过;无版本变更,无 changelog 要求。
24+
25+
## 备注
26+
27+
- finding B(hooks 管道重排)不修,理由见 REQ。
28+
- 修复内容曾暂存在 v1.18.0 发布分支(cc868b3),后按"发布 PR 不得含
29+
功能代码"的约定拆出;发布分支已重置为纯版本簿记。

‎lib/compress/status.ts‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -284,24 +284,45 @@ function renderOverview(
284284
}
285285

286286
if (!fetchFailed) {
287-
const pruneMap = ctx.state.prune.messages.byMessageId
288-
const visibleRaw = rawMessages.filter((msg) => {
289-
const msgId = (msg.info as any)?.id || ""
290-
const entry = pruneMap.get(msgId)
291-
return !entry || entry.activeBlockIds.length === 0
292-
})
293-
const candidates = renderCompressionCandidates(rawMessages, ctx)
294-
if (candidates.length > 0) {
295-
lines.push("")
296-
lines.push(...candidates)
287+
if (ctx.config?.compress?.candidates === true) {
288+
const candidates = renderCompressionCandidates(rawMessages, ctx)
289+
if (candidates.length > 0) {
290+
lines.push("")
291+
lines.push(...candidates)
292+
}
293+
} else {
294+
const pruneMap = ctx.state.prune.messages.byMessageId
295+
const visibleRaw = rawMessages.filter((msg) => {
296+
const msgId = (msg.info as any)?.id || ""
297+
const entry = pruneMap.get(msgId)
298+
return !entry || entry.activeBlockIds.length === 0
299+
})
300+
const protectedRefs = ctx.config?.compress
301+
? computeProtectedRefs(visibleRaw, ctx.state, ctx.config.compress)
302+
: new Set<string>()
303+
const contextRanges = buildCompressibleRanges(
304+
visibleRaw,
305+
ctx.state,
306+
ctx.config?.compress?.protectedTools ?? [],
307+
ctx.config?.protectedFilePatterns ?? [],
308+
protectedRefs,
309+
)
310+
if (contextRanges.compressible.length > 0 || contextRanges.protected.length > 0) {
311+
lines.push("")
312+
lines.push(
313+
formatCompressibleRanges(contextRanges.compressible, contextRanges.protected),
314+
)
315+
}
297316
}
298317
}
299318

300319
lines.push("")
301320

302321
const hintTool = topToolName || "bash"
303322
lines.push(
304-
`Tip: acp_status({scope:"uncompressed", view:"ranges"}) for raw ranges, or acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`,
323+
ctx.config?.compress?.candidates === true
324+
? `Tip: acp_status({scope:"uncompressed", view:"ranges"}) for raw ranges, or acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
325+
: `Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`,
305326
)
306327

307328
return lines

‎lib/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Permission = "ask" | "allow" | "deny"
2929
export type CompressOverridableConfig = Omit<
3030
CompressConfig,
3131
| "permission"
32+
| "candidates"
3233
| "minContextLimit"
3334
| "modelMaxLimits"
3435
| "modelMinLimits"

‎lib/hooks.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,9 @@ export function createChatMessageTransformHandler(
360360
// Keep the full post-filter projection for candidate planning. The
361361
// nudge receives a pruned view, while range validation still needs the
362362
// original ordering to prove tool-pair and protection parity.
363-
const candidateMessages = output.messages.slice()
363+
// Skip the copy entirely when candidates are disabled (default).
364+
const candidateMessages =
365+
config.compress.candidates === true ? output.messages.slice() : undefined
364366
prune(state, logger, config, output.messages)
365367
hideConsumedCompressCalls(state, output.messages)
366368
assignMessageRefs(state, output.messages)

‎lib/messages/inject/inject.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,9 @@ export const injectCompressNudges = (
734734
const lines = [
735735
`[ACP Debug] Recommendation filter:`,
736736
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
737-
` Output: ${candidatePlan.candidates.length} candidate(s) (${candidatePlan.truncatedCount} truncated)`,
737+
candidatesEnabled
738+
? ` Output: ${candidatePlan.candidates.length} candidate(s) (${candidatePlan.truncatedCount} truncated)`
739+
: ` Output: ${recommendedRanges.length} range(s) (last segment marked dangerous)`,
738740
]
739741
logger.debug(lines.join("\n"))
740742
}
@@ -760,8 +762,11 @@ export const injectCompressNudges = (
760762
// warnings — a separate, stronger alert fires at maxLimit (below).
761763
const efficiencyNote =
762764
effectiveTipsVariant !== "maxLimit"
763-
? "\nThis is an efficiency nudge to compress early when content is no longer needed and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" +
764-
COMPRESS_PHILOSOPHY
765+
? candidatesEnabled
766+
? "\nThis is an efficiency nudge to compress early when content is no longer needed and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" +
767+
COMPRESS_PHILOSOPHY
768+
: "\nThis is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n" +
769+
COMPRESS_PHILOSOPHY
765770
: ""
766771
const sysPart =
767772
composition.systemTokens > 0

0 commit comments

Comments
 (0)