opencode-acp 1.12.6 → 1.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/README.zh-CN.md +8 -0
- package/dist/index.js +185 -57
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +11 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/recap.d.ts.map +1 -1
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +19 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/prune.d.ts.map +1 -1
- package/dist/lib/messages/utils.d.ts +1 -1
- package/dist/lib/messages/utils.d.ts.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/dist/lib/ui/notification.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -422,6 +422,14 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
422
422
|
|
|
423
423
|
## Changelog
|
|
424
424
|
|
|
425
|
+
### v1.12.7 — Smart Recommendation Filter + Dangerous Parameter + Ref-Leak Fix + Phantom Turn Fix (PRs #142, #147, #150)
|
|
426
|
+
|
|
427
|
+
**Problem**: Four issues. (1) The recommendation filter used a hardcoded 5× growth threshold for single-message ranges (25% of context), leaked context, showed tiny ranges, and contradicted itself by recommending the last segment while blocking it. (2) Nudge text was injected even when the filter suppressed all ranges — wasting context with an empty recommendation list. (3) Compression block metadata leaked message refs (`m01309–m02150`) via `acp_context_recap` tool input, `acp_status` output, and `recap` tool output — the model copied these into compress calls on already-compressed ranges, creating phantom blocks (#93, #135). (4) `sendIgnoredMessage` during the transform hook persisted an ignored user message that resolved async after the model's response — the loop's `lastUser` detection picked it up → phantom LLM call with no new input → confusion → hallucination → feedback loop ("待命" spam).
|
|
428
|
+
|
|
429
|
+
**Fix**: (1) Rewrote `filterRecommendedRanges`: last segment excluded if < 2× growth threshold; included with `dangerous: true` flag if ≥ 2×. Stateless `dangerous?: boolean` parameter on compress tool schema replaces state-tracking soft-block. Net −70 lines. (2) Suppress nudge text injection when filter has no recommendations. (3) Stopped leaking message refs: `acp_context_recap` tool input now shows `messages: <count>` instead of `range: "(mN–mN)"`; `acp_status` and `recap` tool output show `N msgs` instead of ref ranges. (4) Debug nudge notification uses `client.tui.showToast()` (transient, non-persisting) + `logger.debug()` (file log) instead of `sendIgnoredMessage` — breaks the phantom-turn feedback loop entirely. `dev-deploy.sh` auto-bumps version above npm latest to prevent overwrite on restart.
|
|
430
|
+
|
|
431
|
+
Files: `lib/messages/inject/utils.ts`, `lib/messages/inject/inject.ts`, `lib/compress/pipeline.ts`, `lib/compress/range.ts`, `lib/compress/message.ts`, `lib/messages/prune.ts`, `lib/messages/utils.ts`, `lib/compress/recap.ts`, `lib/compress/status.ts`, `lib/ui/notification.ts`, `lib/prompts/system.ts`, `lib/hooks.ts`, `scripts/dev-deploy.sh`. Tests: 725 pass.
|
|
432
|
+
|
|
425
433
|
### v1.12.6 — Stale contextLimitAnchors Fix (PR #143)
|
|
426
434
|
|
|
427
435
|
**Problem**: `contextLimitAnchors` were populated when `overMaxLimit=true` but only cleared on a compress tool call in the current turn (`currentTurnHasCompress`). If context dropped below `maxLimit` via another mechanism (OpenCode compaction, external message deletion), the anchors stayed stale → `applyAnchoredNudges` kept injecting the "⚠️ Context limit reached" template at low context levels (as low as 10% usage).
|
package/README.zh-CN.md
CHANGED
|
@@ -395,6 +395,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
395
395
|
|
|
396
396
|
## 更新日志
|
|
397
397
|
|
|
398
|
+
### v1.12.7 — 智能推荐过滤 + Dangerous 参数 + Ref 泄漏修复 + Phantom Turn 修复(PR #142, #147, #150)
|
|
399
|
+
|
|
400
|
+
**问题**:四个问题。(1)推荐过滤器使用硬编码的 5× 增长阈值(上下文的 25%),太大且容易泄露上下文;推荐最后一段的同时又阻止它,自相矛盾。(2)过滤器抑制所有 range 后仍然注入 nudge 文本——用空推荐列表浪费上下文。(3)压缩块元数据通过 `acp_context_recap` 工具输入、`acp_status` 输出和 `recap` 工具输出泄漏消息 ref(`m01309–m02150`)——模型复制这些 ref 到已压缩范围的 compress 调用中,产生幽灵块(#93, #135)。(4)`sendIgnoredMessage` 在 transform hook 中持久化 ignored 用户消息,异步在模型回复后完成——loop 的 `lastUser` 检测拾取它 → 无新输入的 phantom LLM 调用 → 困惑 → 幻觉 → 反馈循环("待命" 刷屏)。
|
|
401
|
+
|
|
402
|
+
**修复**:(1)重写 `filterRecommendedRanges`:最后一段 < 2× 增长阈值 → 剔除;≥ 2× → 保留并标记 `dangerous: true`。无状态 `dangerous?: boolean` 参数替代状态追踪 soft-block。净减 70 行。(2)过滤器无推荐时抑制 nudge 文本注入。(3)停止泄漏消息 ref:`acp_context_recap` 工具输入改为 `messages: <数量>`;`acp_status` 和 `recap` 工具输出改为 `N msgs`。(4)Debug nudge 通知改用 `client.tui.showToast()`(瞬态,不持久化)+ `logger.debug()`(文件日志),彻底打破 phantom-turn 反馈循环。`dev-deploy.sh` 自动 bump 版本到 npm 最新之上防止重启覆盖。
|
|
403
|
+
|
|
404
|
+
文件:`lib/messages/inject/utils.ts`、`lib/messages/inject/inject.ts`、`lib/compress/pipeline.ts`、`lib/compress/range.ts`、`lib/compress/message.ts`、`lib/messages/prune.ts`、`lib/messages/utils.ts`、`lib/compress/recap.ts`、`lib/compress/status.ts`、`lib/ui/notification.ts`、`lib/prompts/system.ts`、`lib/hooks.ts`、`scripts/dev-deploy.sh`。测试:725 通过。
|
|
405
|
+
|
|
398
406
|
### v1.12.6 — Stale contextLimitAnchors 修复(PR #143)
|
|
399
407
|
|
|
400
408
|
**问题**:`contextLimitAnchors` 仅在 `overMaxLimit=true` 时添加,但只在当前 turn 有 compress 调用时 (`currentTurnHasCompress`) 才清除。如果上下文通过其他机制(OpenCode compaction、外部消息删除)降到 `maxLimit` 以下,anchors 保持 stale → `applyAnchoredNudges` 持续注入 "⚠️ Context limit reached" 模板,即使实际上下文很低(低至 10%)。
|
package/dist/index.js
CHANGED
|
@@ -1592,7 +1592,8 @@ var defaultConfig = {
|
|
|
1592
1592
|
minNudgeGrowthFloor: 5e3,
|
|
1593
1593
|
emergencyThresholdPercent: "98%",
|
|
1594
1594
|
maxVisibleSegments: 50,
|
|
1595
|
-
keepEmbedMaxChars: 2e3
|
|
1595
|
+
keepEmbedMaxChars: 2e3,
|
|
1596
|
+
lastSegmentSoftBlock: true
|
|
1596
1597
|
},
|
|
1597
1598
|
strategies: {
|
|
1598
1599
|
deduplication: {
|
|
@@ -1753,7 +1754,8 @@ function mergeCompress(base, override) {
|
|
|
1753
1754
|
minNudgeGrowthFloor: override.minNudgeGrowthFloor ?? base.minNudgeGrowthFloor,
|
|
1754
1755
|
emergencyThresholdPercent: override.emergencyThresholdPercent ?? base.emergencyThresholdPercent,
|
|
1755
1756
|
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments,
|
|
1756
|
-
keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars
|
|
1757
|
+
keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars,
|
|
1758
|
+
lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock
|
|
1757
1759
|
};
|
|
1758
1760
|
}
|
|
1759
1761
|
function mergeCommands(base, override) {
|
|
@@ -4960,14 +4962,9 @@ function formatEntryRanges(entries, state) {
|
|
|
4960
4962
|
for (const entry of entries) {
|
|
4961
4963
|
const block = state.prune.messages.blocksById.get(entry.blockId);
|
|
4962
4964
|
if (!block) continue;
|
|
4963
|
-
const
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
if (startRef === endRef) {
|
|
4967
|
-
parts.push(`b${entry.blockId}: ${startRef}`);
|
|
4968
|
-
} else {
|
|
4969
|
-
parts.push(`b${entry.blockId}: ${startRef}\u2013${endRef}`);
|
|
4970
|
-
}
|
|
4965
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
4966
|
+
if (count === 0) continue;
|
|
4967
|
+
parts.push(`b${entry.blockId}: ${count} msg${count !== 1 ? "s" : ""}`);
|
|
4971
4968
|
}
|
|
4972
4969
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
4973
4970
|
}
|
|
@@ -5245,6 +5242,31 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
|
|
|
5245
5242
|
contextTokensBefore
|
|
5246
5243
|
);
|
|
5247
5244
|
}
|
|
5245
|
+
function getLastVisibleMessageId(rawMessages, state) {
|
|
5246
|
+
for (let i = rawMessages.length - 1; i >= 0; i--) {
|
|
5247
|
+
const msg = rawMessages[i];
|
|
5248
|
+
const id = msg?.info?.id;
|
|
5249
|
+
if (!id || typeof id !== "string") continue;
|
|
5250
|
+
if (isSyntheticMessage(msg)) continue;
|
|
5251
|
+
if (state.prune.messages.byMessageId.has(id)) continue;
|
|
5252
|
+
return id;
|
|
5253
|
+
}
|
|
5254
|
+
return null;
|
|
5255
|
+
}
|
|
5256
|
+
function checkLastSegmentDangerous(ctx, allPlanMessageIds, rawMessages, dangerous) {
|
|
5257
|
+
if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
|
|
5258
|
+
const lastVisibleId = getLastVisibleMessageId(rawMessages, ctx.state);
|
|
5259
|
+
if (!lastVisibleId) return null;
|
|
5260
|
+
const coversLast = allPlanMessageIds.some((ids) => ids.includes(lastVisibleId));
|
|
5261
|
+
if (!coversLast) return null;
|
|
5262
|
+
if (dangerous) return null;
|
|
5263
|
+
return new Error(
|
|
5264
|
+
`This range includes the most recent message (${lastVisibleId}), which is likely still needed for the current task step.
|
|
5265
|
+
|
|
5266
|
+
If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
|
|
5267
|
+
Otherwise, compress older ranges that do not include the tail of the conversation.`
|
|
5268
|
+
);
|
|
5269
|
+
}
|
|
5248
5270
|
|
|
5249
5271
|
// lib/compress/keep-markers.ts
|
|
5250
5272
|
var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
|
|
@@ -5375,6 +5397,9 @@ function buildSchema(maxSummaryLengthHard) {
|
|
|
5375
5397
|
).describe("Batch of individual message summaries to create in one tool call"),
|
|
5376
5398
|
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
5377
5399
|
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5400
|
+
),
|
|
5401
|
+
dangerous: tool2.schema.boolean().optional().describe(
|
|
5402
|
+
"Set to true ONLY when you are certain the most recent message(s) must be compressed. Required when a range includes the tail of the conversation."
|
|
5378
5403
|
)
|
|
5379
5404
|
};
|
|
5380
5405
|
}
|
|
@@ -5433,6 +5458,14 @@ function createCompressMessageTool(ctx) {
|
|
|
5433
5458
|
);
|
|
5434
5459
|
}
|
|
5435
5460
|
}
|
|
5461
|
+
const dangerous = args.dangerous === true;
|
|
5462
|
+
const lastSegmentError = checkLastSegmentDangerous(
|
|
5463
|
+
ctx,
|
|
5464
|
+
plans.map((p) => p.selection.messageIds),
|
|
5465
|
+
rawMessages,
|
|
5466
|
+
dangerous
|
|
5467
|
+
);
|
|
5468
|
+
if (lastSegmentError) throw lastSegmentError;
|
|
5436
5469
|
const notifications = [];
|
|
5437
5470
|
const preparedPlans = [];
|
|
5438
5471
|
for (const plan of plans) {
|
|
@@ -5529,6 +5562,9 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
5529
5562
|
),
|
|
5530
5563
|
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5531
5564
|
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5565
|
+
),
|
|
5566
|
+
dangerous: tool3.schema.boolean().optional().describe(
|
|
5567
|
+
"Set to true ONLY when you are certain the most recent message(s) must be compressed. Required when a range includes the tail of the conversation."
|
|
5532
5568
|
)
|
|
5533
5569
|
};
|
|
5534
5570
|
}
|
|
@@ -5594,6 +5630,14 @@ function createCompressRangeTool(ctx) {
|
|
|
5594
5630
|
);
|
|
5595
5631
|
}
|
|
5596
5632
|
}
|
|
5633
|
+
const dangerous = args.dangerous === true;
|
|
5634
|
+
const lastSegmentError = checkLastSegmentDangerous(
|
|
5635
|
+
ctx,
|
|
5636
|
+
filteredPlans.map((p) => p.selection.messageIds),
|
|
5637
|
+
rawMessages,
|
|
5638
|
+
dangerous
|
|
5639
|
+
);
|
|
5640
|
+
if (lastSegmentError) throw lastSegmentError;
|
|
5597
5641
|
const notifications = [];
|
|
5598
5642
|
const preparedPlans = [];
|
|
5599
5643
|
let totalCompressedMessages = 0;
|
|
@@ -5793,7 +5837,7 @@ var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") =
|
|
|
5793
5837
|
return { info, parts };
|
|
5794
5838
|
};
|
|
5795
5839
|
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
|
|
5796
|
-
var createSyntheticToolRecap = (baseMessage, summary, blockId,
|
|
5840
|
+
var createSyntheticToolRecap = (baseMessage, summary, blockId, messageCount, stableSeed) => {
|
|
5797
5841
|
const baseInfo = baseMessage.info;
|
|
5798
5842
|
const now = Date.now();
|
|
5799
5843
|
const messageId = generateStableId("msg_acp_recap", stableSeed);
|
|
@@ -5810,7 +5854,7 @@ var createSyntheticToolRecap = (baseMessage, summary, blockId, range, stableSeed
|
|
|
5810
5854
|
status: "completed",
|
|
5811
5855
|
input: {
|
|
5812
5856
|
blockId,
|
|
5813
|
-
...
|
|
5857
|
+
...messageCount !== void 0 ? { messages: messageCount } : {}
|
|
5814
5858
|
},
|
|
5815
5859
|
output: summary,
|
|
5816
5860
|
title: `ACP Context Recap (block ${blockId})`,
|
|
@@ -5961,10 +6005,9 @@ var dropEmptyMessages = (messages) => {
|
|
|
5961
6005
|
};
|
|
5962
6006
|
|
|
5963
6007
|
// lib/messages/prune.ts
|
|
5964
|
-
var
|
|
5965
|
-
if (!
|
|
5966
|
-
|
|
5967
|
-
return `(${startId}\u2013${endId})`;
|
|
6008
|
+
var computeBlockCoverage = (effectiveMessageIds) => {
|
|
6009
|
+
if (!effectiveMessageIds || effectiveMessageIds.length === 0) return void 0;
|
|
6010
|
+
return effectiveMessageIds.length;
|
|
5968
6011
|
};
|
|
5969
6012
|
var prune = (state, logger, config, messages) => {
|
|
5970
6013
|
filterCompressedRanges(state, logger, config, messages);
|
|
@@ -6019,14 +6062,14 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
6019
6062
|
} else {
|
|
6020
6063
|
const cleaned = stripStaleMessageRefs(rawSummaryContent);
|
|
6021
6064
|
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(cleaned) : cleaned;
|
|
6022
|
-
const
|
|
6065
|
+
const blockCoverage = computeBlockCoverage(summary.effectiveMessageIds);
|
|
6023
6066
|
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
6024
6067
|
result.push(
|
|
6025
6068
|
createSyntheticToolRecap(
|
|
6026
6069
|
msg,
|
|
6027
6070
|
summaryContent,
|
|
6028
6071
|
summary.blockId,
|
|
6029
|
-
|
|
6072
|
+
blockCoverage,
|
|
6030
6073
|
summarySeed
|
|
6031
6074
|
)
|
|
6032
6075
|
);
|
|
@@ -6945,12 +6988,71 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
|
|
|
6945
6988
|
protected: protectedGroups
|
|
6946
6989
|
};
|
|
6947
6990
|
}
|
|
6991
|
+
function filterRecommendedRanges(compressible, _protectedRanges, options) {
|
|
6992
|
+
const { modelContextLimit, growthRatio, logger } = options;
|
|
6993
|
+
const log = logger?.debug.bind(logger);
|
|
6994
|
+
if (!modelContextLimit || modelContextLimit <= 0) {
|
|
6995
|
+
log?.("filterRecommendedRanges: modelContextLimit unknown, passthrough", {
|
|
6996
|
+
inputCount: compressible.length
|
|
6997
|
+
});
|
|
6998
|
+
if (compressible.length === 0) return [];
|
|
6999
|
+
const passthrough = [...compressible];
|
|
7000
|
+
passthrough[passthrough.length - 1] = {
|
|
7001
|
+
...passthrough[passthrough.length - 1],
|
|
7002
|
+
dangerous: true
|
|
7003
|
+
};
|
|
7004
|
+
return passthrough;
|
|
7005
|
+
}
|
|
7006
|
+
if (compressible.length === 0) {
|
|
7007
|
+
log?.("filterRecommendedRanges: no compressible ranges, returning empty");
|
|
7008
|
+
return [];
|
|
7009
|
+
}
|
|
7010
|
+
const growthThreshold = modelContextLimit * growthRatio;
|
|
7011
|
+
const lastSegmentFloor = growthThreshold * 2;
|
|
7012
|
+
const lastIndex = compressible.length - 1;
|
|
7013
|
+
const lastRange = compressible[lastIndex];
|
|
7014
|
+
const lastSegmentIncluded = lastRange.tokens >= lastSegmentFloor;
|
|
7015
|
+
let effectiveCompressible = 0;
|
|
7016
|
+
const result = [];
|
|
7017
|
+
for (let i = 0; i < compressible.length; i++) {
|
|
7018
|
+
const r = compressible[i];
|
|
7019
|
+
if (i === lastIndex) {
|
|
7020
|
+
const excess = Math.max(0, r.tokens - lastSegmentFloor);
|
|
7021
|
+
effectiveCompressible += excess;
|
|
7022
|
+
if (lastSegmentIncluded) {
|
|
7023
|
+
result.push({ ...r, dangerous: true });
|
|
7024
|
+
}
|
|
7025
|
+
} else {
|
|
7026
|
+
effectiveCompressible += r.tokens;
|
|
7027
|
+
result.push(r);
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
const suppressed = effectiveCompressible < growthThreshold;
|
|
7031
|
+
log?.("filterRecommendedRanges: decision", {
|
|
7032
|
+
inputRanges: compressible.length,
|
|
7033
|
+
inputTokens: compressible.reduce((s, r) => s + r.tokens, 0),
|
|
7034
|
+
growthThreshold,
|
|
7035
|
+
lastSegmentFloor,
|
|
7036
|
+
lastSegment: {
|
|
7037
|
+
ref: `${lastRange.startRef}\u2013${lastRange.endRef}`,
|
|
7038
|
+
tokens: lastRange.tokens,
|
|
7039
|
+
included: lastSegmentIncluded
|
|
7040
|
+
},
|
|
7041
|
+
effectiveCompressible,
|
|
7042
|
+
suppressed,
|
|
7043
|
+
outputRanges: suppressed ? 0 : result.length
|
|
7044
|
+
});
|
|
7045
|
+
if (suppressed) {
|
|
7046
|
+
return [];
|
|
7047
|
+
}
|
|
7048
|
+
return result;
|
|
7049
|
+
}
|
|
6948
7050
|
function formatCompressibleRanges(ranges, protectedRanges) {
|
|
6949
7051
|
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6950
7052
|
if (!protectedRanges || protectedRanges.length === 0) {
|
|
6951
7053
|
if (ranges.length === 0) return "";
|
|
6952
|
-
const lines2 = ranges.map((r
|
|
6953
|
-
const suffix =
|
|
7054
|
+
const lines2 = ranges.map((r) => {
|
|
7055
|
+
const suffix = r.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
|
|
6954
7056
|
return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
|
|
6955
7057
|
});
|
|
6956
7058
|
return `Compressible ranges (oldest first):
|
|
@@ -6971,7 +7073,8 @@ ${lines2.join("\n")}`;
|
|
|
6971
7073
|
compressibleCount: r.count,
|
|
6972
7074
|
protectedTokens: 0,
|
|
6973
7075
|
protectedCount: 0,
|
|
6974
|
-
protectedTools: []
|
|
7076
|
+
protectedTools: [],
|
|
7077
|
+
dangerous: r.dangerous ?? false
|
|
6975
7078
|
});
|
|
6976
7079
|
}
|
|
6977
7080
|
for (const r of protectedRanges) {
|
|
@@ -6988,7 +7091,8 @@ ${lines2.join("\n")}`;
|
|
|
6988
7091
|
compressibleCount: 0,
|
|
6989
7092
|
protectedTokens: r.tokens,
|
|
6990
7093
|
protectedCount: r.count,
|
|
6991
|
-
protectedTools: [...r.tools]
|
|
7094
|
+
protectedTools: [...r.tools],
|
|
7095
|
+
dangerous: false
|
|
6992
7096
|
});
|
|
6993
7097
|
}
|
|
6994
7098
|
entries.sort((a, b) => a.startNum - b.startNum);
|
|
@@ -7004,6 +7108,7 @@ ${lines2.join("\n")}`;
|
|
|
7004
7108
|
last.compressibleCount += entry.compressibleCount;
|
|
7005
7109
|
last.protectedTokens += entry.protectedTokens;
|
|
7006
7110
|
last.protectedCount += entry.protectedCount;
|
|
7111
|
+
if (entry.dangerous) last.dangerous = true;
|
|
7007
7112
|
for (const t of entry.protectedTools) {
|
|
7008
7113
|
if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
|
|
7009
7114
|
}
|
|
@@ -7011,8 +7116,8 @@ ${lines2.join("\n")}`;
|
|
|
7011
7116
|
merged.push({ ...entry });
|
|
7012
7117
|
}
|
|
7013
7118
|
}
|
|
7014
|
-
const lines = merged.map((e
|
|
7015
|
-
const suffix =
|
|
7119
|
+
const lines = merged.map((e) => {
|
|
7120
|
+
const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain. If you MUST compress this, pass `dangerous: true`." : "";
|
|
7016
7121
|
if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
|
|
7017
7122
|
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
|
|
7018
7123
|
}
|
|
@@ -7227,7 +7332,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7227
7332
|
});
|
|
7228
7333
|
const growthSinceBaseline = currentTokens !== void 0 && growthReference !== void 0 ? currentTokens - growthReference : void 0;
|
|
7229
7334
|
const nudgeAllowed = emergencyOverride || decision.shouldNudge && growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
|
|
7230
|
-
state.nudges.shouldInjectThisTurn = nudgeAllowed;
|
|
7231
7335
|
const effectiveTipsVariant = emergencyOverride ? "maxLimit" : decision.tipsVariant;
|
|
7232
7336
|
if (nudgeAllowed) {
|
|
7233
7337
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
@@ -7242,8 +7346,42 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7242
7346
|
config.compress.protectedTools,
|
|
7243
7347
|
config.protectedFilePatterns
|
|
7244
7348
|
);
|
|
7349
|
+
const contextRanges = buildCompressibleRanges(
|
|
7350
|
+
messages,
|
|
7351
|
+
state,
|
|
7352
|
+
config.compress.protectedTools,
|
|
7353
|
+
config.protectedFilePatterns
|
|
7354
|
+
);
|
|
7355
|
+
const recommendedRanges = filterRecommendedRanges(
|
|
7356
|
+
contextRanges.compressible,
|
|
7357
|
+
contextRanges.protected,
|
|
7358
|
+
{ modelContextLimit, growthRatio: 0.05, logger }
|
|
7359
|
+
);
|
|
7360
|
+
const hasRecommendations = recommendedRanges.length > 0;
|
|
7361
|
+
if (config.debug && contextRanges.compressible.length > 0 && modelContextLimit) {
|
|
7362
|
+
const growthThreshold = modelContextLimit * 0.05;
|
|
7363
|
+
const lastSegmentFloor = growthThreshold * 2;
|
|
7364
|
+
const compressible = contextRanges.compressible;
|
|
7365
|
+
const lastRange = compressible[compressible.length - 1];
|
|
7366
|
+
const lastIncluded = lastRange.tokens >= lastSegmentFloor;
|
|
7367
|
+
const nonLastTokens = compressible.slice(0, -1).reduce((s, r) => s + r.tokens, 0);
|
|
7368
|
+
const effective = nonLastTokens + Math.max(0, lastRange.tokens - lastSegmentFloor);
|
|
7369
|
+
const suppressed = effective < growthThreshold;
|
|
7370
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7371
|
+
const lines = [
|
|
7372
|
+
`[ACP Debug] Recommendation filter decision:`,
|
|
7373
|
+
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7374
|
+
` Thresholds: growth=${fmt(growthThreshold)}, lastSegmentFloor=${fmt(lastSegmentFloor)}`,
|
|
7375
|
+
` Last segment: ${lastRange.startRef}\u2013${lastRange.endRef} ${fmt(lastRange.tokens)} tokens \u2192 ${lastIncluded ? "included (dangerous)" : "excluded (< floor)"}`,
|
|
7376
|
+
` Effective compressible: ${fmt(effective)} \u2192 ${suppressed ? "SUPPRESSED (< growth threshold)" : `${recommendedRanges.length} range(s) recommended`}`
|
|
7377
|
+
];
|
|
7378
|
+
logger.debug(lines.join("\n"));
|
|
7379
|
+
}
|
|
7380
|
+
const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
|
|
7381
|
+
const shouldInject = nudgeAllowed && (!filterSuppressed || emergencyOverride);
|
|
7382
|
+
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
7245
7383
|
let tipsText = null;
|
|
7246
|
-
if (
|
|
7384
|
+
if (shouldInject) {
|
|
7247
7385
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
7248
7386
|
if (suffixMessage && composition.total > 0) {
|
|
7249
7387
|
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
@@ -7262,16 +7400,10 @@ Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%
|
|
|
7262
7400
|
breakdown += `
|
|
7263
7401
|
\u26A0\uFE0F ${fmt(composition.protectedTokens)} tokens are protected (environment-managed tools) \u2014 not compressible. Effective compressible: ~${fmt(compressibleTokens)}.`;
|
|
7264
7402
|
}
|
|
7265
|
-
|
|
7266
|
-
messages,
|
|
7267
|
-
state,
|
|
7268
|
-
config.compress.protectedTools,
|
|
7269
|
-
config.protectedFilePatterns
|
|
7270
|
-
);
|
|
7271
|
-
if (contextRanges.compressible.length > 0) {
|
|
7403
|
+
if (recommendedRanges.length > 0) {
|
|
7272
7404
|
breakdown += `
|
|
7273
7405
|
|
|
7274
|
-
${formatCompressibleRanges(
|
|
7406
|
+
${formatCompressibleRanges(recommendedRanges, contextRanges.protected)}`;
|
|
7275
7407
|
breakdown += `
|
|
7276
7408
|
\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
|
|
7277
7409
|
}
|
|
@@ -7908,11 +8040,8 @@ function pct(n, total) {
|
|
|
7908
8040
|
return Math.max(1, Math.round(n / total * 100));
|
|
7909
8041
|
}
|
|
7910
8042
|
function formatIdRange(block) {
|
|
7911
|
-
const
|
|
7912
|
-
|
|
7913
|
-
if (!start || !end) return "\u2014";
|
|
7914
|
-
if (start === end) return start;
|
|
7915
|
-
return `${start}\u2013${end}`;
|
|
8043
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
8044
|
+
return count > 0 ? `${count} msg${count !== 1 ? "s" : ""}` : "\u2014";
|
|
7916
8045
|
}
|
|
7917
8046
|
function collectVisibleMessages(rawMessages, ctx) {
|
|
7918
8047
|
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
@@ -8203,12 +8332,9 @@ function createAcpStatusTool(ctx) {
|
|
|
8203
8332
|
|
|
8204
8333
|
// lib/compress/recap.ts
|
|
8205
8334
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
8206
|
-
function
|
|
8207
|
-
const
|
|
8208
|
-
|
|
8209
|
-
if (!start || !end) return "\u2014";
|
|
8210
|
-
if (start === end) return start;
|
|
8211
|
-
return `${start}\u2013${end}`;
|
|
8335
|
+
function formatCoverage(block) {
|
|
8336
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
8337
|
+
return count > 0 ? `${count} message${count !== 1 ? "s" : ""}` : "\u2014";
|
|
8212
8338
|
}
|
|
8213
8339
|
var RECAP_TOOL_DESCRIPTION = `Read-only retrieval of compression block summaries.
|
|
8214
8340
|
|
|
@@ -8236,7 +8362,7 @@ function createAcpContextRecapTool(ctx) {
|
|
|
8236
8362
|
if (!block.active) {
|
|
8237
8363
|
return `Block b${args.blockId} is inactive (deactivated by GC or nested compression).`;
|
|
8238
8364
|
}
|
|
8239
|
-
const range =
|
|
8365
|
+
const range = formatCoverage(block);
|
|
8240
8366
|
return `[Compressed conversation section]
|
|
8241
8367
|
${block.summary}
|
|
8242
8368
|
|
|
@@ -8247,7 +8373,7 @@ ${block.summary}
|
|
|
8247
8373
|
for (const id of activeIds) {
|
|
8248
8374
|
const block = msgState.blocksById.get(id);
|
|
8249
8375
|
if (!block || !block.active) continue;
|
|
8250
|
-
const range =
|
|
8376
|
+
const range = formatCoverage(block);
|
|
8251
8377
|
const summaryPreview = block.summary.slice(0, 200);
|
|
8252
8378
|
lines.push(`
|
|
8253
8379
|
b${id} | ${range} | "${block.topic || "(none)"}"`);
|
|
@@ -8591,7 +8717,7 @@ Periodically, as context grows, the system appends a short status line in a synt
|
|
|
8591
8717
|
|
|
8592
8718
|
This line is INFORMATION, not an instruction. Seeing it does not mean you should compress. Compress only when one of the WHEN TO COMPRESS conditions actually holds. Between these lines, context is not under additional pressure \u2014 you do not need to seek things to compress.
|
|
8593
8719
|
|
|
8594
|
-
If you are unsure which \`mNNNNN\` refs are still compressible, or which blocks have already consumed which ranges, call \`acp_status\` first. It returns the visible context breakdown (tool/code/text/summary tokens with largest items) and the compressed block list (block IDs, sizes, message
|
|
8720
|
+
If you are unsure which \`mNNNNN\` refs are still compressible, or which blocks have already consumed which ranges, call \`acp_status\` first. It returns the visible context breakdown (tool/code/text/summary tokens with largest items) and the compressed block list (block IDs, sizes, message counts each covers).
|
|
8595
8721
|
|
|
8596
8722
|
CONTEXT BREAKDOWN
|
|
8597
8723
|
|
|
@@ -10458,15 +10584,17 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
10458
10584
|
output.messages,
|
|
10459
10585
|
prompts.getRuntimePrompts(),
|
|
10460
10586
|
compressionPriorities,
|
|
10461
|
-
config.debug
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10587
|
+
config.debug ? (text) => {
|
|
10588
|
+
logger.debug(`[ACP Debug] Nudge injected:
|
|
10589
|
+
${text}`);
|
|
10590
|
+
client.tui.showToast({
|
|
10591
|
+
body: {
|
|
10592
|
+
title: "ACP: Nudge Injected",
|
|
10593
|
+
message: text.slice(0, 500),
|
|
10594
|
+
variant: "info",
|
|
10595
|
+
duration: 5e3
|
|
10596
|
+
}
|
|
10597
|
+
}).catch(() => {
|
|
10470
10598
|
});
|
|
10471
10599
|
} : void 0,
|
|
10472
10600
|
prePruneTokens
|