opencode-acp 1.5.1 → 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 +291 -330
- 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 +3 -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/prune.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 +6 -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/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
|
@@ -900,6 +900,9 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
900
900
|
"compress.protectedTools",
|
|
901
901
|
"compress.protectTags",
|
|
902
902
|
"compress.protectUserMessages",
|
|
903
|
+
"compress.maxSummaryLength",
|
|
904
|
+
"compress.maxSummaryLengthHard",
|
|
905
|
+
"compress.minCompressRange",
|
|
903
906
|
"gc",
|
|
904
907
|
"gc.algorithm",
|
|
905
908
|
"gc.promotionThreshold",
|
|
@@ -1160,6 +1163,55 @@ function validateConfigTypes(config) {
|
|
|
1160
1163
|
actual: typeof compress.protectUserMessages
|
|
1161
1164
|
});
|
|
1162
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
|
+
}
|
|
1163
1215
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1164
1216
|
errors.push({
|
|
1165
1217
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1393,8 +1445,6 @@ var DEFAULT_PROTECTED_TOOLS = [
|
|
|
1393
1445
|
"todoread",
|
|
1394
1446
|
"compress",
|
|
1395
1447
|
"decompress",
|
|
1396
|
-
"mark_block",
|
|
1397
|
-
"unmark_block",
|
|
1398
1448
|
"batch",
|
|
1399
1449
|
"plan_enter",
|
|
1400
1450
|
"plan_exit",
|
|
@@ -1474,7 +1524,10 @@ var defaultConfig = {
|
|
|
1474
1524
|
nudgeForce: "soft",
|
|
1475
1525
|
protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS],
|
|
1476
1526
|
protectTags: false,
|
|
1477
|
-
protectUserMessages: false
|
|
1527
|
+
protectUserMessages: false,
|
|
1528
|
+
maxSummaryLength: 200,
|
|
1529
|
+
maxSummaryLengthHard: 800,
|
|
1530
|
+
minCompressRange: 2e3
|
|
1478
1531
|
},
|
|
1479
1532
|
strategies: {
|
|
1480
1533
|
deduplication: {
|
|
@@ -1626,7 +1679,10 @@ function mergeCompress(base, override) {
|
|
|
1626
1679
|
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
1627
1680
|
protectedTools: [.../* @__PURE__ */ new Set([...base.protectedTools, ...override.protectedTools ?? []])],
|
|
1628
1681
|
protectTags: override.protectTags ?? base.protectTags,
|
|
1629
|
-
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
|
|
1630
1686
|
};
|
|
1631
1687
|
}
|
|
1632
1688
|
function mergeCommands(base, override) {
|
|
@@ -1990,6 +2046,20 @@ function countAllMessageTokens(msg) {
|
|
|
1990
2046
|
if (texts.length === 0) return 0;
|
|
1991
2047
|
return estimateTokensBatch(texts);
|
|
1992
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
|
+
}
|
|
1993
2063
|
|
|
1994
2064
|
// lib/prompts/extensions/tool.ts
|
|
1995
2065
|
var RANGE_FORMAT_EXTENSION = `
|
|
@@ -3550,20 +3620,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
3550
3620
|
regex += "$";
|
|
3551
3621
|
return new RegExp(regex).test(input);
|
|
3552
3622
|
}
|
|
3553
|
-
function getFilePathsFromParameters(
|
|
3623
|
+
function getFilePathsFromParameters(tool4, parameters) {
|
|
3554
3624
|
if (typeof parameters !== "object" || parameters === null) {
|
|
3555
3625
|
return [];
|
|
3556
3626
|
}
|
|
3557
3627
|
const paths = [];
|
|
3558
3628
|
const params = parameters;
|
|
3559
|
-
if (
|
|
3629
|
+
if (tool4 === "apply_patch" && typeof params.patchText === "string") {
|
|
3560
3630
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3561
3631
|
let match;
|
|
3562
3632
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
3563
3633
|
paths.push(match[1].trim());
|
|
3564
3634
|
}
|
|
3565
3635
|
}
|
|
3566
|
-
if (
|
|
3636
|
+
if (tool4 === "multiedit") {
|
|
3567
3637
|
if (typeof params.filePath === "string") {
|
|
3568
3638
|
paths.push(params.filePath);
|
|
3569
3639
|
}
|
|
@@ -3658,13 +3728,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
3658
3728
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
3659
3729
|
}
|
|
3660
3730
|
};
|
|
3661
|
-
function createToolSignature(
|
|
3731
|
+
function createToolSignature(tool4, parameters) {
|
|
3662
3732
|
if (!parameters) {
|
|
3663
|
-
return
|
|
3733
|
+
return tool4;
|
|
3664
3734
|
}
|
|
3665
3735
|
const normalized = normalizeParameters(parameters);
|
|
3666
3736
|
const sorted = sortObjectKeys(normalized);
|
|
3667
|
-
return `${
|
|
3737
|
+
return `${tool4}::${JSON.stringify(sorted)}`;
|
|
3668
3738
|
}
|
|
3669
3739
|
function normalizeParameters(params) {
|
|
3670
3740
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -3739,9 +3809,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
3739
3809
|
};
|
|
3740
3810
|
|
|
3741
3811
|
// lib/ui/utils.ts
|
|
3742
|
-
function extractParameterKey(
|
|
3812
|
+
function extractParameterKey(tool4, parameters) {
|
|
3743
3813
|
if (!parameters) return "";
|
|
3744
|
-
if (
|
|
3814
|
+
if (tool4 === "read" && parameters.filePath) {
|
|
3745
3815
|
const offset = parameters.offset;
|
|
3746
3816
|
const limit = parameters.limit;
|
|
3747
3817
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -3755,10 +3825,10 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3755
3825
|
}
|
|
3756
3826
|
return parameters.filePath;
|
|
3757
3827
|
}
|
|
3758
|
-
if ((
|
|
3828
|
+
if ((tool4 === "write" || tool4 === "edit" || tool4 === "multiedit") && parameters.filePath) {
|
|
3759
3829
|
return parameters.filePath;
|
|
3760
3830
|
}
|
|
3761
|
-
if (
|
|
3831
|
+
if (tool4 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
3762
3832
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3763
3833
|
const paths = [];
|
|
3764
3834
|
let match;
|
|
@@ -3775,51 +3845,51 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3775
3845
|
}
|
|
3776
3846
|
return "patch";
|
|
3777
3847
|
}
|
|
3778
|
-
if (
|
|
3848
|
+
if (tool4 === "list") {
|
|
3779
3849
|
return parameters.path || "(current directory)";
|
|
3780
3850
|
}
|
|
3781
|
-
if (
|
|
3851
|
+
if (tool4 === "glob") {
|
|
3782
3852
|
if (parameters.pattern) {
|
|
3783
3853
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3784
3854
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3785
3855
|
}
|
|
3786
3856
|
return "(unknown pattern)";
|
|
3787
3857
|
}
|
|
3788
|
-
if (
|
|
3858
|
+
if (tool4 === "grep") {
|
|
3789
3859
|
if (parameters.pattern) {
|
|
3790
3860
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3791
3861
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3792
3862
|
}
|
|
3793
3863
|
return "(unknown pattern)";
|
|
3794
3864
|
}
|
|
3795
|
-
if (
|
|
3865
|
+
if (tool4 === "bash") {
|
|
3796
3866
|
if (parameters.description) return parameters.description;
|
|
3797
3867
|
if (parameters.command) {
|
|
3798
3868
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
3799
3869
|
}
|
|
3800
3870
|
}
|
|
3801
|
-
if (
|
|
3871
|
+
if (tool4 === "webfetch" && parameters.url) {
|
|
3802
3872
|
return parameters.url;
|
|
3803
3873
|
}
|
|
3804
|
-
if (
|
|
3874
|
+
if (tool4 === "websearch" && parameters.query) {
|
|
3805
3875
|
return `"${parameters.query}"`;
|
|
3806
3876
|
}
|
|
3807
|
-
if (
|
|
3877
|
+
if (tool4 === "codesearch" && parameters.query) {
|
|
3808
3878
|
return `"${parameters.query}"`;
|
|
3809
3879
|
}
|
|
3810
|
-
if (
|
|
3880
|
+
if (tool4 === "todowrite") {
|
|
3811
3881
|
return `${parameters.todos?.length || 0} todos`;
|
|
3812
3882
|
}
|
|
3813
|
-
if (
|
|
3883
|
+
if (tool4 === "todoread") {
|
|
3814
3884
|
return "read todo list";
|
|
3815
3885
|
}
|
|
3816
|
-
if (
|
|
3886
|
+
if (tool4 === "task" && parameters.description) {
|
|
3817
3887
|
return parameters.description;
|
|
3818
3888
|
}
|
|
3819
|
-
if (
|
|
3889
|
+
if (tool4 === "skill" && parameters.name) {
|
|
3820
3890
|
return parameters.name;
|
|
3821
3891
|
}
|
|
3822
|
-
if (
|
|
3892
|
+
if (tool4 === "lsp") {
|
|
3823
3893
|
const op = parameters.operation || "lsp";
|
|
3824
3894
|
const path = parameters.filePath || "";
|
|
3825
3895
|
const line = parameters.line;
|
|
@@ -3832,7 +3902,7 @@ function extractParameterKey(tool5, parameters) {
|
|
|
3832
3902
|
}
|
|
3833
3903
|
return op;
|
|
3834
3904
|
}
|
|
3835
|
-
if (
|
|
3905
|
+
if (tool4 === "question") {
|
|
3836
3906
|
const questions = parameters.questions;
|
|
3837
3907
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
3838
3908
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4411,7 +4481,7 @@ ${output}`);
|
|
|
4411
4481
|
}
|
|
4412
4482
|
|
|
4413
4483
|
// lib/compress/message.ts
|
|
4414
|
-
function buildSchema() {
|
|
4484
|
+
function buildSchema(maxSummaryLength) {
|
|
4415
4485
|
return {
|
|
4416
4486
|
topic: tool.schema.string().describe(
|
|
4417
4487
|
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'"
|
|
@@ -4420,7 +4490,9 @@ function buildSchema() {
|
|
|
4420
4490
|
tool.schema.object({
|
|
4421
4491
|
messageId: tool.schema.string().describe("Raw message ID to compress (e.g. m00001)"),
|
|
4422
4492
|
topic: tool.schema.string().describe("Short label (3-5 words) for this one message summary"),
|
|
4423
|
-
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
|
+
)
|
|
4424
4496
|
})
|
|
4425
4497
|
).describe("Batch of individual message summaries to create in one tool call")
|
|
4426
4498
|
};
|
|
@@ -4430,10 +4502,18 @@ function createCompressMessageTool(ctx) {
|
|
|
4430
4502
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4431
4503
|
return tool({
|
|
4432
4504
|
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
4433
|
-
args: buildSchema(),
|
|
4505
|
+
args: buildSchema(ctx.config.compress.maxSummaryLength),
|
|
4434
4506
|
async execute(args, toolCtx) {
|
|
4435
4507
|
const input = args;
|
|
4436
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
|
+
}
|
|
4437
4517
|
const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
|
|
4438
4518
|
const { rawMessages, searchContext } = await prepareSession(
|
|
4439
4519
|
ctx,
|
|
@@ -4449,6 +4529,26 @@ function createCompressMessageTool(ctx) {
|
|
|
4449
4529
|
if (plans.length === 0 && skippedCount > 0) {
|
|
4450
4530
|
throw new Error(formatIssues(skippedIssues, skippedCount));
|
|
4451
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
|
+
}
|
|
4452
4552
|
const notifications = [];
|
|
4453
4553
|
const preparedPlans = [];
|
|
4454
4554
|
for (const plan of plans) {
|
|
@@ -4633,7 +4733,13 @@ function validateSummaryPlaceholders(placeholders, requiredBlockIds, startRefere
|
|
|
4633
4733
|
}
|
|
4634
4734
|
placeholders.length = 0;
|
|
4635
4735
|
placeholders.push(...validPlaceholders);
|
|
4636
|
-
|
|
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;
|
|
4637
4743
|
}
|
|
4638
4744
|
function injectBlockPlaceholders(summary, _placeholders, _summaryByBlockId, _startReference, _endReference) {
|
|
4639
4745
|
return {
|
|
@@ -4649,7 +4755,7 @@ function appendMissingBlockSummaries(summary, _missingBlockIds, _summaryByBlockI
|
|
|
4649
4755
|
}
|
|
4650
4756
|
|
|
4651
4757
|
// lib/compress/range.ts
|
|
4652
|
-
function buildSchema2() {
|
|
4758
|
+
function buildSchema2(maxSummaryLength) {
|
|
4653
4759
|
return {
|
|
4654
4760
|
topic: tool2.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
4655
4761
|
content: tool2.schema.array(
|
|
@@ -4658,7 +4764,9 @@ function buildSchema2() {
|
|
|
4658
4764
|
"Message or block ID marking the beginning of range (e.g. m00001, b2)"
|
|
4659
4765
|
),
|
|
4660
4766
|
endId: tool2.schema.string().describe("Message or block ID marking the end of range (e.g. m00012, b5)"),
|
|
4661
|
-
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
|
+
)
|
|
4662
4770
|
})
|
|
4663
4771
|
).describe(
|
|
4664
4772
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
@@ -4670,10 +4778,18 @@ function createCompressRangeTool(ctx) {
|
|
|
4670
4778
|
const runtimePrompts = ctx.prompts.getRuntimePrompts();
|
|
4671
4779
|
return tool2({
|
|
4672
4780
|
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
|
|
4673
|
-
args: buildSchema2(),
|
|
4781
|
+
args: buildSchema2(ctx.config.compress.maxSummaryLength),
|
|
4674
4782
|
async execute(args, toolCtx) {
|
|
4675
4783
|
const input = args;
|
|
4676
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
|
+
}
|
|
4677
4793
|
const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
|
|
4678
4794
|
const { rawMessages, searchContext } = await prepareSession(
|
|
4679
4795
|
ctx,
|
|
@@ -4682,6 +4798,26 @@ function createCompressRangeTool(ctx) {
|
|
|
4682
4798
|
);
|
|
4683
4799
|
const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
|
|
4684
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
|
+
}
|
|
4685
4821
|
const notifications = [];
|
|
4686
4822
|
const preparedPlans = [];
|
|
4687
4823
|
let totalCompressedMessages = 0;
|
|
@@ -4731,10 +4867,19 @@ function createCompressRangeTool(ctx) {
|
|
|
4731
4867
|
searchContext.summaryByBlockId,
|
|
4732
4868
|
injected.consumedBlockIds
|
|
4733
4869
|
);
|
|
4734
|
-
const
|
|
4870
|
+
const boundaryConsumed = extractBoundaryConsumedBlocks(
|
|
4735
4871
|
plan.selection.startReference,
|
|
4736
4872
|
plan.selection.endReference
|
|
4737
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
|
+
});
|
|
4738
4883
|
preparedPlans.push({
|
|
4739
4884
|
entry: plan.entry,
|
|
4740
4885
|
selection: plan.selection,
|
|
@@ -4981,6 +5126,36 @@ var stripHallucinations = (messages) => {
|
|
|
4981
5126
|
// lib/messages/prune.ts
|
|
4982
5127
|
var prune = (state, logger, config, messages) => {
|
|
4983
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
|
+
}
|
|
4984
5159
|
};
|
|
4985
5160
|
var filterCompressedRanges = (state, logger, config, messages) => {
|
|
4986
5161
|
if (state.prune.messages.byMessageId.size === 0 && state.prune.messages.activeByAnchorMessageId.size === 0) {
|
|
@@ -5217,8 +5392,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
5217
5392
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5218
5393
|
) ? "deny" : basePermission;
|
|
5219
5394
|
};
|
|
5220
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
5221
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
5395
|
+
var hasExplicitToolPermission = (permissionConfig, tool4) => {
|
|
5396
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool4) : false;
|
|
5222
5397
|
};
|
|
5223
5398
|
|
|
5224
5399
|
// lib/compress-permission.ts
|
|
@@ -5250,13 +5425,42 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
|
|
|
5250
5425
|
const lines = [
|
|
5251
5426
|
"Compressed block context:",
|
|
5252
5427
|
`- Active compressed blocks: ${blockCount} (${blockList})`,
|
|
5253
|
-
"-
|
|
5428
|
+
"- System auto-detects blocks in range \u2014 no need to manually list (bN) placeholders. Just write a short prose summary."
|
|
5254
5429
|
];
|
|
5255
5430
|
if (includeHint) {
|
|
5256
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.");
|
|
5257
5432
|
}
|
|
5258
5433
|
if (blockCount > 50) {
|
|
5259
|
-
|
|
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
|
+
}
|
|
5260
5464
|
}
|
|
5261
5465
|
const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
|
|
5262
5466
|
if (gcConfig && usageRatio > 0.5) {
|
|
@@ -5617,11 +5821,11 @@ ${base}`;
|
|
|
5617
5821
|
}
|
|
5618
5822
|
let guidance;
|
|
5619
5823
|
if (pct < minPct) {
|
|
5620
|
-
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.";
|
|
5621
5825
|
} else if (pct < maxPct) {
|
|
5622
|
-
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.";
|
|
5623
5827
|
} else {
|
|
5624
|
-
guidance = " \u{1F525} Context is high \u2014 compress aggressively
|
|
5828
|
+
guidance = " \u{1F525} Context is high \u2014 compress aggressively, preserve only what is essential.";
|
|
5625
5829
|
}
|
|
5626
5830
|
return `
|
|
5627
5831
|
|
|
@@ -5818,7 +6022,15 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
5818
6022
|
const shouldNudge = shouldInjectPerMessageNudge(state, config, currentTokens, modelContextLimit);
|
|
5819
6023
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit, !shouldNudge);
|
|
5820
6024
|
if (config.compress.mode !== "message") {
|
|
5821
|
-
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
|
+
});
|
|
5822
6034
|
if (blockGuidance.trim() && suffixMessage) {
|
|
5823
6035
|
appendToLastTextPart(suffixMessage, "\n\n" + blockGuidance);
|
|
5824
6036
|
}
|
|
@@ -6365,109 +6577,6 @@ function createDecompressTool(ctx) {
|
|
|
6365
6577
|
});
|
|
6366
6578
|
}
|
|
6367
6579
|
|
|
6368
|
-
// lib/compress/mark-block.ts
|
|
6369
|
-
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
6370
|
-
async function prepareMarkSession(ctx, toolCtx) {
|
|
6371
|
-
await toolCtx.ask({
|
|
6372
|
-
permission: "compress",
|
|
6373
|
-
patterns: ["*"],
|
|
6374
|
-
always: ["*"],
|
|
6375
|
-
metadata: {}
|
|
6376
|
-
});
|
|
6377
|
-
toolCtx.metadata({ title: "Mark block" });
|
|
6378
|
-
const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
6379
|
-
await ensureSessionInitialized(
|
|
6380
|
-
ctx.client,
|
|
6381
|
-
ctx.state,
|
|
6382
|
-
toolCtx.sessionID,
|
|
6383
|
-
ctx.logger,
|
|
6384
|
-
rawMessages,
|
|
6385
|
-
ctx.config.manualMode.enabled
|
|
6386
|
-
);
|
|
6387
|
-
assignMessageRefs(ctx.state, rawMessages);
|
|
6388
|
-
}
|
|
6389
|
-
var MARK_DESCRIPTION = `Marks a compressed block for batch merge-cleanup.
|
|
6390
|
-
|
|
6391
|
-
Use this for blocks whose detailed content you no longer need, but whose summaries
|
|
6392
|
-
you want to keep in context for now (to preserve prompt cache). Marked blocks stay
|
|
6393
|
-
fully active with zero immediate effect on context or cache. When context pressure
|
|
6394
|
-
rises, all marked blocks are merge-compressed together into a single summary in one
|
|
6395
|
-
cache break, instead of being handled one at a time.
|
|
6396
|
-
|
|
6397
|
-
Argument: blockId \u2014 the block reference to mark (e.g., "b1", "b3")
|
|
6398
|
-
|
|
6399
|
-
Use mark_block instead of compress when you want deferred cleanup: the block keeps
|
|
6400
|
-
serving cache hits now and gets consolidated later only if context gets tight.`;
|
|
6401
|
-
var UNMARK_DESCRIPTION = `Removes the batch cleanup mark from a compressed block.
|
|
6402
|
-
|
|
6403
|
-
Reverses mark_block. The block returns to normal handling and will not be
|
|
6404
|
-
auto-merged during batch cleanup.
|
|
6405
|
-
|
|
6406
|
-
Argument: blockId \u2014 the block reference to unmark (e.g., "b1", "b3")`;
|
|
6407
|
-
function buildSchema4() {
|
|
6408
|
-
return {
|
|
6409
|
-
blockId: tool4.schema.string().describe('Block reference to mark (e.g., "b1", "b3")')
|
|
6410
|
-
};
|
|
6411
|
-
}
|
|
6412
|
-
function buildUnmarkSchema() {
|
|
6413
|
-
return {
|
|
6414
|
-
blockId: tool4.schema.string().describe('Block reference to unmark (e.g., "b1", "b3")')
|
|
6415
|
-
};
|
|
6416
|
-
}
|
|
6417
|
-
function createMarkBlockTool(ctx) {
|
|
6418
|
-
return tool4({
|
|
6419
|
-
description: MARK_DESCRIPTION,
|
|
6420
|
-
args: buildSchema4(),
|
|
6421
|
-
async execute(args, toolCtx) {
|
|
6422
|
-
await prepareMarkSession(ctx, toolCtx);
|
|
6423
|
-
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6424
|
-
if (targetBlockId === null) {
|
|
6425
|
-
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6426
|
-
}
|
|
6427
|
-
const messagesState = ctx.state.prune.messages;
|
|
6428
|
-
const block = messagesState.blocksById.get(targetBlockId);
|
|
6429
|
-
if (!block) {
|
|
6430
|
-
return `Error: Block ${formatBlockRef(targetBlockId)} does not exist.`;
|
|
6431
|
-
}
|
|
6432
|
-
if (!block.active) {
|
|
6433
|
-
return `Error: Block ${formatBlockRef(targetBlockId)} is not active.`;
|
|
6434
|
-
}
|
|
6435
|
-
messagesState.markedForCleanup.add(targetBlockId);
|
|
6436
|
-
await saveSessionState(ctx.state, ctx.logger);
|
|
6437
|
-
const ref = formatBlockRef(targetBlockId);
|
|
6438
|
-
const markedCount = messagesState.markedForCleanup.size;
|
|
6439
|
-
ctx.logger.info("mark_block: block marked for cleanup", {
|
|
6440
|
-
blockId: targetBlockId,
|
|
6441
|
-
markedCount
|
|
6442
|
-
});
|
|
6443
|
-
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.)`;
|
|
6444
|
-
}
|
|
6445
|
-
});
|
|
6446
|
-
}
|
|
6447
|
-
function createUnmarkBlockTool(ctx) {
|
|
6448
|
-
return tool4({
|
|
6449
|
-
description: UNMARK_DESCRIPTION,
|
|
6450
|
-
args: buildUnmarkSchema(),
|
|
6451
|
-
async execute(args, toolCtx) {
|
|
6452
|
-
await prepareMarkSession(ctx, toolCtx);
|
|
6453
|
-
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6454
|
-
if (targetBlockId === null) {
|
|
6455
|
-
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6456
|
-
}
|
|
6457
|
-
const messagesState = ctx.state.prune.messages;
|
|
6458
|
-
if (!messagesState.markedForCleanup.has(targetBlockId)) {
|
|
6459
|
-
return `Block ${formatBlockRef(targetBlockId)} was not marked for cleanup.`;
|
|
6460
|
-
}
|
|
6461
|
-
messagesState.markedForCleanup.delete(targetBlockId);
|
|
6462
|
-
await saveSessionState(ctx.state, ctx.logger);
|
|
6463
|
-
ctx.logger.info("unmark_block: block unmarked", {
|
|
6464
|
-
blockId: targetBlockId
|
|
6465
|
-
});
|
|
6466
|
-
return `Block ${formatBlockRef(targetBlockId)} unmarked. It will no longer be auto-merged during batch cleanup.`;
|
|
6467
|
-
}
|
|
6468
|
-
});
|
|
6469
|
-
}
|
|
6470
|
-
|
|
6471
6580
|
// lib/logger.ts
|
|
6472
6581
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
6473
6582
|
import { join as join3 } from "path";
|
|
@@ -6676,7 +6785,7 @@ var SYSTEM = `
|
|
|
6676
6785
|
|
|
6677
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.
|
|
6678
6787
|
|
|
6679
|
-
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.
|
|
6680
6789
|
|
|
6681
6790
|
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
6682
6791
|
|
|
@@ -6690,9 +6799,9 @@ Target the largest UNCOMPRESSED content first. Savings scale with original size
|
|
|
6690
6799
|
|
|
6691
6800
|
CONTEXT PRESSURE LEVELS
|
|
6692
6801
|
|
|
6693
|
-
- Normal: Be frugal \u2014 compress
|
|
6694
|
-
- Elevated: Context is growing
|
|
6695
|
-
- 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.
|
|
6696
6805
|
|
|
6697
6806
|
WHAT TO COMPRESS FIRST (high value, low risk)
|
|
6698
6807
|
|
|
@@ -6756,33 +6865,18 @@ Directly quote user messages when they are short enough to include safely. Direc
|
|
|
6756
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.
|
|
6757
6866
|
|
|
6758
6867
|
COMPRESSED BLOCK PLACEHOLDERS
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
- \`(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.
|
|
6762
6869
|
|
|
6763
6870
|
Compressed block sections in context are clearly marked with a header:
|
|
6764
6871
|
|
|
6765
6872
|
- \`[Compressed conversation section]\`
|
|
6766
6873
|
|
|
6767
|
-
Compressed block IDs always use the \`bN\` form (never \`mNNNNN\`) and are represented in the same XML metadata tag format.
|
|
6768
|
-
|
|
6769
6874
|
Rules:
|
|
6770
6875
|
|
|
6771
|
-
-
|
|
6876
|
+
- Write a short prose summary. The system handles block consumption automatically.
|
|
6772
6877
|
- Do not invent placeholders for blocks outside the selected range.
|
|
6773
|
-
- Treat \`(bN)\`
|
|
6774
|
-
- If you need to mention a block in prose, use plain text like \`compressed bN\` (
|
|
6775
|
-
- Preflight check before finalizing: the set of \`(bN)\` placeholders in your summary must exactly match the required set, with no duplicates.
|
|
6776
|
-
|
|
6777
|
-
These placeholders are semantic references. They will be replaced with the full stored compressed block content when the tool processes your output.
|
|
6778
|
-
|
|
6779
|
-
FLOW PRESERVATION WITH PLACEHOLDERS
|
|
6780
|
-
When you use compressed block placeholders, write the surrounding summary text so it still reads correctly AFTER placeholder expansion.
|
|
6781
|
-
|
|
6782
|
-
- Treat each placeholder as a stand-in for a full conversation segment, not as a short label.
|
|
6783
|
-
- Ensure transitions before and after each placeholder preserve chronology and causality.
|
|
6784
|
-
- Do not write text that depends on the placeholder staying literal (for example, "as noted in \`(b2)\`").
|
|
6785
|
-
- 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).
|
|
6786
6880
|
|
|
6787
6881
|
BOUNDARY IDS
|
|
6788
6882
|
You specify boundaries by ID using the injected IDs visible in the conversation:
|
|
@@ -7707,7 +7801,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
7707
7801
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
7708
7802
|
"Return after compress with a brief explanation of what content was compressed."
|
|
7709
7803
|
].join("\n\n");
|
|
7710
|
-
function getTriggerPrompt(
|
|
7804
|
+
function getTriggerPrompt(tool4, state, config, userFocus) {
|
|
7711
7805
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
7712
7806
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
7713
7807
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -7736,8 +7830,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
7736
7830
|
);
|
|
7737
7831
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
7738
7832
|
}
|
|
7739
|
-
async function handleManualTriggerCommand(ctx,
|
|
7740
|
-
return getTriggerPrompt(
|
|
7833
|
+
async function handleManualTriggerCommand(ctx, tool4, userFocus) {
|
|
7834
|
+
return getTriggerPrompt(tool4, ctx.state, ctx.config, userFocus);
|
|
7741
7835
|
}
|
|
7742
7836
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
7743
7837
|
const pending = state.pendingManualTrigger;
|
|
@@ -8267,23 +8361,6 @@ function parseGcThreshold(limit, modelContextLimit) {
|
|
|
8267
8361
|
}
|
|
8268
8362
|
|
|
8269
8363
|
// lib/gc/merge.ts
|
|
8270
|
-
var DEFAULT_BATCH_CLEANUP = {
|
|
8271
|
-
lowThreshold: "55%",
|
|
8272
|
-
highThreshold: "75%",
|
|
8273
|
-
forceThreshold: "90%"
|
|
8274
|
-
};
|
|
8275
|
-
var ESCALATE_MIN_MARKED = 3;
|
|
8276
|
-
var ESCALATE_MIN_RATIO = 0.4;
|
|
8277
|
-
function resolveBatchCleanup(gc) {
|
|
8278
|
-
return gc.batchCleanup ?? DEFAULT_BATCH_CLEANUP;
|
|
8279
|
-
}
|
|
8280
|
-
function percentToTokens(value, modelContextLimit) {
|
|
8281
|
-
if (typeof value === "number") return value;
|
|
8282
|
-
const percent = parseFloat(value.slice(0, -1));
|
|
8283
|
-
if (isNaN(percent)) return modelContextLimit;
|
|
8284
|
-
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
8285
|
-
return Math.round(clamped / 100 * modelContextLimit);
|
|
8286
|
-
}
|
|
8287
8364
|
function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
|
|
8288
8365
|
const blocks = [];
|
|
8289
8366
|
const ids = Array.from(state.prune.messages.activeBlockIds).sort((a, b) => a - b);
|
|
@@ -8296,27 +8373,13 @@ function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
|
|
|
8296
8373
|
}
|
|
8297
8374
|
return blocks;
|
|
8298
8375
|
}
|
|
8299
|
-
function collectActiveMarkedBlocks(state) {
|
|
8300
|
-
const messagesState = state.prune.messages;
|
|
8301
|
-
const ids = Array.from(messagesState.markedForCleanup).sort((a, b) => a - b);
|
|
8302
|
-
const blocks = [];
|
|
8303
|
-
for (const id of ids) {
|
|
8304
|
-
const block = messagesState.blocksById.get(id);
|
|
8305
|
-
if (!block || !block.active) {
|
|
8306
|
-
messagesState.markedForCleanup.delete(id);
|
|
8307
|
-
continue;
|
|
8308
|
-
}
|
|
8309
|
-
blocks.push(block);
|
|
8310
|
-
}
|
|
8311
|
-
return blocks;
|
|
8312
|
-
}
|
|
8313
8376
|
function extractSummaryBody(summary) {
|
|
8314
8377
|
let body = summary;
|
|
8315
8378
|
const headerPrefix = COMPRESSED_BLOCK_HEADER + "\n";
|
|
8316
8379
|
if (body.startsWith(headerPrefix)) {
|
|
8317
8380
|
body = body.slice(headerPrefix.length);
|
|
8318
8381
|
}
|
|
8319
|
-
body = body.replace(/\n
|
|
8382
|
+
body = body.replace(/\n]*>b\d+<\/dcp-message-id>$/, "");
|
|
8320
8383
|
return body.trim();
|
|
8321
8384
|
}
|
|
8322
8385
|
function truncateMergedSummary(merged, maxLength) {
|
|
@@ -8430,55 +8493,6 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
|
|
|
8430
8493
|
const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
|
|
8431
8494
|
return { mergedCount: sourceBlocks.length, savedTokens };
|
|
8432
8495
|
}
|
|
8433
|
-
function estimateTokens(blocks) {
|
|
8434
|
-
return blocks.reduce(
|
|
8435
|
-
(sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
|
|
8436
|
-
0
|
|
8437
|
-
);
|
|
8438
|
-
}
|
|
8439
|
-
function buildNudgeText(state, maxMergedLength) {
|
|
8440
|
-
const marked = collectActiveMarkedBlocks(state);
|
|
8441
|
-
const oldGen = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8442
|
-
if (oldGen.length === 0) return void 0;
|
|
8443
|
-
const oldGenIds = new Set(oldGen.map((b) => b.blockId));
|
|
8444
|
-
const markedOldGen = marked.filter((b) => oldGenIds.has(b.blockId));
|
|
8445
|
-
const markedOldGenCount = markedOldGen.length;
|
|
8446
|
-
const oldGenCount = oldGen.length;
|
|
8447
|
-
const ratio = markedOldGenCount / oldGenCount;
|
|
8448
|
-
const ratioPct = Math.round(ratio * 100);
|
|
8449
|
-
const escalateMinPct = Math.round(ESCALATE_MIN_RATIO * 100);
|
|
8450
|
-
if (markedOldGenCount >= ESCALATE_MIN_MARKED && ratio >= ESCALATE_MIN_RATIO) {
|
|
8451
|
-
const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8452
|
-
const firstRef = formatBlockRef(marked[0].blockId);
|
|
8453
|
-
const lastRef = formatBlockRef(marked[marked.length - 1].blockId);
|
|
8454
|
-
const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
|
|
8455
|
-
return [
|
|
8456
|
-
`\u{1F525} ${markedOldGenCount}/${oldGenCount} old-gen blocks marked (${ratioPct}%) \u2014 ready for batch cleanup.`,
|
|
8457
|
-
`Compressing ${refs} (range ${firstRef}\u2013${lastRef}) would free ~${estimatedSavings} tokens in one cache break.`,
|
|
8458
|
-
`Call compress with this range now to consolidate them.`
|
|
8459
|
-
].join(" ");
|
|
8460
|
-
}
|
|
8461
|
-
if (marked.length >= 1) {
|
|
8462
|
-
const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8463
|
-
const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
|
|
8464
|
-
return [
|
|
8465
|
-
`\u26A0\uFE0F ${marked.length} block(s) marked for batch cleanup (${refs}).`,
|
|
8466
|
-
`Merge-compressing them would free ~${estimatedSavings} tokens.`,
|
|
8467
|
-
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.",
|
|
8468
|
-
`Mark more old-gen blocks (need \u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}%) to trigger batch cleanup sooner.`,
|
|
8469
|
-
"To act now, use compress with a range covering these blocks."
|
|
8470
|
-
].join(" ");
|
|
8471
|
-
}
|
|
8472
|
-
const shown = oldGen.slice(0, 5);
|
|
8473
|
-
const oldGenRefs = shown.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8474
|
-
const more = oldGenCount > 5 ? ` (+${oldGenCount - 5} more)` : "";
|
|
8475
|
-
return [
|
|
8476
|
-
`\u{1F4CB} Context pressure rising \u2014 ${oldGenCount} old-gen compressed block(s) occupy ~${estimateTokens(oldGen)} tokens (${oldGenRefs}${more}).`,
|
|
8477
|
-
`Review which blocks contain information you no longer need, and use mark_block to flag them.`,
|
|
8478
|
-
`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.`,
|
|
8479
|
-
`Do NOT mark blocks you may still need.`
|
|
8480
|
-
].join(" ");
|
|
8481
|
-
}
|
|
8482
8496
|
function runBatchCleanup(state, config, logger, messages) {
|
|
8483
8497
|
const noop = {
|
|
8484
8498
|
tier: 0,
|
|
@@ -8490,74 +8504,31 @@ function runBatchCleanup(state, config, logger, messages) {
|
|
|
8490
8504
|
return noop;
|
|
8491
8505
|
}
|
|
8492
8506
|
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
8496
|
-
const forceTokens = percentToTokens(batchCleanup.forceThreshold, limit);
|
|
8497
|
-
const highTokens = percentToTokens(batchCleanup.highThreshold, limit);
|
|
8498
|
-
const lowTokens = percentToTokens(batchCleanup.lowThreshold, limit);
|
|
8499
|
-
if (currentTokens >= forceTokens) {
|
|
8500
|
-
const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8501
|
-
if (oldGenBlocks.length < 2) {
|
|
8502
|
-
return noop;
|
|
8503
|
-
}
|
|
8504
|
-
const ids = oldGenBlocks.map((b) => b.blockId);
|
|
8505
|
-
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8506
|
-
if (result.mergedCount === 0) {
|
|
8507
|
-
return noop;
|
|
8508
|
-
}
|
|
8509
|
-
logger.info("Batch cleanup tier 3 (force): merged old-gen blocks", {
|
|
8510
|
-
mergedCount: result.mergedCount,
|
|
8511
|
-
savedTokens: result.savedTokens,
|
|
8512
|
-
currentTokens,
|
|
8513
|
-
forceThreshold: batchCleanup.forceThreshold
|
|
8514
|
-
});
|
|
8515
|
-
return {
|
|
8516
|
-
tier: 3,
|
|
8517
|
-
action: "merge",
|
|
8518
|
-
mergedCount: result.mergedCount,
|
|
8519
|
-
savedTokens: result.savedTokens
|
|
8520
|
-
};
|
|
8507
|
+
if (currentTokens < state.modelContextLimit) {
|
|
8508
|
+
return noop;
|
|
8521
8509
|
}
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8527
|
-
if (result.mergedCount > 0) {
|
|
8528
|
-
logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
|
|
8529
|
-
mergedCount: result.mergedCount,
|
|
8530
|
-
savedTokens: result.savedTokens,
|
|
8531
|
-
currentTokens,
|
|
8532
|
-
highThreshold: batchCleanup.highThreshold
|
|
8533
|
-
});
|
|
8534
|
-
return {
|
|
8535
|
-
tier: 2,
|
|
8536
|
-
action: "merge",
|
|
8537
|
-
mergedCount: result.mergedCount,
|
|
8538
|
-
savedTokens: result.savedTokens
|
|
8539
|
-
};
|
|
8540
|
-
}
|
|
8541
|
-
}
|
|
8510
|
+
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
8511
|
+
const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8512
|
+
if (oldGenBlocks.length < 2) {
|
|
8513
|
+
return noop;
|
|
8542
8514
|
}
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
}
|
|
8548
|
-
logger.info("Batch cleanup tier 1 (low): nudge injected", {
|
|
8549
|
-
currentTokens,
|
|
8550
|
-
lowThreshold: batchCleanup.lowThreshold
|
|
8551
|
-
});
|
|
8552
|
-
return {
|
|
8553
|
-
tier: 1,
|
|
8554
|
-
action: "nudge",
|
|
8555
|
-
mergedCount: 0,
|
|
8556
|
-
savedTokens: 0,
|
|
8557
|
-
nudgeText
|
|
8558
|
-
};
|
|
8515
|
+
const ids = oldGenBlocks.map((b) => b.blockId);
|
|
8516
|
+
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8517
|
+
if (result.mergedCount === 0) {
|
|
8518
|
+
return noop;
|
|
8559
8519
|
}
|
|
8560
|
-
|
|
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
|
+
};
|
|
8561
8532
|
}
|
|
8562
8533
|
|
|
8563
8534
|
// lib/hooks.ts
|
|
@@ -8668,11 +8639,6 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8668
8639
|
void saveSessionState(state, logger);
|
|
8669
8640
|
}
|
|
8670
8641
|
}
|
|
8671
|
-
function appendBatchCleanupNudge(messages, nudgeText) {
|
|
8672
|
-
const lastUser = getLastUserMessage(messages);
|
|
8673
|
-
if (!lastUser) return;
|
|
8674
|
-
appendToLastTextPart(lastUser, nudgeText);
|
|
8675
|
-
}
|
|
8676
8642
|
function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
|
|
8677
8643
|
return async (input, output) => {
|
|
8678
8644
|
const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
|
|
@@ -8704,9 +8670,6 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8704
8670
|
buildToolIdList(state, output.messages);
|
|
8705
8671
|
runMajorGC(state, config, logger, output.messages);
|
|
8706
8672
|
const batchResult = runBatchCleanup(state, config, logger, output.messages);
|
|
8707
|
-
if (batchResult.tier === 1 && batchResult.nudgeText) {
|
|
8708
|
-
appendBatchCleanupNudge(output.messages, batchResult.nudgeText);
|
|
8709
|
-
}
|
|
8710
8673
|
if (batchResult.mergedCount > 0) {
|
|
8711
8674
|
void saveSessionState(state, logger);
|
|
8712
8675
|
}
|
|
@@ -9128,9 +9091,7 @@ var server = (async (ctx) => {
|
|
|
9128
9091
|
tool: {
|
|
9129
9092
|
...config.compress.permission !== "deny" && {
|
|
9130
9093
|
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|
|
9131
|
-
decompress: createDecompressTool(compressToolContext)
|
|
9132
|
-
mark_block: createMarkBlockTool(compressToolContext),
|
|
9133
|
-
unmark_block: createUnmarkBlockTool(compressToolContext)
|
|
9094
|
+
decompress: createDecompressTool(compressToolContext)
|
|
9134
9095
|
}
|
|
9135
9096
|
},
|
|
9136
9097
|
config: async (opencodeConfig) => {
|
|
@@ -9146,7 +9107,7 @@ var server = (async (ctx) => {
|
|
|
9146
9107
|
}
|
|
9147
9108
|
const toolsToAdd = [];
|
|
9148
9109
|
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
|
|
9149
|
-
toolsToAdd.push("compress", "decompress"
|
|
9110
|
+
toolsToAdd.push("compress", "decompress");
|
|
9150
9111
|
}
|
|
9151
9112
|
if (toolsToAdd.length > 0) {
|
|
9152
9113
|
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
|