opencode-acp 1.8.2 → 1.9.0
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 +31 -0
- package/README.zh-CN.md +31 -0
- package/dist/index.js +480 -138
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/search.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/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +32 -1
- 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 -0
- package/dist/lib/messages/utils.d.ts.map +1 -1
- package/dist/lib/prompts/compress-message.d.ts +1 -1
- package/dist/lib/prompts/compress-message.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/state/persistence.d.ts +1 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +1 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1497,7 +1497,7 @@ var defaultConfig = {
|
|
|
1497
1497
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
1498
1498
|
protectTags: false,
|
|
1499
1499
|
protectUserMessages: false,
|
|
1500
|
-
maxSummaryLengthHard:
|
|
1500
|
+
maxSummaryLengthHard: 1e4,
|
|
1501
1501
|
minCompressRange: 2e3
|
|
1502
1502
|
},
|
|
1503
1503
|
strategies: {
|
|
@@ -2245,20 +2245,39 @@ function resolveBoundaryIds(context, state, startId, endId) {
|
|
|
2245
2245
|
}
|
|
2246
2246
|
let startReference = lookup.get(parsedStartId.ref);
|
|
2247
2247
|
let endReference = lookup.get(parsedEndId.ref);
|
|
2248
|
+
if (!startReference && parsedStartId.kind === "message") {
|
|
2249
|
+
const clamped = clampMessageRef(parsedStartId, context, state);
|
|
2250
|
+
if (clamped) {
|
|
2251
|
+
startReference = lookup.get(clamped.ref);
|
|
2252
|
+
if (startReference) {
|
|
2253
|
+
parsedStartId.ref = clamped.ref;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (!endReference && parsedEndId.kind === "message") {
|
|
2258
|
+
const clamped = clampMessageRef(parsedEndId, context, state);
|
|
2259
|
+
if (clamped) {
|
|
2260
|
+
endReference = lookup.get(clamped.ref);
|
|
2261
|
+
if (endReference) {
|
|
2262
|
+
parsedEndId.ref = clamped.ref;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2248
2266
|
if (!startReference) {
|
|
2249
2267
|
issues.push(
|
|
2250
|
-
`startId ${parsedStartId.ref} is not available
|
|
2268
|
+
`startId ${parsedStartId.ref} is not available \u2014 likely consumed by an existing block.`
|
|
2251
2269
|
);
|
|
2252
2270
|
}
|
|
2253
2271
|
if (!endReference) {
|
|
2254
2272
|
issues.push(
|
|
2255
|
-
`endId ${parsedEndId.ref} is not available
|
|
2273
|
+
`endId ${parsedEndId.ref} is not available \u2014 likely consumed by an existing block.`
|
|
2256
2274
|
);
|
|
2257
2275
|
}
|
|
2258
2276
|
if (issues.length > 0) {
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2277
|
+
const hint = buildBoundaryRecoveryHint(context, state);
|
|
2278
|
+
const body = issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n");
|
|
2279
|
+
throw new Error(hint ? `${body}
|
|
2280
|
+
${hint}` : body);
|
|
2262
2281
|
}
|
|
2263
2282
|
if (!startReference || !endReference) {
|
|
2264
2283
|
throw new Error("Failed to resolve boundary IDs");
|
|
@@ -2268,6 +2287,40 @@ function resolveBoundaryIds(context, state, startId, endId) {
|
|
|
2268
2287
|
}
|
|
2269
2288
|
return { startReference, endReference };
|
|
2270
2289
|
}
|
|
2290
|
+
function buildBoundaryRecoveryHint(context, state) {
|
|
2291
|
+
const visibleRefs = [];
|
|
2292
|
+
for (const [messageRef, messageId] of state.messageIds.byRef) {
|
|
2293
|
+
if (context.rawMessagesById.has(messageId)) {
|
|
2294
|
+
visibleRefs.push(messageRef);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
const parts = [];
|
|
2298
|
+
if (visibleRefs.length > 0) {
|
|
2299
|
+
visibleRefs.sort();
|
|
2300
|
+
const first = visibleRefs[0];
|
|
2301
|
+
const last = visibleRefs[visibleRefs.length - 1];
|
|
2302
|
+
parts.push(`Current visible: ${first}\u2013${last} (${visibleRefs.length} msgs).`);
|
|
2303
|
+
}
|
|
2304
|
+
const blockCount = context.summaryByBlockId.size;
|
|
2305
|
+
if (blockCount > 0) {
|
|
2306
|
+
parts.push(`${blockCount} active compressed block${blockCount === 1 ? "" : "s"}.`);
|
|
2307
|
+
}
|
|
2308
|
+
if (parts.length === 0) {
|
|
2309
|
+
return "";
|
|
2310
|
+
}
|
|
2311
|
+
return `${parts.join(" ")} Call acp_status() to see which blocks consumed which IDs, then retry with valid IDs.`;
|
|
2312
|
+
}
|
|
2313
|
+
function clampMessageRef(requested, context, state) {
|
|
2314
|
+
if (state.messageIds.byRef.has(requested.ref)) return null;
|
|
2315
|
+
let maxIndex = -1;
|
|
2316
|
+
for (const [messageRef, messageId] of state.messageIds.byRef) {
|
|
2317
|
+
if (!context.rawMessagesById.has(messageId)) continue;
|
|
2318
|
+
const idx = parseMessageRef(messageRef);
|
|
2319
|
+
if (idx !== null && idx > maxIndex) maxIndex = idx;
|
|
2320
|
+
}
|
|
2321
|
+
if (maxIndex < 0 || requested.index <= maxIndex) return null;
|
|
2322
|
+
return { ref: formatMessageRef(maxIndex) };
|
|
2323
|
+
}
|
|
2271
2324
|
function resolveSelection(context, startReference, endReference) {
|
|
2272
2325
|
const startRawIndex = startReference.rawIndex;
|
|
2273
2326
|
const endRawIndex = endReference.rawIndex;
|
|
@@ -3188,6 +3241,7 @@ function resetOnCompaction(state) {
|
|
|
3188
3241
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3189
3242
|
lastPerMessageNudgeTurn: 0,
|
|
3190
3243
|
lastPerMessageNudgeTokens: void 0,
|
|
3244
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3191
3245
|
shouldInjectThisTurn: void 0
|
|
3192
3246
|
};
|
|
3193
3247
|
state.messageIds = {
|
|
@@ -3198,38 +3252,45 @@ function resetOnCompaction(state) {
|
|
|
3198
3252
|
}
|
|
3199
3253
|
|
|
3200
3254
|
// lib/state/persistence.ts
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3255
|
+
function getLegacyStorageDir() {
|
|
3256
|
+
return join2(
|
|
3257
|
+
process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
|
|
3258
|
+
"opencode",
|
|
3259
|
+
"storage",
|
|
3260
|
+
"plugin",
|
|
3261
|
+
"dcp"
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
function getStorageDir() {
|
|
3265
|
+
return join2(
|
|
3266
|
+
process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
|
|
3267
|
+
"opencode",
|
|
3268
|
+
"storage",
|
|
3269
|
+
"plugin",
|
|
3270
|
+
"acp"
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3215
3273
|
function migrateFromLegacyIfNeeded(logger) {
|
|
3216
|
-
|
|
3217
|
-
|
|
3274
|
+
const storageDir = getStorageDir();
|
|
3275
|
+
const legacyDir = getLegacyStorageDir();
|
|
3276
|
+
if (existsSyncSync(storageDir)) return;
|
|
3277
|
+
if (!existsSyncSync(legacyDir)) return;
|
|
3218
3278
|
try {
|
|
3219
|
-
cpSync(
|
|
3220
|
-
logger.info(`[ACP] Migrated storage from ${
|
|
3279
|
+
cpSync(legacyDir, storageDir, { recursive: true });
|
|
3280
|
+
logger.info(`[ACP] Migrated storage from ${legacyDir} \u2192 ${storageDir}`);
|
|
3221
3281
|
} catch (e) {
|
|
3222
3282
|
logger.warn(`[ACP] Storage migration failed: ${e.message}`);
|
|
3223
3283
|
}
|
|
3224
3284
|
}
|
|
3225
3285
|
async function ensureStorageDir(logger) {
|
|
3226
|
-
|
|
3286
|
+
const storageDir = getStorageDir();
|
|
3287
|
+
if (!existsSync2(storageDir)) {
|
|
3227
3288
|
migrateFromLegacyIfNeeded(logger);
|
|
3228
|
-
await fs.mkdir(
|
|
3289
|
+
await fs.mkdir(storageDir, { recursive: true });
|
|
3229
3290
|
}
|
|
3230
3291
|
}
|
|
3231
3292
|
function getSessionFilePath(sessionId) {
|
|
3232
|
-
return join2(
|
|
3293
|
+
return join2(getStorageDir(), `${sessionId}.json`);
|
|
3233
3294
|
}
|
|
3234
3295
|
async function writePersistedSessionState(sessionId, state, logger) {
|
|
3235
3296
|
await ensureStorageDir(logger);
|
|
@@ -3256,7 +3317,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3256
3317
|
turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
|
|
3257
3318
|
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
|
|
3258
3319
|
lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
|
|
3259
|
-
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens
|
|
3320
|
+
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
|
|
3321
|
+
lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens
|
|
3260
3322
|
},
|
|
3261
3323
|
stats: sessionState.stats,
|
|
3262
3324
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3355,14 +3417,15 @@ async function loadAllSessionStats(logger) {
|
|
|
3355
3417
|
sessionCount: 0
|
|
3356
3418
|
};
|
|
3357
3419
|
try {
|
|
3358
|
-
|
|
3420
|
+
const storageDir = getStorageDir();
|
|
3421
|
+
if (!existsSync2(storageDir)) {
|
|
3359
3422
|
return result;
|
|
3360
3423
|
}
|
|
3361
|
-
const files = await fs.readdir(
|
|
3424
|
+
const files = await fs.readdir(storageDir);
|
|
3362
3425
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
3363
3426
|
for (const file of jsonFiles) {
|
|
3364
3427
|
try {
|
|
3365
|
-
const filePath = join2(
|
|
3428
|
+
const filePath = join2(storageDir, file);
|
|
3366
3429
|
const content = await fs.readFile(filePath, "utf-8");
|
|
3367
3430
|
const state = JSON.parse(content);
|
|
3368
3431
|
if (state?.stats?.totalPruneTokens && state?.prune) {
|
|
@@ -3473,6 +3536,7 @@ function createSessionState() {
|
|
|
3473
3536
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3474
3537
|
lastPerMessageNudgeTurn: 0,
|
|
3475
3538
|
lastPerMessageNudgeTokens: void 0,
|
|
3539
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3476
3540
|
shouldInjectThisTurn: void 0
|
|
3477
3541
|
},
|
|
3478
3542
|
stats: {
|
|
@@ -3513,6 +3577,7 @@ function resetSessionState(state) {
|
|
|
3513
3577
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3514
3578
|
lastPerMessageNudgeTurn: 0,
|
|
3515
3579
|
lastPerMessageNudgeTokens: void 0,
|
|
3580
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3516
3581
|
shouldInjectThisTurn: void 0
|
|
3517
3582
|
};
|
|
3518
3583
|
state.stats = {
|
|
@@ -3560,6 +3625,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
3560
3625
|
);
|
|
3561
3626
|
state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
3562
3627
|
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
|
|
3628
|
+
state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
|
|
3563
3629
|
state.stats = {
|
|
3564
3630
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
3565
3631
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
@@ -4589,7 +4655,7 @@ ${output}`);
|
|
|
4589
4655
|
}
|
|
4590
4656
|
|
|
4591
4657
|
// lib/compress/message.ts
|
|
4592
|
-
function buildSchema() {
|
|
4658
|
+
function buildSchema(maxSummaryLengthHard) {
|
|
4593
4659
|
return {
|
|
4594
4660
|
topic: tool2.schema.string().describe(
|
|
4595
4661
|
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
|
|
@@ -4603,7 +4669,7 @@ function buildSchema() {
|
|
|
4603
4669
|
)
|
|
4604
4670
|
})
|
|
4605
4671
|
).describe("Batch of individual message summaries to create in one tool call"),
|
|
4606
|
-
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
4672
|
+
summaryMaxChars: tool2.schema.number().optional().describe(`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.`)
|
|
4607
4673
|
};
|
|
4608
4674
|
}
|
|
4609
4675
|
function createCompressMessageTool(ctx) {
|
|
@@ -4611,7 +4677,7 @@ function createCompressMessageTool(ctx) {
|
|
|
4611
4677
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4612
4678
|
return tool2({
|
|
4613
4679
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
4614
|
-
args: buildSchema(),
|
|
4680
|
+
args: buildSchema(ctx.config.compress.maxSummaryLengthHard),
|
|
4615
4681
|
async execute(args, toolCtx) {
|
|
4616
4682
|
const input = args;
|
|
4617
4683
|
validateArgs(input);
|
|
@@ -4867,7 +4933,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4867
4933
|
}
|
|
4868
4934
|
|
|
4869
4935
|
// lib/compress/range.ts
|
|
4870
|
-
function buildSchema2() {
|
|
4936
|
+
function buildSchema2(maxSummaryLengthHard) {
|
|
4871
4937
|
return {
|
|
4872
4938
|
topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
4873
4939
|
content: tool3.schema.array(
|
|
@@ -4883,7 +4949,7 @@ function buildSchema2() {
|
|
|
4883
4949
|
).describe(
|
|
4884
4950
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
4885
4951
|
),
|
|
4886
|
-
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
4952
|
+
summaryMaxChars: tool3.schema.number().optional().describe(`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.`)
|
|
4887
4953
|
};
|
|
4888
4954
|
}
|
|
4889
4955
|
function createCompressRangeTool(ctx) {
|
|
@@ -4891,7 +4957,7 @@ function createCompressRangeTool(ctx) {
|
|
|
4891
4957
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4892
4958
|
return tool3({
|
|
4893
4959
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
4894
|
-
args: buildSchema2(),
|
|
4960
|
+
args: buildSchema2(ctx.config.compress.maxSummaryLengthHard),
|
|
4895
4961
|
async execute(args, toolCtx) {
|
|
4896
4962
|
const input = args;
|
|
4897
4963
|
validateArgs2(input);
|
|
@@ -5062,10 +5128,11 @@ import { tool as tool4 } from "@opencode-ai/plugin";
|
|
|
5062
5128
|
// lib/messages/utils.ts
|
|
5063
5129
|
import { createHash } from "crypto";
|
|
5064
5130
|
var SUMMARY_ID_HASH_LENGTH = 16;
|
|
5065
|
-
var MERGED_SUMMARY_HEADER = (blockId) =>
|
|
5131
|
+
var MERGED_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
|
|
5132
|
+
[ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
|
|
5066
5133
|
`;
|
|
5067
5134
|
var MERGED_SUMMARY_FOOTER = `
|
|
5068
|
-
|
|
5135
|
+
</acp-compression-summary>
|
|
5069
5136
|
|
|
5070
5137
|
`;
|
|
5071
5138
|
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
|
|
@@ -5076,33 +5143,54 @@ var generateStableId = (prefix, seed) => {
|
|
|
5076
5143
|
const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
|
|
5077
5144
|
return `${prefix}_${hash}`;
|
|
5078
5145
|
};
|
|
5079
|
-
var
|
|
5080
|
-
const
|
|
5146
|
+
var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
|
|
5147
|
+
const baseInfo = baseMessage.info;
|
|
5081
5148
|
const now = Date.now();
|
|
5082
|
-
const deterministicSeed = stableSeed?.trim() ||
|
|
5149
|
+
const deterministicSeed = stableSeed?.trim() || baseInfo.id;
|
|
5083
5150
|
const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
|
|
5084
5151
|
const partId = generateStableId("prt_dcp_summary", deterministicSeed);
|
|
5085
|
-
|
|
5086
|
-
|
|
5152
|
+
const parts = [
|
|
5153
|
+
{
|
|
5154
|
+
id: partId,
|
|
5155
|
+
sessionID: baseInfo.sessionID,
|
|
5156
|
+
messageID: messageId,
|
|
5157
|
+
type: "text",
|
|
5158
|
+
text: content,
|
|
5159
|
+
synthetic: true
|
|
5160
|
+
}
|
|
5161
|
+
];
|
|
5162
|
+
if (role === "assistant") {
|
|
5163
|
+
const isAssistant = baseInfo.role === "assistant";
|
|
5164
|
+
const assistantBase = isAssistant ? baseInfo : void 0;
|
|
5165
|
+
const userModel = !isAssistant ? baseInfo.model : void 0;
|
|
5166
|
+
const info2 = {
|
|
5087
5167
|
id: messageId,
|
|
5088
|
-
sessionID:
|
|
5089
|
-
role: "
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5168
|
+
sessionID: baseInfo.sessionID,
|
|
5169
|
+
role: "assistant",
|
|
5170
|
+
time: { created: now },
|
|
5171
|
+
parentID: assistantBase?.parentID ?? "",
|
|
5172
|
+
modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
|
|
5173
|
+
providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
|
|
5174
|
+
mode: assistantBase?.mode ?? "code",
|
|
5175
|
+
agent: baseInfo.agent ?? "code",
|
|
5176
|
+
path: assistantBase?.path ?? { cwd: "", root: "" },
|
|
5177
|
+
cost: 0,
|
|
5178
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
|
5179
|
+
};
|
|
5180
|
+
return { info: info2, parts };
|
|
5181
|
+
}
|
|
5182
|
+
const userInfo = baseInfo;
|
|
5183
|
+
const info = {
|
|
5184
|
+
id: messageId,
|
|
5185
|
+
sessionID: userInfo.sessionID,
|
|
5186
|
+
role: "user",
|
|
5187
|
+
agent: userInfo.agent,
|
|
5188
|
+
model: userInfo.model,
|
|
5189
|
+
time: { created: now }
|
|
5104
5190
|
};
|
|
5191
|
+
return { info, parts };
|
|
5105
5192
|
};
|
|
5193
|
+
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
|
|
5106
5194
|
var prependCompressionSummary = (message, summary, blockId) => {
|
|
5107
5195
|
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
5108
5196
|
const header = MERGED_SUMMARY_HEADER(blockId);
|
|
@@ -5241,6 +5329,11 @@ var stripHallucinations = (messages) => {
|
|
|
5241
5329
|
};
|
|
5242
5330
|
|
|
5243
5331
|
// lib/messages/prune.ts
|
|
5332
|
+
var STANDALONE_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
|
|
5333
|
+
[ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
|
|
5334
|
+
`;
|
|
5335
|
+
var STANDALONE_SUMMARY_FOOTER = `
|
|
5336
|
+
</acp-compression-summary>`;
|
|
5244
5337
|
var prune = (state, logger, config, messages) => {
|
|
5245
5338
|
filterCompressedRanges(state, logger, config, messages);
|
|
5246
5339
|
stripStepMarkers(messages);
|
|
@@ -5303,41 +5396,18 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
5303
5396
|
summaryLength: summaryContent.length
|
|
5304
5397
|
});
|
|
5305
5398
|
} else {
|
|
5306
|
-
const
|
|
5399
|
+
const taggedContent = STANDALONE_SUMMARY_HEADER(summary.blockId) + summaryContent + STANDALONE_SUMMARY_FOOTER;
|
|
5307
5400
|
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
)
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
const fallbackBase = {
|
|
5319
|
-
info: {
|
|
5320
|
-
id: anchorInfo.id || msgId,
|
|
5321
|
-
sessionID: anchorInfo.sessionID || "",
|
|
5322
|
-
role: "user",
|
|
5323
|
-
agent: anchorInfo.agent || "code",
|
|
5324
|
-
model: anchorInfo.model || {
|
|
5325
|
-
providerID: "",
|
|
5326
|
-
modelID: "",
|
|
5327
|
-
variant: void 0
|
|
5328
|
-
},
|
|
5329
|
-
time: { created: anchorInfo.time?.created || Date.now() }
|
|
5330
|
-
},
|
|
5331
|
-
parts: []
|
|
5332
|
-
};
|
|
5333
|
-
result.push(
|
|
5334
|
-
createSyntheticUserMessage(fallbackBase, summaryContent, summarySeed)
|
|
5335
|
-
);
|
|
5336
|
-
logger.info("Injected compress summary (fallback, no preceding user message)", {
|
|
5337
|
-
anchorMessageId: msgId,
|
|
5338
|
-
summaryLength: summaryContent.length
|
|
5339
|
-
});
|
|
5340
|
-
}
|
|
5401
|
+
const userMessage = getLastUserMessage(messages, i);
|
|
5402
|
+
const baseForSummary = userMessage ?? msg;
|
|
5403
|
+
result.push(
|
|
5404
|
+
createSyntheticMessage(baseForSummary, taggedContent, summarySeed, "assistant")
|
|
5405
|
+
);
|
|
5406
|
+
logger.info("Injected compress summary as assistant role", {
|
|
5407
|
+
anchorMessageId: msgId,
|
|
5408
|
+
summaryLength: taggedContent.length,
|
|
5409
|
+
hadUserBase: userMessage !== null
|
|
5410
|
+
});
|
|
5341
5411
|
}
|
|
5342
5412
|
}
|
|
5343
5413
|
}
|
|
@@ -5797,12 +5867,15 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
|
5797
5867
|
};
|
|
5798
5868
|
}
|
|
5799
5869
|
function computeShouldNudge(params) {
|
|
5800
|
-
const { currentTokens,
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5870
|
+
const { currentTokens, overMinLimit, overMaxLimit } = params;
|
|
5871
|
+
if (currentTokens === void 0) {
|
|
5872
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
5873
|
+
}
|
|
5874
|
+
if (params.lastNudgeTokens === void 0) {
|
|
5875
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
5876
|
+
}
|
|
5877
|
+
const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
|
|
5878
|
+
const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
|
|
5806
5879
|
if (!shouldNudge) {
|
|
5807
5880
|
return { shouldNudge: false, tipsVariant: null };
|
|
5808
5881
|
}
|
|
@@ -5932,7 +6005,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
|
5932
6005
|
return `
|
|
5933
6006
|
|
|
5934
6007
|
Context: ${formatK(currentTokens)} tokens.
|
|
5935
|
-
All compression serves the primary task, but be frugal. Context capacity is precious
|
|
6008
|
+
All compression serves the primary task, but be frugal. Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
|
|
5936
6009
|
}
|
|
5937
6010
|
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
5938
6011
|
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
@@ -6014,6 +6087,85 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
|
|
|
6014
6087
|
""
|
|
6015
6088
|
);
|
|
6016
6089
|
}
|
|
6090
|
+
function estimateCodeTokens(text) {
|
|
6091
|
+
let codeChars = 0;
|
|
6092
|
+
let inCode = false;
|
|
6093
|
+
for (const line of text.split("\n")) {
|
|
6094
|
+
if (line.trim().startsWith("```")) {
|
|
6095
|
+
inCode = !inCode;
|
|
6096
|
+
codeChars += line.length + 1;
|
|
6097
|
+
continue;
|
|
6098
|
+
}
|
|
6099
|
+
if (inCode) codeChars += line.length + 1;
|
|
6100
|
+
}
|
|
6101
|
+
return Math.round(codeChars / 4);
|
|
6102
|
+
}
|
|
6103
|
+
function estimateContextComposition(messages, state) {
|
|
6104
|
+
let toolTokens = 0;
|
|
6105
|
+
let codeTokens = 0;
|
|
6106
|
+
let summaryTokens = 0;
|
|
6107
|
+
let messageTokens = 0;
|
|
6108
|
+
const perMessage = [];
|
|
6109
|
+
const perTool = [];
|
|
6110
|
+
const perCode = [];
|
|
6111
|
+
const perText = [];
|
|
6112
|
+
for (const msg of messages) {
|
|
6113
|
+
const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
|
|
6114
|
+
const msgId = msg.info?.id || "";
|
|
6115
|
+
const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
|
|
6116
|
+
let msgTotal = 0;
|
|
6117
|
+
let msgTool = 0;
|
|
6118
|
+
let msgCode = 0;
|
|
6119
|
+
let msgText = 0;
|
|
6120
|
+
for (const part of msg.parts || []) {
|
|
6121
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6122
|
+
const partText = part.text;
|
|
6123
|
+
const tokens = Math.round(partText.length / 4);
|
|
6124
|
+
msgTotal += tokens;
|
|
6125
|
+
if (isSummary) {
|
|
6126
|
+
summaryTokens += tokens;
|
|
6127
|
+
} else {
|
|
6128
|
+
messageTokens += tokens;
|
|
6129
|
+
msgText += tokens;
|
|
6130
|
+
const cTokens = estimateCodeTokens(partText);
|
|
6131
|
+
if (cTokens > 0) {
|
|
6132
|
+
codeTokens += cTokens;
|
|
6133
|
+
msgCode += cTokens;
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6137
|
+
const raw = JSON.stringify(part);
|
|
6138
|
+
const tokens = Math.round(raw.length / 4);
|
|
6139
|
+
msgTotal += tokens;
|
|
6140
|
+
toolTokens += tokens;
|
|
6141
|
+
msgTool += tokens;
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
if (!isSummary) {
|
|
6145
|
+
const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
|
|
6146
|
+
if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
|
|
6147
|
+
if (msgTool > 500) perTool.push({ ref, tokens: msgTool });
|
|
6148
|
+
if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
|
|
6149
|
+
if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
perMessage.sort((a, b) => b.tokens - a.tokens);
|
|
6153
|
+
perTool.sort((a, b) => b.tokens - a.tokens);
|
|
6154
|
+
perCode.sort((a, b) => b.tokens - a.tokens);
|
|
6155
|
+
perText.sort((a, b) => b.tokens - a.tokens);
|
|
6156
|
+
return {
|
|
6157
|
+
toolTokens,
|
|
6158
|
+
codeTokens,
|
|
6159
|
+
summaryTokens,
|
|
6160
|
+
messageTokens,
|
|
6161
|
+
textTokens: Math.max(0, messageTokens - codeTokens),
|
|
6162
|
+
total: toolTokens + summaryTokens + messageTokens,
|
|
6163
|
+
largestRanges: perMessage.slice(0, 10),
|
|
6164
|
+
largestToolRanges: perTool.slice(0, 5),
|
|
6165
|
+
largestCodeRanges: perCode.slice(0, 5),
|
|
6166
|
+
largestMessageRanges: perText.slice(0, 5)
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6017
6169
|
|
|
6018
6170
|
// lib/messages/inject/inject.ts
|
|
6019
6171
|
var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
|
|
@@ -6046,7 +6198,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6046
6198
|
state.nudges.turnNudgeAnchors.clear();
|
|
6047
6199
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6048
6200
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6049
|
-
|
|
6201
|
+
saveSessionState(state, logger).catch(() => {
|
|
6202
|
+
});
|
|
6050
6203
|
return;
|
|
6051
6204
|
}
|
|
6052
6205
|
let anchorsChanged = false;
|
|
@@ -6109,6 +6262,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6109
6262
|
}
|
|
6110
6263
|
const suffixMessage = createSuffixMessage(messages);
|
|
6111
6264
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
6265
|
+
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
|
|
6266
|
+
if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
|
|
6267
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6268
|
+
}
|
|
6112
6269
|
const decision = computeShouldNudge({
|
|
6113
6270
|
currentTokens,
|
|
6114
6271
|
modelContextLimit,
|
|
@@ -6116,18 +6273,65 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6116
6273
|
overMaxLimit,
|
|
6117
6274
|
lastNudgeTokens: state.nudges.lastPerMessageNudgeTokens,
|
|
6118
6275
|
minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
|
|
6119
|
-
nudgeGrowthTokens
|
|
6276
|
+
nudgeGrowthTokens
|
|
6120
6277
|
});
|
|
6121
6278
|
state.nudges.shouldInjectThisTurn = decision.shouldNudge;
|
|
6279
|
+
if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
|
|
6280
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6281
|
+
}
|
|
6282
|
+
const composition = estimateContextComposition(messages, state);
|
|
6283
|
+
const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? 5e3;
|
|
6284
|
+
let toolOutputReminder = null;
|
|
6285
|
+
if (composition.toolTokens > 0) {
|
|
6286
|
+
if (state.nudges.lastToolOutputNudgeTokens === void 0) {
|
|
6287
|
+
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6288
|
+
} else {
|
|
6289
|
+
const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
|
|
6290
|
+
if (toolGrowth >= toolOutputThreshold) {
|
|
6291
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6292
|
+
const topRanges = composition.largestRanges.slice(0, 5).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
|
|
6293
|
+
toolOutputReminder = `
|
|
6294
|
+
|
|
6295
|
+
\u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
|
|
6296
|
+
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6297
|
+
anchorsChanged = true;
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6122
6301
|
let tipsText = null;
|
|
6123
6302
|
if (decision.shouldNudge) {
|
|
6124
6303
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
6304
|
+
if (suffixMessage && composition.total > 0) {
|
|
6305
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6306
|
+
const pct = (n) => Math.round(n / composition.total * 100);
|
|
6307
|
+
const growth = currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 ? currentTokens - state.nudges.lastPerMessageNudgeTokens : 0;
|
|
6308
|
+
const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
|
|
6309
|
+
const plainTextTokens = composition.textTokens;
|
|
6310
|
+
let breakdown = `
|
|
6311
|
+
Breakdown: ${fmt(composition.toolTokens)} tool (${pct(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct(plainTextTokens)}%)${growthStr}`;
|
|
6312
|
+
const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
|
|
6313
|
+
if (topBlocks.length > 0) {
|
|
6314
|
+
breakdown += `
|
|
6315
|
+
Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt(b.compressedTokens)}\u2192${fmt(b.summaryTokens)}`).join(", ")}`;
|
|
6316
|
+
}
|
|
6317
|
+
breakdown += `
|
|
6318
|
+
\u{1F4A1} Compress incrementally \u2014 compress the largest consumed ranges first.`;
|
|
6319
|
+
if (composition.largestToolRanges.length > 0) {
|
|
6320
|
+
breakdown += `
|
|
6321
|
+
Largest tool outputs: ${composition.largestToolRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6322
|
+
}
|
|
6323
|
+
if (composition.largestCodeRanges.length > 0) {
|
|
6324
|
+
breakdown += `
|
|
6325
|
+
Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6326
|
+
}
|
|
6327
|
+
if (composition.largestMessageRanges.length > 0) {
|
|
6328
|
+
breakdown += `
|
|
6329
|
+
Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6330
|
+
}
|
|
6331
|
+
appendToLastTextPart(suffixMessage, breakdown);
|
|
6332
|
+
}
|
|
6125
6333
|
if (decision.tipsVariant === "maxLimit") {
|
|
6126
6334
|
tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
|
|
6127
|
-
} else if (decision.tipsVariant === "minLimit") {
|
|
6128
|
-
tipsText = "\n\n\u26A0\uFE0F Context is growing \u2014 consider compressing older work. Tools: compress, decompress, search_context.";
|
|
6129
|
-
} else {
|
|
6130
|
-
tipsText = "\n\n\u{1F4A1} Tools: compress, decompress, search_context.";
|
|
6131
6335
|
}
|
|
6132
6336
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6133
6337
|
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
@@ -6150,11 +6354,30 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6150
6354
|
}
|
|
6151
6355
|
injectVisibleIdRange(state, messages, suffixMessage);
|
|
6152
6356
|
}
|
|
6357
|
+
if (toolOutputReminder && suffixMessage) {
|
|
6358
|
+
if (!decision.shouldNudge) {
|
|
6359
|
+
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
6360
|
+
if (composition.total > 0) {
|
|
6361
|
+
const fmt2 = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6362
|
+
const pct2 = (n) => Math.round(n / composition.total * 100);
|
|
6363
|
+
const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
|
|
6364
|
+
let mini = `
|
|
6365
|
+
Breakdown: ${fmt2(composition.toolTokens)} tool outputs (${pct2(composition.toolTokens)}%) | ${fmt2(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt2(composition.messageTokens)} messages (${pct2(composition.messageTokens)}%)`;
|
|
6366
|
+
if (topBlocks.length > 0) {
|
|
6367
|
+
mini += `
|
|
6368
|
+
Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt2(b.compressedTokens)}\u2192${fmt2(b.summaryTokens)}`).join(", ")}`;
|
|
6369
|
+
}
|
|
6370
|
+
appendToLastTextPart(suffixMessage, mini);
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
appendToLastTextPart(suffixMessage, toolOutputReminder);
|
|
6374
|
+
}
|
|
6153
6375
|
if (suffixMessage) {
|
|
6154
6376
|
appendToLastTextPart(suffixMessage, "\n");
|
|
6155
6377
|
}
|
|
6156
6378
|
if (anchorsChanged) {
|
|
6157
|
-
|
|
6379
|
+
saveSessionState(state, logger).catch(() => {
|
|
6380
|
+
});
|
|
6158
6381
|
}
|
|
6159
6382
|
};
|
|
6160
6383
|
function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
@@ -6728,45 +6951,107 @@ ${content}`;
|
|
|
6728
6951
|
|
|
6729
6952
|
// lib/compress/status.ts
|
|
6730
6953
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
6731
|
-
var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns
|
|
6954
|
+
var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns block IDs, sizes, ages, topics, and the message-ID ranges each block consumed \u2014 use this to see what has been compressed away and to choose safe compress boundaries.
|
|
6732
6955
|
|
|
6733
6956
|
Use this tool when:
|
|
6734
|
-
- You
|
|
6957
|
+
- You are unsure which mNNNNN refs are still compressible
|
|
6958
|
+
- Before choosing compress boundaries, if any prior compressions exist
|
|
6735
6959
|
- You want to see block sizes before deciding to decompress
|
|
6736
|
-
-
|
|
6960
|
+
- A compress call failed with "not available" (the ID was likely consumed)
|
|
6961
|
+
|
|
6962
|
+
Args:
|
|
6963
|
+
- mode: "summary" (default) \u2014 one line per block with size/range/topic. "detailed" \u2014 adds age, generation, effective message count, consumed block lineage.
|
|
6964
|
+
- sort: "recent" (default) | "size" (largest compressed first) | "age" (oldest surviving first, nearing GC).
|
|
6965
|
+
- limit: max blocks to show (default 30).`;
|
|
6737
6966
|
function formatTokens(n) {
|
|
6967
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
6738
6968
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6739
6969
|
}
|
|
6970
|
+
function formatSizePair(compressed, summary) {
|
|
6971
|
+
return `${formatTokens(compressed)}\u2192${formatTokens(summary)}`;
|
|
6972
|
+
}
|
|
6973
|
+
function formatIdRange(block) {
|
|
6974
|
+
const start = (block.startId || "").trim();
|
|
6975
|
+
const end = (block.endId || "").trim();
|
|
6976
|
+
if (!start || !end) return "\u2014";
|
|
6977
|
+
if (start === end) return start;
|
|
6978
|
+
return `${start}\u2013${end}`;
|
|
6979
|
+
}
|
|
6980
|
+
function sortBlocks(blocks, sort) {
|
|
6981
|
+
const copy = [...blocks];
|
|
6982
|
+
if (sort === "size") {
|
|
6983
|
+
copy.sort((a, b) => (b.compressedTokens || 0) - (a.compressedTokens || 0));
|
|
6984
|
+
} else if (sort === "age") {
|
|
6985
|
+
copy.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
|
|
6986
|
+
} else {
|
|
6987
|
+
copy.sort((a, b) => b.createdAt - a.createdAt);
|
|
6988
|
+
}
|
|
6989
|
+
return copy;
|
|
6990
|
+
}
|
|
6991
|
+
function renderSummaryRow(block, idWidth) {
|
|
6992
|
+
const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
|
|
6993
|
+
const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
|
|
6994
|
+
const ageStr = formatAge(block.createdAt).padStart(10);
|
|
6995
|
+
const rangeStr = formatIdRange(block).padStart(19);
|
|
6996
|
+
const topic = block.topic || "(no topic)";
|
|
6997
|
+
return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} "${topic}"`;
|
|
6998
|
+
}
|
|
6999
|
+
function renderDetailedRow(block, idWidth) {
|
|
7000
|
+
const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
|
|
7001
|
+
const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
|
|
7002
|
+
const ageStr = formatAge(block.createdAt).padStart(10);
|
|
7003
|
+
const rangeStr = formatIdRange(block).padStart(19);
|
|
7004
|
+
const survived = block.survivedCount ?? 0;
|
|
7005
|
+
const gen = block.generation ?? "young";
|
|
7006
|
+
const effCount = block.effectiveMessageIds?.length ?? 0;
|
|
7007
|
+
const consumedLineage = block.consumedBlockIds && block.consumedBlockIds.length > 0 ? ` nested=[${block.consumedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
|
|
7008
|
+
const topic = block.topic || "(no topic)";
|
|
7009
|
+
return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} age=${survived} ${gen} eff=${effCount}${consumedLineage} "${topic}"`;
|
|
7010
|
+
}
|
|
6740
7011
|
function createAcpStatusTool(ctx) {
|
|
6741
7012
|
ctx.prompts.reload();
|
|
6742
7013
|
return tool5({
|
|
6743
7014
|
description: ACP_STATUS_TOOL_DESCRIPTION,
|
|
6744
|
-
args: {
|
|
6745
|
-
|
|
7015
|
+
args: {
|
|
7016
|
+
mode: tool5.schema.string().optional().describe('Output detail level: "summary" (default) or "detailed"'),
|
|
7017
|
+
sort: tool5.schema.string().optional().describe('Sort order: "recent" (default), "size", or "age"'),
|
|
7018
|
+
limit: tool5.schema.number().optional().describe("Maximum blocks to show (default 30)")
|
|
7019
|
+
},
|
|
7020
|
+
async execute(args) {
|
|
7021
|
+
const mode = args.mode === "detailed" ? "detailed" : "summary";
|
|
7022
|
+
const sort = args.sort === "size" || args.sort === "age" ? args.sort : "recent";
|
|
7023
|
+
const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
|
|
6746
7024
|
const messages = ctx.state.prune.messages;
|
|
6747
7025
|
const activeIds = Array.from(messages.activeBlockIds).sort((a, b) => a - b);
|
|
6748
7026
|
if (activeIds.length === 0) {
|
|
6749
7027
|
return "No compressed blocks. Context is fully visible.";
|
|
6750
7028
|
}
|
|
6751
|
-
const
|
|
6752
|
-
|
|
6753
|
-
|
|
7029
|
+
const allBlocks = activeIds.map((id) => messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
|
|
7030
|
+
if (allBlocks.length === 0) {
|
|
7031
|
+
return "No compressed blocks. Context is fully visible.";
|
|
7032
|
+
}
|
|
7033
|
+
const totalSummary = allBlocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
|
|
7034
|
+
const totalCompressed = allBlocks.reduce((s, b) => s + (b.compressedTokens || 0), 0);
|
|
7035
|
+
const sorted = sortBlocks(allBlocks, sort);
|
|
7036
|
+
const shown = sorted.slice(0, limit);
|
|
7037
|
+
const truncated = sorted.length - shown.length;
|
|
7038
|
+
const idWidth = Math.max(...shown.map((b) => String(b.blockId).length));
|
|
6754
7039
|
const lines = [
|
|
6755
|
-
`ACP Status \u2014 ${
|
|
7040
|
+
`ACP Status \u2014 ${allBlocks.length} active compressed block${allBlocks.length === 1 ? "" : "s"} (${formatTokens(totalSummary)} summary, ${formatTokens(totalCompressed)} original compressed)`,
|
|
6756
7041
|
""
|
|
6757
7042
|
];
|
|
6758
|
-
const
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
lines.push(
|
|
7043
|
+
for (const b of shown) {
|
|
7044
|
+
lines.push(
|
|
7045
|
+
mode === "detailed" ? renderDetailedRow(b, idWidth) : renderSummaryRow(b, idWidth)
|
|
7046
|
+
);
|
|
7047
|
+
}
|
|
7048
|
+
if (truncated > 0) {
|
|
7049
|
+
lines.push("");
|
|
7050
|
+
lines.push(`${shown.length} of ${sorted.length} blocks shown (${truncated} hidden). Raise limit or change sort to see more.`);
|
|
6765
7051
|
}
|
|
6766
7052
|
lines.push("");
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
);
|
|
7053
|
+
const sortHint = sort === "recent" ? 'sorted by recent. Use acp_status({sort:"size"}) for largest, {sort:"age"} for near-GC.' : `sorted by ${sort}.`;
|
|
7054
|
+
lines.push(`${sortHint} Use decompress to restore a block's full content, or search_context to search within compressed blocks.`);
|
|
6770
7055
|
return lines.join("\n");
|
|
6771
7056
|
}
|
|
6772
7057
|
});
|
|
@@ -6978,23 +7263,76 @@ import { homedir as homedir4 } from "os";
|
|
|
6978
7263
|
// lib/prompts/system.ts
|
|
6979
7264
|
var SYSTEM = `
|
|
6980
7265
|
|
|
6981
|
-
You operate in a context-constrained environment. Context management helps preserve retrieval quality, but your primary goal is completing the task at hand. Do not let context management distract from the actual work.
|
|
7266
|
+
You operate in a context-constrained environment. All compression serves the primary task, but be frugal. Context management helps preserve retrieval quality, but your primary goal is completing the task at hand. Do not let context management distract from the actual work.
|
|
7267
|
+
|
|
7268
|
+
ACP TAGS
|
|
6982
7269
|
|
|
6983
|
-
|
|
7270
|
+
\`<acp-context>\` tags wrap ACP (Agent Context Pruning) system metadata \u2014 context management information injected each turn. This is system data, not user input. You may also see \`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags \u2014 these are equivalent (DCP was the previous name for ACP). Treat them as boundary metadata only, not as tool-result content.
|
|
6984
7271
|
|
|
6985
|
-
|
|
7272
|
+
TOOLS
|
|
7273
|
+
|
|
7274
|
+
You have four context-management tools:
|
|
7275
|
+
|
|
7276
|
+
- \`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). Example: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`.
|
|
7277
|
+
- \`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" })\`.
|
|
7278
|
+
- \`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" })\`.
|
|
7279
|
+
- \`acp_status\` \u2014 List all active compressed blocks with their sizes, ages, and the message ranges they consumed. Use when you are unsure which IDs are still compressible, or before choosing compress boundaries. Example: \`acp_status({ mode: "summary", sort: "recent" })\`.
|
|
6986
7280
|
|
|
6987
7281
|
COMPRESSION PHILOSOPHY
|
|
6988
7282
|
|
|
6989
|
-
|
|
7283
|
+
Two failure modes to avoid:
|
|
6990
7284
|
- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
|
|
6991
7285
|
- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
|
|
6992
7286
|
|
|
6993
|
-
Balance is key.
|
|
7287
|
+
Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, it is a candidate. When uncertain, lean toward keeping content.
|
|
6994
7288
|
|
|
6995
7289
|
BE FRUGAL
|
|
6996
7290
|
|
|
6997
|
-
Be frugal with context
|
|
7291
|
+
Be frugal with context. Compress obvious waste proactively when you encounter it \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full before compressing; that harms retrieval quality and risks overflow. When compressing, cover the largest range you can in a single call \u2014 aim for 20+ messages. Compressing 3-5 messages at a time creates many small summaries that collectively waste more tokens than they save. But never let the urge to compress distract from the actual task.
|
|
7292
|
+
|
|
7293
|
+
WHEN TO COMPRESS
|
|
7294
|
+
|
|
7295
|
+
- A sub-agent or delegated task has returned a large result that you have already extracted the key facts from.
|
|
7296
|
+
- Verbose command output (build/test logs, \`git diff\`, \`npm install\`, directory listings) where you have already used the information you need.
|
|
7297
|
+
- Exploration that led nowhere \u2014 compress the dead-ends but preserve the lessons learned: what was tried, what failed, and why.
|
|
7298
|
+
- Repeated reads of the same file or repeated status checks once the decision is recorded.
|
|
7299
|
+
- Resolved discussion threads where a decision has been captured in the summary or in code \u2014 compress the back-and-forth but preserve the decision rationale if it will be referenced later.
|
|
7300
|
+
- Intermediate steps of a completed multi-step task, once the final result is recorded.
|
|
7301
|
+
- When a task phase ends \u2014 such as finishing a bug hunt, locating a root cause, wrapping up a codebase exploration, or completing a research sprint \u2014 proactively compress the phase's redundant churn (exploratory reads, failed attempts, verbose outputs) while preserving what endures: key findings, relevant code and file paths, decision rationale, and lessons learned (what worked, what didn't, what's worth remembering next time).
|
|
7302
|
+
- Any other content where compression serves the primary task \u2014 be frugal.
|
|
7303
|
+
|
|
7304
|
+
WHEN NOT TO COMPRESS
|
|
7305
|
+
|
|
7306
|
+
- Content the current task step is actively reading or reasoning about.
|
|
7307
|
+
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
|
|
7308
|
+
- Outputs from protected tools (e.g. \`task\`, \`skill\`, \`todowrite\`, \`write\`, \`edit\`) \u2014 these are appended to summaries automatically, not compressed away.
|
|
7309
|
+
|
|
7310
|
+
PERIODIC CONTEXT STATUS
|
|
7311
|
+
|
|
7312
|
+
Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
|
|
7313
|
+
|
|
7314
|
+
[ACP] Context: 47.3K tokens. Visible: m00001\u2013m00929, m00944\u2013m00950 (810 msgs). 3 active blocks. \`acp_status\` for details.
|
|
7315
|
+
|
|
7316
|
+
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.
|
|
7317
|
+
|
|
7318
|
+
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 block IDs, their sizes, and the message-ID ranges each covers.
|
|
7319
|
+
|
|
7320
|
+
CONTEXT BREAKDOWN
|
|
7321
|
+
|
|
7322
|
+
When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
|
|
7323
|
+
|
|
7324
|
+
Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text (22%)
|
|
7325
|
+
|
|
7326
|
+
- "tool" = tool call outputs (largest category \u2014 compress first when consumed)
|
|
7327
|
+
- "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
|
|
7328
|
+
- "code" = messages containing code blocks
|
|
7329
|
+
- "text" = plain text messages
|
|
7330
|
+
|
|
7331
|
+
Below the breakdown, the system lists the largest ranges in each category (e.g. \`Largest tool outputs: m00175 (20.7K), m00200 (8.1K)\`). These are high-value compression candidates \u2014 compress those whose content you have already consumed (extracted the facts you need). Keep any you still need to reference.
|
|
7332
|
+
|
|
7333
|
+
Compress incrementally: target one large consumed range per compress call (e.g. m00150\u2192m00200), not the entire context at once. Each compression creates a reusable summary block you can decompress later if needed.
|
|
7334
|
+
|
|
7335
|
+
<acp-compression-summary>\`<acp-compression-summary>\` tags wrap ACP model-generated recaps of previously compressed conversation ranges. These are system-generated metadata, not user messages. Treat them as reference material for the compressed history.
|
|
6998
7336
|
`;
|
|
6999
7337
|
|
|
7000
7338
|
// lib/prompts/compress-range.ts
|
|
@@ -7049,7 +7387,7 @@ When multiple independent ranges are ready and their boundaries do not overlap,
|
|
|
7049
7387
|
var COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
|
|
7050
7388
|
|
|
7051
7389
|
THE SUMMARY
|
|
7052
|
-
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after
|
|
7390
|
+
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings, tool outcomes, and user intent details that matter... EVERYTHING that preserves the value of the selected message after it is summarized. The original content can be restored via decompress if needed later.
|
|
7053
7391
|
|
|
7054
7392
|
USER INTENT FIDELITY
|
|
7055
7393
|
When a selected message contains user intent, preserve that intent with extra care. Do not change scope, constraints, priorities, acceptance criteria, or requested outcomes.
|
|
@@ -8751,7 +9089,8 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8751
9089
|
agedOutTokens,
|
|
8752
9090
|
maxBlockAge
|
|
8753
9091
|
});
|
|
8754
|
-
|
|
9092
|
+
saveSessionState(state, logger).catch(() => {
|
|
9093
|
+
});
|
|
8755
9094
|
}
|
|
8756
9095
|
if (!state.modelContextLimit) return;
|
|
8757
9096
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
@@ -8781,7 +9120,8 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8781
9120
|
currentTokens,
|
|
8782
9121
|
threshold: config.gc.majorGcThresholdPercent
|
|
8783
9122
|
});
|
|
8784
|
-
|
|
9123
|
+
saveSessionState(state, logger).catch(() => {
|
|
9124
|
+
});
|
|
8785
9125
|
}
|
|
8786
9126
|
}
|
|
8787
9127
|
function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
|
|
@@ -8809,14 +9149,16 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8809
9149
|
const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
|
|
8810
9150
|
syncCompressionBlocks(state, logger, output.messages);
|
|
8811
9151
|
if (state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
|
|
8812
|
-
|
|
9152
|
+
saveSessionState(state, logger).catch(() => {
|
|
9153
|
+
});
|
|
8813
9154
|
}
|
|
8814
9155
|
syncToolCache(state, config, logger, output.messages);
|
|
8815
9156
|
buildToolIdList(state, output.messages);
|
|
8816
9157
|
runMajorGC(state, config, logger, output.messages);
|
|
8817
9158
|
const batchResult = runBatchCleanup(state, config, logger, output.messages);
|
|
8818
9159
|
if (batchResult.mergedCount > 0) {
|
|
8819
|
-
|
|
9160
|
+
saveSessionState(state, logger).catch(() => {
|
|
9161
|
+
});
|
|
8820
9162
|
}
|
|
8821
9163
|
prune(state, logger, config, output.messages);
|
|
8822
9164
|
assignMessageRefs(state, output.messages);
|