opencode-acp 1.12.7-dev.1 → 1.12.8
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 +12 -4
- package/README.zh-CN.md +12 -4
- package/dist/index.js +104 -71
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +19 -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/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.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,13 +422,21 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
422
422
|
|
|
423
423
|
## Changelog
|
|
424
424
|
|
|
425
|
-
### v1.12.
|
|
425
|
+
### v1.12.8 — Phantom Block Rejection (PR #148)
|
|
426
426
|
|
|
427
|
-
**Problem**:
|
|
427
|
+
**Problem**: When the model called `compress` on a range that was already covered by an active compression block, `applyCompressionState` still created a new block with `directMessageIds: []`, `compressedTokens: 0`, and `effectiveMessageIds` inherited from the consumed block. The model saw "0 tokens removed" in the notification, retried the same range, and entered a death loop: each phantom block added ~1K of summary overhead while compressing nothing, causing context to *grow* with every compression call (issues #93, #135). User sessions showed 9 consecutive phantom compressions (b12–b20) on the same range before the user manually intervened.
|
|
428
428
|
|
|
429
|
-
**Fix**: (
|
|
429
|
+
**Fix**: Added `checkPhantomBlock()` — a stateless pre-check in `lib/compress/pipeline.ts` that mirrors `applyCompressionState`'s `newlyCompressedMessageIds` computation. For each plan, it builds the effective message set (plan messages + consumed blocks' effective messages) and checks whether ANY message is "new" (i.e., has no active block covering it BEFORE mutation). If no message is new, the plan is a phantom and the entire compress call is rejected with a clear error before any state mutation occurs. Wired into both range-mode (`compress/range.ts`) and message-mode (`compress/message.ts`) after plan preparation, before snapshot. 12 tests cover: empty plans, all-new messages, consumed-block inheritance, GC'd messages (deactivated blocks count as new), and the exact `applyCompressionState` mirroring.
|
|
430
430
|
|
|
431
|
-
Files: `lib/
|
|
431
|
+
Files: `lib/compress/pipeline.ts`, `lib/compress/range.ts`, `lib/compress/message.ts`. Tests: `tests/phantom-block.test.ts` (NEW, 12 tests). 725 tests pass.
|
|
432
|
+
|
|
433
|
+
### v1.12.7 — Smart Recommendation Filter + Dangerous Parameter + Ref-Leak Fix + Phantom Turn Fix (PRs #142, #147, #150)
|
|
434
|
+
|
|
435
|
+
**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).
|
|
436
|
+
|
|
437
|
+
**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.
|
|
438
|
+
|
|
439
|
+
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
440
|
|
|
433
441
|
### v1.12.6 — Stale contextLimitAnchors Fix (PR #143)
|
|
434
442
|
|
package/README.zh-CN.md
CHANGED
|
@@ -395,13 +395,21 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
395
395
|
|
|
396
396
|
## 更新日志
|
|
397
397
|
|
|
398
|
-
### v1.12.
|
|
398
|
+
### v1.12.8 — 幽灵块拒绝(PR #148)
|
|
399
399
|
|
|
400
|
-
|
|
400
|
+
**问题**:当模型对已被活跃压缩块覆盖的范围调用 `compress` 时,`applyCompressionState` 仍然创建新块,`directMessageIds: []`、`compressedTokens: 0`、`effectiveMessageIds` 从被消费的块继承。模型在通知中看到"移除 0 tokens",重试同一范围,进入死亡循环:每个幽灵块增加约 1K 摘要开销却什么都不压缩,导致上下文随每次压缩调用*增长*(issues #93, #135)。用户会话显示同一范围连续 9 次幽灵压缩(b12–b20),直到用户手动干预。
|
|
401
401
|
|
|
402
|
-
|
|
402
|
+
**修复**:新增 `checkPhantomBlock()` —— `lib/compress/pipeline.ts` 中的无状态前置检查,镜像 `applyCompressionState` 的 `newlyCompressedMessageIds` 计算。对每个计划,构建有效消息集(计划消息 + 被消费块的有效消息),检查是否有任何消息是"新的"(即变异前没有活跃块覆盖它)。如果没有新消息,该计划是幽灵的,整个 compress 调用在任何状态变异前以清晰错误被拒绝。接入 range 模式(`compress/range.ts`)和 message 模式(`compress/message.ts`)的计划准备之后、快照之前。12 个测试覆盖:空计划、全新消息、被消费块继承、GC'd 消息(已停用块算新)、以及与 `applyCompressionState` 的精确镜像。
|
|
403
403
|
|
|
404
|
-
文件:`lib/
|
|
404
|
+
文件:`lib/compress/pipeline.ts`、`lib/compress/range.ts`、`lib/compress/message.ts`。测试:`tests/phantom-block.test.ts`(新增,12 个测试)。725 测试通过。
|
|
405
|
+
|
|
406
|
+
### v1.12.7 — 智能推荐过滤 + Dangerous 参数 + Ref 泄漏修复 + Phantom Turn 修复(PR #142, #147, #150)
|
|
407
|
+
|
|
408
|
+
**问题**:四个问题。(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 调用 → 困惑 → 幻觉 → 反馈循环("待命" 刷屏)。
|
|
409
|
+
|
|
410
|
+
**修复**:(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 最新之上防止重启覆盖。
|
|
411
|
+
|
|
412
|
+
文件:`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
413
|
|
|
406
414
|
### v1.12.6 — Stale contextLimitAnchors 修复(PR #143)
|
|
407
415
|
|
package/dist/index.js
CHANGED
|
@@ -4962,14 +4962,9 @@ function formatEntryRanges(entries, state) {
|
|
|
4962
4962
|
for (const entry of entries) {
|
|
4963
4963
|
const block = state.prune.messages.blocksById.get(entry.blockId);
|
|
4964
4964
|
if (!block) continue;
|
|
4965
|
-
const
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
if (startRef === endRef) {
|
|
4969
|
-
parts.push(`b${entry.blockId}: ${startRef}`);
|
|
4970
|
-
} else {
|
|
4971
|
-
parts.push(`b${entry.blockId}: ${startRef}\u2013${endRef}`);
|
|
4972
|
-
}
|
|
4965
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
4966
|
+
if (count === 0) continue;
|
|
4967
|
+
parts.push(`b${entry.blockId}: ${count} msg${count !== 1 ? "s" : ""}`);
|
|
4973
4968
|
}
|
|
4974
4969
|
return parts.length > 0 ? parts.join(", ") : null;
|
|
4975
4970
|
}
|
|
@@ -5258,6 +5253,30 @@ function getLastVisibleMessageId(rawMessages, state) {
|
|
|
5258
5253
|
}
|
|
5259
5254
|
return null;
|
|
5260
5255
|
}
|
|
5256
|
+
function checkPhantomBlock(state, plans) {
|
|
5257
|
+
for (let i = 0; i < plans.length; i++) {
|
|
5258
|
+
const plan = plans[i];
|
|
5259
|
+
const effective = new Set(plan.messageIds);
|
|
5260
|
+
for (const consumedId of plan.consumedBlockIds) {
|
|
5261
|
+
const block = state.prune.messages.blocksById.get(consumedId);
|
|
5262
|
+
if (block) {
|
|
5263
|
+
for (const mid of block.effectiveMessageIds) {
|
|
5264
|
+
effective.add(mid);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
const hasNew = [...effective].some((mid) => {
|
|
5269
|
+
const entry = state.prune.messages.byMessageId.get(mid);
|
|
5270
|
+
return !entry || entry.activeBlockIds.length === 0;
|
|
5271
|
+
});
|
|
5272
|
+
if (!hasNew) {
|
|
5273
|
+
return new Error(
|
|
5274
|
+
`Compression range ${i + 1} contains only already-compressed messages (0 new direct messages, 0 tokens saved). Nothing to compress \u2014 pick a range with visible, uncompressed content. Use \`acp_status({scope:"uncompressed"})\` to see which ranges are still compressible.`
|
|
5275
|
+
);
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
return null;
|
|
5279
|
+
}
|
|
5261
5280
|
function checkLastSegmentDangerous(ctx, allPlanMessageIds, rawMessages, dangerous) {
|
|
5262
5281
|
if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
|
|
5263
5282
|
const lastVisibleId = getLastVisibleMessageId(rawMessages, ctx.state);
|
|
@@ -5496,6 +5515,14 @@ function createCompressMessageTool(ctx) {
|
|
|
5496
5515
|
summaryWithTools
|
|
5497
5516
|
});
|
|
5498
5517
|
}
|
|
5518
|
+
const phantomError = checkPhantomBlock(
|
|
5519
|
+
ctx.state,
|
|
5520
|
+
preparedPlans.map(({ plan }) => ({
|
|
5521
|
+
messageIds: plan.selection.messageIds,
|
|
5522
|
+
consumedBlockIds: []
|
|
5523
|
+
}))
|
|
5524
|
+
);
|
|
5525
|
+
if (phantomError) throw phantomError;
|
|
5499
5526
|
const snapshot = snapshotCompressionState(ctx.state);
|
|
5500
5527
|
const runId = allocateRunId(ctx.state);
|
|
5501
5528
|
try {
|
|
@@ -5714,6 +5741,14 @@ function createCompressRangeTool(ctx) {
|
|
|
5714
5741
|
consumedBlockIds: mergeConsumedBlockIds
|
|
5715
5742
|
});
|
|
5716
5743
|
}
|
|
5744
|
+
const phantomError = checkPhantomBlock(
|
|
5745
|
+
ctx.state,
|
|
5746
|
+
preparedPlans.map((p) => ({
|
|
5747
|
+
messageIds: p.selection.messageIds,
|
|
5748
|
+
consumedBlockIds: p.consumedBlockIds
|
|
5749
|
+
}))
|
|
5750
|
+
);
|
|
5751
|
+
if (phantomError) throw phantomError;
|
|
5717
5752
|
const snapshot = snapshotCompressionState(ctx.state);
|
|
5718
5753
|
const runId = allocateRunId(ctx.state);
|
|
5719
5754
|
try {
|
|
@@ -5842,7 +5877,7 @@ var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") =
|
|
|
5842
5877
|
return { info, parts };
|
|
5843
5878
|
};
|
|
5844
5879
|
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
|
|
5845
|
-
var createSyntheticToolRecap = (baseMessage, summary, blockId,
|
|
5880
|
+
var createSyntheticToolRecap = (baseMessage, summary, blockId, messageCount, stableSeed) => {
|
|
5846
5881
|
const baseInfo = baseMessage.info;
|
|
5847
5882
|
const now = Date.now();
|
|
5848
5883
|
const messageId = generateStableId("msg_acp_recap", stableSeed);
|
|
@@ -5859,7 +5894,7 @@ var createSyntheticToolRecap = (baseMessage, summary, blockId, range, stableSeed
|
|
|
5859
5894
|
status: "completed",
|
|
5860
5895
|
input: {
|
|
5861
5896
|
blockId,
|
|
5862
|
-
...
|
|
5897
|
+
...messageCount !== void 0 ? { messages: messageCount } : {}
|
|
5863
5898
|
},
|
|
5864
5899
|
output: summary,
|
|
5865
5900
|
title: `ACP Context Recap (block ${blockId})`,
|
|
@@ -6010,10 +6045,9 @@ var dropEmptyMessages = (messages) => {
|
|
|
6010
6045
|
};
|
|
6011
6046
|
|
|
6012
6047
|
// lib/messages/prune.ts
|
|
6013
|
-
var
|
|
6014
|
-
if (!
|
|
6015
|
-
|
|
6016
|
-
return `(${startId}\u2013${endId})`;
|
|
6048
|
+
var computeBlockCoverage = (effectiveMessageIds) => {
|
|
6049
|
+
if (!effectiveMessageIds || effectiveMessageIds.length === 0) return void 0;
|
|
6050
|
+
return effectiveMessageIds.length;
|
|
6017
6051
|
};
|
|
6018
6052
|
var prune = (state, logger, config, messages) => {
|
|
6019
6053
|
filterCompressedRanges(state, logger, config, messages);
|
|
@@ -6068,14 +6102,14 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
6068
6102
|
} else {
|
|
6069
6103
|
const cleaned = stripStaleMessageRefs(rawSummaryContent);
|
|
6070
6104
|
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(cleaned) : cleaned;
|
|
6071
|
-
const
|
|
6105
|
+
const blockCoverage = computeBlockCoverage(summary.effectiveMessageIds);
|
|
6072
6106
|
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
6073
6107
|
result.push(
|
|
6074
6108
|
createSyntheticToolRecap(
|
|
6075
6109
|
msg,
|
|
6076
6110
|
summaryContent,
|
|
6077
6111
|
summary.blockId,
|
|
6078
|
-
|
|
6112
|
+
blockCoverage,
|
|
6079
6113
|
summarySeed
|
|
6080
6114
|
)
|
|
6081
6115
|
);
|
|
@@ -7338,7 +7372,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7338
7372
|
});
|
|
7339
7373
|
const growthSinceBaseline = currentTokens !== void 0 && growthReference !== void 0 ? currentTokens - growthReference : void 0;
|
|
7340
7374
|
const nudgeAllowed = emergencyOverride || decision.shouldNudge && growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
|
|
7341
|
-
state.nudges.shouldInjectThisTurn = nudgeAllowed;
|
|
7342
7375
|
const effectiveTipsVariant = emergencyOverride ? "maxLimit" : decision.tipsVariant;
|
|
7343
7376
|
if (nudgeAllowed) {
|
|
7344
7377
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
@@ -7353,8 +7386,42 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7353
7386
|
config.compress.protectedTools,
|
|
7354
7387
|
config.protectedFilePatterns
|
|
7355
7388
|
);
|
|
7389
|
+
const contextRanges = buildCompressibleRanges(
|
|
7390
|
+
messages,
|
|
7391
|
+
state,
|
|
7392
|
+
config.compress.protectedTools,
|
|
7393
|
+
config.protectedFilePatterns
|
|
7394
|
+
);
|
|
7395
|
+
const recommendedRanges = filterRecommendedRanges(
|
|
7396
|
+
contextRanges.compressible,
|
|
7397
|
+
contextRanges.protected,
|
|
7398
|
+
{ modelContextLimit, growthRatio: 0.05, logger }
|
|
7399
|
+
);
|
|
7400
|
+
const hasRecommendations = recommendedRanges.length > 0;
|
|
7401
|
+
if (config.debug && contextRanges.compressible.length > 0 && modelContextLimit) {
|
|
7402
|
+
const growthThreshold = modelContextLimit * 0.05;
|
|
7403
|
+
const lastSegmentFloor = growthThreshold * 2;
|
|
7404
|
+
const compressible = contextRanges.compressible;
|
|
7405
|
+
const lastRange = compressible[compressible.length - 1];
|
|
7406
|
+
const lastIncluded = lastRange.tokens >= lastSegmentFloor;
|
|
7407
|
+
const nonLastTokens = compressible.slice(0, -1).reduce((s, r) => s + r.tokens, 0);
|
|
7408
|
+
const effective = nonLastTokens + Math.max(0, lastRange.tokens - lastSegmentFloor);
|
|
7409
|
+
const suppressed = effective < growthThreshold;
|
|
7410
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7411
|
+
const lines = [
|
|
7412
|
+
`[ACP Debug] Recommendation filter decision:`,
|
|
7413
|
+
` Input: ${compressible.length} range(s), ${fmt(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7414
|
+
` Thresholds: growth=${fmt(growthThreshold)}, lastSegmentFloor=${fmt(lastSegmentFloor)}`,
|
|
7415
|
+
` Last segment: ${lastRange.startRef}\u2013${lastRange.endRef} ${fmt(lastRange.tokens)} tokens \u2192 ${lastIncluded ? "included (dangerous)" : "excluded (< floor)"}`,
|
|
7416
|
+
` Effective compressible: ${fmt(effective)} \u2192 ${suppressed ? "SUPPRESSED (< growth threshold)" : `${recommendedRanges.length} range(s) recommended`}`
|
|
7417
|
+
];
|
|
7418
|
+
logger.debug(lines.join("\n"));
|
|
7419
|
+
}
|
|
7420
|
+
const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
|
|
7421
|
+
const shouldInject = nudgeAllowed && (!filterSuppressed || emergencyOverride);
|
|
7422
|
+
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
7356
7423
|
let tipsText = null;
|
|
7357
|
-
if (
|
|
7424
|
+
if (shouldInject) {
|
|
7358
7425
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
7359
7426
|
if (suffixMessage && composition.total > 0) {
|
|
7360
7427
|
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
@@ -7373,36 +7440,6 @@ Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%
|
|
|
7373
7440
|
breakdown += `
|
|
7374
7441
|
\u26A0\uFE0F ${fmt(composition.protectedTokens)} tokens are protected (environment-managed tools) \u2014 not compressible. Effective compressible: ~${fmt(compressibleTokens)}.`;
|
|
7375
7442
|
}
|
|
7376
|
-
const contextRanges = buildCompressibleRanges(
|
|
7377
|
-
messages,
|
|
7378
|
-
state,
|
|
7379
|
-
config.compress.protectedTools,
|
|
7380
|
-
config.protectedFilePatterns
|
|
7381
|
-
);
|
|
7382
|
-
const recommendedRanges = filterRecommendedRanges(
|
|
7383
|
-
contextRanges.compressible,
|
|
7384
|
-
contextRanges.protected,
|
|
7385
|
-
{ modelContextLimit, growthRatio: 0.05, logger }
|
|
7386
|
-
);
|
|
7387
|
-
if (debugNotify && contextRanges.compressible.length > 0 && modelContextLimit) {
|
|
7388
|
-
const growthThreshold = modelContextLimit * 0.05;
|
|
7389
|
-
const lastSegmentFloor = growthThreshold * 2;
|
|
7390
|
-
const compressible = contextRanges.compressible;
|
|
7391
|
-
const lastRange = compressible[compressible.length - 1];
|
|
7392
|
-
const lastIncluded = lastRange.tokens >= lastSegmentFloor;
|
|
7393
|
-
const nonLastTokens = compressible.slice(0, -1).reduce((s, r) => s + r.tokens, 0);
|
|
7394
|
-
const effective = nonLastTokens + Math.max(0, lastRange.tokens - lastSegmentFloor);
|
|
7395
|
-
const suppressed = effective < growthThreshold;
|
|
7396
|
-
const fmt2 = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7397
|
-
const lines = [
|
|
7398
|
-
`[ACP Debug] Recommendation filter decision:`,
|
|
7399
|
-
` Input: ${compressible.length} range(s), ${fmt2(compressible.reduce((s, r) => s + r.tokens, 0))} tokens`,
|
|
7400
|
-
` Thresholds: growth=${fmt2(growthThreshold)}, lastSegmentFloor=${fmt2(lastSegmentFloor)}`,
|
|
7401
|
-
` Last segment: ${lastRange.startRef}\u2013${lastRange.endRef} ${fmt2(lastRange.tokens)} tokens \u2192 ${lastIncluded ? "included (dangerous)" : "excluded (< floor)"}`,
|
|
7402
|
-
` Effective compressible: ${fmt2(effective)} \u2192 ${suppressed ? "SUPPRESSED (< growth threshold)" : `${recommendedRanges.length} range(s) recommended`}`
|
|
7403
|
-
];
|
|
7404
|
-
debugNotify(lines.join("\n"));
|
|
7405
|
-
}
|
|
7406
7443
|
if (recommendedRanges.length > 0) {
|
|
7407
7444
|
breakdown += `
|
|
7408
7445
|
|
|
@@ -8043,11 +8080,8 @@ function pct(n, total) {
|
|
|
8043
8080
|
return Math.max(1, Math.round(n / total * 100));
|
|
8044
8081
|
}
|
|
8045
8082
|
function formatIdRange(block) {
|
|
8046
|
-
const
|
|
8047
|
-
|
|
8048
|
-
if (!start || !end) return "\u2014";
|
|
8049
|
-
if (start === end) return start;
|
|
8050
|
-
return `${start}\u2013${end}`;
|
|
8083
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
8084
|
+
return count > 0 ? `${count} msg${count !== 1 ? "s" : ""}` : "\u2014";
|
|
8051
8085
|
}
|
|
8052
8086
|
function collectVisibleMessages(rawMessages, ctx) {
|
|
8053
8087
|
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
@@ -8338,12 +8372,9 @@ function createAcpStatusTool(ctx) {
|
|
|
8338
8372
|
|
|
8339
8373
|
// lib/compress/recap.ts
|
|
8340
8374
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
8341
|
-
function
|
|
8342
|
-
const
|
|
8343
|
-
|
|
8344
|
-
if (!start || !end) return "\u2014";
|
|
8345
|
-
if (start === end) return start;
|
|
8346
|
-
return `${start}\u2013${end}`;
|
|
8375
|
+
function formatCoverage(block) {
|
|
8376
|
+
const count = block.effectiveMessageIds?.length || 0;
|
|
8377
|
+
return count > 0 ? `${count} message${count !== 1 ? "s" : ""}` : "\u2014";
|
|
8347
8378
|
}
|
|
8348
8379
|
var RECAP_TOOL_DESCRIPTION = `Read-only retrieval of compression block summaries.
|
|
8349
8380
|
|
|
@@ -8371,7 +8402,7 @@ function createAcpContextRecapTool(ctx) {
|
|
|
8371
8402
|
if (!block.active) {
|
|
8372
8403
|
return `Block b${args.blockId} is inactive (deactivated by GC or nested compression).`;
|
|
8373
8404
|
}
|
|
8374
|
-
const range =
|
|
8405
|
+
const range = formatCoverage(block);
|
|
8375
8406
|
return `[Compressed conversation section]
|
|
8376
8407
|
${block.summary}
|
|
8377
8408
|
|
|
@@ -8382,7 +8413,7 @@ ${block.summary}
|
|
|
8382
8413
|
for (const id of activeIds) {
|
|
8383
8414
|
const block = msgState.blocksById.get(id);
|
|
8384
8415
|
if (!block || !block.active) continue;
|
|
8385
|
-
const range =
|
|
8416
|
+
const range = formatCoverage(block);
|
|
8386
8417
|
const summaryPreview = block.summary.slice(0, 200);
|
|
8387
8418
|
lines.push(`
|
|
8388
8419
|
b${id} | ${range} | "${block.topic || "(none)"}"`);
|
|
@@ -8726,7 +8757,7 @@ Periodically, as context grows, the system appends a short status line in a synt
|
|
|
8726
8757
|
|
|
8727
8758
|
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.
|
|
8728
8759
|
|
|
8729
|
-
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
|
|
8760
|
+
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).
|
|
8730
8761
|
|
|
8731
8762
|
CONTEXT BREAKDOWN
|
|
8732
8763
|
|
|
@@ -10593,15 +10624,17 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
10593
10624
|
output.messages,
|
|
10594
10625
|
prompts.getRuntimePrompts(),
|
|
10595
10626
|
compressionPriorities,
|
|
10596
|
-
config.debug
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10627
|
+
config.debug ? (text) => {
|
|
10628
|
+
logger.debug(`[ACP Debug] Nudge injected:
|
|
10629
|
+
${text}`);
|
|
10630
|
+
client.tui.showToast({
|
|
10631
|
+
body: {
|
|
10632
|
+
title: "ACP: Nudge Injected",
|
|
10633
|
+
message: text.slice(0, 500),
|
|
10634
|
+
variant: "info",
|
|
10635
|
+
duration: 5e3
|
|
10636
|
+
}
|
|
10637
|
+
}).catch(() => {
|
|
10605
10638
|
});
|
|
10606
10639
|
} : void 0,
|
|
10607
10640
|
prePruneTokens
|