opencode-acp 1.3.1 → 1.4.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 +110 -14
- package/README.zh-CN.md +81 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +575 -72
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/index.d.ts +1 -0
- package/dist/lib/compress/index.d.ts.map +1 -1
- package/dist/lib/compress/mark-block.d.ts +5 -0
- package/dist/lib/compress/mark-block.d.ts.map +1 -0
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/gc/merge.d.ts +17 -0
- package/dist/lib/gc/merge.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/utils.d.ts +1 -0
- package/dist/lib/messages/utils.d.ts.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +1 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +1 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts +1 -0
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -905,6 +905,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
905
905
|
"gc.maxBlockAge",
|
|
906
906
|
"gc.maxOldGenSummaryLength",
|
|
907
907
|
"gc.majorGcThresholdPercent",
|
|
908
|
+
"gc.batchCleanup",
|
|
909
|
+
"gc.batchCleanup.lowThreshold",
|
|
910
|
+
"gc.batchCleanup.highThreshold",
|
|
911
|
+
"gc.batchCleanup.forceThreshold",
|
|
908
912
|
"strategies",
|
|
909
913
|
"strategies.deduplication",
|
|
910
914
|
"strategies.deduplication.enabled",
|
|
@@ -1263,6 +1267,36 @@ function validateConfigTypes(config) {
|
|
|
1263
1267
|
});
|
|
1264
1268
|
}
|
|
1265
1269
|
}
|
|
1270
|
+
const validateBatchThreshold = (key, value) => {
|
|
1271
|
+
const isValidNumber = typeof value === "number";
|
|
1272
|
+
const isPercentString = typeof value === "string" && /^\d+(?:\.\d+)?%$/.test(value);
|
|
1273
|
+
if (!isValidNumber && !isPercentString) {
|
|
1274
|
+
errors.push({
|
|
1275
|
+
key,
|
|
1276
|
+
expected: 'number | "${number}%"',
|
|
1277
|
+
actual: JSON.stringify(value)
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
if (gc.batchCleanup !== void 0) {
|
|
1282
|
+
if (typeof gc.batchCleanup !== "object" || gc.batchCleanup === null || Array.isArray(gc.batchCleanup)) {
|
|
1283
|
+
errors.push({
|
|
1284
|
+
key: "gc.batchCleanup",
|
|
1285
|
+
expected: "object",
|
|
1286
|
+
actual: typeof gc.batchCleanup
|
|
1287
|
+
});
|
|
1288
|
+
} else {
|
|
1289
|
+
if (gc.batchCleanup.lowThreshold !== void 0) {
|
|
1290
|
+
validateBatchThreshold("gc.batchCleanup.lowThreshold", gc.batchCleanup.lowThreshold);
|
|
1291
|
+
}
|
|
1292
|
+
if (gc.batchCleanup.highThreshold !== void 0) {
|
|
1293
|
+
validateBatchThreshold("gc.batchCleanup.highThreshold", gc.batchCleanup.highThreshold);
|
|
1294
|
+
}
|
|
1295
|
+
if (gc.batchCleanup.forceThreshold !== void 0) {
|
|
1296
|
+
validateBatchThreshold("gc.batchCleanup.forceThreshold", gc.batchCleanup.forceThreshold);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1266
1300
|
}
|
|
1267
1301
|
}
|
|
1268
1302
|
const strategies = config.strategies;
|
|
@@ -1351,6 +1385,8 @@ var DEFAULT_PROTECTED_TOOLS = [
|
|
|
1351
1385
|
"todoread",
|
|
1352
1386
|
"compress",
|
|
1353
1387
|
"decompress",
|
|
1388
|
+
"mark_block",
|
|
1389
|
+
"unmark_block",
|
|
1354
1390
|
"batch",
|
|
1355
1391
|
"plan_enter",
|
|
1356
1392
|
"plan_exit",
|
|
@@ -1447,7 +1483,12 @@ var defaultConfig = {
|
|
|
1447
1483
|
promotionThreshold: 5,
|
|
1448
1484
|
maxBlockAge: 15,
|
|
1449
1485
|
maxOldGenSummaryLength: 3e3,
|
|
1450
|
-
majorGcThresholdPercent: "100%"
|
|
1486
|
+
majorGcThresholdPercent: "100%",
|
|
1487
|
+
batchCleanup: {
|
|
1488
|
+
lowThreshold: "60%",
|
|
1489
|
+
highThreshold: "75%",
|
|
1490
|
+
forceThreshold: "90%"
|
|
1491
|
+
}
|
|
1451
1492
|
}
|
|
1452
1493
|
};
|
|
1453
1494
|
var GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
|
|
@@ -1507,7 +1548,7 @@ function createDefaultConfig() {
|
|
|
1507
1548
|
console.log("[ACP] Migrated config from dcp.json to acp.jsonc");
|
|
1508
1549
|
} else {
|
|
1509
1550
|
const configContent = `{
|
|
1510
|
-
"$schema": "https://raw.githubusercontent.com/
|
|
1551
|
+
"$schema": "https://raw.githubusercontent.com/ranxianglei/opencode-acp/master/dcp.schema.json"
|
|
1511
1552
|
}
|
|
1512
1553
|
`;
|
|
1513
1554
|
writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, "utf-8");
|
|
@@ -1631,7 +1672,20 @@ function deepCloneConfig(config) {
|
|
|
1631
1672
|
protectedTools: [...config.strategies.purgeErrors.protectedTools]
|
|
1632
1673
|
}
|
|
1633
1674
|
},
|
|
1634
|
-
gc: {
|
|
1675
|
+
gc: {
|
|
1676
|
+
...config.gc,
|
|
1677
|
+
batchCleanup: { ...config.gc.batchCleanup }
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
function mergeGC(base, override) {
|
|
1682
|
+
if (!override) {
|
|
1683
|
+
return base;
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
...base,
|
|
1687
|
+
...override,
|
|
1688
|
+
batchCleanup: { ...base.batchCleanup, ...override.batchCleanup ?? {} }
|
|
1635
1689
|
};
|
|
1636
1690
|
}
|
|
1637
1691
|
function mergeLayer(config, data) {
|
|
@@ -1652,7 +1706,7 @@ function mergeLayer(config, data) {
|
|
|
1652
1706
|
.../* @__PURE__ */ new Set([...config.protectedFilePatterns, ...data.protectedFilePatterns ?? []])
|
|
1653
1707
|
],
|
|
1654
1708
|
compress: mergeCompress(config.compress, data.compress),
|
|
1655
|
-
gc:
|
|
1709
|
+
gc: mergeGC(config.gc, data.gc),
|
|
1656
1710
|
strategies: mergeStrategies(config.strategies, data.strategies)
|
|
1657
1711
|
};
|
|
1658
1712
|
}
|
|
@@ -2747,7 +2801,8 @@ function serializePruneMessagesState(messagesState) {
|
|
|
2747
2801
|
activeBlockIds: Array.from(messagesState.activeBlockIds),
|
|
2748
2802
|
activeByAnchorMessageId: Object.fromEntries(messagesState.activeByAnchorMessageId),
|
|
2749
2803
|
nextBlockId: messagesState.nextBlockId,
|
|
2750
|
-
nextRunId: messagesState.nextRunId
|
|
2804
|
+
nextRunId: messagesState.nextRunId,
|
|
2805
|
+
markedForCleanup: Array.from(messagesState.markedForCleanup)
|
|
2751
2806
|
};
|
|
2752
2807
|
}
|
|
2753
2808
|
async function isSubAgentSession(client, sessionID) {
|
|
@@ -2804,7 +2859,8 @@ function createPruneMessagesState() {
|
|
|
2804
2859
|
activeBlockIds: /* @__PURE__ */ new Set(),
|
|
2805
2860
|
activeByAnchorMessageId: /* @__PURE__ */ new Map(),
|
|
2806
2861
|
nextBlockId: 1,
|
|
2807
|
-
nextRunId: 1
|
|
2862
|
+
nextRunId: 1,
|
|
2863
|
+
markedForCleanup: /* @__PURE__ */ new Set()
|
|
2808
2864
|
};
|
|
2809
2865
|
}
|
|
2810
2866
|
function loadPruneMessagesState(persisted) {
|
|
@@ -2915,6 +2971,13 @@ function loadPruneMessagesState(persisted) {
|
|
|
2915
2971
|
state.nextRunId = block.runId + 1;
|
|
2916
2972
|
}
|
|
2917
2973
|
}
|
|
2974
|
+
if (Array.isArray(persisted.markedForCleanup)) {
|
|
2975
|
+
for (const id of persisted.markedForCleanup) {
|
|
2976
|
+
if (Number.isInteger(id) && id > 0 && state.blocksById.has(id)) {
|
|
2977
|
+
state.markedForCleanup.add(id);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2918
2981
|
return state;
|
|
2919
2982
|
}
|
|
2920
2983
|
function collectTurnNudgeAnchors(messages) {
|
|
@@ -3467,20 +3530,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
3467
3530
|
regex += "$";
|
|
3468
3531
|
return new RegExp(regex).test(input);
|
|
3469
3532
|
}
|
|
3470
|
-
function getFilePathsFromParameters(
|
|
3533
|
+
function getFilePathsFromParameters(tool5, parameters) {
|
|
3471
3534
|
if (typeof parameters !== "object" || parameters === null) {
|
|
3472
3535
|
return [];
|
|
3473
3536
|
}
|
|
3474
3537
|
const paths = [];
|
|
3475
3538
|
const params = parameters;
|
|
3476
|
-
if (
|
|
3539
|
+
if (tool5 === "apply_patch" && typeof params.patchText === "string") {
|
|
3477
3540
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3478
3541
|
let match;
|
|
3479
3542
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
3480
3543
|
paths.push(match[1].trim());
|
|
3481
3544
|
}
|
|
3482
3545
|
}
|
|
3483
|
-
if (
|
|
3546
|
+
if (tool5 === "multiedit") {
|
|
3484
3547
|
if (typeof params.filePath === "string") {
|
|
3485
3548
|
paths.push(params.filePath);
|
|
3486
3549
|
}
|
|
@@ -3575,13 +3638,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
3575
3638
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
3576
3639
|
}
|
|
3577
3640
|
};
|
|
3578
|
-
function createToolSignature(
|
|
3641
|
+
function createToolSignature(tool5, parameters) {
|
|
3579
3642
|
if (!parameters) {
|
|
3580
|
-
return
|
|
3643
|
+
return tool5;
|
|
3581
3644
|
}
|
|
3582
3645
|
const normalized = normalizeParameters(parameters);
|
|
3583
3646
|
const sorted = sortObjectKeys(normalized);
|
|
3584
|
-
return `${
|
|
3647
|
+
return `${tool5}::${JSON.stringify(sorted)}`;
|
|
3585
3648
|
}
|
|
3586
3649
|
function normalizeParameters(params) {
|
|
3587
3650
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -3656,9 +3719,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
3656
3719
|
};
|
|
3657
3720
|
|
|
3658
3721
|
// lib/ui/utils.ts
|
|
3659
|
-
function extractParameterKey(
|
|
3722
|
+
function extractParameterKey(tool5, parameters) {
|
|
3660
3723
|
if (!parameters) return "";
|
|
3661
|
-
if (
|
|
3724
|
+
if (tool5 === "read" && parameters.filePath) {
|
|
3662
3725
|
const offset = parameters.offset;
|
|
3663
3726
|
const limit = parameters.limit;
|
|
3664
3727
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -3672,10 +3735,10 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3672
3735
|
}
|
|
3673
3736
|
return parameters.filePath;
|
|
3674
3737
|
}
|
|
3675
|
-
if ((
|
|
3738
|
+
if ((tool5 === "write" || tool5 === "edit" || tool5 === "multiedit") && parameters.filePath) {
|
|
3676
3739
|
return parameters.filePath;
|
|
3677
3740
|
}
|
|
3678
|
-
if (
|
|
3741
|
+
if (tool5 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
3679
3742
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
3680
3743
|
const paths = [];
|
|
3681
3744
|
let match;
|
|
@@ -3692,51 +3755,51 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3692
3755
|
}
|
|
3693
3756
|
return "patch";
|
|
3694
3757
|
}
|
|
3695
|
-
if (
|
|
3758
|
+
if (tool5 === "list") {
|
|
3696
3759
|
return parameters.path || "(current directory)";
|
|
3697
3760
|
}
|
|
3698
|
-
if (
|
|
3761
|
+
if (tool5 === "glob") {
|
|
3699
3762
|
if (parameters.pattern) {
|
|
3700
3763
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3701
3764
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3702
3765
|
}
|
|
3703
3766
|
return "(unknown pattern)";
|
|
3704
3767
|
}
|
|
3705
|
-
if (
|
|
3768
|
+
if (tool5 === "grep") {
|
|
3706
3769
|
if (parameters.pattern) {
|
|
3707
3770
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
3708
3771
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
3709
3772
|
}
|
|
3710
3773
|
return "(unknown pattern)";
|
|
3711
3774
|
}
|
|
3712
|
-
if (
|
|
3775
|
+
if (tool5 === "bash") {
|
|
3713
3776
|
if (parameters.description) return parameters.description;
|
|
3714
3777
|
if (parameters.command) {
|
|
3715
3778
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
3716
3779
|
}
|
|
3717
3780
|
}
|
|
3718
|
-
if (
|
|
3781
|
+
if (tool5 === "webfetch" && parameters.url) {
|
|
3719
3782
|
return parameters.url;
|
|
3720
3783
|
}
|
|
3721
|
-
if (
|
|
3784
|
+
if (tool5 === "websearch" && parameters.query) {
|
|
3722
3785
|
return `"${parameters.query}"`;
|
|
3723
3786
|
}
|
|
3724
|
-
if (
|
|
3787
|
+
if (tool5 === "codesearch" && parameters.query) {
|
|
3725
3788
|
return `"${parameters.query}"`;
|
|
3726
3789
|
}
|
|
3727
|
-
if (
|
|
3790
|
+
if (tool5 === "todowrite") {
|
|
3728
3791
|
return `${parameters.todos?.length || 0} todos`;
|
|
3729
3792
|
}
|
|
3730
|
-
if (
|
|
3793
|
+
if (tool5 === "todoread") {
|
|
3731
3794
|
return "read todo list";
|
|
3732
3795
|
}
|
|
3733
|
-
if (
|
|
3796
|
+
if (tool5 === "task" && parameters.description) {
|
|
3734
3797
|
return parameters.description;
|
|
3735
3798
|
}
|
|
3736
|
-
if (
|
|
3799
|
+
if (tool5 === "skill" && parameters.name) {
|
|
3737
3800
|
return parameters.name;
|
|
3738
3801
|
}
|
|
3739
|
-
if (
|
|
3802
|
+
if (tool5 === "lsp") {
|
|
3740
3803
|
const op = parameters.operation || "lsp";
|
|
3741
3804
|
const path = parameters.filePath || "";
|
|
3742
3805
|
const line = parameters.line;
|
|
@@ -3749,7 +3812,7 @@ function extractParameterKey(tool4, parameters) {
|
|
|
3749
3812
|
}
|
|
3750
3813
|
return op;
|
|
3751
3814
|
}
|
|
3752
|
-
if (
|
|
3815
|
+
if (tool5 === "question") {
|
|
3753
3816
|
const questions = parameters.questions;
|
|
3754
3817
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
3755
3818
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4717,6 +4780,12 @@ import { tool as tool3 } from "@opencode-ai/plugin";
|
|
|
4717
4780
|
// lib/messages/utils.ts
|
|
4718
4781
|
import { createHash } from "crypto";
|
|
4719
4782
|
var SUMMARY_ID_HASH_LENGTH = 16;
|
|
4783
|
+
var MERGED_SUMMARY_HEADER = (blockId) => `[ACP compressed context summary (block ${blockId}) \u2014 prior conversation recap]
|
|
4784
|
+
`;
|
|
4785
|
+
var MERGED_SUMMARY_FOOTER = `
|
|
4786
|
+
[End ACP compressed context summary]
|
|
4787
|
+
|
|
4788
|
+
`;
|
|
4720
4789
|
var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/dcp-message-id>)/g;
|
|
4721
4790
|
var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/dcp-message-id>/g;
|
|
4722
4791
|
var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi;
|
|
@@ -4751,6 +4820,34 @@ var createSyntheticUserMessage = (baseMessage, content, stableSeed) => {
|
|
|
4751
4820
|
]
|
|
4752
4821
|
};
|
|
4753
4822
|
};
|
|
4823
|
+
var prependCompressionSummary = (message, summary, blockId) => {
|
|
4824
|
+
const parts = Array.isArray(message.parts) ? message.parts : [];
|
|
4825
|
+
const header = MERGED_SUMMARY_HEADER(blockId);
|
|
4826
|
+
const marker = MERGED_SUMMARY_HEADER(blockId).trimEnd();
|
|
4827
|
+
for (const part of parts) {
|
|
4828
|
+
if (part.type !== "text") {
|
|
4829
|
+
continue;
|
|
4830
|
+
}
|
|
4831
|
+
const textPart = part;
|
|
4832
|
+
const existing = typeof textPart.text === "string" ? textPart.text : "";
|
|
4833
|
+
if (existing.includes(marker)) {
|
|
4834
|
+
return true;
|
|
4835
|
+
}
|
|
4836
|
+
textPart.text = `${header}${summary}${MERGED_SUMMARY_FOOTER}${existing}`;
|
|
4837
|
+
return true;
|
|
4838
|
+
}
|
|
4839
|
+
const sessionID = message.info.sessionID ?? "";
|
|
4840
|
+
const messageId = message.info.id;
|
|
4841
|
+
parts.unshift({
|
|
4842
|
+
id: generateStableId("prt_dcp_prepend", `${blockId}:${messageId}`),
|
|
4843
|
+
sessionID,
|
|
4844
|
+
messageID: messageId,
|
|
4845
|
+
type: "text",
|
|
4846
|
+
text: `${header}${summary}${MERGED_SUMMARY_FOOTER}`
|
|
4847
|
+
});
|
|
4848
|
+
message.parts = parts;
|
|
4849
|
+
return true;
|
|
4850
|
+
};
|
|
4754
4851
|
var createSyntheticTextPart = (baseMessage, content, stableSeed) => {
|
|
4755
4852
|
const userInfo = baseMessage.info;
|
|
4756
4853
|
const deterministicSeed = stableSeed?.trim() || userInfo.id;
|
|
@@ -4950,7 +5047,8 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
4950
5047
|
return;
|
|
4951
5048
|
}
|
|
4952
5049
|
const result = [];
|
|
4953
|
-
for (
|
|
5050
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5051
|
+
const msg = messages[i];
|
|
4954
5052
|
const msgId = msg.info.id;
|
|
4955
5053
|
const blockId = state.prune.messages.activeByAnchorMessageId.get(msgId);
|
|
4956
5054
|
const summary = blockId !== void 0 ? state.prune.messages.blocksById.get(blockId) : void 0;
|
|
@@ -4962,46 +5060,52 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
4962
5060
|
blockId: summary.blockId
|
|
4963
5061
|
});
|
|
4964
5062
|
} else {
|
|
4965
|
-
const msgIndex = messages.indexOf(msg);
|
|
4966
|
-
const userMessage = getLastUserMessage(messages, msgIndex);
|
|
4967
5063
|
const _cleaned = stripStaleMessageRefs(rawSummaryContent);
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
createSyntheticUserMessage(userMessage, summaryContent, summarySeed)
|
|
4974
|
-
);
|
|
4975
|
-
logger.info("Injected compress summary", {
|
|
5064
|
+
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
5065
|
+
const nextSurviving = findNextSurvivingMessage(messages, i, state);
|
|
5066
|
+
const merged = nextSurviving !== null && nextSurviving.info.role === "user" && prependCompressionSummary(nextSurviving, summaryContent, summary.blockId);
|
|
5067
|
+
if (merged) {
|
|
5068
|
+
logger.info("Merged compress summary into following user message", {
|
|
4976
5069
|
anchorMessageId: msgId,
|
|
5070
|
+
targetMessageId: nextSurviving.info.id,
|
|
4977
5071
|
summaryLength: summaryContent.length
|
|
4978
5072
|
});
|
|
4979
5073
|
} else {
|
|
4980
|
-
const
|
|
4981
|
-
const fallbackBase = {
|
|
4982
|
-
info: {
|
|
4983
|
-
id: anchorInfo.id || msgId,
|
|
4984
|
-
sessionID: anchorInfo.sessionID || "",
|
|
4985
|
-
role: "user",
|
|
4986
|
-
agent: anchorInfo.agent || "code",
|
|
4987
|
-
model: anchorInfo.model || {
|
|
4988
|
-
providerID: "",
|
|
4989
|
-
modelID: "",
|
|
4990
|
-
variant: void 0
|
|
4991
|
-
},
|
|
4992
|
-
time: { created: anchorInfo.time?.created || Date.now() }
|
|
4993
|
-
},
|
|
4994
|
-
parts: []
|
|
4995
|
-
};
|
|
4996
|
-
const summaryContent = config.compress.mode === "message" ? replaceBlockIdsWithBlocked(_cleaned) : _cleaned;
|
|
5074
|
+
const userMessage = getLastUserMessage(messages, i);
|
|
4997
5075
|
const summarySeed = `${summary.blockId}:${summary.anchorMessageId}`;
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5076
|
+
if (userMessage) {
|
|
5077
|
+
result.push(
|
|
5078
|
+
createSyntheticUserMessage(userMessage, summaryContent, summarySeed)
|
|
5079
|
+
);
|
|
5080
|
+
logger.info("Injected compress summary", {
|
|
5081
|
+
anchorMessageId: msgId,
|
|
5082
|
+
summaryLength: summaryContent.length
|
|
5083
|
+
});
|
|
5084
|
+
} else {
|
|
5085
|
+
const anchorInfo = msg.info;
|
|
5086
|
+
const fallbackBase = {
|
|
5087
|
+
info: {
|
|
5088
|
+
id: anchorInfo.id || msgId,
|
|
5089
|
+
sessionID: anchorInfo.sessionID || "",
|
|
5090
|
+
role: "user",
|
|
5091
|
+
agent: anchorInfo.agent || "code",
|
|
5092
|
+
model: anchorInfo.model || {
|
|
5093
|
+
providerID: "",
|
|
5094
|
+
modelID: "",
|
|
5095
|
+
variant: void 0
|
|
5096
|
+
},
|
|
5097
|
+
time: { created: anchorInfo.time?.created || Date.now() }
|
|
5098
|
+
},
|
|
5099
|
+
parts: []
|
|
5100
|
+
};
|
|
5101
|
+
result.push(
|
|
5102
|
+
createSyntheticUserMessage(fallbackBase, summaryContent, summarySeed)
|
|
5103
|
+
);
|
|
5104
|
+
logger.info("Injected compress summary (fallback, no preceding user message)", {
|
|
5105
|
+
anchorMessageId: msgId,
|
|
5106
|
+
summaryLength: summaryContent.length
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5005
5109
|
}
|
|
5006
5110
|
}
|
|
5007
5111
|
}
|
|
@@ -5014,6 +5118,17 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
5014
5118
|
messages.length = 0;
|
|
5015
5119
|
messages.push(...result);
|
|
5016
5120
|
};
|
|
5121
|
+
var findNextSurvivingMessage = (messages, startIndex, state) => {
|
|
5122
|
+
for (let j = startIndex; j < messages.length; j++) {
|
|
5123
|
+
const candidate = messages[j];
|
|
5124
|
+
const entry = state.prune.messages.byMessageId.get(candidate.info.id);
|
|
5125
|
+
if (entry && entry.activeBlockIds.length > 0) {
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
return candidate;
|
|
5129
|
+
}
|
|
5130
|
+
return null;
|
|
5131
|
+
};
|
|
5017
5132
|
|
|
5018
5133
|
// lib/messages/sync.ts
|
|
5019
5134
|
function sortBlocksByCreation(a, b) {
|
|
@@ -5162,8 +5277,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
5162
5277
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5163
5278
|
) ? "deny" : basePermission;
|
|
5164
5279
|
};
|
|
5165
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
5166
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
5280
|
+
var hasExplicitToolPermission = (permissionConfig, tool5) => {
|
|
5281
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool5) : false;
|
|
5167
5282
|
};
|
|
5168
5283
|
|
|
5169
5284
|
// lib/compress-permission.ts
|
|
@@ -6275,6 +6390,109 @@ function createDecompressTool(ctx) {
|
|
|
6275
6390
|
});
|
|
6276
6391
|
}
|
|
6277
6392
|
|
|
6393
|
+
// lib/compress/mark-block.ts
|
|
6394
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
6395
|
+
async function prepareMarkSession(ctx, toolCtx) {
|
|
6396
|
+
await toolCtx.ask({
|
|
6397
|
+
permission: "compress",
|
|
6398
|
+
patterns: ["*"],
|
|
6399
|
+
always: ["*"],
|
|
6400
|
+
metadata: {}
|
|
6401
|
+
});
|
|
6402
|
+
toolCtx.metadata({ title: "Mark block" });
|
|
6403
|
+
const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
6404
|
+
await ensureSessionInitialized(
|
|
6405
|
+
ctx.client,
|
|
6406
|
+
ctx.state,
|
|
6407
|
+
toolCtx.sessionID,
|
|
6408
|
+
ctx.logger,
|
|
6409
|
+
rawMessages,
|
|
6410
|
+
ctx.config.manualMode.enabled
|
|
6411
|
+
);
|
|
6412
|
+
assignMessageRefs(ctx.state, rawMessages);
|
|
6413
|
+
}
|
|
6414
|
+
var MARK_DESCRIPTION = `Marks a compressed block for batch merge-cleanup.
|
|
6415
|
+
|
|
6416
|
+
Use this for blocks whose detailed content you no longer need, but whose summaries
|
|
6417
|
+
you want to keep in context for now (to preserve prompt cache). Marked blocks stay
|
|
6418
|
+
fully active with zero immediate effect on context or cache. When context pressure
|
|
6419
|
+
rises, all marked blocks are merge-compressed together into a single summary in one
|
|
6420
|
+
cache break, instead of being handled one at a time.
|
|
6421
|
+
|
|
6422
|
+
Argument: blockId \u2014 the block reference to mark (e.g., "b1", "b3")
|
|
6423
|
+
|
|
6424
|
+
Use mark_block instead of compress when you want deferred cleanup: the block keeps
|
|
6425
|
+
serving cache hits now and gets consolidated later only if context gets tight.`;
|
|
6426
|
+
var UNMARK_DESCRIPTION = `Removes the batch cleanup mark from a compressed block.
|
|
6427
|
+
|
|
6428
|
+
Reverses mark_block. The block returns to normal handling and will not be
|
|
6429
|
+
auto-merged during batch cleanup.
|
|
6430
|
+
|
|
6431
|
+
Argument: blockId \u2014 the block reference to unmark (e.g., "b1", "b3")`;
|
|
6432
|
+
function buildSchema4() {
|
|
6433
|
+
return {
|
|
6434
|
+
blockId: tool4.schema.string().describe('Block reference to mark (e.g., "b1", "b3")')
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
function buildUnmarkSchema() {
|
|
6438
|
+
return {
|
|
6439
|
+
blockId: tool4.schema.string().describe('Block reference to unmark (e.g., "b1", "b3")')
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
function createMarkBlockTool(ctx) {
|
|
6443
|
+
return tool4({
|
|
6444
|
+
description: MARK_DESCRIPTION,
|
|
6445
|
+
args: buildSchema4(),
|
|
6446
|
+
async execute(args, toolCtx) {
|
|
6447
|
+
await prepareMarkSession(ctx, toolCtx);
|
|
6448
|
+
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6449
|
+
if (targetBlockId === null) {
|
|
6450
|
+
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6451
|
+
}
|
|
6452
|
+
const messagesState = ctx.state.prune.messages;
|
|
6453
|
+
const block = messagesState.blocksById.get(targetBlockId);
|
|
6454
|
+
if (!block) {
|
|
6455
|
+
return `Error: Block ${formatBlockRef(targetBlockId)} does not exist.`;
|
|
6456
|
+
}
|
|
6457
|
+
if (!block.active) {
|
|
6458
|
+
return `Error: Block ${formatBlockRef(targetBlockId)} is not active.`;
|
|
6459
|
+
}
|
|
6460
|
+
messagesState.markedForCleanup.add(targetBlockId);
|
|
6461
|
+
await saveSessionState(ctx.state, ctx.logger);
|
|
6462
|
+
const ref = formatBlockRef(targetBlockId);
|
|
6463
|
+
const markedCount = messagesState.markedForCleanup.size;
|
|
6464
|
+
ctx.logger.info("mark_block: block marked for cleanup", {
|
|
6465
|
+
blockId: targetBlockId,
|
|
6466
|
+
markedCount
|
|
6467
|
+
});
|
|
6468
|
+
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.)`;
|
|
6469
|
+
}
|
|
6470
|
+
});
|
|
6471
|
+
}
|
|
6472
|
+
function createUnmarkBlockTool(ctx) {
|
|
6473
|
+
return tool4({
|
|
6474
|
+
description: UNMARK_DESCRIPTION,
|
|
6475
|
+
args: buildUnmarkSchema(),
|
|
6476
|
+
async execute(args, toolCtx) {
|
|
6477
|
+
await prepareMarkSession(ctx, toolCtx);
|
|
6478
|
+
const targetBlockId = parseBlockRef(String(args.blockId));
|
|
6479
|
+
if (targetBlockId === null) {
|
|
6480
|
+
return `Error: Invalid block ID "${args.blockId}". Use format "b0", "b1", etc.`;
|
|
6481
|
+
}
|
|
6482
|
+
const messagesState = ctx.state.prune.messages;
|
|
6483
|
+
if (!messagesState.markedForCleanup.has(targetBlockId)) {
|
|
6484
|
+
return `Block ${formatBlockRef(targetBlockId)} was not marked for cleanup.`;
|
|
6485
|
+
}
|
|
6486
|
+
messagesState.markedForCleanup.delete(targetBlockId);
|
|
6487
|
+
await saveSessionState(ctx.state, ctx.logger);
|
|
6488
|
+
ctx.logger.info("unmark_block: block unmarked", {
|
|
6489
|
+
blockId: targetBlockId
|
|
6490
|
+
});
|
|
6491
|
+
return `Block ${formatBlockRef(targetBlockId)} unmarked. It will no longer be auto-merged during batch cleanup.`;
|
|
6492
|
+
}
|
|
6493
|
+
});
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6278
6496
|
// lib/logger.ts
|
|
6279
6497
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
6280
6498
|
import { join as join3 } from "path";
|
|
@@ -6483,7 +6701,7 @@ var SYSTEM = `
|
|
|
6483
6701
|
|
|
6484
6702
|
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.
|
|
6485
6703
|
|
|
6486
|
-
The tools you have for context management are \`compress\` and \`
|
|
6704
|
+
The tools you have for context management are \`compress\`, \`decompress\`, \`mark_block\`, and \`unmark_block\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`mark_block\` flags a compressed block for deferred batch merge-cleanup \u2014 it has zero immediate effect on context or cache, but marked blocks are merge-compressed together in a single cache break when context pressure rises. Use it for blocks you no longer need in detail but want to keep cached for now. \`unmark_block\` removes that flag.
|
|
6487
6705
|
|
|
6488
6706
|
\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.
|
|
6489
6707
|
|
|
@@ -7514,7 +7732,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
7514
7732
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
7515
7733
|
"Return after compress with a brief explanation of what content was compressed."
|
|
7516
7734
|
].join("\n\n");
|
|
7517
|
-
function getTriggerPrompt(
|
|
7735
|
+
function getTriggerPrompt(tool5, state, config, userFocus) {
|
|
7518
7736
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
7519
7737
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
7520
7738
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -7543,8 +7761,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
7543
7761
|
);
|
|
7544
7762
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
7545
7763
|
}
|
|
7546
|
-
async function handleManualTriggerCommand(ctx,
|
|
7547
|
-
return getTriggerPrompt(
|
|
7764
|
+
async function handleManualTriggerCommand(ctx, tool5, userFocus) {
|
|
7765
|
+
return getTriggerPrompt(tool5, ctx.state, ctx.config, userFocus);
|
|
7548
7766
|
}
|
|
7549
7767
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
7550
7768
|
const pending = state.pendingManualTrigger;
|
|
@@ -8073,6 +8291,264 @@ function parseGcThreshold(limit, modelContextLimit) {
|
|
|
8073
8291
|
return Math.round(Math.max(0, Math.min(100, Math.round(percent))) / 100 * modelContextLimit);
|
|
8074
8292
|
}
|
|
8075
8293
|
|
|
8294
|
+
// lib/gc/merge.ts
|
|
8295
|
+
var DEFAULT_BATCH_CLEANUP = {
|
|
8296
|
+
lowThreshold: "60%",
|
|
8297
|
+
highThreshold: "75%",
|
|
8298
|
+
forceThreshold: "90%"
|
|
8299
|
+
};
|
|
8300
|
+
function resolveBatchCleanup(gc) {
|
|
8301
|
+
return gc.batchCleanup ?? DEFAULT_BATCH_CLEANUP;
|
|
8302
|
+
}
|
|
8303
|
+
function percentToTokens(value, modelContextLimit) {
|
|
8304
|
+
if (typeof value === "number") return value;
|
|
8305
|
+
const percent = parseFloat(value.slice(0, -1));
|
|
8306
|
+
if (isNaN(percent)) return modelContextLimit;
|
|
8307
|
+
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
8308
|
+
return Math.round(clamped / 100 * modelContextLimit);
|
|
8309
|
+
}
|
|
8310
|
+
function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
|
|
8311
|
+
const blocks = [];
|
|
8312
|
+
const ids = Array.from(state.prune.messages.activeBlockIds).sort((a, b) => a - b);
|
|
8313
|
+
for (const id of ids) {
|
|
8314
|
+
const block = state.prune.messages.blocksById.get(id);
|
|
8315
|
+
if (!block || !block.active) continue;
|
|
8316
|
+
if (block.generation === "old" || block.generation === void 0 || block.summary.length > maxOldGenSummaryLength) {
|
|
8317
|
+
blocks.push(block);
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
8320
|
+
return blocks;
|
|
8321
|
+
}
|
|
8322
|
+
function collectActiveMarkedBlocks(state) {
|
|
8323
|
+
const ids = Array.from(state.prune.messages.markedForCleanup).sort((a, b) => a - b);
|
|
8324
|
+
const blocks = [];
|
|
8325
|
+
for (const id of ids) {
|
|
8326
|
+
const block = state.prune.messages.blocksById.get(id);
|
|
8327
|
+
if (!block || !block.active) continue;
|
|
8328
|
+
blocks.push(block);
|
|
8329
|
+
}
|
|
8330
|
+
return blocks;
|
|
8331
|
+
}
|
|
8332
|
+
function extractSummaryBody(summary) {
|
|
8333
|
+
let body = summary;
|
|
8334
|
+
const headerPrefix = COMPRESSED_BLOCK_HEADER + "\n";
|
|
8335
|
+
if (body.startsWith(headerPrefix)) {
|
|
8336
|
+
body = body.slice(headerPrefix.length);
|
|
8337
|
+
}
|
|
8338
|
+
body = body.replace(/\n<dcp-message-id[^>]*>b\d+<\/dcp-message-id>$/, "");
|
|
8339
|
+
return body.trim();
|
|
8340
|
+
}
|
|
8341
|
+
function truncateMergedSummary(merged, maxLength) {
|
|
8342
|
+
if (merged.length <= maxLength) return merged;
|
|
8343
|
+
const blocks = merged.split("\n---\n");
|
|
8344
|
+
const headers = blocks.map((b) => b.split("\n")[0] ?? "").filter((h) => h.trim().length > 0);
|
|
8345
|
+
const marker = "\n...\n[merged and truncated by batch cleanup]";
|
|
8346
|
+
const budget = Math.max(0, maxLength - marker.length);
|
|
8347
|
+
const headerJoin = headers.join("\n");
|
|
8348
|
+
if (headerJoin.length <= budget) {
|
|
8349
|
+
return headerJoin + marker;
|
|
8350
|
+
}
|
|
8351
|
+
return headerJoin.slice(0, budget) + marker;
|
|
8352
|
+
}
|
|
8353
|
+
function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
|
|
8354
|
+
const sortedIds = [...new Set(markedIds)].filter(
|
|
8355
|
+
(id) => Number.isInteger(id) && id > 0
|
|
8356
|
+
).sort((a, b) => a - b);
|
|
8357
|
+
const sourceBlocks = [];
|
|
8358
|
+
for (const id of sortedIds) {
|
|
8359
|
+
const block = state.prune.messages.blocksById.get(id);
|
|
8360
|
+
if (!block || !block.active) continue;
|
|
8361
|
+
if (!sourceBlocks.some((b) => b.blockId === id)) {
|
|
8362
|
+
sourceBlocks.push(block);
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
if (sourceBlocks.length < 2) {
|
|
8366
|
+
return { mergedCount: 0, savedTokens: 0 };
|
|
8367
|
+
}
|
|
8368
|
+
const messagesState = state.prune.messages;
|
|
8369
|
+
const newBlockId = allocateBlockId(state);
|
|
8370
|
+
const newRunId = allocateRunId(state);
|
|
8371
|
+
const bodies = sourceBlocks.map((block) => extractSummaryBody(block.summary));
|
|
8372
|
+
const mergedRaw = bodies.join("\n---\n");
|
|
8373
|
+
const mergedBody = truncateMergedSummary(mergedRaw, maxMergedLength);
|
|
8374
|
+
const newSummary = wrapCompressedSummary(newBlockId, mergedBody);
|
|
8375
|
+
const newSummaryTokens = countTokens2(newSummary);
|
|
8376
|
+
const oldest = sourceBlocks[0];
|
|
8377
|
+
const newest = sourceBlocks[sourceBlocks.length - 1];
|
|
8378
|
+
const effectiveMessageIds = /* @__PURE__ */ new Set();
|
|
8379
|
+
const effectiveToolIds = /* @__PURE__ */ new Set();
|
|
8380
|
+
for (const block of sourceBlocks) {
|
|
8381
|
+
for (const id of block.effectiveMessageIds) effectiveMessageIds.add(id);
|
|
8382
|
+
for (const id of block.effectiveToolIds) effectiveToolIds.add(id);
|
|
8383
|
+
}
|
|
8384
|
+
const sourceIds = sourceBlocks.map((b) => b.blockId);
|
|
8385
|
+
const createdAt = Date.now();
|
|
8386
|
+
const mergedBlock = {
|
|
8387
|
+
blockId: newBlockId,
|
|
8388
|
+
runId: newRunId,
|
|
8389
|
+
active: true,
|
|
8390
|
+
deactivatedByUser: false,
|
|
8391
|
+
compressedTokens: 0,
|
|
8392
|
+
summaryTokens: newSummaryTokens,
|
|
8393
|
+
durationMs: 0,
|
|
8394
|
+
mode: "range",
|
|
8395
|
+
topic: "Batch merge cleanup",
|
|
8396
|
+
batchTopic: "Batch merge cleanup",
|
|
8397
|
+
startId: oldest.startId,
|
|
8398
|
+
endId: newest.endId,
|
|
8399
|
+
anchorMessageId: oldest.anchorMessageId,
|
|
8400
|
+
compressMessageId: "",
|
|
8401
|
+
compressCallId: void 0,
|
|
8402
|
+
includedBlockIds: [...sourceIds],
|
|
8403
|
+
consumedBlockIds: [...sourceIds],
|
|
8404
|
+
parentBlockIds: [],
|
|
8405
|
+
directMessageIds: [],
|
|
8406
|
+
directToolIds: [],
|
|
8407
|
+
effectiveMessageIds: [...effectiveMessageIds],
|
|
8408
|
+
effectiveToolIds: [...effectiveToolIds],
|
|
8409
|
+
createdAt,
|
|
8410
|
+
summary: newSummary,
|
|
8411
|
+
survivedCount: 0,
|
|
8412
|
+
generation: "old"
|
|
8413
|
+
};
|
|
8414
|
+
const now = Date.now();
|
|
8415
|
+
for (const block of sourceBlocks) {
|
|
8416
|
+
block.active = false;
|
|
8417
|
+
block.deactivatedAt = now;
|
|
8418
|
+
block.deactivatedByBlockId = newBlockId;
|
|
8419
|
+
if (!block.parentBlockIds.includes(newBlockId)) {
|
|
8420
|
+
block.parentBlockIds.push(newBlockId);
|
|
8421
|
+
}
|
|
8422
|
+
messagesState.activeBlockIds.delete(block.blockId);
|
|
8423
|
+
const mappedId = messagesState.activeByAnchorMessageId.get(block.anchorMessageId);
|
|
8424
|
+
if (mappedId === block.blockId) {
|
|
8425
|
+
messagesState.activeByAnchorMessageId.delete(block.anchorMessageId);
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
messagesState.blocksById.set(newBlockId, mergedBlock);
|
|
8429
|
+
messagesState.activeBlockIds.add(newBlockId);
|
|
8430
|
+
messagesState.activeByAnchorMessageId.set(mergedBlock.anchorMessageId, newBlockId);
|
|
8431
|
+
for (const messageId of effectiveMessageIds) {
|
|
8432
|
+
const entry = messagesState.byMessageId.get(messageId);
|
|
8433
|
+
if (!entry) continue;
|
|
8434
|
+
entry.activeBlockIds = entry.activeBlockIds.filter((id) => !sourceIds.includes(id));
|
|
8435
|
+
if (!entry.activeBlockIds.includes(newBlockId)) {
|
|
8436
|
+
entry.activeBlockIds.push(newBlockId);
|
|
8437
|
+
}
|
|
8438
|
+
if (!entry.allBlockIds.includes(newBlockId)) {
|
|
8439
|
+
entry.allBlockIds.push(newBlockId);
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
for (const id of sourceIds) {
|
|
8443
|
+
messagesState.markedForCleanup.delete(id);
|
|
8444
|
+
}
|
|
8445
|
+
const sourceTokens = sourceBlocks.reduce(
|
|
8446
|
+
(sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
|
|
8447
|
+
0
|
|
8448
|
+
);
|
|
8449
|
+
const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
|
|
8450
|
+
return { mergedCount: sourceBlocks.length, savedTokens };
|
|
8451
|
+
}
|
|
8452
|
+
function buildNudgeText(state, maxMergedLength) {
|
|
8453
|
+
const blocks = collectActiveMarkedBlocks(state);
|
|
8454
|
+
if (blocks.length < 1) return void 0;
|
|
8455
|
+
const refs = blocks.map((b) => formatBlockRef(b.blockId)).join(", ");
|
|
8456
|
+
const sourceTokens = blocks.reduce(
|
|
8457
|
+
(sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
|
|
8458
|
+
0
|
|
8459
|
+
);
|
|
8460
|
+
const estimatedMergedTokens = Math.round(maxMergedLength / 4);
|
|
8461
|
+
const estimatedSavings = Math.max(0, sourceTokens - estimatedMergedTokens);
|
|
8462
|
+
return [
|
|
8463
|
+
`\u26A0\uFE0F ${blocks.length} block(s) marked for batch cleanup (${refs}).`,
|
|
8464
|
+
`Merge-compressing them would free ~${estimatedSavings} tokens.`,
|
|
8465
|
+
blocks.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.",
|
|
8466
|
+
"To act now, use compress with a range covering these blocks."
|
|
8467
|
+
].join(" ");
|
|
8468
|
+
}
|
|
8469
|
+
function runBatchCleanup(state, config, logger, messages) {
|
|
8470
|
+
const noop = {
|
|
8471
|
+
tier: 0,
|
|
8472
|
+
action: "none",
|
|
8473
|
+
mergedCount: 0,
|
|
8474
|
+
savedTokens: 0
|
|
8475
|
+
};
|
|
8476
|
+
if (!state.modelContextLimit || state.modelContextLimit <= 0) {
|
|
8477
|
+
return noop;
|
|
8478
|
+
}
|
|
8479
|
+
const currentTokens = getCurrentTokenUsage(state, messages);
|
|
8480
|
+
const limit = state.modelContextLimit;
|
|
8481
|
+
const batchCleanup = resolveBatchCleanup(config.gc);
|
|
8482
|
+
const maxMergedLength = config.gc.maxOldGenSummaryLength;
|
|
8483
|
+
const forceTokens = percentToTokens(batchCleanup.forceThreshold, limit);
|
|
8484
|
+
const highTokens = percentToTokens(batchCleanup.highThreshold, limit);
|
|
8485
|
+
const lowTokens = percentToTokens(batchCleanup.lowThreshold, limit);
|
|
8486
|
+
if (currentTokens >= forceTokens) {
|
|
8487
|
+
const oldGenBlocks = collectActiveOldGenBlocks(state, maxMergedLength);
|
|
8488
|
+
if (oldGenBlocks.length < 2) {
|
|
8489
|
+
return noop;
|
|
8490
|
+
}
|
|
8491
|
+
const ids = oldGenBlocks.map((b) => b.blockId);
|
|
8492
|
+
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8493
|
+
if (result.mergedCount === 0) {
|
|
8494
|
+
return noop;
|
|
8495
|
+
}
|
|
8496
|
+
logger.info("Batch cleanup tier 3 (force): merged old-gen blocks", {
|
|
8497
|
+
mergedCount: result.mergedCount,
|
|
8498
|
+
savedTokens: result.savedTokens,
|
|
8499
|
+
currentTokens,
|
|
8500
|
+
forceThreshold: batchCleanup.forceThreshold
|
|
8501
|
+
});
|
|
8502
|
+
return {
|
|
8503
|
+
tier: 3,
|
|
8504
|
+
action: "merge",
|
|
8505
|
+
mergedCount: result.mergedCount,
|
|
8506
|
+
savedTokens: result.savedTokens
|
|
8507
|
+
};
|
|
8508
|
+
}
|
|
8509
|
+
if (currentTokens >= highTokens) {
|
|
8510
|
+
const marked = collectActiveMarkedBlocks(state);
|
|
8511
|
+
if (marked.length < 2) {
|
|
8512
|
+
return noop;
|
|
8513
|
+
}
|
|
8514
|
+
const ids = marked.map((b) => b.blockId);
|
|
8515
|
+
const result = mergeMarkedBlocks(state, ids, maxMergedLength);
|
|
8516
|
+
if (result.mergedCount === 0) {
|
|
8517
|
+
return noop;
|
|
8518
|
+
}
|
|
8519
|
+
logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
|
|
8520
|
+
mergedCount: result.mergedCount,
|
|
8521
|
+
savedTokens: result.savedTokens,
|
|
8522
|
+
currentTokens,
|
|
8523
|
+
highThreshold: batchCleanup.highThreshold
|
|
8524
|
+
});
|
|
8525
|
+
return {
|
|
8526
|
+
tier: 2,
|
|
8527
|
+
action: "merge",
|
|
8528
|
+
mergedCount: result.mergedCount,
|
|
8529
|
+
savedTokens: result.savedTokens
|
|
8530
|
+
};
|
|
8531
|
+
}
|
|
8532
|
+
if (currentTokens >= lowTokens) {
|
|
8533
|
+
const nudgeText = buildNudgeText(state, maxMergedLength);
|
|
8534
|
+
if (!nudgeText) {
|
|
8535
|
+
return noop;
|
|
8536
|
+
}
|
|
8537
|
+
logger.info("Batch cleanup tier 1 (low): nudge injected", {
|
|
8538
|
+
currentTokens,
|
|
8539
|
+
lowThreshold: batchCleanup.lowThreshold
|
|
8540
|
+
});
|
|
8541
|
+
return {
|
|
8542
|
+
tier: 1,
|
|
8543
|
+
action: "nudge",
|
|
8544
|
+
mergedCount: 0,
|
|
8545
|
+
savedTokens: 0,
|
|
8546
|
+
nudgeText
|
|
8547
|
+
};
|
|
8548
|
+
}
|
|
8549
|
+
return noop;
|
|
8550
|
+
}
|
|
8551
|
+
|
|
8076
8552
|
// lib/hooks.ts
|
|
8077
8553
|
var INTERNAL_AGENT_SIGNATURES = [
|
|
8078
8554
|
"You are a title generator",
|
|
@@ -8080,6 +8556,15 @@ var INTERNAL_AGENT_SIGNATURES = [
|
|
|
8080
8556
|
"You are an anchored context summarization assistant for coding sessions",
|
|
8081
8557
|
"Summarize what was done in this conversation"
|
|
8082
8558
|
];
|
|
8559
|
+
var INTERNAL_AGENT_NAMES = /* @__PURE__ */ new Set(["title", "summary", "compaction"]);
|
|
8560
|
+
function isInternalAgentRequest(messages) {
|
|
8561
|
+
const lastUserMessage = getLastUserMessage(messages);
|
|
8562
|
+
if (!lastUserMessage) {
|
|
8563
|
+
return false;
|
|
8564
|
+
}
|
|
8565
|
+
const agent = lastUserMessage.info.agent;
|
|
8566
|
+
return typeof agent === "string" && INTERNAL_AGENT_NAMES.has(agent);
|
|
8567
|
+
}
|
|
8083
8568
|
function createSystemPromptHandler(state, logger, config, prompts) {
|
|
8084
8569
|
return async (input, output) => {
|
|
8085
8570
|
if (input.model?.limit?.context) {
|
|
@@ -8172,6 +8657,11 @@ function runMajorGC(state, config, logger, messages) {
|
|
|
8172
8657
|
void saveSessionState(state, logger);
|
|
8173
8658
|
}
|
|
8174
8659
|
}
|
|
8660
|
+
function appendBatchCleanupNudge(messages, nudgeText) {
|
|
8661
|
+
const lastUser = getLastUserMessage(messages);
|
|
8662
|
+
if (!lastUser) return;
|
|
8663
|
+
appendToLastTextPart(lastUser, nudgeText);
|
|
8664
|
+
}
|
|
8175
8665
|
function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
|
|
8176
8666
|
return async (input, output) => {
|
|
8177
8667
|
const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
|
|
@@ -8182,6 +8672,10 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8182
8672
|
usable: messages.length
|
|
8183
8673
|
});
|
|
8184
8674
|
}
|
|
8675
|
+
if (isInternalAgentRequest(messages)) {
|
|
8676
|
+
logger.debug("Skipping message transform for internal agent request");
|
|
8677
|
+
return;
|
|
8678
|
+
}
|
|
8185
8679
|
await checkSession(client, state, logger, output.messages, config.manualMode.enabled);
|
|
8186
8680
|
syncCompressPermissionState(state, config, hostPermissions, output.messages);
|
|
8187
8681
|
if (state.isSubAgent && !config.experimental.allowSubAgents) {
|
|
@@ -8198,6 +8692,13 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
8198
8692
|
syncToolCache(state, config, logger, output.messages);
|
|
8199
8693
|
buildToolIdList(state, output.messages);
|
|
8200
8694
|
runMajorGC(state, config, logger, output.messages);
|
|
8695
|
+
const batchResult = runBatchCleanup(state, config, logger, output.messages);
|
|
8696
|
+
if (batchResult.tier === 1 && batchResult.nudgeText) {
|
|
8697
|
+
appendBatchCleanupNudge(output.messages, batchResult.nudgeText);
|
|
8698
|
+
}
|
|
8699
|
+
if (batchResult.mergedCount > 0) {
|
|
8700
|
+
void saveSessionState(state, logger);
|
|
8701
|
+
}
|
|
8201
8702
|
prune(state, logger, config, output.messages);
|
|
8202
8703
|
assignMessageRefs(state, output.messages);
|
|
8203
8704
|
await injectExtendedSubAgentResults(
|
|
@@ -8616,7 +9117,9 @@ var server = (async (ctx) => {
|
|
|
8616
9117
|
tool: {
|
|
8617
9118
|
...config.compress.permission !== "deny" && {
|
|
8618
9119
|
compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
|
|
8619
|
-
decompress: createDecompressTool(compressToolContext)
|
|
9120
|
+
decompress: createDecompressTool(compressToolContext),
|
|
9121
|
+
mark_block: createMarkBlockTool(compressToolContext),
|
|
9122
|
+
unmark_block: createUnmarkBlockTool(compressToolContext)
|
|
8620
9123
|
}
|
|
8621
9124
|
},
|
|
8622
9125
|
config: async (opencodeConfig) => {
|
|
@@ -8632,7 +9135,7 @@ var server = (async (ctx) => {
|
|
|
8632
9135
|
}
|
|
8633
9136
|
const toolsToAdd = [];
|
|
8634
9137
|
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
|
|
8635
|
-
toolsToAdd.push("compress", "decompress");
|
|
9138
|
+
toolsToAdd.push("compress", "decompress", "mark_block", "unmark_block");
|
|
8636
9139
|
}
|
|
8637
9140
|
if (toolsToAdd.length > 0) {
|
|
8638
9141
|
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [];
|