opencode-acp 1.5.0 → 1.6.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 +9 -17
- package/README.zh-CN.md +7 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +349 -339
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/index.d.ts +0 -1
- package/dist/lib/compress/index.d.ts.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/range-utils.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gc/merge.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 +1 -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.map +1 -1
- package/dist/lib/prompts/compress-range.d.ts +1 -1
- package/dist/lib/prompts/compress-range.d.ts.map +1 -1
- package/dist/lib/prompts/extensions/nudge.d.ts +7 -0
- package/dist/lib/prompts/extensions/nudge.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 +2 -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 +2 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/dist/lib/token-utils.d.ts +1 -0
- package/dist/lib/token-utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/lib/compress/mark-block.d.ts +0 -5
- package/dist/lib/compress/mark-block.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -894,11 +894,15 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
894
894
|
"compress.modelMaxLimits",
|
|
895
895
|
"compress.modelMinLimits",
|
|
896
896
|
"compress.nudgeFrequency",
|
|
897
|
+
"compress.perMessageNudgeGrowthPercent",
|
|
897
898
|
"compress.iterationNudgeThreshold",
|
|
898
899
|
"compress.nudgeForce",
|
|
899
900
|
"compress.protectedTools",
|
|
900
901
|
"compress.protectTags",
|
|
901
902
|
"compress.protectUserMessages",
|
|
903
|
+
"compress.maxSummaryLength",
|
|
904
|
+
"compress.maxSummaryLengthHard",
|
|
905
|
+
"compress.minCompressRange",
|
|
902
906
|
"gc",
|
|
903
907
|
"gc.algorithm",
|
|
904
908
|
"gc.promotionThreshold",
|
|
@@ -1117,6 +1121,13 @@ function validateConfigTypes(config) {
|
|
|
1117
1121
|
actual: `${compress.nudgeFrequency} (will be clamped to 1)`
|
|
1118
1122
|
});
|
|
1119
1123
|
}
|
|
1124
|
+
if (compress.perMessageNudgeGrowthPercent !== void 0 && typeof compress.perMessageNudgeGrowthPercent !== "number") {
|
|
1125
|
+
errors.push({
|
|
1126
|
+
key: "compress.perMessageNudgeGrowthPercent",
|
|
1127
|
+
expected: "number",
|
|
1128
|
+
actual: typeof compress.perMessageNudgeGrowthPercent
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1120
1131
|
if (compress.iterationNudgeThreshold !== void 0 && typeof compress.iterationNudgeThreshold !== "number") {
|
|
1121
1132
|
errors.push({
|
|
1122
1133
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1152,6 +1163,55 @@ function validateConfigTypes(config) {
|
|
|
1152
1163
|
actual: typeof compress.protectUserMessages
|
|
1153
1164
|
});
|
|
1154
1165
|
}
|
|
1166
|
+
if (compress.maxSummaryLength !== void 0 && typeof compress.maxSummaryLength !== "number") {
|
|
1167
|
+
errors.push({
|
|
1168
|
+
key: "compress.maxSummaryLength",
|
|
1169
|
+
expected: "number",
|
|
1170
|
+
actual: typeof compress.maxSummaryLength
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof compress.maxSummaryLength === "number" && compress.maxSummaryLength < 1) {
|
|
1174
|
+
errors.push({
|
|
1175
|
+
key: "compress.maxSummaryLength",
|
|
1176
|
+
expected: "positive number (>= 1)",
|
|
1177
|
+
actual: `${compress.maxSummaryLength}`
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
if (compress.maxSummaryLengthHard !== void 0 && typeof compress.maxSummaryLengthHard !== "number") {
|
|
1181
|
+
errors.push({
|
|
1182
|
+
key: "compress.maxSummaryLengthHard",
|
|
1183
|
+
expected: "number",
|
|
1184
|
+
actual: typeof compress.maxSummaryLengthHard
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (typeof compress.maxSummaryLengthHard === "number" && compress.maxSummaryLengthHard < 1) {
|
|
1188
|
+
errors.push({
|
|
1189
|
+
key: "compress.maxSummaryLengthHard",
|
|
1190
|
+
expected: "positive number (>= 1)",
|
|
1191
|
+
actual: `${compress.maxSummaryLengthHard}`
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (typeof compress.maxSummaryLength === "number" && typeof compress.maxSummaryLengthHard === "number" && compress.maxSummaryLengthHard < compress.maxSummaryLength) {
|
|
1195
|
+
errors.push({
|
|
1196
|
+
key: "compress.maxSummaryLengthHard",
|
|
1197
|
+
expected: `>= maxSummaryLength (${compress.maxSummaryLength})`,
|
|
1198
|
+
actual: `${compress.maxSummaryLengthHard}`
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (compress.minCompressRange !== void 0 && typeof compress.minCompressRange !== "number") {
|
|
1202
|
+
errors.push({
|
|
1203
|
+
key: "compress.minCompressRange",
|
|
1204
|
+
expected: "number",
|
|
1205
|
+
actual: typeof compress.minCompressRange
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof compress.minCompressRange === "number" && compress.minCompressRange < 0) {
|
|
1209
|
+
errors.push({
|
|
1210
|
+
key: "compress.minCompressRange",
|
|
1211
|
+
expected: "non-negative number (>= 0)",
|
|
1212
|
+
actual: `${compress.minCompressRange}`
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1155
1215
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1156
1216
|
errors.push({
|
|
1157
1217
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1385,8 +1445,6 @@ var DEFAULT_PROTECTED_TOOLS = [
|
|
|
1385
1445
|
"todoread",
|
|
1386
1446
|
"compress",
|
|
1387
1447
|
"decompress",
|
|
1388
|
-
"mark_block",
|
|
1389
|
-
"unmark_block",
|
|
1390
1448
|
"batch",
|
|
1391
1449
|
"plan_enter",
|
|
1392
1450
|
"plan_exit",
|
|
@@ -1461,11 +1519,15 @@ var defaultConfig = {
|
|
|
1461
1519
|
maxContextLimit: "55%",
|
|
1462
1520
|
minContextLimit: "45%",
|
|
1463
1521
|
nudgeFrequency: 5,
|
|
1522
|
+
perMessageNudgeGrowthPercent: 3,
|
|
1464
1523
|
iterationNudgeThreshold: 15,
|
|
1465
1524
|
nudgeForce: "soft",
|
|
1466
1525
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
1467
1526
|
protectTags: false,
|
|
1468
|
-
protectUserMessages: false
|
|
1527
|
+
protectUserMessages: false,
|
|
1528
|
+
maxSummaryLength: 200,
|
|
1529
|
+
maxSummaryLengthHard: 800,
|
|
1530
|
+
minCompressRange: 2e3
|
|
1469
1531
|
},
|
|
1470
1532
|
strategies: {
|
|
1471
1533
|
deduplication: {
|
|
@@ -1612,11 +1674,15 @@ function mergeCompress(base, override) {
|
|
|
1612
1674
|
modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
|
|
1613
1675
|
modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
|
|
1614
1676
|
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
1677
|
+
perMessageNudgeGrowthPercent: override.perMessageNudgeGrowthPercent ?? base.perMessageNudgeGrowthPercent,
|
|
1615
1678
|
iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
|
|
1616
1679
|
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
1617
1680
|
protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
|
|
1618
1681
|
protectTags: override.protectTags ?? base.protectTags,
|
|
1619
|
-
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages
|
|
1682
|
+
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
1683
|
+
maxSummaryLength: override.maxSummaryLength ?? base.maxSummaryLength,
|
|
1684
|
+
maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
|
|
1685
|
+
minCompressRange: override.minCompressRange ?? base.minCompressRange
|
|
1620
1686
|
};
|
|
1621
1687
|
}
|
|
1622
1688
|
function mergeCommands(base, override) {
|
|
@@ -1980,6 +2046,20 @@ function countAllMessageTokens(msg) {
|
|
|
1980
2046
|
if (texts.length === 0) return 0;
|
|
1981
2047
|
return estimateTokensBatch(texts);
|
|
1982
2048
|
}
|
|
2049
|
+
function countMessageCharacters(msg) {
|
|
2050
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
2051
|
+
let total = 0;
|
|
2052
|
+
for (const part of parts) {
|
|
2053
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
2054
|
+
total += part.text.length;
|
|
2055
|
+
} else {
|
|
2056
|
+
for (const content of extractToolContent(part)) {
|
|
2057
|
+
total += content.length;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return total;
|
|
2062
|
+
}
|
|
1983
2063
|
|
|
1984
2064
|
// lib/prompts/extensions/tool.ts
|
|
1985
2065
|
var RANGE_FORMAT_EXTENSION = `
|
|
@@ -3019,7 +3099,9 @@ function resetOnCompaction(state) {
|
|
|
3019
3099
|
state.nudges = {
|
|
3020
3100
|
contextLimitAnchors: /* @__PURE__ */ new Set(),
|
|
3021
3101
|
turnNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3022
|
-
iterationNudgeAnchors: /* @__PURE__ */ new Set()
|
|
3102
|
+
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3103
|
+
lastPerMessageNudgeTurn: 0,
|
|
3104
|
+
lastPerMessageNudgeTokens: 0
|
|
3023
3105
|
};
|
|
3024
3106
|
state.messageIds = {
|
|
3025
3107
|
byRawId: /* @__PURE__ */ new Map(),
|
|
@@ -3085,7 +3167,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3085
3167
|
nudges: {
|
|
3086
3168
|
contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
|
|
3087
3169
|
turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
|
|
3088
|
-
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors)
|
|
3170
|
+
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
|
|
3171
|
+
lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
|
|
3172
|
+
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens ?? 0
|
|
3089
3173
|
},
|
|
3090
3174
|
stats: sessionState.stats,
|
|
3091
3175
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3299,7 +3383,9 @@ function createSessionState() {
|
|
|
3299
3383
|
nudges: {
|
|
3300
3384
|
contextLimitAnchors: /* @__PURE__ */ new Set(),
|
|
3301
3385
|
turnNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3302
|
-
iterationNudgeAnchors: /* @__PURE__ */ new Set()
|
|
3386
|
+
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3387
|
+
lastPerMessageNudgeTurn: 0,
|
|
3388
|
+
lastPerMessageNudgeTokens: 0
|
|
3303
3389
|
},
|
|
3304
3390
|
stats: {
|
|
3305
3391
|
pruneTokenCounter: 0,
|
|
@@ -3336,7 +3422,9 @@ function resetSessionState(state) {
|
|
|
3336
3422
|
state.nudges = {
|
|
3337
3423
|
contextLimitAnchors: /* @__PURE__ */ new Set(),
|
|
3338
3424
|
turnNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3339
|
-
iterationNudgeAnchors: /* @__PURE__ */ new Set()
|
|
3425
|
+
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3426
|
+
lastPerMessageNudgeTurn: 0,
|
|
3427
|
+
lastPerMessageNudgeTokens: 0
|
|
3340
3428
|
};
|
|
3341
3429
|
state.stats = {
|
|
3342
3430
|
pruneTokenCounter: 0,
|
|
@@ -3381,6 +3469,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
3381
3469
|
state.nudges.iterationNudgeAnchors = new Set(
|
|
3382
3470
|
persisted.nudges.iterationNudgeAnchors || []
|
|
3383
3471
|
);
|
|
3472
|
+
state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
3473
|
+
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens ?? 0;
|
|
3384
3474
|
state.stats = {
|
|
3385
3475
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
3386
3476
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
@@ -3530,20 +3620,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
3530
3620
|
regex += "$";
|
|
3531
3621
|
return new RegExp(regex).test(input);
|
|
3532
3622
|
}
|
|
3533
|
-
function getFilePathsFromParameters(
|
|
3623
|
+
function getFilePathsFromParameters(tool4, parameters) {
|
|
3534
3624
|
if (typeof parameters !== "object" || parameters === null) {
|
|
3535
3625
|
return [];
|
|
3536
3626
|
}
|
|
3537
3627
|
const paths = [];
|
|
3538
3628
|
const params = parameters;
|
|
3539
|
-
if (
|
|
3629
|
+
if (tool4 === "apply_patch" && typeof params.patchText === "string") {
|
|
3540
3630
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3541
3631
|
let match;
|
|
3542
3632
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
3543
3633
|
paths.push(match[1].trim());
|
|
3544
3634
|
}
|
|
3545
3635
|
}
|
|
3546
|
-
if (
|
|
3636
|
+
if (tool4 === "multiedit") {
|
|
3547
3637
|
if (typeof params.filePath === "string") {
|
|
3548
3638
|
paths.push(params.filePath);
|
|
3549
3639
|
}
|
|
@@ -3638,13 +3728,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
3638
3728
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
3639
3729
|
}
|
|
3640
3730
|
};
|
|
3641
|
-
function createToolSignature(
|
|
3731
|
+
function createToolSignature(tool4, parameters) {
|
|
3642
3732
|
if (!parameters) {
|
|
3643
|
-
return
|
|
3733
|
+
return tool4;
|
|
3644
3734
|
}
|
|
3645
3735
|
const normalized = normalizeParameters(parameters);
|
|
3646
3736
|
const sorted = sortObjectKeys(normalized);
|
|
3647
|
-
return `${
|
|
3737
|
+
return `${tool4}::${JSON.stringify(sorted)}`;
|
|
3648
3738
|
}
|
|
3649
3739
|
function normalizeParameters(params) {
|
|
3650
3740
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -3719,9 +3809,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
3719
3809
|
};
|
|
3720
3810
|
|
|
3721
3811
|
// lib/ui/utils.ts
|
|
3722
|
-
function extractParameterKey(
|
|
3812
|
+
function extractParameterKey(tool4, parameters) {
|
|
3723
3813
|
if (!parameters) return "";
|
|
3724
|
-
if (
|
|
3814
|
+
if (tool4 === "read" && parameters.filePath) {
|
|
3725
3815
|
const offset = parameters.offset;
|
|
3726
3816
|
const limit = parameters.limit;
|
|
3727
3817
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -3735,10 +3825,10 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3735
3825
|
}
|
|
3736
3826
|
return parameters.filePath;
|
|
3737
3827
|
}
|
|
3738
|
-
if ((
|
|
3828
|
+
if ((tool4 === "write" || tool4 === "edit" || tool4 === "multiedit") && parameters.filePath) {
|
|
3739
3829
|
return parameters.filePath;
|
|
3740
3830
|
}
|
|
3741
|
-
if (
|
|
3831
|
+
if (tool4 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
3742
3832
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3743
3833
|
const paths = [];
|
|
3744
3834
|
let match;
|
|
@@ -3755,51 +3845,51 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3755
3845
|
}
|
|
3756
3846
|
return "patch";
|
|
3757
3847
|
}
|
|
3758
|
-
if (
|
|
3848
|
+
if (tool4 === "list") {
|
|
3759
3849
|
return parameters.path || "(current directory)";
|
|
3760
3850
|
}
|
|
3761
|
-
if (
|
|
3851
|
+
if (tool4 === "glob") {
|
|
3762
3852
|
if (parameters.pattern) {
|
|
3763
3853
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3764
3854
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3765
3855
|
}
|
|
3766
3856
|
return "(unknown pattern)";
|
|
3767
3857
|
}
|
|
3768
|
-
if (
|
|
3858
|
+
if (tool4 === "grep") {
|
|
3769
3859
|
if (parameters.pattern) {
|
|
3770
3860
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3771
3861
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3772
3862
|
}
|
|
3773
3863
|
return "(unknown pattern)";
|
|
3774
3864
|
}
|
|
3775
|
-
if (
|
|
3865
|
+
if (tool4 === "bash") {
|
|
3776
3866
|
if (parameters.description) return parameters.description;
|
|
3777
3867
|
if (parameters.command) {
|
|
3778
3868
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
3779
3869
|
}
|
|
3780
3870
|
}
|
|
3781
|
-
if (
|
|
3871
|
+
if (tool4 === "webfetch" && parameters.url) {
|
|
3782
3872
|
return parameters.url;
|
|
3783
3873
|
}
|
|
3784
|
-
if (
|
|
3874
|
+
if (tool4 === "websearch" && parameters.query) {
|
|
3785
3875
|
return `"${parameters.query}"`;
|
|
3786
3876
|
}
|
|
3787
|
-
if (
|
|
3877
|
+
if (tool4 === "codesearch" && parameters.query) {
|
|
3788
3878
|
return `"${parameters.query}"`;
|
|
3789
3879
|
}
|
|
3790
|
-
if (
|
|
3880
|
+
if (tool4 === "todowrite") {
|
|
3791
3881
|
return `${parameters.todos?.length || 0} todos`;
|
|
3792
3882
|
}
|
|
3793
|
-
if (
|
|
3883
|
+
if (tool4 === "todoread") {
|
|
3794
3884
|
return "read todo list";
|
|
3795
3885
|
}
|
|
3796
|
-
if (
|
|
3886
|
+
if (tool4 === "task" && parameters.description) {
|
|
3797
3887
|
return parameters.description;
|
|
3798
3888
|
}
|
|
3799
|
-
if (
|
|
3889
|
+
if (tool4 === "skill" && parameters.name) {
|
|
3800
3890
|
return parameters.name;
|
|
3801
3891
|
}
|
|
3802
|
-
if (
|
|
3892
|
+
if (tool4 === "lsp") {
|
|
3803
3893
|
const op = parameters.operation || "lsp";
|
|
3804
3894
|
const path = parameters.filePath || "";
|
|
3805
3895
|
const line = parameters.line;
|
|
@@ -3812,7 +3902,7 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3812
3902
|
}
|
|
3813
3903
|
return op;
|
|
3814
3904
|
}
|
|
3815
|
-
if (
|
|
3905
|
+
if (tool4 === "question") {
|
|
3816
3906
|
const questions = parameters.questions;
|
|
3817
3907
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
3818
3908
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4391,7 +4481,7 @@ ${output}`);
|
|
|
4391
4481
|
}
|
|
4392
4482
|
|
|
4393
4483
|
// lib/compress/message.ts
|
|
4394
|
-
function buildSchema() {
|
|
4484
|
+
function buildSchema(maxSummaryLength) {
|
|
4395
4485
|
return {
|
|
4396
4486
|
topic: tool.schema.string().describe(
|
|
4397
4487
|
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
|
|
@@ -4400,7 +4490,9 @@ function buildSchema() {
|
|
|
4400
4490
|
tool.schema.object({
|
|
4401
4491
|
messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m00001)"),
|
|
4402
4492
|
topic: tool.schema.string().describe("Short label (3-5 words) for this one message summary"),
|
|
4403
|
-
summary: tool.schema.string().describe(
|
|
4493
|
+
summary: tool.schema.string().describe(
|
|
4494
|
+
`Complete technical summary replacing that one message. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`
|
|
4495
|
+
)
|
|
4404
4496
|
})
|
|
4405
4497
|
).describe("Batch of individual message summaries to create in one tool call")
|
|
4406
4498
|
};
|
|
@@ -4410,10 +4502,18 @@ function createCompressMessageTool(ctx) {
|
|
|
4410
4502
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4411
4503
|
return tool({
|
|
4412
4504
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
4413
|
-
args: buildSchema(),
|
|
4505
|
+
args: buildSchema(ctx.config.compress.maxSummaryLength),
|
|
4414
4506
|
async execute(args, toolCtx) {
|
|
4415
4507
|
const input = args;
|
|
4416
4508
|
validateArgs(input);
|
|
4509
|
+
const maxSummaryLengthHard = ctx.config.compress.maxSummaryLengthHard;
|
|
4510
|
+
for (const entry of input.content) {
|
|
4511
|
+
if (entry.summary.length > maxSummaryLengthHard) {
|
|
4512
|
+
throw new Error(
|
|
4513
|
+
`Summary too long (${entry.summary.length} chars, hard ceiling ${maxSummaryLengthHard}). Aim for <=${ctx.config.compress.maxSummaryLength}; exceed only when strictly necessary. Rewrite more concisely.`
|
|
4514
|
+
);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4417
4517
|
const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
|
|
4418
4518
|
const { rawMessages, searchContext } = await prepareSession(
|
|
4419
4519
|
ctx,
|
|
@@ -4429,6 +4529,26 @@ function createCompressMessageTool(ctx) {
|
|
|
4429
4529
|
if (plans.length === 0 && skippedCount > 0) {
|
|
4430
4530
|
throw new Error(formatIssues(skippedIssues, skippedCount));
|
|
4431
4531
|
}
|
|
4532
|
+
const minCompressRange = ctx.config.compress.minCompressRange;
|
|
4533
|
+
if (minCompressRange > 0) {
|
|
4534
|
+
let totalChars = 0;
|
|
4535
|
+
const counted = /* @__PURE__ */ new Set();
|
|
4536
|
+
for (const plan of plans) {
|
|
4537
|
+
for (const messageId of plan.selection.messageIds) {
|
|
4538
|
+
if (counted.has(messageId)) continue;
|
|
4539
|
+
counted.add(messageId);
|
|
4540
|
+
const rawMessage = searchContext.rawMessagesById.get(messageId);
|
|
4541
|
+
if (rawMessage) {
|
|
4542
|
+
totalChars += countMessageCharacters(rawMessage);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
if (totalChars < minCompressRange) {
|
|
4547
|
+
throw new Error(
|
|
4548
|
+
`Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
|
|
4549
|
+
);
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4432
4552
|
const notifications = [];
|
|
4433
4553
|
const preparedPlans = [];
|
|
4434
4554
|
for (const plan of plans) {
|
|
@@ -4613,7 +4733,13 @@ function validateSummaryPlaceholders(placeholders, requiredBlockIds, startRefere
|
|
|
4613
4733
|
}
|
|
4614
4734
|
placeholders.length = 0;
|
|
4615
4735
|
placeholders.push(...validPlaceholders);
|
|
4616
|
-
|
|
4736
|
+
const missingIds = strictRequiredIds.filter((id) => !keptPlaceholderIds.has(id));
|
|
4737
|
+
if (missingIds.length > 0) {
|
|
4738
|
+
console.warn(
|
|
4739
|
+
`[ACP] compress summary omitted placeholders for required blocks: ${missingIds.map((id) => `b${id}`).join(", ")}. They will be auto-attached as consumed blocks.`
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
return missingIds;
|
|
4617
4743
|
}
|
|
4618
4744
|
function injectBlockPlaceholders(summary, _placeholders, _summaryByBlockId, _startReference, _endReference) {
|
|
4619
4745
|
return {
|
|
@@ -4629,7 +4755,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4629
4755
|
}
|
|
4630
4756
|
|
|
4631
4757
|
// lib/compress/range.ts
|
|
4632
|
-
function buildSchema2() {
|
|
4758
|
+
function buildSchema2(maxSummaryLength) {
|
|
4633
4759
|
return {
|
|
4634
4760
|
topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
4635
4761
|
content: tool2.schema.array(
|
|
@@ -4638,7 +4764,9 @@ function buildSchema2() {
|
|
|
4638
4764
|
"Message or block ID marking the beginning of range (e.g. m00001, b2)"
|
|
4639
4765
|
),
|
|
4640
4766
|
endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
|
|
4641
|
-
summary: tool2.schema.string().describe(
|
|
4767
|
+
summary: tool2.schema.string().describe(
|
|
4768
|
+
`Complete technical summary replacing all content in range. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`
|
|
4769
|
+
)
|
|
4642
4770
|
})
|
|
4643
4771
|
).describe(
|
|
4644
4772
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
@@ -4650,10 +4778,18 @@ function createCompressRangeTool(ctx) {
|
|
|
4650
4778
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4651
4779
|
return tool2({
|
|
4652
4780
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
4653
|
-
args: buildSchema2(),
|
|
4781
|
+
args: buildSchema2(ctx.config.compress.maxSummaryLength),
|
|
4654
4782
|
async execute(args, toolCtx) {
|
|
4655
4783
|
const input = args;
|
|
4656
4784
|
validateArgs2(input);
|
|
4785
|
+
const maxSummaryLengthHard = ctx.config.compress.maxSummaryLengthHard;
|
|
4786
|
+
for (const entry of input.content) {
|
|
4787
|
+
if (entry.summary.length > maxSummaryLengthHard) {
|
|
4788
|
+
throw new Error(
|
|
4789
|
+
`Summary too long (${entry.summary.length} chars, hard ceiling ${maxSummaryLengthHard}). Aim for <=${ctx.config.compress.maxSummaryLength}; exceed only when strictly necessary. Rewrite more concisely.`
|
|
4790
|
+
);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4657
4793
|
const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
|
|
4658
4794
|
const { rawMessages, searchContext } = await prepareSession(
|
|
4659
4795
|
ctx,
|
|
@@ -4662,6 +4798,26 @@ function createCompressRangeTool(ctx) {
|
|
|
4662
4798
|
);
|
|
4663
4799
|
const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
|
|
4664
4800
|
validateNonOverlapping(resolvedPlans);
|
|
4801
|
+
const minCompressRange = ctx.config.compress.minCompressRange;
|
|
4802
|
+
if (minCompressRange > 0) {
|
|
4803
|
+
let totalChars = 0;
|
|
4804
|
+
const counted = /* @__PURE__ */ new Set();
|
|
4805
|
+
for (const plan of resolvedPlans) {
|
|
4806
|
+
for (const messageId of plan.selection.messageIds) {
|
|
4807
|
+
if (counted.has(messageId)) continue;
|
|
4808
|
+
counted.add(messageId);
|
|
4809
|
+
const rawMessage = searchContext.rawMessagesById.get(messageId);
|
|
4810
|
+
if (rawMessage) {
|
|
4811
|
+
totalChars += countMessageCharacters(rawMessage);
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
if (totalChars < minCompressRange) {
|
|
4816
|
+
throw new Error(
|
|
4817
|
+
`Range too small (${totalChars} chars, min ${minCompressRange}). Not worth compressing \u2014 overhead exceeds savings.`
|
|
4818
|
+
);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4665
4821
|
const notifications = [];
|
|
4666
4822
|
const preparedPlans = [];
|
|
4667
4823
|
let totalCompressedMessages = 0;
|
|
@@ -4711,10 +4867,19 @@ function createCompressRangeTool(ctx) {
|
|
|
4711
4867
|
searchContext.summaryByBlockId,
|
|
4712
4868
|
injected.consumedBlockIds
|
|
4713
4869
|
);
|
|
4714
|
-
const
|
|
4870
|
+
const boundaryConsumed = extractBoundaryConsumedBlocks(
|
|
4715
4871
|
plan.selection.startReference,
|
|
4716
4872
|
plan.selection.endReference
|
|
4717
4873
|
);
|
|
4874
|
+
const seenConsumed = /* @__PURE__ */ new Set();
|
|
4875
|
+
const mergeConsumedBlockIds = [
|
|
4876
|
+
...plan.selection.requiredBlockIds,
|
|
4877
|
+
...boundaryConsumed
|
|
4878
|
+
].filter((id) => {
|
|
4879
|
+
if (seenConsumed.has(id)) return false;
|
|
4880
|
+
seenConsumed.add(id);
|
|
4881
|
+
return true;
|
|
4882
|
+
});
|
|
4718
4883
|
preparedPlans.push({
|
|
4719
4884
|
entry: plan.entry,
|
|
4720
4885
|
selection: plan.selection,
|
|
@@ -4815,7 +4980,8 @@ var createSyntheticUserMessage = (baseMessage, content, stableSeed) => {
|
|
|
4815
4980
|
sessionID: userInfo.sessionID,
|
|
4816
4981
|
messageID: messageId,
|
|
4817
4982
|
type: "text",
|
|
4818
|
-
text: content
|
|
4983
|
+
text: content,
|
|
4984
|
+
synthetic: true
|
|
4819
4985
|
}
|
|
4820
4986
|
]
|
|
4821
4987
|
};
|
|
@@ -4960,6 +5126,36 @@ var stripHallucinations = (messages) => {
|
|
|
4960
5126
|
// lib/messages/prune.ts
|
|
4961
5127
|
var prune = (state, logger, config, messages) => {
|
|
4962
5128
|
filterCompressedRanges(state, logger, config, messages);
|
|
5129
|
+
stripStepMarkers(messages);
|
|
5130
|
+
};
|
|
5131
|
+
var MAX_STEP_FINISH_REASON = 50;
|
|
5132
|
+
var stripStepMarkers = (messages) => {
|
|
5133
|
+
for (const msg of messages) {
|
|
5134
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : [];
|
|
5135
|
+
let changed = false;
|
|
5136
|
+
const filtered = [];
|
|
5137
|
+
for (const part of parts) {
|
|
5138
|
+
if (part.type === "step-start") {
|
|
5139
|
+
changed = true;
|
|
5140
|
+
continue;
|
|
5141
|
+
}
|
|
5142
|
+
if (part.type === "step-finish") {
|
|
5143
|
+
const reason = part.reason;
|
|
5144
|
+
if (typeof reason === "string" && reason.length > MAX_STEP_FINISH_REASON) {
|
|
5145
|
+
const truncated = reason.slice(0, MAX_STEP_FINISH_REASON) + "...";
|
|
5146
|
+
if (truncated !== reason) {
|
|
5147
|
+
filtered.push({ ...part, reason: truncated });
|
|
5148
|
+
changed = true;
|
|
5149
|
+
continue;
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
filtered.push(part);
|
|
5154
|
+
}
|
|
5155
|
+
if (changed) {
|
|
5156
|
+
msg.parts = filtered;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
4963
5159
|
};
|
|
4964
5160
|
var filterCompressedRanges = (state, logger, config, messages) => {
|
|
4965
5161
|
if (state.prune.messages.byMessageId.size === 0 && state.prune.messages.activeByAnchorMessageId.size === 0) {
|
|
@@ -5196,8 +5392,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
5196
5392
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5197
5393
|
) ? "deny" : basePermission;
|
|
5198
5394
|
};
|
|
5199
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
5200
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
5395
|
+
var hasExplicitToolPermission = (permissionConfig, tool4) => {
|
|
5396
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool4) : false;
|
|
5201
5397
|
};
|
|
5202
5398
|
|
|
5203
5399
|
// lib/compress-permission.ts
|
|
@@ -5225,12 +5421,47 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
5225
5421
|
const recent = refs.slice(-20).join(", ");
|
|
5226
5422
|
blockList = `${recent} (+${blockCount - 20} older, use decompress to access by ID)`;
|
|
5227
5423
|
}
|
|
5424
|
+
const includeHint = context?.includeHint ?? true;
|
|
5228
5425
|
const lines = [
|
|
5229
5426
|
"Compressed block context:",
|
|
5230
5427
|
`- Active compressed blocks: ${blockCount} (${blockList})`,
|
|
5231
|
-
"-
|
|
5232
|
-
"- \u{1F4A1} When you've finished using tool outputs, compress them \u2014 you can decompress later if needed. Lean context improves accuracy."
|
|
5428
|
+
"- System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary."
|
|
5233
5429
|
];
|
|
5430
|
+
if (includeHint) {
|
|
5431
|
+
lines.push("- \u{1F4A1} When you've finished using tool outputs, compress them \u2014 you can decompress later if needed. Lean context improves accuracy.");
|
|
5432
|
+
}
|
|
5433
|
+
if (blockCount > 50) {
|
|
5434
|
+
const oldBlockIds = activeBlockIds.slice(0, Math.max(0, blockCount - 20));
|
|
5435
|
+
const allOldBlocks = oldBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0);
|
|
5436
|
+
const visibleMessageIds = context?.visibleMessageIds;
|
|
5437
|
+
const visibleOldBlocks = visibleMessageIds === void 0 ? allOldBlocks : allOldBlocks.filter((b) => b.anchorMessageId && visibleMessageIds.has(b.anchorMessageId));
|
|
5438
|
+
if (visibleOldBlocks.length > 5) {
|
|
5439
|
+
const blocksWithRef = visibleOldBlocks.map((block) => {
|
|
5440
|
+
const ref = state.messageIds.byRawId.get(block.anchorMessageId);
|
|
5441
|
+
return ref ? { block, ref } : null;
|
|
5442
|
+
}).filter((x) => x !== null).sort((a, b) => a.ref.localeCompare(b.ref));
|
|
5443
|
+
const totalTokens = blocksWithRef.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
|
|
5444
|
+
const totalK = Math.max(1, Math.round(totalTokens / 1e3));
|
|
5445
|
+
const targets = [];
|
|
5446
|
+
const chunkSize = Math.ceil(blocksWithRef.length / 3);
|
|
5447
|
+
for (let i = 0; i < 3 && i * chunkSize < blocksWithRef.length; i++) {
|
|
5448
|
+
const chunk = blocksWithRef.slice(i * chunkSize, (i + 1) * chunkSize);
|
|
5449
|
+
if (chunk.length < 2) continue;
|
|
5450
|
+
const startRef = chunk[0].ref;
|
|
5451
|
+
const endRef = chunk[chunk.length - 1].ref;
|
|
5452
|
+
const chunkTokens = chunk.reduce((s, x) => s + (x.block.summaryTokens ?? 0), 0);
|
|
5453
|
+
const chunkK = Math.max(1, Math.round(chunkTokens / 1e3));
|
|
5454
|
+
targets.push(` \u2022 compress ${startRef}\u2192${endRef}: ${chunk.length} blocks (~${chunkK}K tokens)`);
|
|
5455
|
+
}
|
|
5456
|
+
if (targets.length > 0) {
|
|
5457
|
+
lines.push(`- \u{1F500} ${blocksWithRef.length} old blocks using ~${totalK}K tokens. Consolidate into ${targets.length}:`);
|
|
5458
|
+
lines.push(...targets);
|
|
5459
|
+
lines.push(` System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary.`);
|
|
5460
|
+
}
|
|
5461
|
+
} else {
|
|
5462
|
+
lines.push(`- \u{1F500} You have ${blockCount} blocks \u2014 use compress to consolidate adjacent same-topic blocks.`);
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5234
5465
|
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
5235
5466
|
if (gcConfig && usageRatio > 0.5) {
|
|
5236
5467
|
const promotionThreshold = gcConfig.promotionThreshold;
|
|
@@ -5573,7 +5804,7 @@ function resolveThresholdPercent(threshold, modelContextLimit) {
|
|
|
5573
5804
|
const parsed = parseFloat(threshold);
|
|
5574
5805
|
return isNaN(parsed) ? void 0 : parsed;
|
|
5575
5806
|
}
|
|
5576
|
-
function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
5807
|
+
function buildContextUsageGuidance(config, currentTokens, modelContextLimit, minimal = false) {
|
|
5577
5808
|
if (currentTokens === void 0 || modelContextLimit === void 0 || modelContextLimit === 0) {
|
|
5578
5809
|
return "";
|
|
5579
5810
|
}
|
|
@@ -5583,13 +5814,18 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
|
5583
5814
|
const minPct = resolveThresholdPercent(config.compress.minContextLimit, modelContextLimit) ?? 45;
|
|
5584
5815
|
const maxPct = resolveThresholdPercent(config.compress.maxContextLimit, modelContextLimit) ?? 55;
|
|
5585
5816
|
const base = `Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%).`;
|
|
5817
|
+
if (minimal) {
|
|
5818
|
+
return `
|
|
5819
|
+
|
|
5820
|
+
${base}`;
|
|
5821
|
+
}
|
|
5586
5822
|
let guidance;
|
|
5587
5823
|
if (pct < minPct) {
|
|
5588
|
-
guidance = " \u{1F4A1} Be frugal with context
|
|
5824
|
+
guidance = " \u{1F4A1} Be frugal with context. If any visible tool output exceeds 5000 characters and you've finished reading it, compress it into a summary now \u2014 don't keep large outputs 'just in case'. You can decompress later if needed.";
|
|
5589
5825
|
} else if (pct < maxPct) {
|
|
5590
|
-
guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now.
|
|
5826
|
+
guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now.";
|
|
5591
5827
|
} else {
|
|
5592
|
-
guidance = " \u{1F525} Context is high \u2014 compress aggressively
|
|
5828
|
+
guidance = " \u{1F525} Context is high \u2014 compress aggressively, preserve only what is essential.";
|
|
5593
5829
|
}
|
|
5594
5830
|
return `
|
|
5595
5831
|
|
|
@@ -5687,6 +5923,18 @@ function createSuffixMessage(messages) {
|
|
|
5687
5923
|
messages.push(synthetic);
|
|
5688
5924
|
return synthetic;
|
|
5689
5925
|
}
|
|
5926
|
+
function shouldInjectPerMessageNudge(state, config, currentTokens, modelContextLimit) {
|
|
5927
|
+
const turn = state.currentTurn ?? 0;
|
|
5928
|
+
const lastTurn = state.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
5929
|
+
const turnsSinceLast = turn - lastTurn;
|
|
5930
|
+
const tokens = currentTokens ?? 0;
|
|
5931
|
+
const lastTokens = state.nudges.lastPerMessageNudgeTokens ?? 0;
|
|
5932
|
+
const tokenGrowth = tokens - lastTokens;
|
|
5933
|
+
const tokenGrowthPercent = modelContextLimit ? tokenGrowth / modelContextLimit * 100 : 0;
|
|
5934
|
+
const frequency = config.compress.nudgeFrequency ?? 5;
|
|
5935
|
+
const growthThreshold = config.compress.perMessageNudgeGrowthPercent ?? 3;
|
|
5936
|
+
return turnsSinceLast >= frequency || tokenGrowthPercent >= growthThreshold;
|
|
5937
|
+
}
|
|
5690
5938
|
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
5691
5939
|
if (compressPermission(state, config) === "deny") {
|
|
5692
5940
|
return;
|
|
@@ -5771,21 +6019,34 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
5771
6019
|
}
|
|
5772
6020
|
const suffixMessage = createSuffixMessage(messages);
|
|
5773
6021
|
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
|
|
5774
|
-
|
|
6022
|
+
const shouldNudge = shouldInjectPerMessageNudge(state, config, currentTokens, modelContextLimit);
|
|
6023
|
+
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit, !shouldNudge);
|
|
5775
6024
|
if (config.compress.mode !== "message") {
|
|
5776
|
-
const
|
|
6025
|
+
const visibleMessageIds = new Set(
|
|
6026
|
+
messages.map((message) => message.info.id)
|
|
6027
|
+
);
|
|
6028
|
+
const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
|
|
6029
|
+
currentTokens,
|
|
6030
|
+
modelContextLimit,
|
|
6031
|
+
includeHint: shouldNudge,
|
|
6032
|
+
visibleMessageIds
|
|
6033
|
+
});
|
|
5777
6034
|
if (blockGuidance.trim() && suffixMessage) {
|
|
5778
6035
|
appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
|
|
5779
6036
|
}
|
|
5780
6037
|
}
|
|
6038
|
+
if (shouldNudge) {
|
|
6039
|
+
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
6040
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens ?? 0;
|
|
6041
|
+
}
|
|
5781
6042
|
injectVisibleIdRange(state, messages, suffixMessage);
|
|
5782
6043
|
if (anchorsChanged) {
|
|
5783
6044
|
void saveSessionState(state, logger);
|
|
5784
6045
|
}
|
|
5785
6046
|
};
|
|
5786
|
-
function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
6047
|
+
function injectContextUsage(target, config, currentTokens, modelContextLimit, minimal = false) {
|
|
5787
6048
|
if (!target) return;
|
|
5788
|
-
const usageTag = buildContextUsageGuidance(config, currentTokens, modelContextLimit);
|
|
6049
|
+
const usageTag = buildContextUsageGuidance(config, currentTokens, modelContextLimit, minimal);
|
|
5789
6050
|
if (!usageTag) return;
|
|
5790
6051
|
for (const part of target.parts) {
|
|
5791
6052
|
if (part.type === "text") {
|
|
@@ -6316,109 +6577,6 @@ function createDecompressTool(ctx) {
|
|
|
6316
6577
|
});
|
|
6317
6578
|
}
|
|
6318
6579
|
|
|
6319
|
-
// lib/compress/mark-block.ts
|
|
6320
|
-
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
6321
|
-
async function prepareMarkSession(ctx, toolCtx) {
|
|
6322
|
-
await toolCtx.ask({
|
|
6323
|
-
permission: "compress",
|
|
6324
|
-
patterns: ["*"],
|
|
6325
|
-
always: ["*"],
|
|
6326
|
-
metadata: {}
|
|
6327
|
-
});
|
|
6328
|
-
toolCtx.metadata({ title: "Mark block" });
|
|
6329
|
-
const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
6330
|
-
await ensureSessionInitialized(
|
|
6331
|
-
ctx.client,
|
|
6332
|
-
ctx.state,
|
|
6333
|
-
toolCtx.sessionID,
|
|
6334
|
-
ctx.logger,
|
|
6335
|
-
rawMessages,
|
|
6336
|
-
ctx.config.manualMode.enabled
|
|
6337
|
-
);
|
|
6338
|
-
assignMessageRefs(ctx.state, rawMessages);
|
|
6339
|
-
}
|
|
6340
|
-
var MARK_DESCRIPTION = `Marks a compressed block for batch merge-cleanup.
|
|
6341
|
-
|
|
6342
|
-
Use this for blocks whose detailed content you no longer need, but whose summaries
|
|
6343
|
-
you want to keep in context for now (to preserve prompt cache). Marked blocks stay
|
|
6344
|
-
fully active with zero immediate effect on context or cache. When context pressure
|
|
6345
|
-
rises, all marked blocks are merge-compressed together into a single summary in one
|
|
6346
|
-
cache break, instead of being handled one at a time.
|
|
6347
|
-
|
|
6348
|
-
Argument: blockId \u2014 the block reference to mark (e.g., "b1", "b3")
|
|
6349
|
-
|
|
6350
|
-
Use mark_block instead of compress when you want deferred cleanup: the block keeps
|
|
6351
|
-
serving cache hits now and gets consolidated later only if context gets tight.`;
|
|
6352
|
-
var UNMARK_DESCRIPTION = `Removes the batch cleanup mark from a compressed block.
|
|
6353
|
-
|
|
6354
|
-
Reverses mark_block. The block returns to normal handling and will not be
|
|
6355
|
-
auto-merged during batch cleanup.
|
|
6356
|
-
|
|
6357
|
-
Argument: blockId \u2014 the block reference to unmark (e.g., "b1", "b3")`;
|
|
6358
|
-
function buildSchema4() {
|
|
6359
|
-
return {
|
|
6360
|
-
blockId: tool4.schema.string().describe('Block reference to mark (e.g., "b1", "b3")')
|
|
6361
|
-
};
|
|
6362
|
-
}
|
|
6363
|
-
function buildUnmarkSchema() {
|
|
6364
|
-
return {
|
|
6365
|
-
blockId: tool4.schema.string().describe('Block reference to unmark (e.g., "b1", "b3")')
|
|
6366
|
-
};
|
|
6367
|
-
}
|
|
6368
|
-
function createMarkBlockTool(ctx) {
|
|
6369
|
-
return tool4({
|
|
6370
|
-
description: MARK_DESCRIPTION,
|
|
6371
|
-
args: buildSchema4(),
|
|
6372
|
-
async execute(args, toolCtx) {
|
|
6373
|
-
await prepareMarkSession(ctx, toolCtx);
|
|
6374
|
-
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6375
|
-
if (targetBlockId === null) {
|
|
6376
|
-
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6377
|
-
}
|
|
6378
|
-
const messagesState = ctx.state.prune.messages;
|
|
6379
|
-
const block = messagesState.blocksById.get(targetBlockId);
|
|
6380
|
-
if (!block) {
|
|
6381
|
-
return `Error: Block ${formatBlockRef(targetBlockId)} does not exist.`;
|
|
6382
|
-
}
|
|
6383
|
-
if (!block.active) {
|
|
6384
|
-
return `Error: Block ${formatBlockRef(targetBlockId)} is not active.`;
|
|
6385
|
-
}
|
|
6386
|
-
messagesState.markedForCleanup.add(targetBlockId);
|
|
6387
|
-
await saveSessionState(ctx.state, ctx.logger);
|
|
6388
|
-
const ref = formatBlockRef(targetBlockId);
|
|
6389
|
-
const markedCount = messagesState.markedForCleanup.size;
|
|
6390
|
-
ctx.logger.info("mark_block: block marked for cleanup", {
|
|
6391
|
-
blockId: targetBlockId,
|
|
6392
|
-
markedCount
|
|
6393
|
-
});
|
|
6394
|
-
return `Block ${ref} marked for cleanup. It will be merge-compressed together with other marked blocks when context pressure rises. No immediate effect on context or cache. (${markedCount} block(s) currently marked.)`;
|
|
6395
|
-
}
|
|
6396
|
-
});
|
|
6397
|
-
}
|
|
6398
|
-
function createUnmarkBlockTool(ctx) {
|
|
6399
|
-
return tool4({
|
|
6400
|
-
description: UNMARK_DESCRIPTION,
|
|
6401
|
-
args: buildUnmarkSchema(),
|
|
6402
|
-
async execute(args, toolCtx) {
|
|
6403
|
-
await prepareMarkSession(ctx, toolCtx);
|
|
6404
|
-
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6405
|
-
if (targetBlockId === null) {
|
|
6406
|
-
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6407
|
-
}
|
|
6408
|
-
const messagesState = ctx.state.prune.messages;
|
|
6409
|
-
if (!messagesState.markedForCleanup.has(targetBlockId)) {
|
|
6410
|
-
return `Block ${formatBlockRef(targetBlockId)} was not marked for cleanup.`;
|
|
6411
|
-
}
|
|
6412
|
-
messagesState.markedForCleanup.delete(targetBlockId);
|
|
6413
|
-
await saveSessionState(ctx.state, ctx.logger);
|
|
6414
|
-
ctx.logger.info("unmark_block: block unmarked", {
|
|
6415
|
-
blockId: targetBlockId
|
|
6416
|
-
});
|
|
6417
|
-
return `Block ${formatBlockRef(targetBlockId)} unmarked. It will no longer be auto-merged during batch cleanup.`;
|
|
6418
|
-
}
|
|
6419
|
-
});
|
|
6420
|
-
}
|
|
6421
|
-
|
|
6422
6580
|
// lib/logger.ts
|
|
6423
6581
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
6424
6582
|
import { join as join3 } from "path";
|
|
@@ -6627,7 +6785,7 @@ var SYSTEM = `
|
|
|
6627
6785
|
|
|
6628
6786
|
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.
|
|
6629
6787
|
|
|
6630
|
-
The tools you have for context management are \`compress
|
|
6788
|
+
The tools you have for context management are \`compress\` and \`decompress\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details.
|
|
6631
6789
|
|
|
6632
6790
|
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
6633
6791
|
|
|
@@ -6641,9 +6799,9 @@ Target the largest UNCOMPRESSED content first. Savings scale with original size
|
|
|
6641
6799
|
|
|
6642
6800
|
CONTEXT PRESSURE LEVELS
|
|
6643
6801
|
|
|
6644
|
-
- Normal: Be frugal \u2014 compress
|
|
6645
|
-
- Elevated: Context is growing
|
|
6646
|
-
- Critical: Compress aggressively now
|
|
6802
|
+
- Normal: Be frugal \u2014 compress large completed outputs into summaries. You can decompress later if needed.
|
|
6803
|
+
- Elevated: Context is growing \u2014 compress completed sections and high-token waste now.
|
|
6804
|
+
- Critical: Compress aggressively now \u2014 preserve only what is essential for the current task.
|
|
6647
6805
|
|
|
6648
6806
|
WHAT TO COMPRESS FIRST (high value, low risk)
|
|
6649
6807
|
|
|
@@ -6707,33 +6865,18 @@ Directly quote user messages when they are short enough to include safely. Direc
|
|
|
6707
6865
|
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
6708
6866
|
|
|
6709
6867
|
COMPRESSED BLOCK PLACEHOLDERS
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
- \`(bN)\`
|
|
6868
|
+
The system auto-detects any previously compressed blocks whose anchor messages fall inside your selected range. You do NOT need to manually list \`(bN)\` placeholders in your summary \u2014 every consumed block is tracked automatically.
|
|
6713
6869
|
|
|
6714
6870
|
Compressed block sections in context are clearly marked with a header:
|
|
6715
6871
|
|
|
6716
6872
|
- \`[Compressed conversation section]\`
|
|
6717
6873
|
|
|
6718
|
-
Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format.
|
|
6719
|
-
|
|
6720
6874
|
Rules:
|
|
6721
6875
|
|
|
6722
|
-
-
|
|
6876
|
+
- Write a short prose summary. The system handles block consumption automatically.
|
|
6723
6877
|
- Do not invent placeholders for blocks outside the selected range.
|
|
6724
|
-
- Treat \`(bN)\`
|
|
6725
|
-
- If you need to mention a block in prose, use plain text like \`compressed bN\` (
|
|
6726
|
-
- Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary must exactly match the required set, with no duplicates.
|
|
6727
|
-
|
|
6728
|
-
These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
|
|
6729
|
-
|
|
6730
|
-
FLOW PRESERVATION WITH PLACEHOLDERS
|
|
6731
|
-
When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
|
|
6732
|
-
|
|
6733
|
-
- Treat each placeholder as a stand-in for a full conversation segment, not as a short label.
|
|
6734
|
-
- Ensure transitions before and after each placeholder preserve chronology and causality.
|
|
6735
|
-
- Do not write text that depends on the placeholder staying literal (for example, "as noted in \`(b2)\`").
|
|
6736
|
-
- Your final meaning must be coherent once each placeholder is replaced with its full compressed block content.
|
|
6878
|
+
- Treat \`(bN)\` as a RESERVED TOKEN. Do not emit \`(bN)\` text anywhere in the summary.
|
|
6879
|
+
- If you need to mention a block in prose, use plain text like \`compressed bN\` (never as a placeholder).
|
|
6737
6880
|
|
|
6738
6881
|
BOUNDARY IDS
|
|
6739
6882
|
You specify boundaries by ID using the injected IDs visible in the conversation:
|
|
@@ -7658,7 +7801,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
7658
7801
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
7659
7802
|
"Return after compress with a brief explanation of what content was compressed."
|
|
7660
7803
|
].join("\n\n");
|
|
7661
|
-
function getTriggerPrompt(
|
|
7804
|
+
function getTriggerPrompt(tool4, state, config, userFocus) {
|
|
7662
7805
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
7663
7806
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
7664
7807
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -7687,8 +7830,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
7687
7830
|
);
|
|
7688
7831
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
7689
7832
|
}
|
|
7690
|
-
async function handleManualTriggerCommand(ctx,
|
|
7691
|
-
return getTriggerPrompt(
|
|
7833
|
+
async function handleManualTriggerCommand(ctx, tool4, userFocus) {
|
|
7834
|
+
return getTriggerPrompt(tool4, ctx.state, ctx.config, userFocus);
|
|
7692
7835
|
}
|
|
7693
7836
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
7694
7837
|
const pending = state.pendingManualTrigger;
|
|
@@ -8218,23 +8361,6 @@ function parseGcThreshold(limit, modelContextLimit) {
|
|
|
8218
8361
|
}
|
|
8219
8362
|
|
|
8220
8363
|
// lib/gc/merge.ts
|
|
8221
|
-
var DEFAULT_BATCH_CLEANUP = {
|
|
8222
|
-
lowThreshold: "55%",
|
|
8223
|
-
highThreshold: "75%",
|
|
8224
|
-
forceThreshold: "90%"
|
|
8225
|
-
};
|
|
8226
|
-
var ESCALATE_MIN_MARKED = 3;
|
|
8227
|
-
var ESCALATE_MIN_RATIO = 0.4;
|
|
8228
|
-
function resolveBatchCleanup(gc) {
|
|
8229
|
-
return gc.batchCleanup ?? DEFAULT_BATCH_CLEANUP;
|
|
8230
|
-
}
|
|
8231
|
-
function percentToTokens(value, modelContextLimit) {
|
|
8232
|
-
if (typeof value === "number") return value;
|
|
8233
|
-
const percent = parseFloat(value.slice(0, -1));
|
|
8234
|
-
if (isNaN(percent)) return modelContextLimit;
|
|
8235
|
-
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
8236
|
-
return Math.round(clamped / 100 * modelContextLimit);
|
|
8237
|
-
}
|
|
8238
8364
|
function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
|
|
8239
8365
|
const blocks = [];
|
|
8240
8366
|
const ids = Array.from(state.prune.messages.activeBlockIds).sort((a, b) => a - b);
|
|
@@ -8247,27 +8373,13 @@ function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
|
|
|
8247
8373
|
}
|
|
8248
8374
|
return blocks;
|
|
8249
8375
|
}
|
|
8250
|
-
function collectActiveMarkedBlocks(state) {
|
|
8251
|
-
const messagesState = state.prune.messages;
|
|
8252
|
-
const ids = Array.from(messagesState.markedForCleanup).sort((a, b) => a - b);
|
|
8253
|
-
const blocks = [];
|
|
8254
|
-
for (const id of ids) {
|
|
8255
|
-
const block = messagesState.blocksById.get(id);
|
|
8256
|
-
if (!block || !block.active) {
|
|
8257
|
-
messagesState.markedForCleanup.delete(id);
|
|
8258
|
-
continue;
|
|
8259
|
-
}
|
|
8260
|
-
blocks.push(block);
|
|
8261
|
-
}
|
|
8262
|
-
return blocks;
|
|
8263
|
-
}
|
|
8264
8376
|
function extractSummaryBody(summary) {
|
|
8265
8377
|
let body = summary;
|
|
8266
8378
|
const headerPrefix = COMPRESSED_BLOCK_HEADER + "\n";
|
|
8267
8379
|
if (body.startsWith(headerPrefix)) {
|
|
8268
8380
|
body = body.slice(headerPrefix.length);
|
|
8269
8381
|
}
|
|
8270
|
-
body = body.replace(/\n
|
|
8382
|
+
body = body.replace(/\n]*>b\d+<\/dcp-message-id>$/, "");
|
|
8271
8383
|
return body.trim();
|
|
8272
8384
|
}
|
|
8273
8385
|
function truncateMergedSummary(merged, maxLength) {
|
|
@@ -8381,55 +8493,6 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
|
|
|
8381
8493
|
const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
|
|
8382
8494
|
return { mergedCount: sourceBlocks.length, savedTokens };
|
|
8383
8495
|
}
|
|
8384
|
-
function estimateTokens(blocks) {
|
|
8385
|
-
return blocks.reduce(
|
|
8386
|
-
(sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
|
|
8387
|
-
0
|
|
8388
|
-
);
|
|
8389
|
-
}
|
|
8390
|
-
function buildNudgeText(state, maxMergedLength) {
|
|
8391
|
-
const marked = collectActiveMarkedBlocks(state);
|
|
8392
|
-
const oldGen = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8393
|
-
if (oldGen.length === 0) return void 0;
|
|
8394
|
-
const oldGenIds = new Set(oldGen.map((b) => b.blockId));
|
|
8395
|
-
const markedOldGen = marked.filter((b) => oldGenIds.has(b.blockId));
|
|
8396
|
-
const markedOldGenCount = markedOldGen.length;
|
|
8397
|
-
const oldGenCount = oldGen.length;
|
|
8398
|
-
const ratio = markedOldGenCount / oldGenCount;
|
|
8399
|
-
const ratioPct = Math.round(ratio * 100);
|
|
8400
|
-
const escalateMinPct = Math.round(ESCALATE_MIN_RATIO * 100);
|
|
8401
|
-
if (markedOldGenCount >= ESCALATE_MIN_MARKED && ratio >= ESCALATE_MIN_RATIO) {
|
|
8402
|
-
const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8403
|
-
const firstRef = formatBlockRef(marked[0].blockId);
|
|
8404
|
-
const lastRef = formatBlockRef(marked[marked.length - 1].blockId);
|
|
8405
|
-
const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
|
|
8406
|
-
return [
|
|
8407
|
-
`\u{1F525} ${markedOldGenCount}/${oldGenCount} old-gen blocks marked (${ratioPct}%) \u2014 ready for batch cleanup.`,
|
|
8408
|
-
`Compressing ${refs} (range ${firstRef}\u2013${lastRef}) would free ~${estimatedSavings} tokens in one cache break.`,
|
|
8409
|
-
`Call compress with this range now to consolidate them.`
|
|
8410
|
-
].join(" ");
|
|
8411
|
-
}
|
|
8412
|
-
if (marked.length >= 1) {
|
|
8413
|
-
const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8414
|
-
const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
|
|
8415
|
-
return [
|
|
8416
|
-
`\u26A0\uFE0F ${marked.length} block(s) marked for batch cleanup (${refs}).`,
|
|
8417
|
-
`Merge-compressing them would free ~${estimatedSavings} tokens.`,
|
|
8418
|
-
marked.length >= 2 ? "They will auto-merge when context pressure reaches the high threshold." : "A single marked block won't auto-merge on its own \u2014 use compress to consolidate it, or unmark_block if no longer needed.",
|
|
8419
|
-
`Mark more old-gen blocks (need \u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}%) to trigger batch cleanup sooner.`,
|
|
8420
|
-
"To act now, use compress with a range covering these blocks."
|
|
8421
|
-
].join(" ");
|
|
8422
|
-
}
|
|
8423
|
-
const shown = oldGen.slice(0, 5);
|
|
8424
|
-
const oldGenRefs = shown.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8425
|
-
const more = oldGenCount > 5 ? ` (+${oldGenCount - 5} more)` : "";
|
|
8426
|
-
return [
|
|
8427
|
-
`\u{1F4CB} Context pressure rising \u2014 ${oldGenCount} old-gen compressed block(s) occupy ~${estimateTokens(oldGen)} tokens (${oldGenRefs}${more}).`,
|
|
8428
|
-
`Review which blocks contain information you no longer need, and use mark_block to flag them.`,
|
|
8429
|
-
`Once enough are marked (\u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}% of old-gen), they'll be batch-merged in one cache break to preserve cache hit rate.`,
|
|
8430
|
-
`Do NOT mark blocks you may still need.`
|
|
8431
|
-
].join(" ");
|
|
8432
|
-
}
|
|
8433
8496
|
function runBatchCleanup(state, config, logger, messages) {
|
|
8434
8497
|
const noop = {
|
|
8435
8498
|
tier: 0,
|
|
@@ -8441,74 +8504,31 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
8441
8504
|
return noop;
|
|
8442
8505
|
}
|
|
8443
8506
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
8447
|
-
const forceTokens = percentToTokens(batchCleanup.forceThreshold, limit);
|
|
8448
|
-
const highTokens = percentToTokens(batchCleanup.highThreshold, limit);
|
|
8449
|
-
const lowTokens = percentToTokens(batchCleanup.lowThreshold, limit);
|
|
8450
|
-
if (currentTokens >= forceTokens) {
|
|
8451
|
-
const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8452
|
-
if (oldGenBlocks.length < 2) {
|
|
8453
|
-
return noop;
|
|
8454
|
-
}
|
|
8455
|
-
const ids = oldGenBlocks.map((b) => b.blockId);
|
|
8456
|
-
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8457
|
-
if (result.mergedCount === 0) {
|
|
8458
|
-
return noop;
|
|
8459
|
-
}
|
|
8460
|
-
logger.info("Batch cleanup tier 3 (force): merged old-gen blocks", {
|
|
8461
|
-
mergedCount: result.mergedCount,
|
|
8462
|
-
savedTokens: result.savedTokens,
|
|
8463
|
-
currentTokens,
|
|
8464
|
-
forceThreshold: batchCleanup.forceThreshold
|
|
8465
|
-
});
|
|
8466
|
-
return {
|
|
8467
|
-
tier: 3,
|
|
8468
|
-
action: "merge",
|
|
8469
|
-
mergedCount: result.mergedCount,
|
|
8470
|
-
savedTokens: result.savedTokens
|
|
8471
|
-
};
|
|
8507
|
+
if (currentTokens < state.modelContextLimit) {
|
|
8508
|
+
return noop;
|
|
8472
8509
|
}
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8478
|
-
if (result.mergedCount > 0) {
|
|
8479
|
-
logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
|
|
8480
|
-
mergedCount: result.mergedCount,
|
|
8481
|
-
savedTokens: result.savedTokens,
|
|
8482
|
-
currentTokens,
|
|
8483
|
-
highThreshold: batchCleanup.highThreshold
|
|
8484
|
-
});
|
|
8485
|
-
return {
|
|
8486
|
-
tier: 2,
|
|
8487
|
-
action: "merge",
|
|
8488
|
-
mergedCount: result.mergedCount,
|
|
8489
|
-
savedTokens: result.savedTokens
|
|
8490
|
-
};
|
|
8491
|
-
}
|
|
8492
|
-
}
|
|
8510
|
+
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
8511
|
+
const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8512
|
+
if (oldGenBlocks.length < 2) {
|
|
8513
|
+
return noop;
|
|
8493
8514
|
}
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
}
|
|
8499
|
-
logger.info("Batch cleanup tier 1 (low): nudge injected", {
|
|
8500
|
-
currentTokens,
|
|
8501
|
-
lowThreshold: batchCleanup.lowThreshold
|
|
8502
|
-
});
|
|
8503
|
-
return {
|
|
8504
|
-
tier: 1,
|
|
8505
|
-
action: "nudge",
|
|
8506
|
-
mergedCount: 0,
|
|
8507
|
-
savedTokens: 0,
|
|
8508
|
-
nudgeText
|
|
8509
|
-
};
|
|
8515
|
+
const ids = oldGenBlocks.map((b) => b.blockId);
|
|
8516
|
+
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8517
|
+
if (result.mergedCount === 0) {
|
|
8518
|
+
return noop;
|
|
8510
8519
|
}
|
|
8511
|
-
|
|
8520
|
+
logger.info("Batch cleanup force fallback (100%): merged old-gen blocks", {
|
|
8521
|
+
mergedCount: result.mergedCount,
|
|
8522
|
+
savedTokens: result.savedTokens,
|
|
8523
|
+
currentTokens,
|
|
8524
|
+
contextLimit: state.modelContextLimit
|
|
8525
|
+
});
|
|
8526
|
+
return {
|
|
8527
|
+
tier: 3,
|
|
8528
|
+
action: "merge",
|
|
8529
|
+
mergedCount: result.mergedCount,
|
|
8530
|
+
savedTokens: result.savedTokens
|
|
8531
|
+
};
|
|
8512
8532
|
}
|
|
8513
8533
|
|
|
8514
8534
|
// lib/hooks.ts
|
|
@@ -8619,11 +8639,6 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8619
8639
|
void saveSessionState(state, logger);
|
|
8620
8640
|
}
|
|
8621
8641
|
}
|
|
8622
|
-
function appendBatchCleanupNudge(messages, nudgeText) {
|
|
8623
|
-
const lastUser = getLastUserMessage(messages);
|
|
8624
|
-
if (!lastUser) return;
|
|
8625
|
-
appendToLastTextPart(lastUser, nudgeText);
|
|
8626
|
-
}
|
|
8627
8642
|
function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
|
|
8628
8643
|
return async (input, output) => {
|
|
8629
8644
|
const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
|
|
@@ -8655,9 +8670,6 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8655
8670
|
buildToolIdList(state, output.messages);
|
|
8656
8671
|
runMajorGC(state, config, logger, output.messages);
|
|
8657
8672
|
const batchResult = runBatchCleanup(state, config, logger, output.messages);
|
|
8658
|
-
if (batchResult.tier === 1 && batchResult.nudgeText) {
|
|
8659
|
-
appendBatchCleanupNudge(output.messages, batchResult.nudgeText);
|
|
8660
|
-
}
|
|
8661
8673
|
if (batchResult.mergedCount > 0) {
|
|
8662
8674
|
void saveSessionState(state, logger);
|
|
8663
8675
|
}
|
|
@@ -9079,9 +9091,7 @@ var server = (async (ctx) => {
|
|
|
9079
9091
|
tool: {
|
|
9080
9092
|
...config.compress.permission !== "deny" && {
|
|
9081
9093
|
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|
|
9082
|
-
decompress: createDecompressTool(compressToolContext)
|
|
9083
|
-
mark_block: createMarkBlockTool(compressToolContext),
|
|
9084
|
-
unmark_block: createUnmarkBlockTool(compressToolContext)
|
|
9094
|
+
decompress: createDecompressTool(compressToolContext)
|
|
9085
9095
|
}
|
|
9086
9096
|
},
|
|
9087
9097
|
config: async (opencodeConfig) => {
|
|
@@ -9097,7 +9107,7 @@ var server = (async (ctx) => {
|
|
|
9097
9107
|
}
|
|
9098
9108
|
const toolsToAdd = [];
|
|
9099
9109
|
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
|
|
9100
|
-
toolsToAdd.push("compress", "decompress"
|
|
9110
|
+
toolsToAdd.push("compress", "decompress");
|
|
9101
9111
|
}
|
|
9102
9112
|
if (toolsToAdd.length > 0) {
|
|
9103
9113
|
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
|