opencode-acp 1.13.9-dev.1 → 1.14.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 +72 -11
- package/README.zh-CN.md +55 -8
- package/dist/index.js +592 -66
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/compression-targets.d.ts.map +1 -1
- package/dist/lib/commands/recompress.d.ts.map +1 -1
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/compress/decompress-logic.d.ts +3 -1
- package/dist/lib/compress/decompress-logic.d.ts.map +1 -1
- package/dist/lib/compress/decompress.d.ts.map +1 -1
- package/dist/lib/compress/hide-consumed.d.ts +17 -0
- package/dist/lib/compress/hide-consumed.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +1 -0
- package/dist/lib/compress/index.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +11 -4
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/state.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 +6 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +18 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/sync.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 +4 -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 +12 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts +12 -1
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -909,6 +909,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
909
909
|
"compress.emergencyThresholdPercent",
|
|
910
910
|
"compress.maxVisibleSegments",
|
|
911
911
|
"compress.keepEmbedMaxChars",
|
|
912
|
+
"compress.lastSegmentSoftBlock",
|
|
913
|
+
"compress.preserveRecentMessages",
|
|
914
|
+
"compress.preserveRecentTokens",
|
|
915
|
+
"compress.preserveLastUserMessage",
|
|
912
916
|
"gc",
|
|
913
917
|
"gc.algorithm",
|
|
914
918
|
"gc.promotionThreshold",
|
|
@@ -1277,6 +1281,48 @@ function validateConfigTypes(config) {
|
|
|
1277
1281
|
actual: `${compress.keepEmbedMaxChars}`
|
|
1278
1282
|
});
|
|
1279
1283
|
}
|
|
1284
|
+
if (compress.lastSegmentSoftBlock !== void 0 && typeof compress.lastSegmentSoftBlock !== "boolean") {
|
|
1285
|
+
errors.push({
|
|
1286
|
+
key: "compress.lastSegmentSoftBlock",
|
|
1287
|
+
expected: "boolean",
|
|
1288
|
+
actual: typeof compress.lastSegmentSoftBlock
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
if (compress.preserveRecentMessages !== void 0 && typeof compress.preserveRecentMessages !== "number") {
|
|
1292
|
+
errors.push({
|
|
1293
|
+
key: "compress.preserveRecentMessages",
|
|
1294
|
+
expected: "number",
|
|
1295
|
+
actual: typeof compress.preserveRecentMessages
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (typeof compress.preserveRecentMessages === "number" && compress.preserveRecentMessages < 0) {
|
|
1299
|
+
errors.push({
|
|
1300
|
+
key: "compress.preserveRecentMessages",
|
|
1301
|
+
expected: "non-negative number (>= 0)",
|
|
1302
|
+
actual: `${compress.preserveRecentMessages}`
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
if (compress.preserveRecentTokens !== void 0 && typeof compress.preserveRecentTokens !== "number") {
|
|
1306
|
+
errors.push({
|
|
1307
|
+
key: "compress.preserveRecentTokens",
|
|
1308
|
+
expected: "number",
|
|
1309
|
+
actual: typeof compress.preserveRecentTokens
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
if (typeof compress.preserveRecentTokens === "number" && compress.preserveRecentTokens < 0) {
|
|
1313
|
+
errors.push({
|
|
1314
|
+
key: "compress.preserveRecentTokens",
|
|
1315
|
+
expected: "non-negative number (>= 0)",
|
|
1316
|
+
actual: `${compress.preserveRecentTokens}`
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
if (compress.preserveLastUserMessage !== void 0 && typeof compress.preserveLastUserMessage !== "boolean") {
|
|
1320
|
+
errors.push({
|
|
1321
|
+
key: "compress.preserveLastUserMessage",
|
|
1322
|
+
expected: "boolean",
|
|
1323
|
+
actual: typeof compress.preserveLastUserMessage
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1280
1326
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1281
1327
|
errors.push({
|
|
1282
1328
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1601,7 +1647,10 @@ var defaultConfig = {
|
|
|
1601
1647
|
emergencyThresholdPercent: "98%",
|
|
1602
1648
|
maxVisibleSegments: 50,
|
|
1603
1649
|
keepEmbedMaxChars: 2e3,
|
|
1604
|
-
lastSegmentSoftBlock: true
|
|
1650
|
+
lastSegmentSoftBlock: true,
|
|
1651
|
+
preserveRecentMessages: 20,
|
|
1652
|
+
preserveRecentTokens: 2e4,
|
|
1653
|
+
preserveLastUserMessage: true
|
|
1605
1654
|
},
|
|
1606
1655
|
strategies: {
|
|
1607
1656
|
deduplication: {
|
|
@@ -1776,7 +1825,10 @@ function mergeCompress(base, override) {
|
|
|
1776
1825
|
emergencyThresholdPercent: override.emergencyThresholdPercent ?? base.emergencyThresholdPercent,
|
|
1777
1826
|
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments,
|
|
1778
1827
|
keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars,
|
|
1779
|
-
lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock
|
|
1828
|
+
lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock,
|
|
1829
|
+
preserveRecentMessages: override.preserveRecentMessages ?? base.preserveRecentMessages,
|
|
1830
|
+
preserveRecentTokens: override.preserveRecentTokens ?? base.preserveRecentTokens,
|
|
1831
|
+
preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage
|
|
1780
1832
|
};
|
|
1781
1833
|
}
|
|
1782
1834
|
function mergeCommands(base, override) {
|
|
@@ -3062,7 +3114,20 @@ ${footer}`;
|
|
|
3062
3114
|
function applyCompressionState(state, input, selection, anchorMessageId, blockId, summary, consumedBlockIds, gcConfig) {
|
|
3063
3115
|
const messagesState = state.prune.messages;
|
|
3064
3116
|
const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))];
|
|
3065
|
-
const
|
|
3117
|
+
const createdAt = Date.now();
|
|
3118
|
+
let minConsumedTier;
|
|
3119
|
+
for (const consumedBlockId of consumed) {
|
|
3120
|
+
const cb = messagesState.blocksById.get(consumedBlockId);
|
|
3121
|
+
if (cb) {
|
|
3122
|
+
const cbTier = cb.tier ?? 1;
|
|
3123
|
+
if (minConsumedTier === void 0 || cbTier < minConsumedTier) {
|
|
3124
|
+
minConsumedTier = cbTier;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
const effectiveMinTier = minConsumedTier ?? 0;
|
|
3129
|
+
const outputTier = Math.min(3, effectiveMinTier + 1);
|
|
3130
|
+
const targetTierForConsumption = effectiveMinTier;
|
|
3066
3131
|
const effectiveMessageIds = new Set(selection.messageIds);
|
|
3067
3132
|
const effectiveToolIds = new Set(selection.toolIds);
|
|
3068
3133
|
for (const consumedBlockId of consumed) {
|
|
@@ -3070,6 +3135,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3070
3135
|
if (!consumedBlock) {
|
|
3071
3136
|
continue;
|
|
3072
3137
|
}
|
|
3138
|
+
if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3073
3141
|
for (const messageId of consumedBlock.effectiveMessageIds) {
|
|
3074
3142
|
effectiveMessageIds.add(messageId);
|
|
3075
3143
|
}
|
|
@@ -3094,7 +3162,6 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3094
3162
|
initiallyActiveToolIds.add(toolId);
|
|
3095
3163
|
}
|
|
3096
3164
|
}
|
|
3097
|
-
const createdAt = Date.now();
|
|
3098
3165
|
const block = {
|
|
3099
3166
|
blockId,
|
|
3100
3167
|
runId: input.runId,
|
|
@@ -3104,6 +3171,7 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3104
3171
|
summaryTokens: input.summaryTokens,
|
|
3105
3172
|
durationMs: 0,
|
|
3106
3173
|
mode: input.mode,
|
|
3174
|
+
tier: outputTier,
|
|
3107
3175
|
topic: input.topic,
|
|
3108
3176
|
batchTopic: input.batchTopic,
|
|
3109
3177
|
startId: input.startId,
|
|
@@ -3111,8 +3179,11 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3111
3179
|
anchorMessageId,
|
|
3112
3180
|
compressMessageId: input.compressMessageId,
|
|
3113
3181
|
compressCallId: input.compressCallId,
|
|
3114
|
-
includedBlockIds:
|
|
3115
|
-
consumedBlockIds: consumed
|
|
3182
|
+
includedBlockIds: [...consumed],
|
|
3183
|
+
consumedBlockIds: consumed.filter((id) => {
|
|
3184
|
+
const cb = messagesState.blocksById.get(id);
|
|
3185
|
+
return cb && (cb.tier ?? 1) === targetTierForConsumption;
|
|
3186
|
+
}),
|
|
3116
3187
|
parentBlockIds: [],
|
|
3117
3188
|
directMessageIds: [],
|
|
3118
3189
|
directToolIds: [],
|
|
@@ -3141,6 +3212,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3141
3212
|
if (!consumedBlock || !consumedBlock.active) {
|
|
3142
3213
|
continue;
|
|
3143
3214
|
}
|
|
3215
|
+
if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3144
3218
|
consumedBlock.active = false;
|
|
3145
3219
|
consumedBlock.deactivatedAt = deactivatedAt;
|
|
3146
3220
|
consumedBlock.deactivatedByBlockId = blockId;
|
|
@@ -3166,6 +3240,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3166
3240
|
if (!consumedBlock) {
|
|
3167
3241
|
continue;
|
|
3168
3242
|
}
|
|
3243
|
+
if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3169
3246
|
for (const messageId of consumedBlock.effectiveMessageIds) {
|
|
3170
3247
|
const entry = messagesState.byMessageId.get(messageId);
|
|
3171
3248
|
if (!entry) {
|
|
@@ -3231,6 +3308,14 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
|
|
|
3231
3308
|
block.directMessageIds = [...newlyCompressedMessageIds];
|
|
3232
3309
|
block.directToolIds = [...newlyCompressedToolIds];
|
|
3233
3310
|
block.compressedTokens = compressedTokens;
|
|
3311
|
+
let effectiveTokens = compressedTokens;
|
|
3312
|
+
for (const consumedBlockId of consumed) {
|
|
3313
|
+
const cb = messagesState.blocksById.get(consumedBlockId);
|
|
3314
|
+
if (cb && (cb.tier ?? 1) === targetTierForConsumption) {
|
|
3315
|
+
effectiveTokens += cb.effectiveCompressedTokens ?? cb.compressedTokens;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
block.effectiveCompressedTokens = effectiveTokens;
|
|
3234
3319
|
state.stats.pruneTokenCounter += compressedTokens;
|
|
3235
3320
|
state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
|
|
3236
3321
|
state.stats.pruneTokenCounter = 0;
|
|
@@ -3593,6 +3678,7 @@ function loadPruneMessagesState(persisted) {
|
|
|
3593
3678
|
runId: typeof block.runId === "number" && Number.isInteger(block.runId) && block.runId > 0 ? block.runId : blockId,
|
|
3594
3679
|
active: block.active === true,
|
|
3595
3680
|
deactivatedByUser: block.deactivatedByUser === true,
|
|
3681
|
+
deactivatedByUserDeep: block.deactivatedByUserDeep === true ? true : void 0,
|
|
3596
3682
|
compressedTokens: typeof block.compressedTokens === "number" && Number.isFinite(block.compressedTokens) ? Math.max(0, block.compressedTokens) : 0,
|
|
3597
3683
|
summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens2(block.summary) : 0,
|
|
3598
3684
|
durationMs: typeof block.durationMs === "number" && Number.isFinite(block.durationMs) ? Math.max(0, block.durationMs) : 0,
|
|
@@ -3616,7 +3702,9 @@ function loadPruneMessagesState(persisted) {
|
|
|
3616
3702
|
deactivatedByBlockId: typeof block.deactivatedByBlockId === "number" && Number.isInteger(block.deactivatedByBlockId) ? block.deactivatedByBlockId : void 0,
|
|
3617
3703
|
summary: typeof block.summary === "string" ? block.summary : "",
|
|
3618
3704
|
survivedCount: typeof block.survivedCount === "number" && Number.isFinite(block.survivedCount) ? Math.max(0, Math.floor(block.survivedCount)) : 0,
|
|
3619
|
-
generation: block.generation === "young" || block.generation === "old" ? block.generation : void 0
|
|
3705
|
+
generation: block.generation === "young" || block.generation === "old" ? block.generation : void 0,
|
|
3706
|
+
tier: block.tier === 1 || block.tier === 2 || block.tier === 3 ? block.tier : void 0,
|
|
3707
|
+
effectiveCompressedTokens: typeof block.effectiveCompressedTokens === "number" && Number.isFinite(block.effectiveCompressedTokens) ? Math.max(0, block.effectiveCompressedTokens) : void 0
|
|
3620
3708
|
});
|
|
3621
3709
|
}
|
|
3622
3710
|
}
|
|
@@ -3675,17 +3763,38 @@ function collectTurnNudgeAnchors(messages) {
|
|
|
3675
3763
|
}
|
|
3676
3764
|
return anchors;
|
|
3677
3765
|
}
|
|
3678
|
-
function getActiveSummaryTokenUsage(state) {
|
|
3766
|
+
function getActiveSummaryTokenUsage(state, visibleMessageIds) {
|
|
3679
3767
|
let total = 0;
|
|
3680
3768
|
for (const blockId of state.prune.messages.activeBlockIds) {
|
|
3681
3769
|
const block = state.prune.messages.blocksById.get(blockId);
|
|
3682
3770
|
if (!block || !block.active) {
|
|
3683
3771
|
continue;
|
|
3684
3772
|
}
|
|
3773
|
+
if (visibleMessageIds && block.compressMessageId) {
|
|
3774
|
+
if (!visibleMessageIds.has(block.compressMessageId)) {
|
|
3775
|
+
continue;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3685
3778
|
total += block.summaryTokens;
|
|
3686
3779
|
}
|
|
3687
3780
|
return total;
|
|
3688
3781
|
}
|
|
3782
|
+
function getTierTokenUsage(state) {
|
|
3783
|
+
let tier1Tokens = 0;
|
|
3784
|
+
let tier2Tokens = 0;
|
|
3785
|
+
let tier3Tokens = 0;
|
|
3786
|
+
for (const blockId of state.prune.messages.activeBlockIds) {
|
|
3787
|
+
const block = state.prune.messages.blocksById.get(blockId);
|
|
3788
|
+
if (!block || !block.active) {
|
|
3789
|
+
continue;
|
|
3790
|
+
}
|
|
3791
|
+
const tier = block.tier ?? 1;
|
|
3792
|
+
if (tier === 1) tier1Tokens += block.summaryTokens;
|
|
3793
|
+
else if (tier === 2) tier2Tokens += block.summaryTokens;
|
|
3794
|
+
else tier3Tokens += block.summaryTokens;
|
|
3795
|
+
}
|
|
3796
|
+
return { tier1Tokens, tier2Tokens, tier3Tokens };
|
|
3797
|
+
}
|
|
3689
3798
|
function resetOnCompaction(state) {
|
|
3690
3799
|
state.toolParameters.clear();
|
|
3691
3800
|
state.prune.tools = /* @__PURE__ */ new Map();
|
|
@@ -3697,6 +3806,8 @@ function resetOnCompaction(state) {
|
|
|
3697
3806
|
lastPerMessageNudgeTokens: void 0,
|
|
3698
3807
|
lastNudgeShownTokens: void 0,
|
|
3699
3808
|
lastToolOutputNudgeTokens: void 0,
|
|
3809
|
+
lastTier2NudgeTokens: void 0,
|
|
3810
|
+
lastTier3NudgeTokens: void 0,
|
|
3700
3811
|
shouldInjectThisTurn: void 0,
|
|
3701
3812
|
compressBaselineSet: false
|
|
3702
3813
|
};
|
|
@@ -3773,6 +3884,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3773
3884
|
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
|
|
3774
3885
|
lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
|
|
3775
3886
|
lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
|
|
3887
|
+
lastTier2NudgeTokens: sessionState.nudges.lastTier2NudgeTokens,
|
|
3888
|
+
lastTier3NudgeTokens: sessionState.nudges.lastTier3NudgeTokens,
|
|
3776
3889
|
compressBaselineSet: sessionState.nudges.compressBaselineSet
|
|
3777
3890
|
},
|
|
3778
3891
|
stats: sessionState.stats,
|
|
@@ -4385,6 +4498,8 @@ function createSessionState() {
|
|
|
4385
4498
|
lastPerMessageNudgeTokens: void 0,
|
|
4386
4499
|
lastNudgeShownTokens: void 0,
|
|
4387
4500
|
lastToolOutputNudgeTokens: void 0,
|
|
4501
|
+
lastTier2NudgeTokens: void 0,
|
|
4502
|
+
lastTier3NudgeTokens: void 0,
|
|
4388
4503
|
shouldInjectThisTurn: void 0,
|
|
4389
4504
|
compressBaselineSet: false
|
|
4390
4505
|
},
|
|
@@ -4428,6 +4543,8 @@ function resetSessionState(state) {
|
|
|
4428
4543
|
lastPerMessageNudgeTokens: void 0,
|
|
4429
4544
|
lastNudgeShownTokens: void 0,
|
|
4430
4545
|
lastToolOutputNudgeTokens: void 0,
|
|
4546
|
+
lastTier2NudgeTokens: void 0,
|
|
4547
|
+
lastTier3NudgeTokens: void 0,
|
|
4431
4548
|
shouldInjectThisTurn: void 0,
|
|
4432
4549
|
compressBaselineSet: false
|
|
4433
4550
|
};
|
|
@@ -4484,6 +4601,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
4484
4601
|
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
|
|
4485
4602
|
state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
|
|
4486
4603
|
state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
|
|
4604
|
+
state.nudges.lastTier2NudgeTokens = persisted.nudges.lastTier2NudgeTokens ?? persisted.nudges.lastTierNudgeTokens;
|
|
4605
|
+
state.nudges.lastTier3NudgeTokens = persisted.nudges.lastTier3NudgeTokens;
|
|
4487
4606
|
state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
|
|
4488
4607
|
state.stats = {
|
|
4489
4608
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
@@ -5047,7 +5166,7 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5047
5166
|
const logTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic ?? "?");
|
|
5048
5167
|
const logCompressedTokens = entries.reduce((sum, e) => {
|
|
5049
5168
|
const block = state.prune.messages.blocksById.get(e.blockId);
|
|
5050
|
-
return sum + (block?.compressedTokens ?? 0);
|
|
5169
|
+
return sum + (block?.effectiveCompressedTokens ?? block?.compressedTokens ?? 0);
|
|
5051
5170
|
}, 0);
|
|
5052
5171
|
const logSummaryTokens = entries.reduce((sum, e) => sum + e.summaryTokens, 0);
|
|
5053
5172
|
logger.info("Compression completed", {
|
|
@@ -5991,7 +6110,7 @@ function evaluatePreCommitQuality(rawMessages, messageIds, messageTokenById, sum
|
|
|
5991
6110
|
}
|
|
5992
6111
|
}
|
|
5993
6112
|
|
|
5994
|
-
// node_modules/context-compress-algorithms/dist/chunk-
|
|
6113
|
+
// node_modules/context-compress-algorithms/dist/chunk-EBKMI537.js
|
|
5995
6114
|
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
5996
6115
|
- All compression serves the primary task, but be frugal.
|
|
5997
6116
|
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
@@ -6036,6 +6155,69 @@ PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
|
6036
6155
|
5. Lessons learned: what failed and why.
|
|
6037
6156
|
|
|
6038
6157
|
Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
|
|
6158
|
+
var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
|
|
6159
|
+
|
|
6160
|
+
You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
|
|
6161
|
+
|
|
6162
|
+
KEEP \u2014 these are the only things that survive distillation:
|
|
6163
|
+
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
|
|
6164
|
+
- Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
|
|
6165
|
+
- Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
|
|
6166
|
+
- Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
|
|
6167
|
+
- Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
|
|
6168
|
+
- Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
|
|
6169
|
+
- Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
|
|
6170
|
+
- Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
|
|
6171
|
+
|
|
6172
|
+
DROP \u2014 these were useful during the work but are no longer needed:
|
|
6173
|
+
- Exact line numbers, diffs, verbose function signatures, full code listings.
|
|
6174
|
+
- Build/deploy process details, test execution steps.
|
|
6175
|
+
- Review process details (who reviewed, what rounds, test counts).
|
|
6176
|
+
- Verbose logs, command output, intermediate debugging steps.
|
|
6177
|
+
|
|
6178
|
+
FORMAT:
|
|
6179
|
+
- Start each distilled block with a source header line:
|
|
6180
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
6181
|
+
Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
|
|
6182
|
+
- 3-5 bullet points per source block, each a self-contained fact.
|
|
6183
|
+
- Dense, scannable \u2014 no narrative prose.
|
|
6184
|
+
- Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
|
|
6185
|
+
- Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
|
|
6186
|
+
|
|
6187
|
+
SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]".`;
|
|
6188
|
+
var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
|
|
6189
|
+
|
|
6190
|
+
You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
|
|
6191
|
+
|
|
6192
|
+
PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
|
|
6193
|
+
1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
|
|
6194
|
+
2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
|
|
6195
|
+
3. Key decisions with architectural impact ("chose X over Y because Z").
|
|
6196
|
+
4. Critical constraints ("must support Node 22").
|
|
6197
|
+
Drop everything else. Tier 3 is a lookup index, not a knowledge base.
|
|
6198
|
+
|
|
6199
|
+
FORMAT:
|
|
6200
|
+
- Start with a source header line:
|
|
6201
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
6202
|
+
- Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
|
|
6203
|
+
- No explanations, no rationale, no process \u2014 just the fact.
|
|
6204
|
+
- Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
|
|
6205
|
+
- Merge related facts from different source blocks if they concern the same topic.
|
|
6206
|
+
|
|
6207
|
+
EXAMPLES:
|
|
6208
|
+
- "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
|
|
6209
|
+
- "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
|
|
6210
|
+
- "Bug 1214 fixed \u2014 compress consumed all user messages"
|
|
6211
|
+
- "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
|
|
6212
|
+
- "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
|
|
6213
|
+
|
|
6214
|
+
DROP:
|
|
6215
|
+
- Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
|
|
6216
|
+
- Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
|
|
6217
|
+
- Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
|
|
6218
|
+
- Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
|
|
6219
|
+
|
|
6220
|
+
SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
|
|
6039
6221
|
|
|
6040
6222
|
// lib/compress/quality-gate/rejection.ts
|
|
6041
6223
|
function formatMetric(result, name) {
|
|
@@ -6180,17 +6362,6 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
|
|
|
6180
6362
|
contextTokensBefore
|
|
6181
6363
|
);
|
|
6182
6364
|
}
|
|
6183
|
-
function getLastVisibleMessageId(rawMessages, state) {
|
|
6184
|
-
for (let i = rawMessages.length - 1; i >= 0; i--) {
|
|
6185
|
-
const msg = rawMessages[i];
|
|
6186
|
-
const id = msg?.info?.id;
|
|
6187
|
-
if (!id || typeof id !== "string") continue;
|
|
6188
|
-
if (isSyntheticMessage(msg)) continue;
|
|
6189
|
-
if (state.prune.messages.byMessageId.has(id)) continue;
|
|
6190
|
-
return id;
|
|
6191
|
-
}
|
|
6192
|
-
return null;
|
|
6193
|
-
}
|
|
6194
6365
|
function checkPhantomBlock(state, plans) {
|
|
6195
6366
|
for (let i = 0; i < plans.length; i++) {
|
|
6196
6367
|
const plan = plans[i];
|
|
@@ -6208,6 +6379,16 @@ function checkPhantomBlock(state, plans) {
|
|
|
6208
6379
|
return !entry || entry.activeBlockIds.length === 0;
|
|
6209
6380
|
});
|
|
6210
6381
|
if (!hasNew) {
|
|
6382
|
+
if (plan.consumedBlockIds.length >= 2) {
|
|
6383
|
+
const tiers = new Set(
|
|
6384
|
+
plan.consumedBlockIds.map(
|
|
6385
|
+
(id) => state.prune.messages.blocksById.get(id)?.tier ?? 1
|
|
6386
|
+
)
|
|
6387
|
+
);
|
|
6388
|
+
if (tiers.size === 1) {
|
|
6389
|
+
continue;
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6211
6392
|
return new Error(
|
|
6212
6393
|
`Compression range ${i + 1} contains only already-compressed messages (0 new direct messages, 0 tokens saved). Nothing to compress \u2014 pick a range with visible, uncompressed content. Use \`acp_status({scope:"uncompressed"})\` to see which ranges are still compressible.`
|
|
6213
6394
|
);
|
|
@@ -6215,16 +6396,71 @@ function checkPhantomBlock(state, plans) {
|
|
|
6215
6396
|
}
|
|
6216
6397
|
return null;
|
|
6217
6398
|
}
|
|
6218
|
-
function
|
|
6399
|
+
function computeProtectedRawIds(rawMessages, state, compress) {
|
|
6400
|
+
const preserveN = compress.preserveRecentMessages ?? 20;
|
|
6401
|
+
const preserveTokens = compress.preserveRecentTokens ?? 2e4;
|
|
6402
|
+
const preserveLastUser = compress.preserveLastUserMessage ?? true;
|
|
6403
|
+
const result = /* @__PURE__ */ new Set();
|
|
6404
|
+
const visible = [];
|
|
6405
|
+
for (const msg of rawMessages) {
|
|
6406
|
+
const id = msg?.info?.id;
|
|
6407
|
+
if (!id || typeof id !== "string") continue;
|
|
6408
|
+
if (isSyntheticMessage(msg)) continue;
|
|
6409
|
+
if (isIgnoredUserMessage(msg)) continue;
|
|
6410
|
+
if (state.prune.messages.byMessageId.has(id)) continue;
|
|
6411
|
+
let tokens = 0;
|
|
6412
|
+
for (const part of msg.parts || []) {
|
|
6413
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6414
|
+
tokens += Math.round(part.text.length / 4);
|
|
6415
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6416
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
visible.push({ id, tokens, isUser: msg.info.role === "user" });
|
|
6420
|
+
}
|
|
6421
|
+
if (preserveN > 0) {
|
|
6422
|
+
for (const m of visible.slice(-preserveN)) {
|
|
6423
|
+
result.add(m.id);
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
if (preserveTokens > 0) {
|
|
6427
|
+
let tokenAccum = 0;
|
|
6428
|
+
for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
|
|
6429
|
+
result.add(visible[i].id);
|
|
6430
|
+
tokenAccum += visible[i].tokens;
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
if (preserveLastUser) {
|
|
6434
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
6435
|
+
if (visible[i].isUser) {
|
|
6436
|
+
result.add(visible[i].id);
|
|
6437
|
+
break;
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
return result;
|
|
6442
|
+
}
|
|
6443
|
+
function checkProtectedRange(ctx, allPlanMessageIds, rawMessages, dangerous) {
|
|
6219
6444
|
if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
|
|
6220
|
-
const
|
|
6221
|
-
if (
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6445
|
+
const protectedIds = computeProtectedRawIds(rawMessages, ctx.state, ctx.config.compress);
|
|
6446
|
+
if (protectedIds.size === 0) return null;
|
|
6447
|
+
const coveredProtected = [];
|
|
6448
|
+
for (const ids of allPlanMessageIds) {
|
|
6449
|
+
for (const id of ids) {
|
|
6450
|
+
if (protectedIds.has(id)) {
|
|
6451
|
+
coveredProtected.push(id);
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
if (coveredProtected.length === 0) return null;
|
|
6224
6456
|
if (dangerous) return null;
|
|
6457
|
+
const sample = coveredProtected.slice(0, 3).join(", ");
|
|
6458
|
+
const nMsgs = ctx.config.compress.preserveRecentMessages ?? 20;
|
|
6459
|
+
const nToks = ctx.config.compress.preserveRecentTokens ?? 2e4;
|
|
6225
6460
|
return new Error(
|
|
6226
|
-
`This range includes
|
|
6461
|
+
`This range includes ${coveredProtected.length} protected recent message(s) (${sample}), which are likely still needed for the current task step.
|
|
6227
6462
|
|
|
6463
|
+
Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens + most recent user message.
|
|
6228
6464
|
If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
|
|
6229
6465
|
Otherwise, compress older ranges that do not include the tail of the conversation.`
|
|
6230
6466
|
);
|
|
@@ -6423,7 +6659,7 @@ function createCompressMessageTool(factoryCtx) {
|
|
|
6423
6659
|
}
|
|
6424
6660
|
}
|
|
6425
6661
|
const dangerous = args.dangerous === true;
|
|
6426
|
-
const lastSegmentError =
|
|
6662
|
+
const lastSegmentError = checkProtectedRange(
|
|
6427
6663
|
ctx,
|
|
6428
6664
|
plans.map((p) => p.selection.messageIds),
|
|
6429
6665
|
rawMessages,
|
|
@@ -6643,7 +6879,7 @@ function createCompressRangeTool(factoryCtx) {
|
|
|
6643
6879
|
}
|
|
6644
6880
|
}
|
|
6645
6881
|
const dangerous = args.dangerous === true;
|
|
6646
|
-
const lastSegmentError =
|
|
6882
|
+
const lastSegmentError = checkProtectedRange(
|
|
6647
6883
|
ctx,
|
|
6648
6884
|
filteredPlans.map((p) => p.selection.messageIds),
|
|
6649
6885
|
rawMessages,
|
|
@@ -6915,10 +7151,9 @@ var syncCompressionBlocks = (state, logger, messages) => {
|
|
|
6915
7151
|
messagesState.activeBlockIds.clear();
|
|
6916
7152
|
messagesState.activeByAnchorMessageId.clear();
|
|
6917
7153
|
const now = Date.now();
|
|
6918
|
-
const missingOriginBlockIds = [];
|
|
6919
7154
|
const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
|
|
6920
7155
|
for (const block of orderedBlocks) {
|
|
6921
|
-
if (block.deactivatedByUser) {
|
|
7156
|
+
if (block.deactivatedByUser || block.deactivatedByUserDeep) {
|
|
6922
7157
|
block.active = false;
|
|
6923
7158
|
if (block.deactivatedAt === void 0) {
|
|
6924
7159
|
block.deactivatedAt = now;
|
|
@@ -6926,12 +7161,6 @@ var syncCompressionBlocks = (state, logger, messages) => {
|
|
|
6926
7161
|
block.deactivatedByBlockId = void 0;
|
|
6927
7162
|
continue;
|
|
6928
7163
|
}
|
|
6929
|
-
if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
|
|
6930
|
-
block.active = false;
|
|
6931
|
-
block.deactivatedAt = now;
|
|
6932
|
-
block.deactivatedByBlockId = void 0;
|
|
6933
|
-
continue;
|
|
6934
|
-
}
|
|
6935
7164
|
for (const consumedBlockId of block.consumedBlockIds) {
|
|
6936
7165
|
if (!messagesState.activeBlockIds.has(consumedBlockId)) {
|
|
6937
7166
|
continue;
|
|
@@ -6976,9 +7205,8 @@ var syncCompressionBlocks = (state, logger, messages) => {
|
|
|
6976
7205
|
reactivatedCount++;
|
|
6977
7206
|
}
|
|
6978
7207
|
}
|
|
6979
|
-
if (
|
|
7208
|
+
if (deactivatedCount > 0 || reactivatedCount > 0) {
|
|
6980
7209
|
logger.info("Synced compress block state", {
|
|
6981
|
-
missingOriginCount: missingOriginBlockIds.length,
|
|
6982
7210
|
deactivatedCount,
|
|
6983
7211
|
reactivatedCount
|
|
6984
7212
|
});
|
|
@@ -7389,7 +7617,7 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
|
|
|
7389
7617
|
return refs;
|
|
7390
7618
|
}
|
|
7391
7619
|
|
|
7392
|
-
// node_modules/context-compress-algorithms/dist/chunk-
|
|
7620
|
+
// node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
|
|
7393
7621
|
var NUDGE_GROWTH_FLOOR = 6e3;
|
|
7394
7622
|
var NUDGE_GROWTH_CAP = 5e4;
|
|
7395
7623
|
var NUDGE_GROWTH_RATIO = 0.05;
|
|
@@ -7522,7 +7750,10 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
|
|
|
7522
7750
|
return parseLimitValue(globalLimit);
|
|
7523
7751
|
}
|
|
7524
7752
|
function isContextOverLimits(config, state, providerId, modelId, messages) {
|
|
7525
|
-
const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
|
|
7753
|
+
const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
|
|
7754
|
+
state,
|
|
7755
|
+
new Set(messages.map((m) => m.info.id))
|
|
7756
|
+
) : 0;
|
|
7526
7757
|
const resolvedMaxContextLimit = resolveContextTokenLimit(
|
|
7527
7758
|
config,
|
|
7528
7759
|
state,
|
|
@@ -8145,6 +8376,57 @@ ${lines2.join("\n")}`;
|
|
|
8145
8376
|
return `Compressible ranges (oldest first):
|
|
8146
8377
|
${lines.join("\n")}`;
|
|
8147
8378
|
}
|
|
8379
|
+
function computeProtectedRefs(messages, state, compress) {
|
|
8380
|
+
if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
|
|
8381
|
+
const preserveN = compress.preserveRecentMessages ?? 20;
|
|
8382
|
+
const preserveTokens = compress.preserveRecentTokens ?? 2e4;
|
|
8383
|
+
const preserveLastUser = compress.preserveLastUserMessage ?? true;
|
|
8384
|
+
const result = /* @__PURE__ */ new Set();
|
|
8385
|
+
const visible = [];
|
|
8386
|
+
for (const msg of messages) {
|
|
8387
|
+
if (isSyntheticMessage(msg)) continue;
|
|
8388
|
+
if (isIgnoredUserMessage(msg)) continue;
|
|
8389
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
8390
|
+
if (!ref) continue;
|
|
8391
|
+
if (state.prune.messages.byMessageId.has(msg.info.id)) continue;
|
|
8392
|
+
let tokens = 0;
|
|
8393
|
+
for (const part of msg.parts || []) {
|
|
8394
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
8395
|
+
tokens += Math.round(part.text.length / 4);
|
|
8396
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
8397
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
visible.push({ ref, tokens, isUser: msg.info.role === "user" });
|
|
8401
|
+
}
|
|
8402
|
+
if (preserveN > 0) {
|
|
8403
|
+
for (const m of visible.slice(-preserveN)) {
|
|
8404
|
+
result.add(m.ref);
|
|
8405
|
+
}
|
|
8406
|
+
}
|
|
8407
|
+
if (preserveTokens > 0) {
|
|
8408
|
+
let tokenAccum = 0;
|
|
8409
|
+
for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
|
|
8410
|
+
result.add(visible[i].ref);
|
|
8411
|
+
tokenAccum += visible[i].tokens;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
if (preserveLastUser) {
|
|
8415
|
+
for (let i = visible.length - 1; i >= 0; i--) {
|
|
8416
|
+
if (visible[i].isUser) {
|
|
8417
|
+
result.add(visible[i].ref);
|
|
8418
|
+
break;
|
|
8419
|
+
}
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8422
|
+
return result;
|
|
8423
|
+
}
|
|
8424
|
+
function excludeProtectedRanges(ranges, protectedRefs) {
|
|
8425
|
+
if (protectedRefs.size === 0) return ranges;
|
|
8426
|
+
return ranges.filter(
|
|
8427
|
+
(r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef)
|
|
8428
|
+
);
|
|
8429
|
+
}
|
|
8148
8430
|
|
|
8149
8431
|
// lib/messages/inject/inject.ts
|
|
8150
8432
|
var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
|
|
@@ -8184,6 +8466,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
8184
8466
|
state.nudges.iterationNudgeAnchors.clear();
|
|
8185
8467
|
state.nudges.lastNudgeShownTokens = void 0;
|
|
8186
8468
|
state.nudges.lastToolOutputNudgeTokens = void 0;
|
|
8469
|
+
state.nudges.lastTier2NudgeTokens = void 0;
|
|
8470
|
+
state.nudges.lastTier3NudgeTokens = void 0;
|
|
8187
8471
|
if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
|
|
8188
8472
|
const baseline = state.nudges.lastPerMessageNudgeTokens;
|
|
8189
8473
|
const postCompress = currentTokens;
|
|
@@ -8322,8 +8606,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
8322
8606
|
config.compress.protectedTools,
|
|
8323
8607
|
config.protectedFilePatterns
|
|
8324
8608
|
);
|
|
8609
|
+
const protectedRefs = computeProtectedRefs(messages, state, config.compress);
|
|
8610
|
+
const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
|
|
8325
8611
|
const recommendedRanges = filterRecommendedRanges(
|
|
8326
|
-
|
|
8612
|
+
unprotectedCompressible,
|
|
8327
8613
|
contextRanges.protected,
|
|
8328
8614
|
{ modelContextLimit, growthRatio: 0.05, logger }
|
|
8329
8615
|
);
|
|
@@ -8350,11 +8636,92 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
8350
8636
|
const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
|
|
8351
8637
|
const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
|
|
8352
8638
|
const nothingToCompress = filterSuppressed || allProtected;
|
|
8353
|
-
|
|
8639
|
+
let shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
|
|
8354
8640
|
if (nudgeAllowed && nothingToCompress && !emergencyOverride && currentTokens !== void 0) {
|
|
8355
8641
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
8356
8642
|
state.nudges.lastNudgeShownTokens = void 0;
|
|
8357
8643
|
}
|
|
8644
|
+
if (suffixMessage && !shouldInject) {
|
|
8645
|
+
const tierUsage = getTierTokenUsage(state);
|
|
8646
|
+
const tierChecks = [
|
|
8647
|
+
{ triggerTier: 2, targetTier: 1, tokens: tierUsage.tier1Tokens, lastNudge: state.nudges.lastTier2NudgeTokens },
|
|
8648
|
+
{ triggerTier: 3, targetTier: 2, tokens: tierUsage.tier2Tokens, lastNudge: state.nudges.lastTier3NudgeTokens }
|
|
8649
|
+
];
|
|
8650
|
+
for (const tc of tierChecks) {
|
|
8651
|
+
if (tc.tokens < nudgeGrowthTokens) continue;
|
|
8652
|
+
const cadenceMet = tc.lastNudge === void 0 || currentTokens !== void 0 && currentTokens - tc.lastNudge >= growthFloor;
|
|
8653
|
+
if (!cadenceMet) continue;
|
|
8654
|
+
let candidates = [...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active && (b.tier ?? 1) === tc.targetTier).sort((a, b) => a.blockId - b.blockId);
|
|
8655
|
+
if (candidates.length >= 2) {
|
|
8656
|
+
const firstId = candidates[0].blockId;
|
|
8657
|
+
const lastId = candidates[candidates.length - 1].blockId;
|
|
8658
|
+
const nonTargetInIdRange = new Set(
|
|
8659
|
+
[...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter(
|
|
8660
|
+
(b) => b !== void 0 && b.active && (b.tier ?? 1) !== tc.targetTier && b.blockId > firstId && b.blockId < lastId
|
|
8661
|
+
).map((b) => b.blockId)
|
|
8662
|
+
);
|
|
8663
|
+
if (nonTargetInIdRange.size > 0) {
|
|
8664
|
+
let bestStart = 0;
|
|
8665
|
+
let bestLen = 1;
|
|
8666
|
+
let curStart = 0;
|
|
8667
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
8668
|
+
const prevId = candidates[i - 1].blockId;
|
|
8669
|
+
const currId = candidates[i].blockId;
|
|
8670
|
+
let hasGap = false;
|
|
8671
|
+
for (const nid of nonTargetInIdRange) {
|
|
8672
|
+
if (nid > prevId && nid < currId) {
|
|
8673
|
+
hasGap = true;
|
|
8674
|
+
break;
|
|
8675
|
+
}
|
|
8676
|
+
}
|
|
8677
|
+
if (hasGap) {
|
|
8678
|
+
const curLen = i - curStart;
|
|
8679
|
+
if (curLen > bestLen) {
|
|
8680
|
+
bestLen = curLen;
|
|
8681
|
+
bestStart = curStart;
|
|
8682
|
+
}
|
|
8683
|
+
curStart = i;
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
const finalLen = candidates.length - curStart;
|
|
8687
|
+
if (finalLen > bestLen) {
|
|
8688
|
+
bestLen = finalLen;
|
|
8689
|
+
bestStart = curStart;
|
|
8690
|
+
}
|
|
8691
|
+
candidates = candidates.slice(bestStart, bestStart + bestLen);
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
if (candidates.length < 2) continue;
|
|
8695
|
+
const rules = tc.triggerTier === 2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES;
|
|
8696
|
+
const candidateTokens = candidates.reduce((s, b) => s + b.summaryTokens, 0);
|
|
8697
|
+
const firstBlock = candidates[0];
|
|
8698
|
+
const lastBlock = candidates[candidates.length - 1];
|
|
8699
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
8700
|
+
const sourceTier = tc.triggerTier === 2 ? "Tier 1" : "Tier 2";
|
|
8701
|
+
const action = tc.triggerTier === 2 ? "Distill" : "Condense";
|
|
8702
|
+
const blockList = candidates.slice(0, 10).map((b) => `b${b.blockId} (age=${b.survivedCount}, ${fmt(b.summaryTokens)}tok): "${b.topic}"`).join("\n");
|
|
8703
|
+
const extraCount = candidates.length > 10 ? `
|
|
8704
|
+
...and ${candidates.length - 10} more` : "";
|
|
8705
|
+
const tierText = `
|
|
8706
|
+
|
|
8707
|
+
[Tier ${tc.triggerTier} Trigger] ${sourceTier} summaries accumulated (${fmt(candidateTokens)} tokens across ${candidates.length} blocks). ${action} them to free context.
|
|
8708
|
+
|
|
8709
|
+
Target blocks (oldest first):
|
|
8710
|
+
${blockList}${extraCount}
|
|
8711
|
+
|
|
8712
|
+
Compress range: \`content: [{ startId: "b${firstBlock.blockId}", endId: "b${lastBlock.blockId}", summary: "..." }]\`
|
|
8713
|
+
|
|
8714
|
+
${rules}`;
|
|
8715
|
+
appendToLastTextPart(suffixMessage, tierText);
|
|
8716
|
+
shouldInject = true;
|
|
8717
|
+
if (tc.triggerTier === 2) {
|
|
8718
|
+
state.nudges.lastTier2NudgeTokens = currentTokens;
|
|
8719
|
+
} else {
|
|
8720
|
+
state.nudges.lastTier3NudgeTokens = currentTokens;
|
|
8721
|
+
}
|
|
8722
|
+
break;
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8358
8725
|
state.nudges.shouldInjectThisTurn = shouldInject;
|
|
8359
8726
|
let tipsText = null;
|
|
8360
8727
|
if (shouldInject) {
|
|
@@ -8559,7 +8926,10 @@ function buildTarget(blocks) {
|
|
|
8559
8926
|
displayId: first.blockId,
|
|
8560
8927
|
runId: first.runId,
|
|
8561
8928
|
topic: grouped ? first.batchTopic || first.topic : first.topic,
|
|
8562
|
-
compressedTokens: ordered.reduce(
|
|
8929
|
+
compressedTokens: ordered.reduce(
|
|
8930
|
+
(total, block) => total + (block.effectiveCompressedTokens ?? block.compressedTokens),
|
|
8931
|
+
0
|
|
8932
|
+
),
|
|
8563
8933
|
durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
|
|
8564
8934
|
grouped,
|
|
8565
8935
|
blocks: ordered
|
|
@@ -8727,17 +9097,25 @@ function snapshotActiveMessages(messagesState) {
|
|
|
8727
9097
|
}
|
|
8728
9098
|
return activeMessages;
|
|
8729
9099
|
}
|
|
8730
|
-
function deactivateCompressionTarget(messagesState, target) {
|
|
9100
|
+
function deactivateCompressionTarget(messagesState, target, options) {
|
|
8731
9101
|
const deactivatedAt = Date.now();
|
|
8732
9102
|
for (const block of target.blocks) {
|
|
8733
9103
|
block.active = false;
|
|
8734
9104
|
block.deactivatedByUser = true;
|
|
8735
9105
|
block.deactivatedAt = deactivatedAt;
|
|
8736
9106
|
block.deactivatedByBlockId = void 0;
|
|
8737
|
-
|
|
8738
|
-
const
|
|
8739
|
-
|
|
8740
|
-
|
|
9107
|
+
if (options?.full) {
|
|
9108
|
+
const visited = /* @__PURE__ */ new Set();
|
|
9109
|
+
const queue = [...block.consumedBlockIds];
|
|
9110
|
+
while (queue.length > 0) {
|
|
9111
|
+
const consumedId = queue.shift();
|
|
9112
|
+
if (visited.has(consumedId)) continue;
|
|
9113
|
+
visited.add(consumedId);
|
|
9114
|
+
const consumedBlock = messagesState.blocksById.get(consumedId);
|
|
9115
|
+
if (consumedBlock) {
|
|
9116
|
+
consumedBlock.deactivatedByUserDeep = true;
|
|
9117
|
+
queue.push(...consumedBlock.consumedBlockIds);
|
|
9118
|
+
}
|
|
8741
9119
|
}
|
|
8742
9120
|
}
|
|
8743
9121
|
}
|
|
@@ -8953,14 +9331,18 @@ ARGUMENTS:
|
|
|
8953
9331
|
IMPORTANT:
|
|
8954
9332
|
- Decompressing inflates context. Check context usage before decompressing.
|
|
8955
9333
|
- Message-mode blocks from the same batch (same runId) are restored together.
|
|
8956
|
-
-
|
|
9334
|
+
- TIER-AWARE: by default, decompressing a multi-tier block restores the PREVIOUS tier's
|
|
9335
|
+
summaries (e.g., decompress T2 \u2192 T1 summaries visible, not raw messages). Use full:true
|
|
9336
|
+
to restore all the way to original messages (can be very expensive for T2/T3 blocks).
|
|
9337
|
+
- After decompression, the restored content will appear in full in your next context window.
|
|
8957
9338
|
- Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
|
|
8958
9339
|
function buildSchema3() {
|
|
8959
9340
|
return {
|
|
8960
9341
|
blockId: tool4.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
|
|
8961
9342
|
startId: tool4.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
|
|
8962
9343
|
endId: tool4.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
|
|
8963
|
-
toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'")
|
|
9344
|
+
toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"),
|
|
9345
|
+
full: tool4.schema.boolean().optional().describe("If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it.")
|
|
8964
9346
|
};
|
|
8965
9347
|
}
|
|
8966
9348
|
function extractMessageId(m) {
|
|
@@ -9030,7 +9412,7 @@ function createDecompressTool(factoryCtx) {
|
|
|
9030
9412
|
const activeMessagesBefore = snapshotActiveMessages(messagesState);
|
|
9031
9413
|
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
9032
9414
|
for (const target of targets) {
|
|
9033
|
-
deactivateCompressionTarget(messagesState, target);
|
|
9415
|
+
deactivateCompressionTarget(messagesState, target, { full: args.full === true });
|
|
9034
9416
|
}
|
|
9035
9417
|
syncCompressionBlocks(ctx.state, ctx.logger, rawMessages);
|
|
9036
9418
|
const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
|
|
@@ -9109,13 +9491,54 @@ function formatIdRange(block) {
|
|
|
9109
9491
|
const count = block.effectiveMessageIds?.length || 0;
|
|
9110
9492
|
return count > 0 ? `${count} msg${count !== 1 ? "s" : ""}` : "\u2014";
|
|
9111
9493
|
}
|
|
9494
|
+
function getEffectiveCompressedTokens(block, blocksById, visited = /* @__PURE__ */ new Set()) {
|
|
9495
|
+
if (block.effectiveCompressedTokens !== void 0) {
|
|
9496
|
+
return block.effectiveCompressedTokens;
|
|
9497
|
+
}
|
|
9498
|
+
if (visited.has(block.blockId)) return 0;
|
|
9499
|
+
visited.add(block.blockId);
|
|
9500
|
+
let total = block.compressedTokens || 0;
|
|
9501
|
+
for (const consumedId of block.consumedBlockIds || []) {
|
|
9502
|
+
const consumed = blocksById.get(consumedId);
|
|
9503
|
+
if (consumed) {
|
|
9504
|
+
total += getEffectiveCompressedTokens(consumed, blocksById, visited);
|
|
9505
|
+
}
|
|
9506
|
+
}
|
|
9507
|
+
return total;
|
|
9508
|
+
}
|
|
9509
|
+
function tierLabel(block) {
|
|
9510
|
+
const tier = block.tier ?? 1;
|
|
9511
|
+
return `T${tier}`;
|
|
9512
|
+
}
|
|
9513
|
+
function tierBreakdown(blocks) {
|
|
9514
|
+
const tierTokens = {};
|
|
9515
|
+
for (const b of blocks) {
|
|
9516
|
+
const t = b.tier ?? 1;
|
|
9517
|
+
tierTokens[t] = (tierTokens[t] || 0) + (b.summaryTokens || 0);
|
|
9518
|
+
}
|
|
9519
|
+
const tiers = Object.keys(tierTokens).map(Number);
|
|
9520
|
+
if (tiers.length <= 1 && (!tierTokens[2] || tierTokens[2] === 0) && (!tierTokens[3] || tierTokens[3] === 0)) {
|
|
9521
|
+
return null;
|
|
9522
|
+
}
|
|
9523
|
+
const parts = [];
|
|
9524
|
+
for (const t of [1, 2, 3]) {
|
|
9525
|
+
if (tierTokens[t]) {
|
|
9526
|
+
parts.push(`T${t}: ${formatTokens(tierTokens[t])}`);
|
|
9527
|
+
}
|
|
9528
|
+
}
|
|
9529
|
+
return parts.join(" | ");
|
|
9530
|
+
}
|
|
9112
9531
|
function collectVisibleMessages(rawMessages, ctx) {
|
|
9113
9532
|
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
9114
9533
|
const byRawId = ctx.state.messageIds.byRawId;
|
|
9115
9534
|
const result = [];
|
|
9116
9535
|
let summaryTokens = 0;
|
|
9536
|
+
const visibleMessageIds = new Set(rawMessages.map((m) => m.info.id));
|
|
9117
9537
|
const activeBlocks = Array.from(ctx.state.prune.messages.activeBlockIds).map((id) => ctx.state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
|
|
9118
9538
|
for (const block of activeBlocks) {
|
|
9539
|
+
if (block.compressMessageId && !visibleMessageIds.has(block.compressMessageId)) {
|
|
9540
|
+
continue;
|
|
9541
|
+
}
|
|
9119
9542
|
summaryTokens += block.summaryTokens || 0;
|
|
9120
9543
|
}
|
|
9121
9544
|
rawMessages.forEach((msg, idx) => {
|
|
@@ -9176,19 +9599,32 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
|
|
|
9176
9599
|
lines.push("COMPRESSED BLOCKS");
|
|
9177
9600
|
lines.push(" No compressed blocks.");
|
|
9178
9601
|
} else {
|
|
9602
|
+
const blocksById = ctx.state.prune.messages.blocksById;
|
|
9179
9603
|
const totalSummary = blocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
|
|
9180
|
-
const
|
|
9181
|
-
|
|
9182
|
-
|
|
9604
|
+
const totalEffective = blocks.reduce(
|
|
9605
|
+
(s, b) => s + getEffectiveCompressedTokens(b, blocksById),
|
|
9606
|
+
0
|
|
9183
9607
|
);
|
|
9608
|
+
const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens(totalSummary)} summary, ${formatTokens(totalEffective)} original)`;
|
|
9609
|
+
lines.push(header);
|
|
9610
|
+
const breakdown = tierBreakdown(blocks);
|
|
9611
|
+
if (breakdown) {
|
|
9612
|
+
lines.push(` Tier usage: ${breakdown}`);
|
|
9613
|
+
}
|
|
9184
9614
|
lines.push("");
|
|
9185
|
-
const sorted = [...blocks].sort((a, b) =>
|
|
9615
|
+
const sorted = [...blocks].sort((a, b) => {
|
|
9616
|
+
const effA = getEffectiveCompressedTokens(a, blocksById);
|
|
9617
|
+
const effB = getEffectiveCompressedTokens(b, blocksById);
|
|
9618
|
+
return effB - effA || b.createdAt - a.createdAt;
|
|
9619
|
+
});
|
|
9186
9620
|
for (const b of sorted.slice(0, 30)) {
|
|
9187
9621
|
const ageStr = formatAge(b.createdAt);
|
|
9188
9622
|
const range = formatIdRange(b);
|
|
9189
9623
|
const topic = b.topic || "(no topic)";
|
|
9624
|
+
const tier = tierLabel(b);
|
|
9625
|
+
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
9190
9626
|
lines.push(
|
|
9191
|
-
` b${b.blockId} ${formatTokens(
|
|
9627
|
+
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
|
|
9192
9628
|
);
|
|
9193
9629
|
}
|
|
9194
9630
|
}
|
|
@@ -9292,7 +9728,7 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
|
9292
9728
|
}
|
|
9293
9729
|
return lines;
|
|
9294
9730
|
}
|
|
9295
|
-
function renderCompressedDrilldown(blocks, sort, limit) {
|
|
9731
|
+
function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
|
|
9296
9732
|
const lines = [];
|
|
9297
9733
|
let sorted = [...blocks];
|
|
9298
9734
|
if (sort === "time") {
|
|
@@ -9300,13 +9736,22 @@ function renderCompressedDrilldown(blocks, sort, limit) {
|
|
|
9300
9736
|
} else if (sort === "age") {
|
|
9301
9737
|
sorted.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
|
|
9302
9738
|
} else {
|
|
9303
|
-
sorted.sort(
|
|
9739
|
+
sorted.sort(
|
|
9740
|
+
(a, b) => getEffectiveCompressedTokens(b, blocksById) - getEffectiveCompressedTokens(a, blocksById) || b.createdAt - a.createdAt
|
|
9741
|
+
);
|
|
9304
9742
|
}
|
|
9305
9743
|
const totalSummary = sorted.reduce((s, b) => s + (b.summaryTokens || 0), 0);
|
|
9306
|
-
const
|
|
9744
|
+
const totalEffective = sorted.reduce(
|
|
9745
|
+
(s, b) => s + getEffectiveCompressedTokens(b, blocksById),
|
|
9746
|
+
0
|
|
9747
|
+
);
|
|
9307
9748
|
lines.push(
|
|
9308
|
-
`COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(
|
|
9749
|
+
`COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalEffective)} original \u2192 ${formatTokens(totalSummary)} summary`
|
|
9309
9750
|
);
|
|
9751
|
+
const breakdown = tierBreakdown(sorted);
|
|
9752
|
+
if (breakdown) {
|
|
9753
|
+
lines.push(`Tier usage: ${breakdown}`);
|
|
9754
|
+
}
|
|
9310
9755
|
lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
|
|
9311
9756
|
lines.push("");
|
|
9312
9757
|
const shown = sorted.slice(0, limit);
|
|
@@ -9314,10 +9759,12 @@ function renderCompressedDrilldown(blocks, sort, limit) {
|
|
|
9314
9759
|
const survived = b.survivedCount ?? 0;
|
|
9315
9760
|
const gen = b.generation ?? "young";
|
|
9316
9761
|
const effCount = b.effectiveMessageIds?.length ?? 0;
|
|
9317
|
-
const consumed = b.
|
|
9762
|
+
const consumed = b.includedBlockIds && b.includedBlockIds.length > 0 ? ` nested=[${b.includedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
|
|
9318
9763
|
const topic = b.topic || "(no topic)";
|
|
9764
|
+
const tier = tierLabel(b);
|
|
9765
|
+
const effTokens = getEffectiveCompressedTokens(b, blocksById);
|
|
9319
9766
|
lines.push(
|
|
9320
|
-
` b${b.blockId} ${formatTokens(
|
|
9767
|
+
` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
|
|
9321
9768
|
);
|
|
9322
9769
|
lines.push(` "${topic}"`);
|
|
9323
9770
|
}
|
|
@@ -9358,7 +9805,14 @@ function createAcpStatusTool(factoryCtx) {
|
|
|
9358
9805
|
const allBlocks = activeIds.map((id) => msgState.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
|
|
9359
9806
|
const lines = [];
|
|
9360
9807
|
if (scope === "compressed") {
|
|
9361
|
-
lines.push(
|
|
9808
|
+
lines.push(
|
|
9809
|
+
...renderCompressedDrilldown(
|
|
9810
|
+
allBlocks,
|
|
9811
|
+
sort,
|
|
9812
|
+
limit,
|
|
9813
|
+
msgState.blocksById
|
|
9814
|
+
)
|
|
9815
|
+
);
|
|
9362
9816
|
return lines.join("\n");
|
|
9363
9817
|
}
|
|
9364
9818
|
let visibleMsgs = [];
|
|
@@ -9517,6 +9971,53 @@ IMPORTANT: This was an automatic context pruning. You MUST continue your previou
|
|
|
9517
9971
|
});
|
|
9518
9972
|
}
|
|
9519
9973
|
|
|
9974
|
+
// lib/compress/hide-consumed.ts
|
|
9975
|
+
function hideConsumedCompressCalls(state, messages) {
|
|
9976
|
+
if (state.prune.messages.blocksById.size === 0) {
|
|
9977
|
+
return 0;
|
|
9978
|
+
}
|
|
9979
|
+
const consumedMessageIds = /* @__PURE__ */ new Set();
|
|
9980
|
+
for (const block of state.prune.messages.blocksById.values()) {
|
|
9981
|
+
if (block.active) continue;
|
|
9982
|
+
if (block.deactivatedByUser) continue;
|
|
9983
|
+
if (block.deactivatedByUserDeep) continue;
|
|
9984
|
+
if (block.deactivatedByBlockId === void 0) continue;
|
|
9985
|
+
if (!block.compressMessageId) continue;
|
|
9986
|
+
consumedMessageIds.add(block.compressMessageId);
|
|
9987
|
+
}
|
|
9988
|
+
if (consumedMessageIds.size === 0) {
|
|
9989
|
+
return 0;
|
|
9990
|
+
}
|
|
9991
|
+
const lastUserIdx = messages.findLastIndex(
|
|
9992
|
+
(m) => m.info.role === "user" && !isIgnoredUserMessage(m)
|
|
9993
|
+
);
|
|
9994
|
+
let hidden = 0;
|
|
9995
|
+
for (let i = 0; i < messages.length; i++) {
|
|
9996
|
+
if (lastUserIdx >= 0 && i >= lastUserIdx) break;
|
|
9997
|
+
const msg = messages[i];
|
|
9998
|
+
if (!consumedMessageIds.has(msg.info.id)) continue;
|
|
9999
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
10000
|
+
let changed = false;
|
|
10001
|
+
const remaining = parts.filter((p) => {
|
|
10002
|
+
if (p.type === "tool" && p.tool === "compress") {
|
|
10003
|
+
hidden++;
|
|
10004
|
+
changed = true;
|
|
10005
|
+
return false;
|
|
10006
|
+
}
|
|
10007
|
+
return true;
|
|
10008
|
+
});
|
|
10009
|
+
if (changed) {
|
|
10010
|
+
if (remaining.length > 0) {
|
|
10011
|
+
messages[i] = { ...msg, parts: remaining };
|
|
10012
|
+
} else {
|
|
10013
|
+
messages.splice(i, 1);
|
|
10014
|
+
i--;
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
10017
|
+
}
|
|
10018
|
+
return hidden;
|
|
10019
|
+
}
|
|
10020
|
+
|
|
9520
10021
|
// lib/logger.ts
|
|
9521
10022
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
9522
10023
|
import { join as join3 } from "path";
|
|
@@ -9745,7 +10246,7 @@ TOOLS
|
|
|
9745
10246
|
You have five context-management tools:
|
|
9746
10247
|
|
|
9747
10248
|
- \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
|
|
9748
|
-
- \`decompress\` \u2014 Restore a previously compressed block's
|
|
10249
|
+
- \`decompress\` \u2014 Restore a previously compressed block's content. By default restores one tier up (T2\u2192T1 summaries, not raw messages). Use \`full: true\` to restore all the way to original messages. Use \`toFile\` to write to file instead of inflating context. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\` or \`decompress({ blockId: "b5", full: true })\`.
|
|
9749
10250
|
- \`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" })\`.
|
|
9750
10251
|
- \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
|
|
9751
10252
|
- \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.
|
|
@@ -9779,6 +10280,16 @@ WHEN NOT TO COMPRESS
|
|
|
9779
10280
|
|
|
9780
10281
|
${HOW_TO_COMPRESS_RULES}
|
|
9781
10282
|
|
|
10283
|
+
MULTI-TIER COMPRESSION
|
|
10284
|
+
|
|
10285
|
+
Summaries accumulate as the session grows. When tier-1 summaries pile up, the system injects a [Tier 2 Trigger] prompting you to DISTILL old blocks into a single tier-2 summary. If tier-2 summaries also accumulate, a [Tier 3 Trigger] asks you to CONDENSE them further.
|
|
10286
|
+
|
|
10287
|
+
- Tier 1 (default): Full-detail compression of conversation ranges. Uses HOW TO COMPRESS rules above.
|
|
10288
|
+
- Tier 2: Distillation of old tier-1 block summaries. Uses TIER 2 DISTILLATION rules (decisions/outcomes only, drop paths/code/process).
|
|
10289
|
+
- Tier 3: Ultra-condensation of tier-2 summaries. Uses TIER 3 CONDENSATION rules (bare facts, 1-3 lines per block).
|
|
10290
|
+
|
|
10291
|
+
To compress blocks: use block IDs as boundaries: \`compress({ content: [{ startId: "b3", endId: "b15", summary: "..." }] })\`. This deactivates the consumed blocks and creates a new higher-tier block. The system prompt at the trigger tells you which rules to follow.
|
|
10292
|
+
|
|
9782
10293
|
PERIODIC CONTEXT STATUS
|
|
9783
10294
|
|
|
9784
10295
|
Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
|
|
@@ -10956,8 +11467,21 @@ async function handleRecompressCommand(ctx) {
|
|
|
10956
11467
|
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
|
|
10957
11468
|
for (const block of target.blocks) {
|
|
10958
11469
|
block.deactivatedByUser = false;
|
|
11470
|
+
block.deactivatedByUserDeep = false;
|
|
10959
11471
|
block.deactivatedAt = void 0;
|
|
10960
11472
|
block.deactivatedByBlockId = void 0;
|
|
11473
|
+
const queue = [...block.consumedBlockIds];
|
|
11474
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11475
|
+
while (queue.length > 0) {
|
|
11476
|
+
const consumedId = queue.shift();
|
|
11477
|
+
if (visited.has(consumedId)) continue;
|
|
11478
|
+
visited.add(consumedId);
|
|
11479
|
+
const consumedBlock = messagesState.blocksById.get(consumedId);
|
|
11480
|
+
if (consumedBlock) {
|
|
11481
|
+
consumedBlock.deactivatedByUserDeep = false;
|
|
11482
|
+
queue.push(...consumedBlock.consumedBlockIds);
|
|
11483
|
+
}
|
|
11484
|
+
}
|
|
10961
11485
|
}
|
|
10962
11486
|
syncCompressionBlocks(state, logger, messages);
|
|
10963
11487
|
let recompressedMessageCount = 0;
|
|
@@ -11044,8 +11568,9 @@ function formatCompressionTime(ms) {
|
|
|
11044
11568
|
async function handleStatsCommand(ctx) {
|
|
11045
11569
|
const { client, state, logger, sessionId, messages } = ctx;
|
|
11046
11570
|
const sessionTokens = state.stats.totalPruneTokens;
|
|
11571
|
+
const visibleMessageIds = new Set(messages.map((m) => m.info.id));
|
|
11047
11572
|
const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
|
|
11048
|
-
(total, block) => block.active ? total + block.summaryTokens : total,
|
|
11573
|
+
(total, block) => block.active && visibleMessageIds.has(block.compressMessageId ?? "") ? total + block.summaryTokens : total,
|
|
11049
11574
|
0
|
|
11050
11575
|
);
|
|
11051
11576
|
const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
|
|
@@ -11623,6 +12148,7 @@ function createChatMessageTransformHandler(client, registry3, logger, config, pr
|
|
|
11623
12148
|
}
|
|
11624
12149
|
const prePruneTokens = getCurrentTokenUsage(state, output.messages);
|
|
11625
12150
|
prune(state, logger, config, output.messages);
|
|
12151
|
+
hideConsumedCompressCalls(state, output.messages);
|
|
11626
12152
|
assignMessageRefs(state, output.messages);
|
|
11627
12153
|
const compressionPriorities = buildPriorityMap(config, state, output.messages);
|
|
11628
12154
|
prompts.reload();
|