opencode-acp 1.8.2 → 1.9.1
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 +43 -0
- package/README.zh-CN.md +43 -0
- package/dist/index.js +573 -156
- 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-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts +18 -0
- 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
|
@@ -903,6 +903,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
903
903
|
"compress.protectUserMessages",
|
|
904
904
|
"compress.maxSummaryLengthHard",
|
|
905
905
|
"compress.minCompressRange",
|
|
906
|
+
"compress.maxVisibleSegments",
|
|
906
907
|
"gc",
|
|
907
908
|
"gc.algorithm",
|
|
908
909
|
"gc.promotionThreshold",
|
|
@@ -1184,6 +1185,20 @@ function validateConfigTypes(config) {
|
|
|
1184
1185
|
actual: `${compress.minCompressRange}`
|
|
1185
1186
|
});
|
|
1186
1187
|
}
|
|
1188
|
+
if (compress.maxVisibleSegments !== void 0 && typeof compress.maxVisibleSegments !== "number") {
|
|
1189
|
+
errors.push({
|
|
1190
|
+
key: "compress.maxVisibleSegments",
|
|
1191
|
+
expected: "number",
|
|
1192
|
+
actual: typeof compress.maxVisibleSegments
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof compress.maxVisibleSegments === "number" && compress.maxVisibleSegments < 1) {
|
|
1196
|
+
errors.push({
|
|
1197
|
+
key: "compress.maxVisibleSegments",
|
|
1198
|
+
expected: "positive number (>= 1)",
|
|
1199
|
+
actual: `${compress.maxVisibleSegments}`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1187
1202
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1188
1203
|
errors.push({
|
|
1189
1204
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1497,8 +1512,9 @@ var defaultConfig = {
|
|
|
1497
1512
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
1498
1513
|
protectTags: false,
|
|
1499
1514
|
protectUserMessages: false,
|
|
1500
|
-
maxSummaryLengthHard:
|
|
1501
|
-
minCompressRange: 2e3
|
|
1515
|
+
maxSummaryLengthHard: 1e4,
|
|
1516
|
+
minCompressRange: 2e3,
|
|
1517
|
+
maxVisibleSegments: 50
|
|
1502
1518
|
},
|
|
1503
1519
|
strategies: {
|
|
1504
1520
|
deduplication: {
|
|
@@ -1653,7 +1669,8 @@ function mergeCompress(base, override) {
|
|
|
1653
1669
|
protectTags: override.protectTags ?? base.protectTags,
|
|
1654
1670
|
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
1655
1671
|
maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
|
|
1656
|
-
minCompressRange: override.minCompressRange ?? base.minCompressRange
|
|
1672
|
+
minCompressRange: override.minCompressRange ?? base.minCompressRange,
|
|
1673
|
+
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments
|
|
1657
1674
|
};
|
|
1658
1675
|
}
|
|
1659
1676
|
function mergeCommands(base, override) {
|
|
@@ -2245,20 +2262,39 @@ function resolveBoundaryIds(context, state, startId, endId) {
|
|
|
2245
2262
|
}
|
|
2246
2263
|
let startReference = lookup.get(parsedStartId.ref);
|
|
2247
2264
|
let endReference = lookup.get(parsedEndId.ref);
|
|
2265
|
+
if (!startReference && parsedStartId.kind === "message") {
|
|
2266
|
+
const clamped = clampMessageRef(parsedStartId, context, state);
|
|
2267
|
+
if (clamped) {
|
|
2268
|
+
startReference = lookup.get(clamped.ref);
|
|
2269
|
+
if (startReference) {
|
|
2270
|
+
parsedStartId.ref = clamped.ref;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (!endReference && parsedEndId.kind === "message") {
|
|
2275
|
+
const clamped = clampMessageRef(parsedEndId, context, state);
|
|
2276
|
+
if (clamped) {
|
|
2277
|
+
endReference = lookup.get(clamped.ref);
|
|
2278
|
+
if (endReference) {
|
|
2279
|
+
parsedEndId.ref = clamped.ref;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2248
2283
|
if (!startReference) {
|
|
2249
2284
|
issues.push(
|
|
2250
|
-
`startId ${parsedStartId.ref} is not available
|
|
2285
|
+
`startId ${parsedStartId.ref} is not available \u2014 likely consumed by an existing block.`
|
|
2251
2286
|
);
|
|
2252
2287
|
}
|
|
2253
2288
|
if (!endReference) {
|
|
2254
2289
|
issues.push(
|
|
2255
|
-
`endId ${parsedEndId.ref} is not available
|
|
2290
|
+
`endId ${parsedEndId.ref} is not available \u2014 likely consumed by an existing block.`
|
|
2256
2291
|
);
|
|
2257
2292
|
}
|
|
2258
2293
|
if (issues.length > 0) {
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2294
|
+
const hint = buildBoundaryRecoveryHint(context, state);
|
|
2295
|
+
const body = issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n");
|
|
2296
|
+
throw new Error(hint ? `${body}
|
|
2297
|
+
${hint}` : body);
|
|
2262
2298
|
}
|
|
2263
2299
|
if (!startReference || !endReference) {
|
|
2264
2300
|
throw new Error("Failed to resolve boundary IDs");
|
|
@@ -2268,6 +2304,40 @@ function resolveBoundaryIds(context, state, startId, endId) {
|
|
|
2268
2304
|
}
|
|
2269
2305
|
return { startReference, endReference };
|
|
2270
2306
|
}
|
|
2307
|
+
function buildBoundaryRecoveryHint(context, state) {
|
|
2308
|
+
const visibleRefs = [];
|
|
2309
|
+
for (const [messageRef, messageId] of state.messageIds.byRef) {
|
|
2310
|
+
if (context.rawMessagesById.has(messageId)) {
|
|
2311
|
+
visibleRefs.push(messageRef);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
const parts = [];
|
|
2315
|
+
if (visibleRefs.length > 0) {
|
|
2316
|
+
visibleRefs.sort();
|
|
2317
|
+
const first = visibleRefs[0];
|
|
2318
|
+
const last = visibleRefs[visibleRefs.length - 1];
|
|
2319
|
+
parts.push(`Current visible: ${first}\u2013${last} (${visibleRefs.length} msgs).`);
|
|
2320
|
+
}
|
|
2321
|
+
const blockCount = context.summaryByBlockId.size;
|
|
2322
|
+
if (blockCount > 0) {
|
|
2323
|
+
parts.push(`${blockCount} active compressed block${blockCount === 1 ? "" : "s"}.`);
|
|
2324
|
+
}
|
|
2325
|
+
if (parts.length === 0) {
|
|
2326
|
+
return "";
|
|
2327
|
+
}
|
|
2328
|
+
return `${parts.join(" ")} Call acp_status() to see which blocks consumed which IDs, then retry with valid IDs.`;
|
|
2329
|
+
}
|
|
2330
|
+
function clampMessageRef(requested, context, state) {
|
|
2331
|
+
if (state.messageIds.byRef.has(requested.ref)) return null;
|
|
2332
|
+
let maxIndex = -1;
|
|
2333
|
+
for (const [messageRef, messageId] of state.messageIds.byRef) {
|
|
2334
|
+
if (!context.rawMessagesById.has(messageId)) continue;
|
|
2335
|
+
const idx = parseMessageRef(messageRef);
|
|
2336
|
+
if (idx !== null && idx > maxIndex) maxIndex = idx;
|
|
2337
|
+
}
|
|
2338
|
+
if (maxIndex < 0 || requested.index <= maxIndex) return null;
|
|
2339
|
+
return { ref: formatMessageRef(maxIndex) };
|
|
2340
|
+
}
|
|
2271
2341
|
function resolveSelection(context, startReference, endReference) {
|
|
2272
2342
|
const startRawIndex = startReference.rawIndex;
|
|
2273
2343
|
const endRawIndex = endReference.rawIndex;
|
|
@@ -3188,6 +3258,7 @@ function resetOnCompaction(state) {
|
|
|
3188
3258
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3189
3259
|
lastPerMessageNudgeTurn: 0,
|
|
3190
3260
|
lastPerMessageNudgeTokens: void 0,
|
|
3261
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3191
3262
|
shouldInjectThisTurn: void 0
|
|
3192
3263
|
};
|
|
3193
3264
|
state.messageIds = {
|
|
@@ -3198,38 +3269,45 @@ function resetOnCompaction(state) {
|
|
|
3198
3269
|
}
|
|
3199
3270
|
|
|
3200
3271
|
// lib/state/persistence.ts
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3272
|
+
function getLegacyStorageDir() {
|
|
3273
|
+
return join2(
|
|
3274
|
+
process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
|
|
3275
|
+
"opencode",
|
|
3276
|
+
"storage",
|
|
3277
|
+
"plugin",
|
|
3278
|
+
"dcp"
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
function getStorageDir() {
|
|
3282
|
+
return join2(
|
|
3283
|
+
process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
|
|
3284
|
+
"opencode",
|
|
3285
|
+
"storage",
|
|
3286
|
+
"plugin",
|
|
3287
|
+
"acp"
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3215
3290
|
function migrateFromLegacyIfNeeded(logger) {
|
|
3216
|
-
|
|
3217
|
-
|
|
3291
|
+
const storageDir = getStorageDir();
|
|
3292
|
+
const legacyDir = getLegacyStorageDir();
|
|
3293
|
+
if (existsSyncSync(storageDir)) return;
|
|
3294
|
+
if (!existsSyncSync(legacyDir)) return;
|
|
3218
3295
|
try {
|
|
3219
|
-
cpSync(
|
|
3220
|
-
logger.info(`[ACP] Migrated storage from ${
|
|
3296
|
+
cpSync(legacyDir, storageDir, { recursive: true });
|
|
3297
|
+
logger.info(`[ACP] Migrated storage from ${legacyDir} \u2192 ${storageDir}`);
|
|
3221
3298
|
} catch (e) {
|
|
3222
3299
|
logger.warn(`[ACP] Storage migration failed: ${e.message}`);
|
|
3223
3300
|
}
|
|
3224
3301
|
}
|
|
3225
3302
|
async function ensureStorageDir(logger) {
|
|
3226
|
-
|
|
3303
|
+
const storageDir = getStorageDir();
|
|
3304
|
+
if (!existsSync2(storageDir)) {
|
|
3227
3305
|
migrateFromLegacyIfNeeded(logger);
|
|
3228
|
-
await fs.mkdir(
|
|
3306
|
+
await fs.mkdir(storageDir, { recursive: true });
|
|
3229
3307
|
}
|
|
3230
3308
|
}
|
|
3231
3309
|
function getSessionFilePath(sessionId) {
|
|
3232
|
-
return join2(
|
|
3310
|
+
return join2(getStorageDir(), `${sessionId}.json`);
|
|
3233
3311
|
}
|
|
3234
3312
|
async function writePersistedSessionState(sessionId, state, logger) {
|
|
3235
3313
|
await ensureStorageDir(logger);
|
|
@@ -3256,7 +3334,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3256
3334
|
turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
|
|
3257
3335
|
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
|
|
3258
3336
|
lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
|
|
3259
|
-
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens
|
|
3337
|
+
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
|
|
3338
|
+
lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens
|
|
3260
3339
|
},
|
|
3261
3340
|
stats: sessionState.stats,
|
|
3262
3341
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3355,14 +3434,15 @@ async function loadAllSessionStats(logger) {
|
|
|
3355
3434
|
sessionCount: 0
|
|
3356
3435
|
};
|
|
3357
3436
|
try {
|
|
3358
|
-
|
|
3437
|
+
const storageDir = getStorageDir();
|
|
3438
|
+
if (!existsSync2(storageDir)) {
|
|
3359
3439
|
return result;
|
|
3360
3440
|
}
|
|
3361
|
-
const files = await fs.readdir(
|
|
3441
|
+
const files = await fs.readdir(storageDir);
|
|
3362
3442
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
3363
3443
|
for (const file of jsonFiles) {
|
|
3364
3444
|
try {
|
|
3365
|
-
const filePath = join2(
|
|
3445
|
+
const filePath = join2(storageDir, file);
|
|
3366
3446
|
const content = await fs.readFile(filePath, "utf-8");
|
|
3367
3447
|
const state = JSON.parse(content);
|
|
3368
3448
|
if (state?.stats?.totalPruneTokens && state?.prune) {
|
|
@@ -3473,6 +3553,7 @@ function createSessionState() {
|
|
|
3473
3553
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3474
3554
|
lastPerMessageNudgeTurn: 0,
|
|
3475
3555
|
lastPerMessageNudgeTokens: void 0,
|
|
3556
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3476
3557
|
shouldInjectThisTurn: void 0
|
|
3477
3558
|
},
|
|
3478
3559
|
stats: {
|
|
@@ -3513,6 +3594,7 @@ function resetSessionState(state) {
|
|
|
3513
3594
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3514
3595
|
lastPerMessageNudgeTurn: 0,
|
|
3515
3596
|
lastPerMessageNudgeTokens: void 0,
|
|
3597
|
+
lastToolOutputNudgeTokens: void 0,
|
|
3516
3598
|
shouldInjectThisTurn: void 0
|
|
3517
3599
|
};
|
|
3518
3600
|
state.stats = {
|
|
@@ -3560,6 +3642,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
3560
3642
|
);
|
|
3561
3643
|
state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
3562
3644
|
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
|
|
3645
|
+
state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
|
|
3563
3646
|
state.stats = {
|
|
3564
3647
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
3565
3648
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
@@ -4589,7 +4672,7 @@ ${output}`);
|
|
|
4589
4672
|
}
|
|
4590
4673
|
|
|
4591
4674
|
// lib/compress/message.ts
|
|
4592
|
-
function buildSchema() {
|
|
4675
|
+
function buildSchema(maxSummaryLengthHard) {
|
|
4593
4676
|
return {
|
|
4594
4677
|
topic: tool2.schema.string().describe(
|
|
4595
4678
|
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
|
|
@@ -4603,7 +4686,7 @@ function buildSchema() {
|
|
|
4603
4686
|
)
|
|
4604
4687
|
})
|
|
4605
4688
|
).describe("Batch of individual message summaries to create in one tool call"),
|
|
4606
|
-
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
4689
|
+
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
4690
|
};
|
|
4608
4691
|
}
|
|
4609
4692
|
function createCompressMessageTool(ctx) {
|
|
@@ -4611,7 +4694,7 @@ function createCompressMessageTool(ctx) {
|
|
|
4611
4694
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4612
4695
|
return tool2({
|
|
4613
4696
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
4614
|
-
args: buildSchema(),
|
|
4697
|
+
args: buildSchema(ctx.config.compress.maxSummaryLengthHard),
|
|
4615
4698
|
async execute(args, toolCtx) {
|
|
4616
4699
|
const input = args;
|
|
4617
4700
|
validateArgs(input);
|
|
@@ -4867,7 +4950,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4867
4950
|
}
|
|
4868
4951
|
|
|
4869
4952
|
// lib/compress/range.ts
|
|
4870
|
-
function buildSchema2() {
|
|
4953
|
+
function buildSchema2(maxSummaryLengthHard) {
|
|
4871
4954
|
return {
|
|
4872
4955
|
topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
4873
4956
|
content: tool3.schema.array(
|
|
@@ -4883,7 +4966,7 @@ function buildSchema2() {
|
|
|
4883
4966
|
).describe(
|
|
4884
4967
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
4885
4968
|
),
|
|
4886
|
-
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
4969
|
+
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
4970
|
};
|
|
4888
4971
|
}
|
|
4889
4972
|
function createCompressRangeTool(ctx) {
|
|
@@ -4891,7 +4974,7 @@ function createCompressRangeTool(ctx) {
|
|
|
4891
4974
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4892
4975
|
return tool3({
|
|
4893
4976
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
4894
|
-
args: buildSchema2(),
|
|
4977
|
+
args: buildSchema2(ctx.config.compress.maxSummaryLengthHard),
|
|
4895
4978
|
async execute(args, toolCtx) {
|
|
4896
4979
|
const input = args;
|
|
4897
4980
|
validateArgs2(input);
|
|
@@ -5062,10 +5145,11 @@ import { tool as tool4 } from "@opencode-ai/plugin";
|
|
|
5062
5145
|
// lib/messages/utils.ts
|
|
5063
5146
|
import { createHash } from "crypto";
|
|
5064
5147
|
var SUMMARY_ID_HASH_LENGTH = 16;
|
|
5065
|
-
var MERGED_SUMMARY_HEADER = (blockId) =>
|
|
5148
|
+
var MERGED_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
|
|
5149
|
+
[ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
|
|
5066
5150
|
`;
|
|
5067
5151
|
var MERGED_SUMMARY_FOOTER = `
|
|
5068
|
-
|
|
5152
|
+
</acp-compression-summary>
|
|
5069
5153
|
|
|
5070
5154
|
`;
|
|
5071
5155
|
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
|
|
@@ -5076,33 +5160,54 @@ var generateStableId = (prefix, seed) => {
|
|
|
5076
5160
|
const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
|
|
5077
5161
|
return `${prefix}_${hash}`;
|
|
5078
5162
|
};
|
|
5079
|
-
var
|
|
5080
|
-
const
|
|
5163
|
+
var createSyntheticMessage = (baseMessage, content, stableSeed, role = "user") => {
|
|
5164
|
+
const baseInfo = baseMessage.info;
|
|
5081
5165
|
const now = Date.now();
|
|
5082
|
-
const deterministicSeed = stableSeed?.trim() ||
|
|
5166
|
+
const deterministicSeed = stableSeed?.trim() || baseInfo.id;
|
|
5083
5167
|
const messageId = generateStableId("msg_dcp_summary", deterministicSeed);
|
|
5084
5168
|
const partId = generateStableId("prt_dcp_summary", deterministicSeed);
|
|
5085
|
-
|
|
5086
|
-
|
|
5169
|
+
const parts = [
|
|
5170
|
+
{
|
|
5171
|
+
id: partId,
|
|
5172
|
+
sessionID: baseInfo.sessionID,
|
|
5173
|
+
messageID: messageId,
|
|
5174
|
+
type: "text",
|
|
5175
|
+
text: content,
|
|
5176
|
+
synthetic: true
|
|
5177
|
+
}
|
|
5178
|
+
];
|
|
5179
|
+
if (role === "assistant") {
|
|
5180
|
+
const isAssistant = baseInfo.role === "assistant";
|
|
5181
|
+
const assistantBase = isAssistant ? baseInfo : void 0;
|
|
5182
|
+
const userModel = !isAssistant ? baseInfo.model : void 0;
|
|
5183
|
+
const info2 = {
|
|
5087
5184
|
id: messageId,
|
|
5088
|
-
sessionID:
|
|
5089
|
-
role: "
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5185
|
+
sessionID: baseInfo.sessionID,
|
|
5186
|
+
role: "assistant",
|
|
5187
|
+
time: { created: now },
|
|
5188
|
+
parentID: assistantBase?.parentID ?? "",
|
|
5189
|
+
modelID: assistantBase?.modelID ?? userModel?.modelID ?? "",
|
|
5190
|
+
providerID: assistantBase?.providerID ?? userModel?.providerID ?? "",
|
|
5191
|
+
mode: assistantBase?.mode ?? "code",
|
|
5192
|
+
agent: baseInfo.agent ?? "code",
|
|
5193
|
+
path: assistantBase?.path ?? { cwd: "", root: "" },
|
|
5194
|
+
cost: 0,
|
|
5195
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
|
5196
|
+
};
|
|
5197
|
+
return { info: info2, parts };
|
|
5198
|
+
}
|
|
5199
|
+
const userInfo = baseInfo;
|
|
5200
|
+
const info = {
|
|
5201
|
+
id: messageId,
|
|
5202
|
+
sessionID: userInfo.sessionID,
|
|
5203
|
+
role: "user",
|
|
5204
|
+
agent: userInfo.agent,
|
|
5205
|
+
model: userInfo.model,
|
|
5206
|
+
time: { created: now }
|
|
5104
5207
|
};
|
|
5208
|
+
return { info, parts };
|
|
5105
5209
|
};
|
|
5210
|
+
var createSyntheticUserMessage = (baseMessage, content, stableSeed) => createSyntheticMessage(baseMessage, content, stableSeed, "user");
|
|
5106
5211
|
var prependCompressionSummary = (message, summary, blockId) => {
|
|
5107
5212
|
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
5108
5213
|
const header = MERGED_SUMMARY_HEADER(blockId);
|
|
@@ -5241,6 +5346,11 @@ var stripHallucinations = (messages) => {
|
|
|
5241
5346
|
};
|
|
5242
5347
|
|
|
5243
5348
|
// lib/messages/prune.ts
|
|
5349
|
+
var STANDALONE_SUMMARY_HEADER = (blockId) => `<acp-compression-summary>
|
|
5350
|
+
[ACP model-generated recap (block ${blockId}) \u2014 NOT a user message]
|
|
5351
|
+
`;
|
|
5352
|
+
var STANDALONE_SUMMARY_FOOTER = `
|
|
5353
|
+
</acp-compression-summary>`;
|
|
5244
5354
|
var prune = (state, logger, config, messages) => {
|
|
5245
5355
|
filterCompressedRanges(state, logger, config, messages);
|
|
5246
5356
|
stripStepMarkers(messages);
|
|
@@ -5303,41 +5413,18 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
5303
5413
|
summaryLength: summaryContent.length
|
|
5304
5414
|
});
|
|
5305
5415
|
} else {
|
|
5306
|
-
const
|
|
5416
|
+
const taggedContent = STANDALONE_SUMMARY_HEADER(summary.blockId) + summaryContent + STANDALONE_SUMMARY_FOOTER;
|
|
5307
5417
|
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
|
-
}
|
|
5418
|
+
const userMessage = getLastUserMessage(messages, i);
|
|
5419
|
+
const baseForSummary = userMessage ?? msg;
|
|
5420
|
+
result.push(
|
|
5421
|
+
createSyntheticMessage(baseForSummary, taggedContent, summarySeed, "assistant")
|
|
5422
|
+
);
|
|
5423
|
+
logger.info("Injected compress summary as assistant role", {
|
|
5424
|
+
anchorMessageId: msgId,
|
|
5425
|
+
summaryLength: taggedContent.length,
|
|
5426
|
+
hadUserBase: userMessage !== null
|
|
5427
|
+
});
|
|
5341
5428
|
}
|
|
5342
5429
|
}
|
|
5343
5430
|
}
|
|
@@ -5797,12 +5884,15 @@ function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
|
5797
5884
|
};
|
|
5798
5885
|
}
|
|
5799
5886
|
function computeShouldNudge(params) {
|
|
5800
|
-
const { currentTokens,
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5887
|
+
const { currentTokens, overMinLimit, overMaxLimit } = params;
|
|
5888
|
+
if (currentTokens === void 0) {
|
|
5889
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
5890
|
+
}
|
|
5891
|
+
if (params.lastNudgeTokens === void 0) {
|
|
5892
|
+
return { shouldNudge: false, tipsVariant: null };
|
|
5893
|
+
}
|
|
5894
|
+
const growthSinceLastNudge = currentTokens - params.lastNudgeTokens;
|
|
5895
|
+
const shouldNudge = growthSinceLastNudge >= params.nudgeGrowthTokens || overMaxLimit;
|
|
5806
5896
|
if (!shouldNudge) {
|
|
5807
5897
|
return { shouldNudge: false, tipsVariant: null };
|
|
5808
5898
|
}
|
|
@@ -5932,7 +6022,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
|
5932
6022
|
return `
|
|
5933
6023
|
|
|
5934
6024
|
Context: ${formatK(currentTokens)} tokens.
|
|
5935
|
-
All compression serves the primary task, but be frugal. Context capacity is precious
|
|
6025
|
+
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
6026
|
}
|
|
5937
6027
|
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
5938
6028
|
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
@@ -6014,6 +6104,85 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
|
|
|
6014
6104
|
""
|
|
6015
6105
|
);
|
|
6016
6106
|
}
|
|
6107
|
+
function estimateCodeTokens(text) {
|
|
6108
|
+
let codeChars = 0;
|
|
6109
|
+
let inCode = false;
|
|
6110
|
+
for (const line of text.split("\n")) {
|
|
6111
|
+
if (line.trim().startsWith("```")) {
|
|
6112
|
+
inCode = !inCode;
|
|
6113
|
+
codeChars += line.length + 1;
|
|
6114
|
+
continue;
|
|
6115
|
+
}
|
|
6116
|
+
if (inCode) codeChars += line.length + 1;
|
|
6117
|
+
}
|
|
6118
|
+
return Math.round(codeChars / 4);
|
|
6119
|
+
}
|
|
6120
|
+
function estimateContextComposition(messages, state) {
|
|
6121
|
+
let toolTokens = 0;
|
|
6122
|
+
let codeTokens = 0;
|
|
6123
|
+
let summaryTokens = 0;
|
|
6124
|
+
let messageTokens = 0;
|
|
6125
|
+
const perMessage = [];
|
|
6126
|
+
const perTool = [];
|
|
6127
|
+
const perCode = [];
|
|
6128
|
+
const perText = [];
|
|
6129
|
+
for (const msg of messages) {
|
|
6130
|
+
const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
|
|
6131
|
+
const msgId = msg.info?.id || "";
|
|
6132
|
+
const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
|
|
6133
|
+
let msgTotal = 0;
|
|
6134
|
+
let msgTool = 0;
|
|
6135
|
+
let msgCode = 0;
|
|
6136
|
+
let msgText = 0;
|
|
6137
|
+
for (const part of msg.parts || []) {
|
|
6138
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6139
|
+
const partText = part.text;
|
|
6140
|
+
const tokens = Math.round(partText.length / 4);
|
|
6141
|
+
msgTotal += tokens;
|
|
6142
|
+
if (isSummary) {
|
|
6143
|
+
summaryTokens += tokens;
|
|
6144
|
+
} else {
|
|
6145
|
+
messageTokens += tokens;
|
|
6146
|
+
msgText += tokens;
|
|
6147
|
+
const cTokens = estimateCodeTokens(partText);
|
|
6148
|
+
if (cTokens > 0) {
|
|
6149
|
+
codeTokens += cTokens;
|
|
6150
|
+
msgCode += cTokens;
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6154
|
+
const raw = JSON.stringify(part);
|
|
6155
|
+
const tokens = Math.round(raw.length / 4);
|
|
6156
|
+
msgTotal += tokens;
|
|
6157
|
+
toolTokens += tokens;
|
|
6158
|
+
msgTool += tokens;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if (!isSummary) {
|
|
6162
|
+
const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
|
|
6163
|
+
if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
|
|
6164
|
+
if (msgTool > 500) perTool.push({ ref, tokens: msgTool });
|
|
6165
|
+
if (msgCode > 300) perCode.push({ ref, tokens: msgCode });
|
|
6166
|
+
if (msgText > 500 && msgCode === 0) perText.push({ ref, tokens: msgText });
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
perMessage.sort((a, b) => b.tokens - a.tokens);
|
|
6170
|
+
perTool.sort((a, b) => b.tokens - a.tokens);
|
|
6171
|
+
perCode.sort((a, b) => b.tokens - a.tokens);
|
|
6172
|
+
perText.sort((a, b) => b.tokens - a.tokens);
|
|
6173
|
+
return {
|
|
6174
|
+
toolTokens,
|
|
6175
|
+
codeTokens,
|
|
6176
|
+
summaryTokens,
|
|
6177
|
+
messageTokens,
|
|
6178
|
+
textTokens: Math.max(0, messageTokens - codeTokens),
|
|
6179
|
+
total: toolTokens + summaryTokens + messageTokens,
|
|
6180
|
+
largestRanges: perMessage.slice(0, 10),
|
|
6181
|
+
largestToolRanges: perTool.slice(0, 5),
|
|
6182
|
+
largestCodeRanges: perCode.slice(0, 5),
|
|
6183
|
+
largestMessageRanges: perText.slice(0, 5)
|
|
6184
|
+
};
|
|
6185
|
+
}
|
|
6017
6186
|
|
|
6018
6187
|
// lib/messages/inject/inject.ts
|
|
6019
6188
|
var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
|
|
@@ -6046,7 +6215,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6046
6215
|
state.nudges.turnNudgeAnchors.clear();
|
|
6047
6216
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6048
6217
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6049
|
-
|
|
6218
|
+
saveSessionState(state, logger).catch(() => {
|
|
6219
|
+
});
|
|
6050
6220
|
return;
|
|
6051
6221
|
}
|
|
6052
6222
|
let anchorsChanged = false;
|
|
@@ -6109,6 +6279,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6109
6279
|
}
|
|
6110
6280
|
const suffixMessage = createSuffixMessage(messages);
|
|
6111
6281
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
6282
|
+
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
|
|
6283
|
+
if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
|
|
6284
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6285
|
+
}
|
|
6112
6286
|
const decision = computeShouldNudge({
|
|
6113
6287
|
currentTokens,
|
|
6114
6288
|
modelContextLimit,
|
|
@@ -6116,18 +6290,67 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6116
6290
|
overMaxLimit,
|
|
6117
6291
|
lastNudgeTokens: state.nudges.lastPerMessageNudgeTokens,
|
|
6118
6292
|
minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
|
|
6119
|
-
nudgeGrowthTokens
|
|
6293
|
+
nudgeGrowthTokens
|
|
6120
6294
|
});
|
|
6121
6295
|
state.nudges.shouldInjectThisTurn = decision.shouldNudge;
|
|
6296
|
+
if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
|
|
6297
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6298
|
+
}
|
|
6299
|
+
const composition = estimateContextComposition(messages, state);
|
|
6300
|
+
const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? 5e3;
|
|
6301
|
+
let toolOutputReminder = null;
|
|
6302
|
+
if (composition.toolTokens > 0) {
|
|
6303
|
+
if (state.nudges.lastToolOutputNudgeTokens === void 0) {
|
|
6304
|
+
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6305
|
+
} else {
|
|
6306
|
+
const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
|
|
6307
|
+
if (toolGrowth >= toolOutputThreshold) {
|
|
6308
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6309
|
+
const topRanges = composition.largestRanges.slice(0, 5).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
|
|
6310
|
+
toolOutputReminder = `
|
|
6311
|
+
|
|
6312
|
+
\u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
|
|
6313
|
+
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6314
|
+
anchorsChanged = true;
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6122
6318
|
let tipsText = null;
|
|
6123
6319
|
if (decision.shouldNudge) {
|
|
6124
6320
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
6321
|
+
if (suffixMessage && composition.total > 0) {
|
|
6322
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6323
|
+
const pct = (n) => Math.round(n / composition.total * 100);
|
|
6324
|
+
const growth = currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 ? currentTokens - state.nudges.lastPerMessageNudgeTokens : 0;
|
|
6325
|
+
const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
|
|
6326
|
+
const plainTextTokens = composition.textTokens;
|
|
6327
|
+
const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
|
|
6328
|
+
This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.` : "";
|
|
6329
|
+
let breakdown = `${efficiencyNote}
|
|
6330
|
+
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}`;
|
|
6331
|
+
const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
|
|
6332
|
+
if (topBlocks.length > 0) {
|
|
6333
|
+
breakdown += `
|
|
6334
|
+
Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt(b.compressedTokens)}\u2192${fmt(b.summaryTokens)}`).join(", ")}`;
|
|
6335
|
+
}
|
|
6336
|
+
if (composition.largestToolRanges.length > 0) {
|
|
6337
|
+
breakdown += `
|
|
6338
|
+
Largest tool outputs: ${composition.largestToolRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6339
|
+
}
|
|
6340
|
+
if (composition.largestCodeRanges.length > 0) {
|
|
6341
|
+
breakdown += `
|
|
6342
|
+
Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6343
|
+
}
|
|
6344
|
+
if (composition.largestMessageRanges.length > 0) {
|
|
6345
|
+
breakdown += `
|
|
6346
|
+
Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6347
|
+
}
|
|
6348
|
+
breakdown += `
|
|
6349
|
+
\u{1F4A1} Compress incrementally: target the ranges above whose content you have already extracted for this step. Size alone is not a reason to compress \u2014 if a large range is still needed in full, keep it.`;
|
|
6350
|
+
appendToLastTextPart(suffixMessage, breakdown);
|
|
6351
|
+
}
|
|
6125
6352
|
if (decision.tipsVariant === "maxLimit") {
|
|
6126
6353
|
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
6354
|
}
|
|
6132
6355
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6133
6356
|
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
@@ -6148,13 +6371,32 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6148
6371
|
if (tipsText && suffixMessage) {
|
|
6149
6372
|
appendToLastTextPart(suffixMessage, tipsText);
|
|
6150
6373
|
}
|
|
6151
|
-
injectVisibleIdRange(state, messages, suffixMessage);
|
|
6374
|
+
injectVisibleIdRange(state, config, messages, suffixMessage);
|
|
6375
|
+
}
|
|
6376
|
+
if (toolOutputReminder && suffixMessage) {
|
|
6377
|
+
if (!decision.shouldNudge) {
|
|
6378
|
+
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
6379
|
+
if (composition.total > 0) {
|
|
6380
|
+
const fmt2 = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6381
|
+
const pct2 = (n) => Math.round(n / composition.total * 100);
|
|
6382
|
+
const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
|
|
6383
|
+
let mini = `
|
|
6384
|
+
Breakdown: ${fmt2(composition.toolTokens)} tool outputs (${pct2(composition.toolTokens)}%) | ${fmt2(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt2(composition.messageTokens)} messages (${pct2(composition.messageTokens)}%)`;
|
|
6385
|
+
if (topBlocks.length > 0) {
|
|
6386
|
+
mini += `
|
|
6387
|
+
Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt2(b.compressedTokens)}\u2192${fmt2(b.summaryTokens)}`).join(", ")}`;
|
|
6388
|
+
}
|
|
6389
|
+
appendToLastTextPart(suffixMessage, mini);
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
appendToLastTextPart(suffixMessage, toolOutputReminder);
|
|
6152
6393
|
}
|
|
6153
6394
|
if (suffixMessage) {
|
|
6154
6395
|
appendToLastTextPart(suffixMessage, "\n");
|
|
6155
6396
|
}
|
|
6156
6397
|
if (anchorsChanged) {
|
|
6157
|
-
|
|
6398
|
+
saveSessionState(state, logger).catch(() => {
|
|
6399
|
+
});
|
|
6158
6400
|
}
|
|
6159
6401
|
};
|
|
6160
6402
|
function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
@@ -6170,22 +6412,78 @@ function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
|
6170
6412
|
}
|
|
6171
6413
|
target.parts.push(createSyntheticTextPart(target, usageTag));
|
|
6172
6414
|
}
|
|
6173
|
-
function
|
|
6415
|
+
function refNumber(ref) {
|
|
6416
|
+
const n = parseInt(ref.slice(1), 10);
|
|
6417
|
+
return Number.isNaN(n) ? -1 : n;
|
|
6418
|
+
}
|
|
6419
|
+
function buildVisibleSegments(state, messages) {
|
|
6420
|
+
const refInfo = /* @__PURE__ */ new Map();
|
|
6421
|
+
for (const msg of messages) {
|
|
6422
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
6423
|
+
if (!ref) continue;
|
|
6424
|
+
let tokens = 0;
|
|
6425
|
+
let hasTool = false;
|
|
6426
|
+
for (const part of msg.parts || []) {
|
|
6427
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6428
|
+
tokens += Math.round(part.text.length / 4);
|
|
6429
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6430
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6431
|
+
hasTool = true;
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
refInfo.set(ref, { tokens, hasTool });
|
|
6435
|
+
}
|
|
6436
|
+
if (refInfo.size === 0) return [];
|
|
6437
|
+
const refs = Array.from(refInfo.keys()).sort((a, b) => refNumber(a) - refNumber(b));
|
|
6438
|
+
const segments = [];
|
|
6439
|
+
let cur = null;
|
|
6440
|
+
let prevNum = -2;
|
|
6441
|
+
for (const ref of refs) {
|
|
6442
|
+
const num = refNumber(ref);
|
|
6443
|
+
const info = refInfo.get(ref);
|
|
6444
|
+
if (cur && num === prevNum + 1) {
|
|
6445
|
+
cur.endRef = ref;
|
|
6446
|
+
cur.count++;
|
|
6447
|
+
cur.tokens += info.tokens;
|
|
6448
|
+
if (info.hasTool) cur.hasTool = true;
|
|
6449
|
+
} else {
|
|
6450
|
+
if (cur) segments.push(cur);
|
|
6451
|
+
cur = { startRef: ref, endRef: ref, count: 1, tokens: info.tokens, hasTool: info.hasTool };
|
|
6452
|
+
}
|
|
6453
|
+
prevNum = num;
|
|
6454
|
+
}
|
|
6455
|
+
if (cur) segments.push(cur);
|
|
6456
|
+
return segments;
|
|
6457
|
+
}
|
|
6458
|
+
function formatSegment(seg) {
|
|
6459
|
+
return seg.startRef === seg.endRef ? seg.startRef : `${seg.startRef}\u2013${seg.endRef}`;
|
|
6460
|
+
}
|
|
6461
|
+
function formatVisibleGuidance(segments, maxSegs) {
|
|
6462
|
+
if (segments.length === 0) return "";
|
|
6463
|
+
const totalMsgs = segments.reduce((s, seg) => s + seg.count, 0);
|
|
6464
|
+
const totalSegs = segments.length;
|
|
6465
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6466
|
+
if (totalSegs <= maxSegs) {
|
|
6467
|
+
return `[Visible: ${segments.map(formatSegment).join(", ")} (${totalMsgs} msg${totalMsgs === 1 ? "" : "s"}, ${totalSegs} segment${totalSegs === 1 ? "" : "s"})]`;
|
|
6468
|
+
}
|
|
6469
|
+
const keepSet = new Set(
|
|
6470
|
+
[...segments].sort((a, b) => {
|
|
6471
|
+
if (a.hasTool !== b.hasTool) return a.hasTool ? -1 : 1;
|
|
6472
|
+
return b.tokens - a.tokens;
|
|
6473
|
+
}).slice(0, maxSegs)
|
|
6474
|
+
);
|
|
6475
|
+
const shown = segments.filter((s) => keepSet.has(s));
|
|
6476
|
+
const omitted = segments.filter((s) => !keepSet.has(s));
|
|
6477
|
+
const omittedTokens = omitted.reduce((sum, s) => sum + s.tokens, 0);
|
|
6478
|
+
const omittedMsgs = omitted.reduce((sum, s) => sum + s.count, 0);
|
|
6479
|
+
return `[Visible (top ${shown.length} of ${totalSegs} segments, ${totalMsgs} msgs): ${shown.map(formatSegment).join(", ")} | +${omitted.length} smaller segment${omitted.length === 1 ? "" : "s"} (~${fmt(omittedTokens)} tokens, ${omittedMsgs} msg${omittedMsgs === 1 ? "" : "s"}) omitted]`;
|
|
6480
|
+
}
|
|
6481
|
+
function injectVisibleIdRange(state, config, messages, target) {
|
|
6174
6482
|
if (!target) return;
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
visibleRefs.push(ref);
|
|
6180
|
-
}
|
|
6181
|
-
}
|
|
6182
|
-
if (visibleRefs.length === 0) return;
|
|
6183
|
-
visibleRefs.sort();
|
|
6184
|
-
const first = visibleRefs[0];
|
|
6185
|
-
const last = visibleRefs[visibleRefs.length - 1];
|
|
6186
|
-
const rangeTag = `
|
|
6187
|
-
|
|
6188
|
-
[Visible messages: ${first} to ${last} (${visibleRefs.length} messages)]`;
|
|
6483
|
+
const segments = buildVisibleSegments(state, messages);
|
|
6484
|
+
if (segments.length === 0) return;
|
|
6485
|
+
const maxSegs = config.compress?.maxVisibleSegments ?? 50;
|
|
6486
|
+
const rangeTag = "\n\n" + formatVisibleGuidance(segments, maxSegs);
|
|
6189
6487
|
for (const part of target.parts) {
|
|
6190
6488
|
if (part.type === "text") {
|
|
6191
6489
|
appendToTextPart(part, rangeTag);
|
|
@@ -6728,45 +7026,107 @@ ${content}`;
|
|
|
6728
7026
|
|
|
6729
7027
|
// lib/compress/status.ts
|
|
6730
7028
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
6731
|
-
var ACP_STATUS_TOOL_DESCRIPTION = `Show detailed status of all active compressed context blocks. Returns
|
|
7029
|
+
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
7030
|
|
|
6733
7031
|
Use this tool when:
|
|
6734
|
-
- You
|
|
7032
|
+
- You are unsure which mNNNNN refs are still compressible
|
|
7033
|
+
- Before choosing compress boundaries, if any prior compressions exist
|
|
6735
7034
|
- You want to see block sizes before deciding to decompress
|
|
6736
|
-
-
|
|
7035
|
+
- A compress call failed with "not available" (the ID was likely consumed)
|
|
7036
|
+
|
|
7037
|
+
Args:
|
|
7038
|
+
- mode: "summary" (default) \u2014 one line per block with size/range/topic. "detailed" \u2014 adds age, generation, effective message count, consumed block lineage.
|
|
7039
|
+
- sort: "recent" (default) | "size" (largest compressed first) | "age" (oldest surviving first, nearing GC).
|
|
7040
|
+
- limit: max blocks to show (default 30).`;
|
|
6737
7041
|
function formatTokens(n) {
|
|
7042
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
6738
7043
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6739
7044
|
}
|
|
7045
|
+
function formatSizePair(compressed, summary) {
|
|
7046
|
+
return `${formatTokens(compressed)}\u2192${formatTokens(summary)}`;
|
|
7047
|
+
}
|
|
7048
|
+
function formatIdRange(block) {
|
|
7049
|
+
const start = (block.startId || "").trim();
|
|
7050
|
+
const end = (block.endId || "").trim();
|
|
7051
|
+
if (!start || !end) return "\u2014";
|
|
7052
|
+
if (start === end) return start;
|
|
7053
|
+
return `${start}\u2013${end}`;
|
|
7054
|
+
}
|
|
7055
|
+
function sortBlocks(blocks, sort) {
|
|
7056
|
+
const copy = [...blocks];
|
|
7057
|
+
if (sort === "size") {
|
|
7058
|
+
copy.sort((a, b) => (b.compressedTokens || 0) - (a.compressedTokens || 0));
|
|
7059
|
+
} else if (sort === "age") {
|
|
7060
|
+
copy.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
|
|
7061
|
+
} else {
|
|
7062
|
+
copy.sort((a, b) => b.createdAt - a.createdAt);
|
|
7063
|
+
}
|
|
7064
|
+
return copy;
|
|
7065
|
+
}
|
|
7066
|
+
function renderSummaryRow(block, idWidth) {
|
|
7067
|
+
const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
|
|
7068
|
+
const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
|
|
7069
|
+
const ageStr = formatAge(block.createdAt).padStart(10);
|
|
7070
|
+
const rangeStr = formatIdRange(block).padStart(19);
|
|
7071
|
+
const topic = block.topic || "(no topic)";
|
|
7072
|
+
return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} "${topic}"`;
|
|
7073
|
+
}
|
|
7074
|
+
function renderDetailedRow(block, idWidth) {
|
|
7075
|
+
const idStr = `b${block.blockId}`.padEnd(idWidth + 1);
|
|
7076
|
+
const sizeStr = formatSizePair(block.compressedTokens, block.summaryTokens).padStart(13);
|
|
7077
|
+
const ageStr = formatAge(block.createdAt).padStart(10);
|
|
7078
|
+
const rangeStr = formatIdRange(block).padStart(19);
|
|
7079
|
+
const survived = block.survivedCount ?? 0;
|
|
7080
|
+
const gen = block.generation ?? "young";
|
|
7081
|
+
const effCount = block.effectiveMessageIds?.length ?? 0;
|
|
7082
|
+
const consumedLineage = block.consumedBlockIds && block.consumedBlockIds.length > 0 ? ` nested=[${block.consumedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
|
|
7083
|
+
const topic = block.topic || "(no topic)";
|
|
7084
|
+
return ` ${idStr} ${sizeStr} ${ageStr} ${rangeStr} age=${survived} ${gen} eff=${effCount}${consumedLineage} "${topic}"`;
|
|
7085
|
+
}
|
|
6740
7086
|
function createAcpStatusTool(ctx) {
|
|
6741
7087
|
ctx.prompts.reload();
|
|
6742
7088
|
return tool5({
|
|
6743
7089
|
description: ACP_STATUS_TOOL_DESCRIPTION,
|
|
6744
|
-
args: {
|
|
6745
|
-
|
|
7090
|
+
args: {
|
|
7091
|
+
mode: tool5.schema.string().optional().describe('Output detail level: "summary" (default) or "detailed"'),
|
|
7092
|
+
sort: tool5.schema.string().optional().describe('Sort order: "recent" (default), "size", or "age"'),
|
|
7093
|
+
limit: tool5.schema.number().optional().describe("Maximum blocks to show (default 30)")
|
|
7094
|
+
},
|
|
7095
|
+
async execute(args) {
|
|
7096
|
+
const mode = args.mode === "detailed" ? "detailed" : "summary";
|
|
7097
|
+
const sort = args.sort === "size" || args.sort === "age" ? args.sort : "recent";
|
|
7098
|
+
const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
|
|
6746
7099
|
const messages = ctx.state.prune.messages;
|
|
6747
7100
|
const activeIds = Array.from(messages.activeBlockIds).sort((a, b) => a - b);
|
|
6748
7101
|
if (activeIds.length === 0) {
|
|
6749
7102
|
return "No compressed blocks. Context is fully visible.";
|
|
6750
7103
|
}
|
|
6751
|
-
const
|
|
6752
|
-
|
|
6753
|
-
|
|
7104
|
+
const allBlocks = activeIds.map((id) => messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
|
|
7105
|
+
if (allBlocks.length === 0) {
|
|
7106
|
+
return "No compressed blocks. Context is fully visible.";
|
|
7107
|
+
}
|
|
7108
|
+
const totalSummary = allBlocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
|
|
7109
|
+
const totalCompressed = allBlocks.reduce((s, b) => s + (b.compressedTokens || 0), 0);
|
|
7110
|
+
const sorted = sortBlocks(allBlocks, sort);
|
|
7111
|
+
const shown = sorted.slice(0, limit);
|
|
7112
|
+
const truncated = sorted.length - shown.length;
|
|
7113
|
+
const idWidth = Math.max(...shown.map((b) => String(b.blockId).length));
|
|
6754
7114
|
const lines = [
|
|
6755
|
-
`ACP Status \u2014 ${
|
|
7115
|
+
`ACP Status \u2014 ${allBlocks.length} active compressed block${allBlocks.length === 1 ? "" : "s"} (${formatTokens(totalSummary)} summary, ${formatTokens(totalCompressed)} original compressed)`,
|
|
6756
7116
|
""
|
|
6757
7117
|
];
|
|
6758
|
-
const
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
lines.push(
|
|
7118
|
+
for (const b of shown) {
|
|
7119
|
+
lines.push(
|
|
7120
|
+
mode === "detailed" ? renderDetailedRow(b, idWidth) : renderSummaryRow(b, idWidth)
|
|
7121
|
+
);
|
|
7122
|
+
}
|
|
7123
|
+
if (truncated > 0) {
|
|
7124
|
+
lines.push("");
|
|
7125
|
+
lines.push(`${shown.length} of ${sorted.length} blocks shown (${truncated} hidden). Raise limit or change sort to see more.`);
|
|
6765
7126
|
}
|
|
6766
7127
|
lines.push("");
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
);
|
|
7128
|
+
const sortHint = sort === "recent" ? 'sorted by recent. Use acp_status({sort:"size"}) for largest, {sort:"age"} for near-GC.' : `sorted by ${sort}.`;
|
|
7129
|
+
lines.push(`${sortHint} Use decompress to restore a block's full content, or search_context to search within compressed blocks.`);
|
|
6770
7130
|
return lines.join("\n");
|
|
6771
7131
|
}
|
|
6772
7132
|
});
|
|
@@ -6978,23 +7338,76 @@ import { homedir as homedir4 } from "os";
|
|
|
6978
7338
|
// lib/prompts/system.ts
|
|
6979
7339
|
var SYSTEM = `
|
|
6980
7340
|
|
|
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.
|
|
7341
|
+
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.
|
|
7342
|
+
|
|
7343
|
+
ACP TAGS
|
|
7344
|
+
|
|
7345
|
+
\`<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.
|
|
6982
7346
|
|
|
6983
|
-
|
|
7347
|
+
TOOLS
|
|
6984
7348
|
|
|
6985
|
-
|
|
7349
|
+
You have four context-management tools:
|
|
7350
|
+
|
|
7351
|
+
- \`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: "..." }] })\`.
|
|
7352
|
+
- \`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" })\`.
|
|
7353
|
+
- \`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" })\`.
|
|
7354
|
+
- \`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
7355
|
|
|
6987
7356
|
COMPRESSION PHILOSOPHY
|
|
6988
7357
|
|
|
6989
|
-
|
|
7358
|
+
Two failure modes to avoid:
|
|
6990
7359
|
- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
|
|
6991
7360
|
- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
|
|
6992
7361
|
|
|
6993
|
-
Balance is key.
|
|
7362
|
+
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
7363
|
|
|
6995
7364
|
BE FRUGAL
|
|
6996
7365
|
|
|
6997
|
-
Be frugal with context
|
|
7366
|
+
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.
|
|
7367
|
+
|
|
7368
|
+
WHEN TO COMPRESS
|
|
7369
|
+
|
|
7370
|
+
- A sub-agent or delegated task has returned a large result that you have already extracted the key facts from.
|
|
7371
|
+
- Verbose command output (build/test logs, \`git diff\`, \`npm install\`, directory listings) where you have already used the information you need.
|
|
7372
|
+
- Exploration that led nowhere \u2014 compress the dead-ends but preserve the lessons learned: what was tried, what failed, and why.
|
|
7373
|
+
- Repeated reads of the same file or repeated status checks once the decision is recorded.
|
|
7374
|
+
- 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.
|
|
7375
|
+
- Intermediate steps of a completed multi-step task, once the final result is recorded.
|
|
7376
|
+
- 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).
|
|
7377
|
+
- Any other content where compression serves the primary task \u2014 be frugal.
|
|
7378
|
+
|
|
7379
|
+
WHEN NOT TO COMPRESS
|
|
7380
|
+
|
|
7381
|
+
- Content the current task step is actively reading or reasoning about.
|
|
7382
|
+
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
|
|
7383
|
+
- Outputs from protected tools (e.g. \`task\`, \`skill\`, \`todowrite\`, \`write\`, \`edit\`) \u2014 these are appended to summaries automatically, not compressed away.
|
|
7384
|
+
|
|
7385
|
+
PERIODIC CONTEXT STATUS
|
|
7386
|
+
|
|
7387
|
+
Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
|
|
7388
|
+
|
|
7389
|
+
[ACP] Context: 47.3K tokens. Visible: m00001\u2013m00929, m00944\u2013m00950 (810 msgs). 3 active blocks. \`acp_status\` for details.
|
|
7390
|
+
|
|
7391
|
+
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.
|
|
7392
|
+
|
|
7393
|
+
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.
|
|
7394
|
+
|
|
7395
|
+
CONTEXT BREAKDOWN
|
|
7396
|
+
|
|
7397
|
+
When context usage passes a threshold, the system appends a breakdown showing where your context tokens are spent:
|
|
7398
|
+
|
|
7399
|
+
Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text (22%)
|
|
7400
|
+
|
|
7401
|
+
- "tool" = tool call outputs (largest category \u2014 compress first when consumed)
|
|
7402
|
+
- "summaries" = existing compression block summaries (already compressed; do not re-compress standalone)
|
|
7403
|
+
- "code" = messages containing code blocks
|
|
7404
|
+
- "text" = plain text messages
|
|
7405
|
+
|
|
7406
|
+
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.
|
|
7407
|
+
|
|
7408
|
+
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.
|
|
7409
|
+
|
|
7410
|
+
<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
7411
|
`;
|
|
6999
7412
|
|
|
7000
7413
|
// lib/prompts/compress-range.ts
|
|
@@ -7049,7 +7462,7 @@ When multiple independent ranges are ready and their boundaries do not overlap,
|
|
|
7049
7462
|
var COMPRESS_MESSAGE = `Collapse selected individual messages in the conversation into detailed summaries.
|
|
7050
7463
|
|
|
7051
7464
|
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
|
|
7465
|
+
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
7466
|
|
|
7054
7467
|
USER INTENT FIDELITY
|
|
7055
7468
|
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 +9164,8 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8751
9164
|
agedOutTokens,
|
|
8752
9165
|
maxBlockAge
|
|
8753
9166
|
});
|
|
8754
|
-
|
|
9167
|
+
saveSessionState(state, logger).catch(() => {
|
|
9168
|
+
});
|
|
8755
9169
|
}
|
|
8756
9170
|
if (!state.modelContextLimit) return;
|
|
8757
9171
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
@@ -8781,7 +9195,8 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8781
9195
|
currentTokens,
|
|
8782
9196
|
threshold: config.gc.majorGcThresholdPercent
|
|
8783
9197
|
});
|
|
8784
|
-
|
|
9198
|
+
saveSessionState(state, logger).catch(() => {
|
|
9199
|
+
});
|
|
8785
9200
|
}
|
|
8786
9201
|
}
|
|
8787
9202
|
function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
|
|
@@ -8809,14 +9224,16 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8809
9224
|
const activeBlockCountBefore = state.prune.messages.activeBlockIds.size;
|
|
8810
9225
|
syncCompressionBlocks(state, logger, output.messages);
|
|
8811
9226
|
if (state.prune.messages.activeBlockIds.size !== activeBlockCountBefore) {
|
|
8812
|
-
|
|
9227
|
+
saveSessionState(state, logger).catch(() => {
|
|
9228
|
+
});
|
|
8813
9229
|
}
|
|
8814
9230
|
syncToolCache(state, config, logger, output.messages);
|
|
8815
9231
|
buildToolIdList(state, output.messages);
|
|
8816
9232
|
runMajorGC(state, config, logger, output.messages);
|
|
8817
9233
|
const batchResult = runBatchCleanup(state, config, logger, output.messages);
|
|
8818
9234
|
if (batchResult.mergedCount > 0) {
|
|
8819
|
-
|
|
9235
|
+
saveSessionState(state, logger).catch(() => {
|
|
9236
|
+
});
|
|
8820
9237
|
}
|
|
8821
9238
|
prune(state, logger, config, output.messages);
|
|
8822
9239
|
assignMessageRefs(state, output.messages);
|