opencode-acp 1.12.9 → 1.12.10
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 +280 -102
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/decompress-logic.d.ts +9 -0
- package/dist/lib/compress/decompress-logic.d.ts.map +1 -1
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/range-utils.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/types.d.ts +5 -2
- package/dist/lib/compress/types.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.map +1 -1
- package/dist/lib/prompts/compress-range.d.ts +1 -1
- package/dist/lib/prompts/compress-range.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/tool.d.ts +1 -1
- package/dist/lib/prompts/extensions/tool.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.10 — Batch Compress + Decompress Range Mode + GC Memory-Loss Fix + Token Classification + Nudge Quality (PRs #73, #155, #156, #157, #158, #159, #161)
|
|
426
|
+
|
|
427
|
+
**Problem**: Seven issues across compression UX, token accounting, GC safety, and nudge quality. (1) `decompress` required a per-block `acp_status` → decompress-per-block loop to restore multiple compressed blocks. (2) Since v1.12.9 (compress-as-anchor), compress tool `summary` content was misclassified as `toolTokens` instead of `summaryTokens`, inflating tool% and deflating summary% in the context breakdown. (3) The `compress` tool only accepted a single range per call — the model had to issue multiple calls to compress unrelated ranges, wasting turns. (4) The `[PROTECTED: ...]` label listed every tool in a protected message instead of only the triggering tools. (5) When all visible content was protected, the nudge still fired with an empty recommendation list. (6) When a nudge was suppressed, the next-turn check re-evaluated every turn. (7) **The GC system was silently destroying model-written summaries**: any block with `summary.length > 6000` chars was force-truncated to 3000 regardless of context pressure (0% pressure triggered truncation), and blocks with high `survivedCount` were auto-deactivated — causing irrecoverable memory loss across hundreds of sessions.
|
|
428
|
+
|
|
429
|
+
**Fix**: (1) **PR #73** — Added optional `startId`/`endId` to `decompress` schema; range mode batch-restores every active block whose `effectiveMessageIds` overlaps the resolved range. (2) **PR #155** — In `estimateContextComposition`, when `toolName === "compress"`, extract `summary` text and classify it as `summaryTokens`. (3) **PR #156** — The `compress` tool now accepts a `content` array of `{ topic, startId, endId, summary }` entries, allowing the model to compress multiple unrelated ranges in a single call with per-entry topics. (4) **PR #157** — `buildCompressibleRanges` only adds tools that actually trigger protection. (5) **PR #158** — Added `allProtected` check to suppress nudges when there's genuinely nothing to compress. (6) **PR #159** — When a nudge is suppressed, advance `lastPerMessageNudgeTokens` to `currentTokens` for discrete 5% check intervals. (7) **PR #161** — Removed the GC oversized-block override (`hasOversizedBlocks` bypass that truncated at 0% context) and the age-based deactivation loop entirely. Truncation now only fires at `majorGcThresholdPercent` (default 100%). `gc.maxBlockAge` is now a no-op. Aging warning threshold raised from 50% to 90% context to stop misleading the model.
|
|
430
|
+
|
|
431
|
+
Files: `lib/hooks.ts`, `lib/config.ts`, `lib/prompts/extensions/nudge.ts`, `lib/compress/decompress.ts`, `lib/compress/decompress-logic.ts`, `lib/messages/inject/utils.ts`, `lib/messages/inject/inject.ts`. Tests: 758 pass.
|
|
432
|
+
|
|
425
433
|
### v1.12.9 — Compress-as-Anchor (PR #153)
|
|
426
434
|
|
|
427
435
|
**Problem**: Since v1.12.1, compression summaries were injected as synthetic tool-result messages via a registered `acp_context_recap` tool, while `stripStaleCompressCalls` removed past `compress` tool calls from the API context to avoid duplication. This doubled summary overhead: each block's summary existed both as a synthetic recap message AND in the original (now-stripped) compress call's `summary` parameter. For sessions with many compressions, this "recap overhead" consumed 10–20% of context with no additional information value. The `acp_context_recap` tool description also claimed it "automatically injects" summaries, which was misleading.
|
package/README.zh-CN.md
CHANGED
|
@@ -395,6 +395,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
395
395
|
|
|
396
396
|
## 更新日志
|
|
397
397
|
|
|
398
|
+
### v1.12.10 — 批量压缩 + Decompress 范围模式 + GC 记忆丢失修复 + Token 分类 + Nudge 质量(PR #73, #155, #156, #157, #158, #159, #161)
|
|
399
|
+
|
|
400
|
+
**问题**:七个问题,涉及压缩 UX、token 统计、GC 安全和 nudge 质量。(1)`decompress` 需要先 `acp_status` 再逐块 decompress 的循环才能恢复多个压缩块。(2)自 v1.12.9 起,compress 工具的 `summary` 内容被错误分类为 `toolTokens` 而非 `summaryTokens`,导致上下文分布中 tool% 虚高、summary% 虚低。(3)`compress` 工具每次调用只能压缩一个范围 —— 模型需要多次调用才能压缩不相关的范围,浪费轮次。(4)`[PROTECTED: ...]` 标签列出受保护消息中的所有工具而非仅触发保护的工具。(5)当所有可见内容都是受保护内容时,nudge 仍然以空推荐列表注入。(6)当 nudge 被抑制时,下一轮检查每轮都重新评估。(7)**GC 系统在静默销毁模型编写的 summary**:任何 `summary.length > 6000` 字符的块在零上下文压力下被强制截断到 3000,且 `survivedCount` 过高的块被自动 deactivate —— 导致数百个会话的不可恢复记忆丢失。
|
|
401
|
+
|
|
402
|
+
**修复**:(1)**PR #73** —— 为 `decompress` schema 新增可选 `startId`/`endId`;范围模式批量恢复所有 `effectiveMessageIds` 与解析范围重叠的活跃块。(2)**PR #155** —— 在 `estimateContextComposition` 中,当 `toolName === "compress"` 时,提取 `summary` 文本并分类为 `summaryTokens`。(3)**PR #156** —— `compress` 工具现在接受 `content` 数组(`{ topic, startId, endId, summary }`),允许模型在单次调用中压缩多个不相关范围,每个范围有独立 topic。(4)**PR #157** —— `buildCompressibleRanges` 只添加实际触发保护的工具。(5)**PR #158** —— 新增 `allProtected` 检查,当确实没有可压缩内容时抑制 nudge。(6)**PR #159** —— 当 nudge 被抑制时,将 `lastPerMessageNudgeTokens` 前进到 `currentTokens`,创建离散 5% 检查间隔。(7)**PR #161** —— 删除 GC oversized-block 旁路(`hasOversizedBlocks`,在 0% 上下文压力下截断)和 age-based 自动 deactivate 循环。截断现在只在 `majorGcThresholdPercent`(默认 100%)时触发。`gc.maxBlockAge` 变为 no-op。aging warning 门槛从 50% 提高到 90%,不再误导模型。
|
|
403
|
+
|
|
404
|
+
文件:`lib/hooks.ts`、`lib/config.ts`、`lib/prompts/extensions/nudge.ts`、`lib/compress/decompress.ts`、`lib/compress/decompress-logic.ts`、`lib/messages/inject/utils.ts`、`lib/messages/inject/inject.ts`。测试:758 通过。
|
|
405
|
+
|
|
398
406
|
### v1.12.9 — Compress-as-Anchor(PR #153)
|
|
399
407
|
|
|
400
408
|
**问题**:自 v1.12.1 起,压缩摘要通过注册的 `acp_context_recap` 工具以 synthetic tool-result 消息注入,同时 `stripStaleCompressCalls` 从 API 上下文中移除历史 `compress` 工具调用以避免重复。这导致摘要开销翻倍:每个块的摘要同时存在于 synthetic recap 消息和原始(已移除的)compress 调用的 `summary` 参数中。对于压缩频繁的会话,这个 "recap 开销" 占用 10–20% 上下文却没有额外信息价值。`acp_context_recap` 工具描述也声称它"自动注入"摘要,具有误导性。
|
package/dist/index.js
CHANGED
|
@@ -1609,7 +1609,8 @@ var defaultConfig = {
|
|
|
1609
1609
|
gc: {
|
|
1610
1610
|
algorithm: "truncate",
|
|
1611
1611
|
promotionThreshold: 5,
|
|
1612
|
-
maxBlockAge:
|
|
1612
|
+
maxBlockAge: Number.MAX_SAFE_INTEGER,
|
|
1613
|
+
// no-op: age-based deactivation removed (memory-loss fix)
|
|
1613
1614
|
maxOldGenSummaryLength: 3e3,
|
|
1614
1615
|
majorGcThresholdPercent: "100%",
|
|
1615
1616
|
batchCleanup: {
|
|
@@ -2131,20 +2132,27 @@ function countMessageCharacters(msg) {
|
|
|
2131
2132
|
|
|
2132
2133
|
// lib/prompts/extensions/tool.ts
|
|
2133
2134
|
var RANGE_FORMAT_EXTENSION = `
|
|
2135
|
+
|
|
2134
2136
|
THE FORMAT OF COMPRESS
|
|
2135
2137
|
|
|
2136
2138
|
\`\`\`
|
|
2137
2139
|
{
|
|
2138
|
-
topic
|
|
2140
|
+
topic?: string, // OPTIONAL fallback topic for entries without their own.
|
|
2141
|
+
// Omit when every content entry specifies its own topic.
|
|
2139
2142
|
content: [ // One or more ranges to compress
|
|
2140
2143
|
{
|
|
2144
|
+
topic?: string, // OPTIONAL per-entry topic for this range.
|
|
2145
|
+
// Falls back to top-level topic.
|
|
2146
|
+
// Give each entry its own topic when compressing
|
|
2147
|
+
// unrelated ranges in one call.
|
|
2141
2148
|
startId: string, // Boundary ID at range start: mNNNNN or bN
|
|
2142
2149
|
endId: string, // Boundary ID at range end: mNNNNN or bN
|
|
2143
2150
|
summary: string // Complete technical summary replacing all content in range
|
|
2144
2151
|
}
|
|
2145
2152
|
]
|
|
2146
2153
|
}
|
|
2147
|
-
|
|
2154
|
+
\`\`\`
|
|
2155
|
+
Each entry needs a topic \u2014 either its own or the top-level fallback.`;
|
|
2148
2156
|
var MESSAGE_FORMAT_EXTENSION = `
|
|
2149
2157
|
THE FORMAT OF COMPRESS
|
|
2150
2158
|
|
|
@@ -3969,9 +3977,7 @@ function applyPendingCompressionDurations(state) {
|
|
|
3969
3977
|
// lib/compress/range-utils.ts
|
|
3970
3978
|
var BLOCK_PLACEHOLDER_REGEX = /\(b(\d+)\)|\{block_(\d+)\}/gi;
|
|
3971
3979
|
function validateArgs2(args) {
|
|
3972
|
-
|
|
3973
|
-
throw new Error("topic is required and must be a non-empty string");
|
|
3974
|
-
}
|
|
3980
|
+
const hasTopLevelTopic = typeof args.topic === "string" && args.topic.trim().length > 0;
|
|
3975
3981
|
if (!Array.isArray(args.content) || args.content.length === 0) {
|
|
3976
3982
|
throw new Error("content is required and must be a non-empty array");
|
|
3977
3983
|
}
|
|
@@ -3987,11 +3993,18 @@ function validateArgs2(args) {
|
|
|
3987
3993
|
if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
|
|
3988
3994
|
throw new Error(`${prefix}.summary is required and must be a non-empty string`);
|
|
3989
3995
|
}
|
|
3996
|
+
const hasEntryTopic = typeof entry?.topic === "string" && entry.topic.trim().length > 0;
|
|
3997
|
+
if (!hasEntryTopic && !hasTopLevelTopic) {
|
|
3998
|
+
throw new Error(
|
|
3999
|
+
`${prefix} needs a topic \u2014 provide ${prefix}.topic or the top-level topic`
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
3990
4002
|
}
|
|
3991
4003
|
}
|
|
3992
4004
|
function resolveRanges(args, searchContext, state) {
|
|
3993
4005
|
return args.content.map((entry, index) => {
|
|
3994
4006
|
const normalizedEntry = {
|
|
4007
|
+
topic: typeof entry.topic === "string" && entry.topic.trim().length > 0 ? entry.topic.trim() : void 0,
|
|
3995
4008
|
startId: entry.startId.trim(),
|
|
3996
4009
|
endId: entry.endId.trim(),
|
|
3997
4010
|
summary: entry.summary
|
|
@@ -4185,8 +4198,8 @@ function rebuildRangeInvocation(state, input, searchContext, invocation, protect
|
|
|
4185
4198
|
applyCompressionState(
|
|
4186
4199
|
state,
|
|
4187
4200
|
{
|
|
4188
|
-
topic: input.topic,
|
|
4189
|
-
batchTopic: input.topic,
|
|
4201
|
+
topic: plan.entry.topic ?? input.topic ?? "",
|
|
4202
|
+
batchTopic: typeof input.topic === "string" ? input.topic : void 0,
|
|
4190
4203
|
startId: plan.entry.startId,
|
|
4191
4204
|
endId: plan.entry.endId,
|
|
4192
4205
|
mode: "range",
|
|
@@ -4253,7 +4266,7 @@ function rebuildMessageInvocation(state, input, searchContext, invocation, gcCon
|
|
|
4253
4266
|
state,
|
|
4254
4267
|
{
|
|
4255
4268
|
topic: entry.topic,
|
|
4256
|
-
batchTopic: input.topic,
|
|
4269
|
+
batchTopic: typeof input.topic === "string" ? input.topic : void 0,
|
|
4257
4270
|
startId: entry.messageId,
|
|
4258
4271
|
endId: entry.messageId,
|
|
4259
4272
|
mode: "message",
|
|
@@ -5076,7 +5089,8 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5076
5089
|
newlyCompressedToolIds.push(toolId);
|
|
5077
5090
|
}
|
|
5078
5091
|
}
|
|
5079
|
-
const
|
|
5092
|
+
const entryBlockTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic).filter((t) => typeof t === "string" && t.length > 0);
|
|
5093
|
+
const topic = batchTopic ?? (entries.length === 1 ? state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ?? "(unknown topic)" : entryBlockTopics.length > 0 ? entryBlockTopics.join(" \xB7 ") : "(unknown topic)");
|
|
5080
5094
|
const contextTokensAfter = Math.max(
|
|
5081
5095
|
0,
|
|
5082
5096
|
contextTokensBefore - compressedTokens + summaryTokens
|
|
@@ -5578,9 +5592,14 @@ function createCompressMessageTool(ctx) {
|
|
|
5578
5592
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
5579
5593
|
function buildSchema2(maxSummaryLengthHard) {
|
|
5580
5594
|
return {
|
|
5581
|
-
topic: tool3.schema.string().
|
|
5595
|
+
topic: tool3.schema.string().optional().describe(
|
|
5596
|
+
"Fallback topic for entries without their own. Omit when each content entry specifies its own topic."
|
|
5597
|
+
),
|
|
5582
5598
|
content: tool3.schema.array(
|
|
5583
5599
|
tool3.schema.object({
|
|
5600
|
+
topic: tool3.schema.string().optional().describe(
|
|
5601
|
+
"Short label (3-5 words) for THIS range, e.g. 'Auth System Exploration'. Omit to use top-level topic. When compressing multiple unrelated ranges, give each its own topic for better quality."
|
|
5602
|
+
),
|
|
5584
5603
|
startId: tool3.schema.string().describe(
|
|
5585
5604
|
"Message or block ID marking the beginning of range (e.g. m00001, b2)"
|
|
5586
5605
|
),
|
|
@@ -5590,7 +5609,7 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
5590
5609
|
)
|
|
5591
5610
|
})
|
|
5592
5611
|
).describe(
|
|
5593
|
-
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
5612
|
+
"One or more ranges to compress, each with start/end boundaries and a summary. When compressing multiple unrelated ranges in one call, give each its own topic."
|
|
5594
5613
|
),
|
|
5595
5614
|
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5596
5615
|
`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.`
|
|
@@ -5624,7 +5643,7 @@ function createCompressRangeTool(ctx) {
|
|
|
5624
5643
|
const { rawMessages, searchContext } = await prepareSession(
|
|
5625
5644
|
ctx,
|
|
5626
5645
|
toolCtx,
|
|
5627
|
-
`Compress Range: ${input.topic}`
|
|
5646
|
+
`Compress Range: ${input.topic ?? "(batch)"}`
|
|
5628
5647
|
);
|
|
5629
5648
|
const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
|
|
5630
5649
|
validateNonOverlapping(resolvedPlans);
|
|
@@ -5766,7 +5785,7 @@ function createCompressRangeTool(ctx) {
|
|
|
5766
5785
|
const applied = applyCompressionState(
|
|
5767
5786
|
ctx.state,
|
|
5768
5787
|
{
|
|
5769
|
-
topic: input.topic,
|
|
5788
|
+
topic: preparedPlan.entry.topic ?? input.topic ?? "",
|
|
5770
5789
|
batchTopic: input.topic,
|
|
5771
5790
|
startId: preparedPlan.entry.startId,
|
|
5772
5791
|
endId: preparedPlan.entry.endId,
|
|
@@ -5791,7 +5810,13 @@ function createCompressRangeTool(ctx) {
|
|
|
5791
5810
|
summaryTokens
|
|
5792
5811
|
});
|
|
5793
5812
|
}
|
|
5794
|
-
await finalizeSession(
|
|
5813
|
+
await finalizeSession(
|
|
5814
|
+
ctx,
|
|
5815
|
+
toolCtx,
|
|
5816
|
+
rawMessages,
|
|
5817
|
+
notifications,
|
|
5818
|
+
input.topic
|
|
5819
|
+
);
|
|
5795
5820
|
} catch (error) {
|
|
5796
5821
|
restoreCompressionState(ctx.state, snapshot);
|
|
5797
5822
|
throw error;
|
|
@@ -6248,7 +6273,7 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
6248
6273
|
}
|
|
6249
6274
|
}
|
|
6250
6275
|
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
6251
|
-
if (gcConfig && usageRatio > 0.
|
|
6276
|
+
if (gcConfig && usageRatio > 0.9) {
|
|
6252
6277
|
const promotionThreshold = gcConfig.promotionThreshold;
|
|
6253
6278
|
const agingBlocks = [];
|
|
6254
6279
|
for (const blockId of activeBlockIds) {
|
|
@@ -6266,10 +6291,10 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
6266
6291
|
}
|
|
6267
6292
|
if (agingBlocks.length > 0) {
|
|
6268
6293
|
lines.push("");
|
|
6269
|
-
lines.push("\u26A0\uFE0F Block aging warning \u2014 these blocks may be truncated by GC
|
|
6294
|
+
lines.push("\u26A0\uFE0F Block aging warning \u2014 context near limit, these blocks may be truncated by last-resort GC:");
|
|
6270
6295
|
lines.push(...agingBlocks);
|
|
6271
6296
|
lines.push(
|
|
6272
|
-
"
|
|
6297
|
+
"Re-summarize these blocks into concise new ones to preserve key facts. At 100% context, oversized blocks are auto-truncated as a last resort."
|
|
6273
6298
|
);
|
|
6274
6299
|
}
|
|
6275
6300
|
}
|
|
@@ -6765,10 +6790,23 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
|
|
|
6765
6790
|
const raw = JSON.stringify(part);
|
|
6766
6791
|
const tokens = Math.round(raw.length / 4);
|
|
6767
6792
|
msgTotal += tokens;
|
|
6768
|
-
toolTokens += tokens;
|
|
6769
|
-
msgTool += tokens;
|
|
6770
6793
|
const toolName = part?.tool || "unknown";
|
|
6771
|
-
|
|
6794
|
+
let summaryPartTokens = 0;
|
|
6795
|
+
if (toolName === "compress") {
|
|
6796
|
+
const input = part?.state?.input;
|
|
6797
|
+
if (input?.content && Array.isArray(input.content)) {
|
|
6798
|
+
for (const entry of input.content) {
|
|
6799
|
+
if (typeof entry?.summary === "string") {
|
|
6800
|
+
summaryPartTokens += Math.round(entry.summary.length / 4);
|
|
6801
|
+
}
|
|
6802
|
+
}
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const toolPartTokens = Math.max(0, tokens - summaryPartTokens);
|
|
6806
|
+
toolTokens += toolPartTokens;
|
|
6807
|
+
msgTool += toolPartTokens;
|
|
6808
|
+
summaryTokens += summaryPartTokens;
|
|
6809
|
+
toolTypeMap.set(toolName, (toolTypeMap.get(toolName) || 0) + toolPartTokens);
|
|
6772
6810
|
if (!msgToolName) msgToolName = toolName;
|
|
6773
6811
|
}
|
|
6774
6812
|
}
|
|
@@ -6824,7 +6862,20 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
|
|
|
6824
6862
|
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6825
6863
|
tokens2 += Math.round(JSON.stringify(part).length / 4);
|
|
6826
6864
|
const toolName = part?.tool;
|
|
6827
|
-
|
|
6865
|
+
const callID = part?.callID;
|
|
6866
|
+
if (toolName && callID) {
|
|
6867
|
+
if (isToolNameProtected(toolName, protectedTools)) {
|
|
6868
|
+
tools.add(toolName);
|
|
6869
|
+
} else if (protectedFilePatterns.length > 0) {
|
|
6870
|
+
const filePaths = getFilePathsFromParameters(
|
|
6871
|
+
toolName,
|
|
6872
|
+
part?.state?.input
|
|
6873
|
+
);
|
|
6874
|
+
if (isFilePathProtected(filePaths, protectedFilePatterns)) {
|
|
6875
|
+
tools.add(toolName);
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6828
6879
|
}
|
|
6829
6880
|
}
|
|
6830
6881
|
protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
|
|
@@ -7297,7 +7348,13 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
7297
7348
|
logger.debug(lines.join("\n"));
|
|
7298
7349
|
}
|
|
7299
7350
|
const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
|
|
7300
|
-
const
|
|
7351
|
+
const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
|
|
7352
|
+
const nothingToCompress = filterSuppressed || allProtected;
|
|
7353
|
+
const shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
|
|
7354
|
+
if (nudgeAllowed && nothingToCompress && !emergencyOverride && currentTokens !== void 0) {
|
|
7355
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
7356
|
+
state.nudges.lastNudgeShownTokens = void 0;
|
|
7357
|
+
}
|
|
7301
7358
|
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
7302
7359
|
let tipsText = null;
|
|
7303
7360
|
if (shouldInject) {
|
|
@@ -7656,6 +7713,37 @@ function parseBlockIdArg(arg) {
|
|
|
7656
7713
|
const parsed = Number.parseInt(normalized, 10);
|
|
7657
7714
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
7658
7715
|
}
|
|
7716
|
+
function resolveDecompressMode(args) {
|
|
7717
|
+
const hasBlockId = typeof args.blockId === "string" && args.blockId.trim() !== "";
|
|
7718
|
+
const hasStartId = typeof args.startId === "string" && args.startId.trim() !== "";
|
|
7719
|
+
const hasEndId = typeof args.endId === "string" && args.endId.trim() !== "";
|
|
7720
|
+
if (hasBlockId && (hasStartId || hasEndId)) {
|
|
7721
|
+
return { ok: false, error: "Cannot specify both blockId and startId/endId. Choose one mode." };
|
|
7722
|
+
}
|
|
7723
|
+
if (!hasBlockId && !(hasStartId && hasEndId)) {
|
|
7724
|
+
return { ok: false, error: "Must specify either blockId, or both startId and endId." };
|
|
7725
|
+
}
|
|
7726
|
+
return { ok: true, mode: hasBlockId ? "block" : "range" };
|
|
7727
|
+
}
|
|
7728
|
+
function findActiveBlocksOverlappingMessages(messagesState, messageIds) {
|
|
7729
|
+
if (messageIds.size === 0) {
|
|
7730
|
+
return [];
|
|
7731
|
+
}
|
|
7732
|
+
const matched = /* @__PURE__ */ new Map();
|
|
7733
|
+
for (const [blockId, block] of messagesState.blocksById) {
|
|
7734
|
+
if (!block.active) {
|
|
7735
|
+
continue;
|
|
7736
|
+
}
|
|
7737
|
+
const effectiveIds = block.effectiveMessageIds ?? [];
|
|
7738
|
+
for (const msgId of effectiveIds) {
|
|
7739
|
+
if (messageIds.has(msgId)) {
|
|
7740
|
+
matched.set(blockId, block);
|
|
7741
|
+
break;
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
return Array.from(matched.values()).sort((a, b) => a.blockId - b.blockId);
|
|
7746
|
+
}
|
|
7659
7747
|
function findActiveParentBlockId(messagesState, block) {
|
|
7660
7748
|
const queue = [...block.parentBlockIds];
|
|
7661
7749
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -7806,12 +7894,120 @@ async function prepareDecompressSession(ctx, toolCtx) {
|
|
|
7806
7894
|
async function finalizeDecompressSession(ctx) {
|
|
7807
7895
|
await saveSessionState(ctx.state, ctx.logger);
|
|
7808
7896
|
}
|
|
7809
|
-
|
|
7897
|
+
function resolveTargets(args, state, rawMessages, logger) {
|
|
7898
|
+
const mode = resolveDecompressMode(args);
|
|
7899
|
+
if (!mode.ok) {
|
|
7900
|
+
return { ok: false, error: `Error: ${mode.error}` };
|
|
7901
|
+
}
|
|
7902
|
+
const messagesState = state.prune.messages;
|
|
7903
|
+
if (mode.mode === "block") {
|
|
7904
|
+
return resolveSingleBlockTarget(messagesState, args.blockId);
|
|
7905
|
+
}
|
|
7906
|
+
return resolveRangeTarget(state, rawMessages, args.startId, args.endId, logger);
|
|
7907
|
+
}
|
|
7908
|
+
function resolveSingleBlockTarget(messagesState, blockIdArg) {
|
|
7909
|
+
const targetBlockId = parseBlockIdArg(blockIdArg);
|
|
7910
|
+
if (targetBlockId === null) {
|
|
7911
|
+
return {
|
|
7912
|
+
ok: false,
|
|
7913
|
+
error: `Error: Invalid block ID "${blockIdArg}". Use format "b0", "b1", etc.`
|
|
7914
|
+
};
|
|
7915
|
+
}
|
|
7916
|
+
const target = resolveCompressionTarget(messagesState, targetBlockId);
|
|
7917
|
+
if (!target) {
|
|
7918
|
+
return {
|
|
7919
|
+
ok: false,
|
|
7920
|
+
error: `Error: Block ${targetBlockId} does not exist. No compression found with that ID.`
|
|
7921
|
+
};
|
|
7922
|
+
}
|
|
7923
|
+
const activeBlocks = target.blocks.filter((block) => block.active);
|
|
7924
|
+
if (activeBlocks.length === 0) {
|
|
7925
|
+
const activeAncestorBlockId = findActiveAncestorBlockId(messagesState, target);
|
|
7926
|
+
if (activeAncestorBlockId !== null) {
|
|
7927
|
+
return {
|
|
7928
|
+
ok: false,
|
|
7929
|
+
error: `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
return {
|
|
7933
|
+
ok: false,
|
|
7934
|
+
error: `Error: Block ${target.displayId} is not active. It may have already been decompressed.`
|
|
7935
|
+
};
|
|
7936
|
+
}
|
|
7937
|
+
return { ok: true, targets: [target] };
|
|
7938
|
+
}
|
|
7939
|
+
function resolveRangeTarget(state, rawMessages, startId, endId, logger) {
|
|
7940
|
+
const searchContext = buildSearchContext(state, rawMessages);
|
|
7941
|
+
let startReference;
|
|
7942
|
+
let endReference;
|
|
7943
|
+
try {
|
|
7944
|
+
const resolved = resolveBoundaryIds(searchContext, state, startId, endId);
|
|
7945
|
+
startReference = resolved.startReference;
|
|
7946
|
+
endReference = resolved.endReference;
|
|
7947
|
+
} catch (err) {
|
|
7948
|
+
return { ok: false, error: `Error: ${err.message}` };
|
|
7949
|
+
}
|
|
7950
|
+
let selection;
|
|
7951
|
+
try {
|
|
7952
|
+
selection = resolveSelection(searchContext, startReference, endReference);
|
|
7953
|
+
} catch (err) {
|
|
7954
|
+
return { ok: false, error: `Error: ${err.message}` };
|
|
7955
|
+
}
|
|
7956
|
+
if (selection.messageIds.length === 0) {
|
|
7957
|
+
return {
|
|
7958
|
+
ok: false,
|
|
7959
|
+
error: `Error: No messages found in range ${startId}..${endId}.`
|
|
7960
|
+
};
|
|
7961
|
+
}
|
|
7962
|
+
const messageIdSet = new Set(selection.messageIds);
|
|
7963
|
+
const messagesState = state.prune.messages;
|
|
7964
|
+
const overlappingBlocks = findActiveBlocksOverlappingMessages(messagesState, messageIdSet);
|
|
7965
|
+
if (overlappingBlocks.length === 0) {
|
|
7966
|
+
return {
|
|
7967
|
+
ok: false,
|
|
7968
|
+
error: `Error: No active compression blocks overlap the range ${startId}..${endId}. The content may already be fully visible. Use acp_status to review block coverage.`
|
|
7969
|
+
};
|
|
7970
|
+
}
|
|
7971
|
+
const targetMap = /* @__PURE__ */ new Map();
|
|
7972
|
+
for (const block of overlappingBlocks) {
|
|
7973
|
+
const target = resolveCompressionTarget(messagesState, block.blockId);
|
|
7974
|
+
if (target) {
|
|
7975
|
+
targetMap.set(target.displayId, target);
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
const targets = Array.from(targetMap.values());
|
|
7979
|
+
logger.info("range decompress resolved", {
|
|
7980
|
+
range: `${startId}..${endId}`,
|
|
7981
|
+
matchedBlocks: overlappingBlocks.map((b) => b.blockId),
|
|
7982
|
+
targets: targets.map((t) => t.displayId)
|
|
7983
|
+
});
|
|
7984
|
+
return { ok: true, targets };
|
|
7985
|
+
}
|
|
7986
|
+
var TOOL_DESCRIPTION = `Restores previously compressed content.
|
|
7810
7987
|
|
|
7811
|
-
Use this tool when you need exact details from
|
|
7988
|
+
Use this tool when you need exact details from compressed content that the summary cannot provide.
|
|
7812
7989
|
The tool returns a condensed preview of the restored content so you can reason about it immediately.
|
|
7813
7990
|
|
|
7814
|
-
|
|
7991
|
+
TWO MODES:
|
|
7992
|
+
|
|
7993
|
+
1. Block mode (default): decompress a single block by ID.
|
|
7994
|
+
- blockId: block reference to decompress (e.g., "b0", "b2")
|
|
7995
|
+
|
|
7996
|
+
2. Range mode: decompress ALL blocks overlapping a message range. Use this to restore
|
|
7997
|
+
content across multiple blocks without calling acp_status + decompress repeatedly.
|
|
7998
|
+
- startId: starting message or block ref (e.g., "m00150")
|
|
7999
|
+
- endId: ending message or block ref (e.g., "m00200")
|
|
8000
|
+
|
|
8001
|
+
Range mode finds every active block whose effectiveMessageIds touch the range and
|
|
8002
|
+
batch-restores them. Partial overlap decompresses the whole block (content cannot be
|
|
8003
|
+
partially restored). Nested blocks are handled automatically.
|
|
8004
|
+
|
|
8005
|
+
ARGUMENTS:
|
|
8006
|
+
- blockId?: string \u2014 use this OR startId+endId (mutually exclusive)
|
|
8007
|
+
- startId?: string \u2014 range start (message or block ref)
|
|
8008
|
+
- endId?: string \u2014 range end (message or block ref)
|
|
8009
|
+
- toFile?: string \u2014 if provided, writes restored content to this file path (must be under
|
|
8010
|
+
/tmp or ~/.cache/opencode/) instead of inflating context. Block(s) stay compressed.
|
|
7815
8011
|
|
|
7816
8012
|
IMPORTANT:
|
|
7817
8013
|
- Decompressing inflates context. Check context usage before decompressing.
|
|
@@ -7820,10 +8016,22 @@ IMPORTANT:
|
|
|
7820
8016
|
- Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
|
|
7821
8017
|
function buildSchema3() {
|
|
7822
8018
|
return {
|
|
7823
|
-
blockId: tool4.schema.string().describe('Block reference to decompress (e.g., "b0", "b2")'),
|
|
7824
|
-
|
|
8019
|
+
blockId: tool4.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
|
|
8020
|
+
startId: tool4.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
|
|
8021
|
+
endId: tool4.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
|
|
8022
|
+
toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'")
|
|
7825
8023
|
};
|
|
7826
8024
|
}
|
|
8025
|
+
function extractMessageId(m) {
|
|
8026
|
+
return m.id ?? m.messageId ?? "";
|
|
8027
|
+
}
|
|
8028
|
+
function extractMessageText(m) {
|
|
8029
|
+
const msg = m;
|
|
8030
|
+
const role = msg.role || msg.type || "unknown";
|
|
8031
|
+
const content = typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : JSON.stringify(msg.content || msg.text || "");
|
|
8032
|
+
return `[${role}]
|
|
8033
|
+
${content}`;
|
|
8034
|
+
}
|
|
7827
8035
|
function createDecompressTool(ctx) {
|
|
7828
8036
|
return tool4({
|
|
7829
8037
|
description: TOOL_DESCRIPTION,
|
|
@@ -7833,22 +8041,19 @@ function createDecompressTool(ctx) {
|
|
|
7833
8041
|
const contextUsageBefore = ctx.state.modelContextLimit ? Math.round(
|
|
7834
8042
|
getCurrentTokenUsage(ctx.state, rawMessages) / ctx.state.modelContextLimit * 100
|
|
7835
8043
|
) : void 0;
|
|
7836
|
-
const
|
|
7837
|
-
if (
|
|
7838
|
-
return
|
|
8044
|
+
const resolved = resolveTargets(args, ctx.state, rawMessages, ctx.logger);
|
|
8045
|
+
if (!resolved.ok) {
|
|
8046
|
+
return resolved.error;
|
|
7839
8047
|
}
|
|
8048
|
+
const targets = resolved.targets;
|
|
7840
8049
|
const messagesState = ctx.state.prune.messages;
|
|
7841
|
-
const
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
const activeAncestorBlockId = findActiveAncestorBlockId(messagesState, target);
|
|
7848
|
-
if (activeAncestorBlockId !== null) {
|
|
7849
|
-
return `Error: Block ${target.displayId} is nested inside active block ${activeAncestorBlockId}. Decompress block ${activeAncestorBlockId} first.`;
|
|
8050
|
+
const activeBlocks = [];
|
|
8051
|
+
for (const target of targets) {
|
|
8052
|
+
for (const block of target.blocks) {
|
|
8053
|
+
if (block.active) {
|
|
8054
|
+
activeBlocks.push(block);
|
|
8055
|
+
}
|
|
7850
8056
|
}
|
|
7851
|
-
return `Error: Block ${target.displayId} is not active. It may have already been decompressed.`;
|
|
7852
8057
|
}
|
|
7853
8058
|
if (args.toFile) {
|
|
7854
8059
|
const targetPath = args.toFile;
|
|
@@ -7858,35 +8063,33 @@ function createDecompressTool(ctx) {
|
|
|
7858
8063
|
os.tmpdir() + "/",
|
|
7859
8064
|
path.join(os.homedir(), ".cache", "opencode") + "/"
|
|
7860
8065
|
];
|
|
7861
|
-
const
|
|
8066
|
+
const resolvedPath = path.resolve(targetPath);
|
|
7862
8067
|
const isAllowed = allowedDirs.some((dir) => {
|
|
7863
|
-
const rel = path.relative(dir,
|
|
8068
|
+
const rel = path.relative(dir, resolvedPath);
|
|
7864
8069
|
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
7865
8070
|
});
|
|
7866
8071
|
if (!isAllowed) {
|
|
7867
8072
|
return `Error: toFile path must be under ${os.tmpdir()} or ~/.cache/opencode/. Got: ${targetPath}`;
|
|
7868
8073
|
}
|
|
7869
|
-
const
|
|
7870
|
-
const
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
}
|
|
7875
|
-
const
|
|
7876
|
-
|
|
7877
|
-
const role = msg.role || msg.type || "unknown";
|
|
7878
|
-
const content = typeof msg.content === "string" ? msg.content : typeof msg.text === "string" ? msg.text : JSON.stringify(msg.content || msg.text || "");
|
|
7879
|
-
return `[${role}]
|
|
7880
|
-
${content}`;
|
|
7881
|
-
});
|
|
8074
|
+
const msgIdSet = /* @__PURE__ */ new Set();
|
|
8075
|
+
for (const block of activeBlocks) {
|
|
8076
|
+
for (const id of block.effectiveMessageIds ?? []) {
|
|
8077
|
+
msgIdSet.add(id);
|
|
8078
|
+
}
|
|
8079
|
+
}
|
|
8080
|
+
const blockMessages = rawMessages.filter((m) => msgIdSet.has(extractMessageId(m)));
|
|
8081
|
+
const lines2 = blockMessages.map(extractMessageText);
|
|
7882
8082
|
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
7883
|
-
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") :
|
|
7884
|
-
await writeFile3(
|
|
7885
|
-
|
|
8083
|
+
const fileContent = lines2.length > 0 ? lines2.join("\n\n---\n\n") : activeBlocks[0]?.summary ?? "(no content available)";
|
|
8084
|
+
await writeFile3(targetPath, fileContent, "utf-8");
|
|
8085
|
+
const displayIds2 = targets.map((t) => `b${t.displayId}`).join(", ");
|
|
8086
|
+
return `Block(s) ${displayIds2} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${targetPath}. Block(s) stay compressed \u2014 context unchanged. Use read tool to access specific parts.`;
|
|
7886
8087
|
}
|
|
7887
8088
|
const activeMessagesBefore = snapshotActiveMessages(messagesState);
|
|
7888
8089
|
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
7889
|
-
|
|
8090
|
+
for (const target of targets) {
|
|
8091
|
+
deactivateCompressionTarget(messagesState, target);
|
|
8092
|
+
}
|
|
7890
8093
|
syncCompressionBlocks(ctx.state, ctx.logger, rawMessages);
|
|
7891
8094
|
const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
|
|
7892
8095
|
messagesState,
|
|
@@ -7909,9 +8112,11 @@ ${content}`;
|
|
|
7909
8112
|
activeMessagesBefore,
|
|
7910
8113
|
messagesState
|
|
7911
8114
|
);
|
|
8115
|
+
const displayIds = targets.map((t) => `b${t.displayId}`).join(", ");
|
|
7912
8116
|
const lines = [];
|
|
8117
|
+
const headerNoun = targets.length === 1 ? "block" : "blocks";
|
|
7913
8118
|
lines.push(
|
|
7914
|
-
`Decompressed
|
|
8119
|
+
`Decompressed ${headerNoun} ${displayIds}. Restored ${restoredMessageCount} message(s) (~${formatTokenCount(restoredTokens)}).`
|
|
7915
8120
|
);
|
|
7916
8121
|
if (contextUsageBefore !== void 0 && contextUsageAfter !== void 0) {
|
|
7917
8122
|
lines.push(`Context usage: ${contextUsageBefore}% \u2192 ${contextUsageAfter}%.`);
|
|
@@ -7926,8 +8131,8 @@ ${content}`;
|
|
|
7926
8131
|
lines.push(restoredContentPreview);
|
|
7927
8132
|
}
|
|
7928
8133
|
ctx.logger.info("Decompress tool completed", {
|
|
7929
|
-
|
|
7930
|
-
|
|
8134
|
+
mode: typeof args.startId === "string" ? "range" : "block",
|
|
8135
|
+
targetBlockIds: targets.map((t) => t.displayId),
|
|
7931
8136
|
restoredMessageCount,
|
|
7932
8137
|
restoredTokens,
|
|
7933
8138
|
reactivatedBlockIds
|
|
@@ -8594,7 +8799,7 @@ TOOLS
|
|
|
8594
8799
|
|
|
8595
8800
|
You have five context-management tools:
|
|
8596
8801
|
|
|
8597
|
-
- \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step).
|
|
8802
|
+
- \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
|
|
8598
8803
|
- \`decompress\` \u2014 Restore a previously compressed block's full original content, optionally to a file for large blocks. Use when a summary lacks the exact detail you need. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\`.
|
|
8599
8804
|
- \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
|
|
8600
8805
|
- \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
|
|
@@ -8693,6 +8898,16 @@ Rules:
|
|
|
8693
8898
|
BATCHING
|
|
8694
8899
|
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
8695
8900
|
|
|
8901
|
+
When the ranges cover unrelated topics, give each entry its own \`topic\` for better summary quality \u2014 do not force unrelated content under a single shared topic. Omit the top-level \`topic\` when every entry has its own. Use the top-level \`topic\` only as a fallback when entries don't specify one.
|
|
8902
|
+
|
|
8903
|
+
\`\`\`
|
|
8904
|
+
compress({ content: [
|
|
8905
|
+
{ topic: "Auth System Exploration", startId: "m00010", endId: "m00050", summary: "..." },
|
|
8906
|
+
{ topic: "Bug Hunt", startId: "m00060", endId: "m00080", summary: "..." },
|
|
8907
|
+
{ topic: "Deployment", startId: "m00090", endId: "m00110", summary: "..." },
|
|
8908
|
+
]})
|
|
8909
|
+
\`\`\`
|
|
8910
|
+
|
|
8696
8911
|
KEEP AND REF MARKERS
|
|
8697
8912
|
When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
|
|
8698
8913
|
|
|
@@ -10387,46 +10602,9 @@ function createSystemPromptHandler(state, logger, config, prompts) {
|
|
|
10387
10602
|
};
|
|
10388
10603
|
}
|
|
10389
10604
|
function runMajorGC(state, config, logger, messages) {
|
|
10390
|
-
const maxBlockAge = config.gc.maxBlockAge ?? 15;
|
|
10391
|
-
let agedOutCount = 0;
|
|
10392
|
-
let agedOutTokens = 0;
|
|
10393
|
-
const now = Date.now();
|
|
10394
|
-
for (const [blockId, block] of state.prune.messages.blocksById) {
|
|
10395
|
-
if (!block.active) continue;
|
|
10396
|
-
const age = block.survivedCount ?? 0;
|
|
10397
|
-
if (age > maxBlockAge) {
|
|
10398
|
-
block.active = false;
|
|
10399
|
-
block.deactivatedAt = now;
|
|
10400
|
-
block.deactivatedByBlockId = void 0;
|
|
10401
|
-
state.prune.messages.activeBlockIds.delete(Number(blockId));
|
|
10402
|
-
const anchorMapped = state.prune.messages.activeByAnchorMessageId.get(block.anchorMessageId);
|
|
10403
|
-
if (anchorMapped === Number(blockId)) {
|
|
10404
|
-
state.prune.messages.activeByAnchorMessageId.delete(block.anchorMessageId);
|
|
10405
|
-
}
|
|
10406
|
-
agedOutCount++;
|
|
10407
|
-
agedOutTokens += block.summaryTokens ?? Math.round(block.summary.length / 4);
|
|
10408
|
-
}
|
|
10409
|
-
}
|
|
10410
|
-
if (agedOutCount > 0) {
|
|
10411
|
-
logger.info("Major GC: deactivated aged-out blocks", {
|
|
10412
|
-
agedOutCount,
|
|
10413
|
-
agedOutTokens,
|
|
10414
|
-
maxBlockAge
|
|
10415
|
-
});
|
|
10416
|
-
saveSessionState(state, logger).catch(() => {
|
|
10417
|
-
});
|
|
10418
|
-
}
|
|
10419
10605
|
if (!state.modelContextLimit) return;
|
|
10420
10606
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
10421
|
-
|
|
10422
|
-
let hasOversizedBlocks = false;
|
|
10423
|
-
for (const [, block] of state.prune.messages.blocksById) {
|
|
10424
|
-
if (block.active && block.summary.length > oversizedThreshold) {
|
|
10425
|
-
hasOversizedBlocks = true;
|
|
10426
|
-
break;
|
|
10427
|
-
}
|
|
10428
|
-
}
|
|
10429
|
-
if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc) && !hasOversizedBlocks) return;
|
|
10607
|
+
if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc)) return;
|
|
10430
10608
|
const oldBlocks = [];
|
|
10431
10609
|
for (const [blockId, block] of state.prune.messages.blocksById) {
|
|
10432
10610
|
if (!block.active) continue;
|