opencode-acp 1.11.4 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/README.zh-CN.md +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -210
- 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/keep-markers.d.ts +11 -0
- package/dist/lib/compress/keep-markers.d.ts.map +1 -0
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/recap.d.ts +4 -0
- package/dist/lib/compress/recap.d.ts.map +1 -0
- package/dist/lib/compress/status.d.ts.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/messages/index.d.ts +1 -1
- package/dist/lib/messages/index.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts +1 -1
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.d.ts +10 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/prune.d.ts +1 -0
- 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/compression-rules.d.ts +7 -1
- package/dist/lib/prompts/compression-rules.d.ts.map +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
- package/dist/lib/prompts/iteration-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/prompts/turn-nudge.d.ts +1 -1
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
- package/dist/lib/state/persistence.d.ts +2 -0
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/types.d.ts +10 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/dist/lib/ui/notification.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -905,6 +905,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
905
905
|
"compress.maxSummaryLengthHard",
|
|
906
906
|
"compress.minCompressRange",
|
|
907
907
|
"compress.maxVisibleSegments",
|
|
908
|
+
"compress.keepEmbedMaxChars",
|
|
908
909
|
"gc",
|
|
909
910
|
"gc.algorithm",
|
|
910
911
|
"gc.promotionThreshold",
|
|
@@ -1200,6 +1201,20 @@ function validateConfigTypes(config) {
|
|
|
1200
1201
|
actual: `${compress.maxVisibleSegments}`
|
|
1201
1202
|
});
|
|
1202
1203
|
}
|
|
1204
|
+
if (compress.keepEmbedMaxChars !== void 0 && typeof compress.keepEmbedMaxChars !== "number") {
|
|
1205
|
+
errors.push({
|
|
1206
|
+
key: "compress.keepEmbedMaxChars",
|
|
1207
|
+
expected: "number",
|
|
1208
|
+
actual: typeof compress.keepEmbedMaxChars
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof compress.keepEmbedMaxChars === "number" && compress.keepEmbedMaxChars < 100) {
|
|
1212
|
+
errors.push({
|
|
1213
|
+
key: "compress.keepEmbedMaxChars",
|
|
1214
|
+
expected: "positive number (>= 100)",
|
|
1215
|
+
actual: `${compress.keepEmbedMaxChars}`
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1203
1218
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1204
1219
|
errors.push({
|
|
1205
1220
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1515,7 +1530,8 @@ var defaultConfig = {
|
|
|
1515
1530
|
protectUserMessages: false,
|
|
1516
1531
|
maxSummaryLengthHard: 1e4,
|
|
1517
1532
|
minCompressRange: 2e3,
|
|
1518
|
-
maxVisibleSegments: 50
|
|
1533
|
+
maxVisibleSegments: 50,
|
|
1534
|
+
keepEmbedMaxChars: 2e3
|
|
1519
1535
|
},
|
|
1520
1536
|
strategies: {
|
|
1521
1537
|
deduplication: {
|
|
@@ -1672,7 +1688,8 @@ function mergeCompress(base, override) {
|
|
|
1672
1688
|
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
1673
1689
|
maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
|
|
1674
1690
|
minCompressRange: override.minCompressRange ?? base.minCompressRange,
|
|
1675
|
-
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments
|
|
1691
|
+
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments,
|
|
1692
|
+
keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars
|
|
1676
1693
|
};
|
|
1677
1694
|
}
|
|
1678
1695
|
function mergeCommands(base, override) {
|
|
@@ -2660,20 +2677,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
2660
2677
|
regex += "$";
|
|
2661
2678
|
return new RegExp(regex).test(input);
|
|
2662
2679
|
}
|
|
2663
|
-
function getFilePathsFromParameters(
|
|
2680
|
+
function getFilePathsFromParameters(tool8, parameters) {
|
|
2664
2681
|
if (typeof parameters !== "object" || parameters === null) {
|
|
2665
2682
|
return [];
|
|
2666
2683
|
}
|
|
2667
2684
|
const paths = [];
|
|
2668
2685
|
const params = parameters;
|
|
2669
|
-
if (
|
|
2686
|
+
if (tool8 === "apply_patch" && typeof params.patchText === "string") {
|
|
2670
2687
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
2671
2688
|
let match;
|
|
2672
2689
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
2673
2690
|
paths.push(match[1].trim());
|
|
2674
2691
|
}
|
|
2675
2692
|
}
|
|
2676
|
-
if (
|
|
2693
|
+
if (tool8 === "multiedit") {
|
|
2677
2694
|
if (typeof params.filePath === "string") {
|
|
2678
2695
|
paths.push(params.filePath);
|
|
2679
2696
|
}
|
|
@@ -3645,8 +3662,10 @@ function resetOnCompaction(state) {
|
|
|
3645
3662
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
3646
3663
|
lastPerMessageNudgeTurn: 0,
|
|
3647
3664
|
lastPerMessageNudgeTokens: void 0,
|
|
3665
|
+
lastNudgeShownTokens: void 0,
|
|
3648
3666
|
lastToolOutputNudgeTokens: void 0,
|
|
3649
|
-
shouldInjectThisTurn: void 0
|
|
3667
|
+
shouldInjectThisTurn: void 0,
|
|
3668
|
+
compressBaselineSet: false
|
|
3650
3669
|
};
|
|
3651
3670
|
state.messageIds = {
|
|
3652
3671
|
byRawId: /* @__PURE__ */ new Map(),
|
|
@@ -3719,7 +3738,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3719
3738
|
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
|
|
3720
3739
|
lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
|
|
3721
3740
|
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
|
|
3722
|
-
|
|
3741
|
+
lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
|
|
3742
|
+
lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
|
|
3743
|
+
compressBaselineSet: sessionState.nudges.compressBaselineSet
|
|
3723
3744
|
},
|
|
3724
3745
|
stats: sessionState.stats,
|
|
3725
3746
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4291,8 +4312,10 @@ function createSessionState() {
|
|
|
4291
4312
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
4292
4313
|
lastPerMessageNudgeTurn: 0,
|
|
4293
4314
|
lastPerMessageNudgeTokens: void 0,
|
|
4315
|
+
lastNudgeShownTokens: void 0,
|
|
4294
4316
|
lastToolOutputNudgeTokens: void 0,
|
|
4295
|
-
shouldInjectThisTurn: void 0
|
|
4317
|
+
shouldInjectThisTurn: void 0,
|
|
4318
|
+
compressBaselineSet: false
|
|
4296
4319
|
},
|
|
4297
4320
|
stats: {
|
|
4298
4321
|
pruneTokenCounter: 0,
|
|
@@ -4332,8 +4355,10 @@ function resetSessionState(state) {
|
|
|
4332
4355
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
4333
4356
|
lastPerMessageNudgeTurn: 0,
|
|
4334
4357
|
lastPerMessageNudgeTokens: void 0,
|
|
4358
|
+
lastNudgeShownTokens: void 0,
|
|
4335
4359
|
lastToolOutputNudgeTokens: void 0,
|
|
4336
|
-
shouldInjectThisTurn: void 0
|
|
4360
|
+
shouldInjectThisTurn: void 0,
|
|
4361
|
+
compressBaselineSet: false
|
|
4337
4362
|
};
|
|
4338
4363
|
state.stats = {
|
|
4339
4364
|
pruneTokenCounter: 0,
|
|
@@ -4386,7 +4411,9 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
4386
4411
|
);
|
|
4387
4412
|
state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
4388
4413
|
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
|
|
4414
|
+
state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
|
|
4389
4415
|
state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
|
|
4416
|
+
state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
|
|
4390
4417
|
state.stats = {
|
|
4391
4418
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
4392
4419
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
@@ -4551,13 +4578,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
4551
4578
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
4552
4579
|
}
|
|
4553
4580
|
};
|
|
4554
|
-
function createToolSignature(
|
|
4581
|
+
function createToolSignature(tool8, parameters) {
|
|
4555
4582
|
if (!parameters) {
|
|
4556
|
-
return
|
|
4583
|
+
return tool8;
|
|
4557
4584
|
}
|
|
4558
4585
|
const normalized = normalizeParameters(parameters);
|
|
4559
4586
|
const sorted = sortObjectKeys(normalized);
|
|
4560
|
-
return `${
|
|
4587
|
+
return `${tool8}::${JSON.stringify(sorted)}`;
|
|
4561
4588
|
}
|
|
4562
4589
|
function normalizeParameters(params) {
|
|
4563
4590
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -4632,9 +4659,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
4632
4659
|
};
|
|
4633
4660
|
|
|
4634
4661
|
// lib/ui/utils.ts
|
|
4635
|
-
function extractParameterKey(
|
|
4662
|
+
function extractParameterKey(tool8, parameters) {
|
|
4636
4663
|
if (!parameters) return "";
|
|
4637
|
-
if (
|
|
4664
|
+
if (tool8 === "read" && parameters.filePath) {
|
|
4638
4665
|
const offset = parameters.offset;
|
|
4639
4666
|
const limit = parameters.limit;
|
|
4640
4667
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -4648,10 +4675,10 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4648
4675
|
}
|
|
4649
4676
|
return parameters.filePath;
|
|
4650
4677
|
}
|
|
4651
|
-
if ((
|
|
4678
|
+
if ((tool8 === "write" || tool8 === "edit" || tool8 === "multiedit") && parameters.filePath) {
|
|
4652
4679
|
return parameters.filePath;
|
|
4653
4680
|
}
|
|
4654
|
-
if (
|
|
4681
|
+
if (tool8 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
4655
4682
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
4656
4683
|
const paths = [];
|
|
4657
4684
|
let match;
|
|
@@ -4668,51 +4695,51 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4668
4695
|
}
|
|
4669
4696
|
return "patch";
|
|
4670
4697
|
}
|
|
4671
|
-
if (
|
|
4698
|
+
if (tool8 === "list") {
|
|
4672
4699
|
return parameters.path || "(current directory)";
|
|
4673
4700
|
}
|
|
4674
|
-
if (
|
|
4701
|
+
if (tool8 === "glob") {
|
|
4675
4702
|
if (parameters.pattern) {
|
|
4676
4703
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
4677
4704
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
4678
4705
|
}
|
|
4679
4706
|
return "(unknown pattern)";
|
|
4680
4707
|
}
|
|
4681
|
-
if (
|
|
4708
|
+
if (tool8 === "grep") {
|
|
4682
4709
|
if (parameters.pattern) {
|
|
4683
4710
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
4684
4711
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
4685
4712
|
}
|
|
4686
4713
|
return "(unknown pattern)";
|
|
4687
4714
|
}
|
|
4688
|
-
if (
|
|
4715
|
+
if (tool8 === "bash") {
|
|
4689
4716
|
if (parameters.description) return parameters.description;
|
|
4690
4717
|
if (parameters.command) {
|
|
4691
4718
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
4692
4719
|
}
|
|
4693
4720
|
}
|
|
4694
|
-
if (
|
|
4721
|
+
if (tool8 === "webfetch" && parameters.url) {
|
|
4695
4722
|
return parameters.url;
|
|
4696
4723
|
}
|
|
4697
|
-
if (
|
|
4724
|
+
if (tool8 === "websearch" && parameters.query) {
|
|
4698
4725
|
return `"${parameters.query}"`;
|
|
4699
4726
|
}
|
|
4700
|
-
if (
|
|
4727
|
+
if (tool8 === "codesearch" && parameters.query) {
|
|
4701
4728
|
return `"${parameters.query}"`;
|
|
4702
4729
|
}
|
|
4703
|
-
if (
|
|
4730
|
+
if (tool8 === "todowrite") {
|
|
4704
4731
|
return `${parameters.todos?.length || 0} todos`;
|
|
4705
4732
|
}
|
|
4706
|
-
if (
|
|
4733
|
+
if (tool8 === "todoread") {
|
|
4707
4734
|
return "read todo list";
|
|
4708
4735
|
}
|
|
4709
|
-
if (
|
|
4736
|
+
if (tool8 === "task" && parameters.description) {
|
|
4710
4737
|
return parameters.description;
|
|
4711
4738
|
}
|
|
4712
|
-
if (
|
|
4739
|
+
if (tool8 === "skill" && parameters.name) {
|
|
4713
4740
|
return parameters.name;
|
|
4714
4741
|
}
|
|
4715
|
-
if (
|
|
4742
|
+
if (tool8 === "lsp") {
|
|
4716
4743
|
const op = parameters.operation || "lsp";
|
|
4717
4744
|
const path = parameters.filePath || "";
|
|
4718
4745
|
const line = parameters.line;
|
|
@@ -4725,7 +4752,7 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4725
4752
|
}
|
|
4726
4753
|
return op;
|
|
4727
4754
|
}
|
|
4728
|
-
if (
|
|
4755
|
+
if (tool8 === "question") {
|
|
4729
4756
|
const questions = parameters.questions;
|
|
4730
4757
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
4731
4758
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4869,6 +4896,22 @@ function formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) {
|
|
|
4869
4896
|
var TOAST_BODY_MAX_LINES = 12;
|
|
4870
4897
|
var TOAST_SUMMARY_MAX_CHARS = 600;
|
|
4871
4898
|
var NOTIFICATION_SUMMARY_MAX_CHARS = 1500;
|
|
4899
|
+
function formatEntryRanges(entries, state) {
|
|
4900
|
+
const parts = [];
|
|
4901
|
+
for (const entry of entries) {
|
|
4902
|
+
const block = state.prune.messages.blocksById.get(entry.blockId);
|
|
4903
|
+
if (!block) continue;
|
|
4904
|
+
const startRef = block.startId;
|
|
4905
|
+
const endRef = block.endId;
|
|
4906
|
+
if (!startRef || !endRef) continue;
|
|
4907
|
+
if (startRef === endRef) {
|
|
4908
|
+
parts.push(`b${entry.blockId}: ${startRef}`);
|
|
4909
|
+
} else {
|
|
4910
|
+
parts.push(`b${entry.blockId}: ${startRef}\u2013${endRef}`);
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
4914
|
+
}
|
|
4872
4915
|
function truncateToastBody(body, maxLines = TOAST_BODY_MAX_LINES) {
|
|
4873
4916
|
const lines = body.split("\n");
|
|
4874
4917
|
if (lines.length <= maxLines) {
|
|
@@ -4889,18 +4932,24 @@ function buildCompressionSummary(entries, state) {
|
|
|
4889
4932
|
if (entries.length === 1) {
|
|
4890
4933
|
return entries[0]?.summary ?? "";
|
|
4891
4934
|
}
|
|
4935
|
+
const perEntryMax = Math.floor(NOTIFICATION_SUMMARY_MAX_CHARS / entries.length);
|
|
4892
4936
|
let result = "";
|
|
4893
|
-
|
|
4937
|
+
let shown = 0;
|
|
4938
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4939
|
+
const entry = entries[i];
|
|
4894
4940
|
const topic = state.prune.messages.blocksById.get(entry.blockId)?.topic ?? "(unknown topic)";
|
|
4941
|
+
const truncated = entry.summary.length > perEntryMax ? entry.summary.slice(0, perEntryMax - 3) + "..." : entry.summary;
|
|
4895
4942
|
const section = `### ${topic}
|
|
4896
|
-
${
|
|
4943
|
+
${truncated}`;
|
|
4897
4944
|
if (result.length + section.length + 2 > NOTIFICATION_SUMMARY_MAX_CHARS) {
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4945
|
+
const remaining = entries.length - shown;
|
|
4946
|
+
if (remaining > 0) {
|
|
4947
|
+
result += (result ? "\n\n" : "") + `... and ${remaining} more`;
|
|
4948
|
+
}
|
|
4901
4949
|
break;
|
|
4902
4950
|
}
|
|
4903
4951
|
result += (result ? "\n\n" : "") + section;
|
|
4952
|
+
shown++;
|
|
4904
4953
|
}
|
|
4905
4954
|
return result;
|
|
4906
4955
|
}
|
|
@@ -4980,6 +5029,7 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
4980
5029
|
contextTokensBefore,
|
|
4981
5030
|
contextTokensAfter
|
|
4982
5031
|
)}`;
|
|
5032
|
+
let displaySummary = summary;
|
|
4983
5033
|
if (config.pruneNotification === "minimal") {
|
|
4984
5034
|
message = `${notificationHeader} \u2014 ${compressionLabel}`;
|
|
4985
5035
|
} else {
|
|
@@ -5001,6 +5051,11 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5001
5051
|
${progressBar}`;
|
|
5002
5052
|
message += `
|
|
5003
5053
|
\u25A3 ${compressionLabel} ${formatCompressionMetrics(compressedTokens, summaryTokens)}`;
|
|
5054
|
+
const rangeStr = formatEntryRanges(entries, state);
|
|
5055
|
+
if (rangeStr) {
|
|
5056
|
+
message += `
|
|
5057
|
+
\u2192 Range: ${rangeStr}`;
|
|
5058
|
+
}
|
|
5004
5059
|
message += `
|
|
5005
5060
|
\u2192 Topic: ${topic}`;
|
|
5006
5061
|
message += `
|
|
@@ -5011,24 +5066,17 @@ ${progressBar}`;
|
|
|
5011
5066
|
message += ` compressed`;
|
|
5012
5067
|
}
|
|
5013
5068
|
if (config.compress.showCompression) {
|
|
5014
|
-
|
|
5069
|
+
if (config.pruneNotification === "detailed") {
|
|
5070
|
+
displaySummary = summary;
|
|
5071
|
+
} else {
|
|
5072
|
+
displaySummary = summary.length > NOTIFICATION_SUMMARY_MAX_CHARS ? truncateToastSummary(summary, NOTIFICATION_SUMMARY_MAX_CHARS) : summary;
|
|
5073
|
+
}
|
|
5015
5074
|
message += `
|
|
5016
5075
|
\u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
|
|
5017
5076
|
}
|
|
5018
5077
|
}
|
|
5019
5078
|
if (config.pruneNotificationType === "toast") {
|
|
5020
5079
|
let toastMessage = message;
|
|
5021
|
-
if (config.compress.showCompression) {
|
|
5022
|
-
const truncatedSummary = truncateToastSummary(summary);
|
|
5023
|
-
if (truncatedSummary !== summary) {
|
|
5024
|
-
toastMessage = toastMessage.replace(
|
|
5025
|
-
`
|
|
5026
|
-
\u2192 Compression (~${summaryTokensStr}): ${truncateToastSummary(summary, NOTIFICATION_SUMMARY_MAX_CHARS)}`,
|
|
5027
|
-
`
|
|
5028
|
-
\u2192 Compression (~${summaryTokensStr}): ${truncatedSummary}`
|
|
5029
|
-
);
|
|
5030
|
-
}
|
|
5031
|
-
}
|
|
5032
5080
|
toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
|
|
5033
5081
|
await client.tui.showToast({
|
|
5034
5082
|
body: {
|
|
@@ -5127,6 +5175,118 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
|
|
|
5127
5175
|
);
|
|
5128
5176
|
}
|
|
5129
5177
|
|
|
5178
|
+
// lib/compress/keep-markers.ts
|
|
5179
|
+
var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
|
|
5180
|
+
var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
|
|
5181
|
+
function resolveKeepMarkers(summary, messages, state, config) {
|
|
5182
|
+
const msgByRef = /* @__PURE__ */ new Map();
|
|
5183
|
+
for (const msg of messages) {
|
|
5184
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
5185
|
+
if (ref) msgByRef.set(ref, msg);
|
|
5186
|
+
}
|
|
5187
|
+
const maxChars = config.compress?.keepEmbedMaxChars ?? 2e3;
|
|
5188
|
+
let expandedCount = 0;
|
|
5189
|
+
let refCount = 0;
|
|
5190
|
+
const unresolvedRefs = [];
|
|
5191
|
+
const expanded = summary.replace(KEEP_REGEX, (match, ref) => {
|
|
5192
|
+
const normalized = normalizeRef(ref);
|
|
5193
|
+
const msg = normalized ? msgByRef.get(normalized) : void 0;
|
|
5194
|
+
if (!msg) {
|
|
5195
|
+
unresolvedRefs.push(ref);
|
|
5196
|
+
return match;
|
|
5197
|
+
}
|
|
5198
|
+
expandedCount++;
|
|
5199
|
+
return formatKeptMessage(msg, normalized, maxChars);
|
|
5200
|
+
}).replace(REF_REGEX, (_match, ref, desc) => {
|
|
5201
|
+
const normalized = normalizeRef(ref);
|
|
5202
|
+
const msg = normalized ? msgByRef.get(normalized) : void 0;
|
|
5203
|
+
if (!msg) {
|
|
5204
|
+
unresolvedRefs.push(ref);
|
|
5205
|
+
return _match;
|
|
5206
|
+
}
|
|
5207
|
+
refCount++;
|
|
5208
|
+
return `[\u2192 ${normalized}: ${desc.trim()}]`;
|
|
5209
|
+
});
|
|
5210
|
+
return { summary: expanded, expandedCount, refCount, unresolvedRefs };
|
|
5211
|
+
}
|
|
5212
|
+
function normalizeRef(ref) {
|
|
5213
|
+
const idx = parseMessageRef(ref);
|
|
5214
|
+
if (idx === null) return null;
|
|
5215
|
+
return formatMessageRef(idx);
|
|
5216
|
+
}
|
|
5217
|
+
function formatKeptMessage(msg, ref, maxChars) {
|
|
5218
|
+
const formatted = formatByType(msg);
|
|
5219
|
+
const truncated = truncate2(formatted, maxChars);
|
|
5220
|
+
return `
|
|
5221
|
+
--- [${ref}: ${labelForMessage(msg)}] ---
|
|
5222
|
+
${truncated}
|
|
5223
|
+
--- end ---
|
|
5224
|
+
`;
|
|
5225
|
+
}
|
|
5226
|
+
function formatByType(msg) {
|
|
5227
|
+
for (const part of msg.parts || []) {
|
|
5228
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
5229
|
+
return part.text;
|
|
5230
|
+
}
|
|
5231
|
+
if (part.type === "tool") {
|
|
5232
|
+
const tool8 = part.tool || "unknown";
|
|
5233
|
+
const state = part.state || {};
|
|
5234
|
+
const input = state.input || {};
|
|
5235
|
+
const output = state.output || "";
|
|
5236
|
+
switch (tool8) {
|
|
5237
|
+
case "bash":
|
|
5238
|
+
case "interactive_bash": {
|
|
5239
|
+
const cmd = typeof input === "string" ? input : input.command || JSON.stringify(input);
|
|
5240
|
+
return `$ ${cmd}
|
|
5241
|
+
${output}`;
|
|
5242
|
+
}
|
|
5243
|
+
case "read": {
|
|
5244
|
+
const fp = input.filePath || input.path || input.file || "";
|
|
5245
|
+
return output;
|
|
5246
|
+
}
|
|
5247
|
+
case "write":
|
|
5248
|
+
case "edit": {
|
|
5249
|
+
const fp = input.filePath || input.path || "";
|
|
5250
|
+
const content = input.content || input.newString || "";
|
|
5251
|
+
return `${fp}:
|
|
5252
|
+
${content}`;
|
|
5253
|
+
}
|
|
5254
|
+
case "reply": {
|
|
5255
|
+
return output || "[reply posted]";
|
|
5256
|
+
}
|
|
5257
|
+
case "grep":
|
|
5258
|
+
case "glob": {
|
|
5259
|
+
return output;
|
|
5260
|
+
}
|
|
5261
|
+
default: {
|
|
5262
|
+
if (output && typeof output === "string" && output.length > 0) {
|
|
5263
|
+
return output;
|
|
5264
|
+
}
|
|
5265
|
+
const compact = JSON.stringify({ tool: tool8, input }, null, 0);
|
|
5266
|
+
return compact.length > 500 ? compact.slice(0, 500) + "..." : compact;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
return "[empty message]";
|
|
5272
|
+
}
|
|
5273
|
+
function labelForMessage(msg) {
|
|
5274
|
+
for (const part of msg.parts || []) {
|
|
5275
|
+
if (part.type === "tool") {
|
|
5276
|
+
const tool8 = part.tool || "unknown";
|
|
5277
|
+
const input = part.state?.input || {};
|
|
5278
|
+
const fp = input.filePath || input.path || input.command || "";
|
|
5279
|
+
return fp ? `${tool8}: ${String(fp).slice(0, 60)}` : tool8;
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
return msg.info.role === "user" ? "user" : "text";
|
|
5283
|
+
}
|
|
5284
|
+
function truncate2(text, maxChars) {
|
|
5285
|
+
if (text.length <= maxChars) return text;
|
|
5286
|
+
return text.slice(0, maxChars) + `
|
|
5287
|
+
... [truncated, ${text.length} chars total]`;
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5130
5290
|
// lib/compress/message.ts
|
|
5131
5291
|
function buildSchema(maxSummaryLengthHard) {
|
|
5132
5292
|
return {
|
|
@@ -5228,7 +5388,14 @@ function createCompressMessageTool(ctx) {
|
|
|
5228
5388
|
const runId = allocateRunId(ctx.state);
|
|
5229
5389
|
for (const { plan, summaryWithTools } of preparedPlans) {
|
|
5230
5390
|
const blockId = allocateBlockId(ctx.state);
|
|
5231
|
-
const
|
|
5391
|
+
const keepResult = resolveKeepMarkers(
|
|
5392
|
+
summaryWithTools,
|
|
5393
|
+
rawMessages,
|
|
5394
|
+
ctx.state,
|
|
5395
|
+
ctx.config
|
|
5396
|
+
);
|
|
5397
|
+
const resolvedSummary = keepResult.summary;
|
|
5398
|
+
const storedSummary = wrapCompressedSummary(blockId, resolvedSummary);
|
|
5232
5399
|
const summaryTokens = countTokens2(storedSummary);
|
|
5233
5400
|
applyCompressionState(
|
|
5234
5401
|
ctx.state,
|
|
@@ -5253,7 +5420,7 @@ function createCompressMessageTool(ctx) {
|
|
|
5253
5420
|
notifications.push({
|
|
5254
5421
|
blockId,
|
|
5255
5422
|
runId,
|
|
5256
|
-
summary:
|
|
5423
|
+
summary: resolvedSummary,
|
|
5257
5424
|
summaryTokens
|
|
5258
5425
|
});
|
|
5259
5426
|
}
|
|
@@ -5420,6 +5587,13 @@ function createCompressRangeTool(ctx) {
|
|
|
5420
5587
|
const runId = allocateRunId(ctx.state);
|
|
5421
5588
|
for (const preparedPlan of preparedPlans) {
|
|
5422
5589
|
const blockId = allocateBlockId(ctx.state);
|
|
5590
|
+
const keepResult = resolveKeepMarkers(
|
|
5591
|
+
preparedPlan.finalSummary,
|
|
5592
|
+
rawMessages,
|
|
5593
|
+
ctx.state,
|
|
5594
|
+
ctx.config
|
|
5595
|
+
);
|
|
5596
|
+
preparedPlan.finalSummary = keepResult.summary;
|
|
5423
5597
|
const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
|
|
5424
5598
|
const summaryTokens = countTokens2(storedSummary);
|
|
5425
5599
|
const applied = applyCompressionState(
|
|
@@ -5785,6 +5959,40 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
5785
5959
|
messages.length = 0;
|
|
5786
5960
|
messages.push(...result);
|
|
5787
5961
|
};
|
|
5962
|
+
function stripStaleCompressCalls(messages) {
|
|
5963
|
+
const lastUserIdx = messages.findLastIndex(
|
|
5964
|
+
(m) => m.info.role === "user" && !isIgnoredUserMessage(m)
|
|
5965
|
+
);
|
|
5966
|
+
if (lastUserIdx < 0) return 0;
|
|
5967
|
+
let stripped = 0;
|
|
5968
|
+
const result = [];
|
|
5969
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5970
|
+
const msg = messages[i];
|
|
5971
|
+
if (i >= lastUserIdx) {
|
|
5972
|
+
result.push(msg);
|
|
5973
|
+
continue;
|
|
5974
|
+
}
|
|
5975
|
+
const hasCompress = msg.parts.some(
|
|
5976
|
+
(p) => p.type === "tool" && p.tool === "compress"
|
|
5977
|
+
);
|
|
5978
|
+
if (!hasCompress) {
|
|
5979
|
+
result.push(msg);
|
|
5980
|
+
continue;
|
|
5981
|
+
}
|
|
5982
|
+
const remaining = msg.parts.filter(
|
|
5983
|
+
(p) => !(p.type === "tool" && p.tool === "compress")
|
|
5984
|
+
);
|
|
5985
|
+
stripped++;
|
|
5986
|
+
if (remaining.length > 0) {
|
|
5987
|
+
result.push({ ...msg, parts: remaining });
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
if (stripped > 0) {
|
|
5991
|
+
messages.length = 0;
|
|
5992
|
+
messages.push(...result);
|
|
5993
|
+
}
|
|
5994
|
+
return stripped;
|
|
5995
|
+
}
|
|
5788
5996
|
|
|
5789
5997
|
// lib/messages/sync.ts
|
|
5790
5998
|
function sortBlocksByCreation(a, b) {
|
|
@@ -5933,8 +6141,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
5933
6141
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
5934
6142
|
) ? "deny" : basePermission;
|
|
5935
6143
|
};
|
|
5936
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
5937
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
6144
|
+
var hasExplicitToolPermission = (permissionConfig, tool8) => {
|
|
6145
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool8) : false;
|
|
5938
6146
|
};
|
|
5939
6147
|
|
|
5940
6148
|
// lib/compress-permission.ts
|
|
@@ -6358,8 +6566,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
|
6358
6566
|
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6359
6567
|
return `
|
|
6360
6568
|
|
|
6361
|
-
Context: ${formatK(currentTokens)} tokens
|
|
6362
|
-
All compression serves the primary task, but be frugal. Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
|
|
6569
|
+
Context: ${formatK(currentTokens)} tokens.`;
|
|
6363
6570
|
}
|
|
6364
6571
|
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
6365
6572
|
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
@@ -6512,7 +6719,7 @@ function estimateContextComposition(messages, state) {
|
|
|
6512
6719
|
perTool.sort((a, b) => b.tokens - a.tokens);
|
|
6513
6720
|
perCode.sort((a, b) => b.tokens - a.tokens);
|
|
6514
6721
|
perText.sort((a, b) => b.tokens - a.tokens);
|
|
6515
|
-
const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([
|
|
6722
|
+
const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens);
|
|
6516
6723
|
return {
|
|
6517
6724
|
toolTokens,
|
|
6518
6725
|
codeTokens,
|
|
@@ -6527,8 +6734,78 @@ function estimateContextComposition(messages, state) {
|
|
|
6527
6734
|
toolTypeBreakdown
|
|
6528
6735
|
};
|
|
6529
6736
|
}
|
|
6737
|
+
function buildCompressibleRanges(messages, state) {
|
|
6738
|
+
const msgInfo = [];
|
|
6739
|
+
for (const msg of messages) {
|
|
6740
|
+
if (isSyntheticMessage(msg)) continue;
|
|
6741
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
6742
|
+
if (!ref) continue;
|
|
6743
|
+
let tokens = 0;
|
|
6744
|
+
let isTool = false;
|
|
6745
|
+
for (const part of msg.parts || []) {
|
|
6746
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6747
|
+
tokens += Math.round(part.text.length / 4);
|
|
6748
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6749
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6750
|
+
isTool = true;
|
|
6751
|
+
}
|
|
6752
|
+
}
|
|
6753
|
+
const refNum = parseInt(ref.slice(1), 10);
|
|
6754
|
+
msgInfo.push({ ref, refNum, tokens, isTool, isUser: msg.info.role === "user" });
|
|
6755
|
+
}
|
|
6756
|
+
if (msgInfo.length === 0) return [];
|
|
6757
|
+
const groups = [];
|
|
6758
|
+
let cur = null;
|
|
6759
|
+
let prevRefNum = -2;
|
|
6760
|
+
for (const info of msgInfo) {
|
|
6761
|
+
const hasGap = info.refNum > prevRefNum + 1;
|
|
6762
|
+
if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
|
|
6763
|
+
groups.push(cur);
|
|
6764
|
+
cur = null;
|
|
6765
|
+
}
|
|
6766
|
+
prevRefNum = info.refNum;
|
|
6767
|
+
if (!cur) {
|
|
6768
|
+
cur = {
|
|
6769
|
+
startRef: info.ref,
|
|
6770
|
+
endRef: info.ref,
|
|
6771
|
+
count: 1,
|
|
6772
|
+
tokens: info.tokens,
|
|
6773
|
+
toolPct: info.isTool ? 100 : 0,
|
|
6774
|
+
textPct: info.isTool ? 0 : 100
|
|
6775
|
+
};
|
|
6776
|
+
} else {
|
|
6777
|
+
cur.endRef = info.ref;
|
|
6778
|
+
cur.count++;
|
|
6779
|
+
cur.tokens += info.tokens;
|
|
6780
|
+
if (info.isTool) {
|
|
6781
|
+
cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
|
|
6782
|
+
} else {
|
|
6783
|
+
cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
|
|
6784
|
+
}
|
|
6785
|
+
cur.textPct = 100 - cur.toolPct;
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
if (cur) groups.push(cur);
|
|
6789
|
+
return groups.filter((g) => g.tokens > 0);
|
|
6790
|
+
}
|
|
6791
|
+
function formatCompressibleRanges(ranges) {
|
|
6792
|
+
if (ranges.length === 0) return "";
|
|
6793
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6794
|
+
const lines = ranges.map((r, i) => {
|
|
6795
|
+
const suffix = i === ranges.length - 1 ? " (recent \u2014 may still be in active use)" : "";
|
|
6796
|
+
return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
|
|
6797
|
+
});
|
|
6798
|
+
return `Compressible ranges (oldest first):
|
|
6799
|
+
${lines.join("\n")}`;
|
|
6800
|
+
}
|
|
6530
6801
|
|
|
6531
6802
|
// lib/prompts/compression-rules.ts
|
|
6803
|
+
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
6804
|
+
- All compression serves the primary task, but be frugal.
|
|
6805
|
+
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
6806
|
+
- Compress by need, not by percentage.
|
|
6807
|
+
- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.
|
|
6808
|
+
- Curate summaries like a well-structured document. User prompts, compressed tool outputs, code, logs, or skill-call intermediate results that are critically important should be preserved \u2014 not by exempting them from compression, but by embedding them in the summary via [[KEEP:mNNNNN]] (auto-expanded verbatim) and [[REF:mNNNNN|description]] (compact link).`;
|
|
6532
6809
|
var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
|
|
6533
6810
|
|
|
6534
6811
|
When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
|
|
@@ -6557,6 +6834,8 @@ DROP \u2014 extract the signal, discard the vessel:
|
|
|
6557
6834
|
|
|
6558
6835
|
For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
|
|
6559
6836
|
|
|
6837
|
+
KEEP MARKERS: \`[[KEEP:mNNNNN]]\` expands original message content into the summary (truncated to a max length). Do NOT use KEEP for verbose command output, diagnostic scripts, log dumps, or any content whose value is in the conclusion rather than the raw output \u2014 summarize these or use \`[[REF:mNNNNN|desc]]\` instead.
|
|
6838
|
+
|
|
6560
6839
|
PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
6561
6840
|
1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
|
|
6562
6841
|
2. Decisions and rationale.
|
|
@@ -6575,7 +6854,7 @@ function createSuffixMessage(messages) {
|
|
|
6575
6854
|
messages.push(synthetic);
|
|
6576
6855
|
return synthetic;
|
|
6577
6856
|
}
|
|
6578
|
-
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
6857
|
+
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify, preCompressTokens) => {
|
|
6579
6858
|
if (compressPermission(state, config) === "deny") {
|
|
6580
6859
|
return;
|
|
6581
6860
|
}
|
|
@@ -6592,18 +6871,46 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6592
6871
|
modelId,
|
|
6593
6872
|
messages
|
|
6594
6873
|
);
|
|
6595
|
-
|
|
6874
|
+
const lastUserIdx = messages.findLastIndex(
|
|
6875
|
+
(m) => m.info.role === "user" && !isIgnoredUserMessage(m)
|
|
6876
|
+
);
|
|
6877
|
+
const currentTurnStart = lastUserIdx >= 0 ? lastUserIdx + 1 : 0;
|
|
6878
|
+
const currentTurnHasCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
|
|
6879
|
+
if (currentTurnHasCompress) {
|
|
6880
|
+
const wasNudgeTriggered = state.nudges.lastNudgeShownTokens !== void 0;
|
|
6596
6881
|
state.nudges.contextLimitAnchors.clear();
|
|
6597
6882
|
state.nudges.turnNudgeAnchors.clear();
|
|
6598
6883
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6599
|
-
state.nudges.
|
|
6884
|
+
state.nudges.lastNudgeShownTokens = void 0;
|
|
6600
6885
|
state.nudges.lastToolOutputNudgeTokens = void 0;
|
|
6886
|
+
if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
|
|
6887
|
+
const baseline = state.nudges.lastPerMessageNudgeTokens;
|
|
6888
|
+
const postCompress = currentTokens;
|
|
6889
|
+
const preCompress = preCompressTokens;
|
|
6890
|
+
if (baseline !== void 0 && postCompress !== void 0 && preCompress !== void 0 && preCompress > postCompress) {
|
|
6891
|
+
const growth = preCompress - baseline;
|
|
6892
|
+
const compressed = preCompress - postCompress;
|
|
6893
|
+
if (growth > 0 && compressed > 0) {
|
|
6894
|
+
const ratio = Math.min(1, compressed / growth);
|
|
6895
|
+
const adjustment = Math.min(1, ratio * 2);
|
|
6896
|
+
const newBaseline = baseline + Math.round((postCompress - baseline) * adjustment);
|
|
6897
|
+
state.nudges.lastPerMessageNudgeTokens = newBaseline;
|
|
6898
|
+
} else {
|
|
6899
|
+
state.nudges.lastPerMessageNudgeTokens = postCompress;
|
|
6900
|
+
}
|
|
6901
|
+
} else {
|
|
6902
|
+
state.nudges.lastPerMessageNudgeTokens = postCompress;
|
|
6903
|
+
}
|
|
6904
|
+
state.nudges.compressBaselineSet = true;
|
|
6905
|
+
}
|
|
6601
6906
|
saveSessionState(state, logger).catch(() => {
|
|
6602
6907
|
});
|
|
6603
6908
|
return;
|
|
6604
6909
|
}
|
|
6910
|
+
state.nudges.compressBaselineSet = false;
|
|
6605
6911
|
let anchorsChanged = false;
|
|
6606
6912
|
let baselineReEstablished = false;
|
|
6913
|
+
let baselineCorrected = false;
|
|
6607
6914
|
if (!overMinLimit) {
|
|
6608
6915
|
const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
|
|
6609
6916
|
const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
|
|
@@ -6666,15 +6973,20 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6666
6973
|
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
|
|
6667
6974
|
if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
|
|
6668
6975
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6976
|
+
state.nudges.lastNudgeShownTokens = void 0;
|
|
6977
|
+
baselineCorrected = true;
|
|
6669
6978
|
}
|
|
6979
|
+
const hasPendingNudge = state.nudges.lastNudgeShownTokens !== void 0;
|
|
6980
|
+
const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
|
|
6981
|
+
const growthReference = state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens;
|
|
6670
6982
|
const decision = computeShouldNudge({
|
|
6671
6983
|
currentTokens,
|
|
6672
6984
|
modelContextLimit,
|
|
6673
6985
|
overMinLimit,
|
|
6674
6986
|
overMaxLimit,
|
|
6675
|
-
lastNudgeTokens:
|
|
6987
|
+
lastNudgeTokens: growthReference,
|
|
6676
6988
|
minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
|
|
6677
|
-
nudgeGrowthTokens
|
|
6989
|
+
nudgeGrowthTokens: effectiveThreshold
|
|
6678
6990
|
});
|
|
6679
6991
|
state.nudges.shouldInjectThisTurn = decision.shouldNudge;
|
|
6680
6992
|
if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
|
|
@@ -6682,24 +6994,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6682
6994
|
baselineReEstablished = true;
|
|
6683
6995
|
}
|
|
6684
6996
|
const composition = estimateContextComposition(messages, state);
|
|
6685
|
-
const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? nudgeGrowthTokens;
|
|
6686
|
-
let toolOutputReminder = null;
|
|
6687
|
-
if (composition.toolTokens > 0) {
|
|
6688
|
-
if (state.nudges.lastToolOutputNudgeTokens === void 0) {
|
|
6689
|
-
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6690
|
-
} else {
|
|
6691
|
-
const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
|
|
6692
|
-
if (toolGrowth >= toolOutputThreshold) {
|
|
6693
|
-
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6694
|
-
const topRanges = composition.largestRanges.slice(0, 15).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
|
|
6695
|
-
toolOutputReminder = `
|
|
6696
|
-
|
|
6697
|
-
\u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
|
|
6698
|
-
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6699
|
-
anchorsChanged = true;
|
|
6700
|
-
}
|
|
6701
|
-
}
|
|
6702
|
-
}
|
|
6703
6997
|
let tipsText = null;
|
|
6704
6998
|
if (decision.shouldNudge) {
|
|
6705
6999
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
@@ -6710,28 +7004,21 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6710
7004
|
const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
|
|
6711
7005
|
const plainTextTokens = composition.textTokens;
|
|
6712
7006
|
const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
|
|
6713
|
-
This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full
|
|
7007
|
+
This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
|
|
7008
|
+
|
|
7009
|
+
${COMPRESS_PHILOSOPHY}` : "";
|
|
6714
7010
|
let breakdown = `${efficiencyNote}
|
|
6715
7011
|
Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%)${growthStr}`;
|
|
6716
|
-
const
|
|
6717
|
-
if (
|
|
7012
|
+
const ranges = buildCompressibleRanges(messages, state);
|
|
7013
|
+
if (ranges.length > 0) {
|
|
6718
7014
|
breakdown += `
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
if (composition.largestToolRanges.length > 0) {
|
|
6722
|
-
breakdown += `
|
|
6723
|
-
Largest tool outputs: ${composition.largestToolRanges.slice(0, 10).map((r) => `${r.ref} (${fmt(r.tokens)})${r.tool ? " " + r.tool : ""}`).join(", ")}`;
|
|
6724
|
-
}
|
|
6725
|
-
if (composition.largestCodeRanges.length > 0) {
|
|
6726
|
-
breakdown += `
|
|
6727
|
-
Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6728
|
-
}
|
|
6729
|
-
if (composition.largestMessageRanges.length > 0) {
|
|
7015
|
+
|
|
7016
|
+
${formatCompressibleRanges(ranges)}`;
|
|
6730
7017
|
breakdown += `
|
|
6731
|
-
|
|
7018
|
+
\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
|
|
6732
7019
|
}
|
|
6733
7020
|
breakdown += `
|
|
6734
|
-
|
|
7021
|
+
Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
|
|
6735
7022
|
if (decision.tipsVariant !== "maxLimit") {
|
|
6736
7023
|
breakdown += `
|
|
6737
7024
|
|
|
@@ -6742,8 +7029,7 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6742
7029
|
if (decision.tipsVariant === "maxLimit") {
|
|
6743
7030
|
tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
|
|
6744
7031
|
}
|
|
6745
|
-
state.nudges.
|
|
6746
|
-
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
7032
|
+
state.nudges.lastNudgeShownTokens = currentTokens;
|
|
6747
7033
|
if (config.compress.mode !== "message") {
|
|
6748
7034
|
const visibleMessageIds = new Set(
|
|
6749
7035
|
messages.map((message) => message.info.id)
|
|
@@ -6761,14 +7047,16 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6761
7047
|
if (tipsText && suffixMessage) {
|
|
6762
7048
|
appendToLastTextPart(suffixMessage, tipsText);
|
|
6763
7049
|
}
|
|
6764
|
-
injectVisibleIdRange(state, config, messages, suffixMessage);
|
|
6765
|
-
}
|
|
6766
|
-
if (toolOutputReminder && suffixMessage) {
|
|
6767
|
-
appendToLastTextPart(suffixMessage, toolOutputReminder);
|
|
6768
7050
|
}
|
|
6769
7051
|
if (suffixMessage) {
|
|
6770
7052
|
if (hasContent(suffixMessage)) {
|
|
6771
7053
|
appendToLastTextPart(suffixMessage, "\n");
|
|
7054
|
+
if (debugNotify) {
|
|
7055
|
+
const text = suffixMessage.parts.filter((p) => p.type === "text").map((p) => p.text || "").join("\n").trim();
|
|
7056
|
+
if (text) {
|
|
7057
|
+
debugNotify(text);
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
6772
7060
|
} else {
|
|
6773
7061
|
const idx = messages.lastIndexOf(suffixMessage);
|
|
6774
7062
|
if (idx !== -1) {
|
|
@@ -6776,7 +7064,7 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6776
7064
|
}
|
|
6777
7065
|
}
|
|
6778
7066
|
}
|
|
6779
|
-
if (anchorsChanged || decision.shouldNudge || baselineReEstablished) {
|
|
7067
|
+
if (anchorsChanged || decision.shouldNudge || baselineReEstablished || baselineCorrected) {
|
|
6780
7068
|
saveSessionState(state, logger).catch(() => {
|
|
6781
7069
|
});
|
|
6782
7070
|
}
|
|
@@ -6794,86 +7082,6 @@ function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
|
6794
7082
|
}
|
|
6795
7083
|
target.parts.push(createSyntheticTextPart(target, usageTag));
|
|
6796
7084
|
}
|
|
6797
|
-
function refNumber(ref) {
|
|
6798
|
-
const n = parseInt(ref.slice(1), 10);
|
|
6799
|
-
return Number.isNaN(n) ? -1 : n;
|
|
6800
|
-
}
|
|
6801
|
-
function buildVisibleSegments(state, messages) {
|
|
6802
|
-
const refInfo = /* @__PURE__ */ new Map();
|
|
6803
|
-
for (const msg of messages) {
|
|
6804
|
-
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
6805
|
-
if (!ref) continue;
|
|
6806
|
-
let tokens = 0;
|
|
6807
|
-
let hasTool = false;
|
|
6808
|
-
for (const part of msg.parts || []) {
|
|
6809
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
6810
|
-
tokens += Math.round(part.text.length / 4);
|
|
6811
|
-
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6812
|
-
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6813
|
-
hasTool = true;
|
|
6814
|
-
}
|
|
6815
|
-
}
|
|
6816
|
-
refInfo.set(ref, { tokens, hasTool });
|
|
6817
|
-
}
|
|
6818
|
-
if (refInfo.size === 0) return [];
|
|
6819
|
-
const refs = Array.from(refInfo.keys()).sort((a, b) => refNumber(a) - refNumber(b));
|
|
6820
|
-
const segments = [];
|
|
6821
|
-
let cur = null;
|
|
6822
|
-
let prevNum = -2;
|
|
6823
|
-
for (const ref of refs) {
|
|
6824
|
-
const num = refNumber(ref);
|
|
6825
|
-
const info = refInfo.get(ref);
|
|
6826
|
-
if (cur && num === prevNum + 1) {
|
|
6827
|
-
cur.endRef = ref;
|
|
6828
|
-
cur.count++;
|
|
6829
|
-
cur.tokens += info.tokens;
|
|
6830
|
-
if (info.hasTool) cur.hasTool = true;
|
|
6831
|
-
} else {
|
|
6832
|
-
if (cur) segments.push(cur);
|
|
6833
|
-
cur = { startRef: ref, endRef: ref, count: 1, tokens: info.tokens, hasTool: info.hasTool };
|
|
6834
|
-
}
|
|
6835
|
-
prevNum = num;
|
|
6836
|
-
}
|
|
6837
|
-
if (cur) segments.push(cur);
|
|
6838
|
-
return segments;
|
|
6839
|
-
}
|
|
6840
|
-
function formatSegment(seg) {
|
|
6841
|
-
return seg.startRef === seg.endRef ? seg.startRef : `${seg.startRef}\u2013${seg.endRef}`;
|
|
6842
|
-
}
|
|
6843
|
-
function formatVisibleGuidance(segments, maxSegs) {
|
|
6844
|
-
if (segments.length === 0) return "";
|
|
6845
|
-
const totalMsgs = segments.reduce((s, seg) => s + seg.count, 0);
|
|
6846
|
-
const totalSegs = segments.length;
|
|
6847
|
-
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6848
|
-
if (totalSegs <= maxSegs) {
|
|
6849
|
-
return `[Visible: ${segments.map(formatSegment).join(", ")} (${totalMsgs} msg${totalMsgs === 1 ? "" : "s"}, ${totalSegs} segment${totalSegs === 1 ? "" : "s"})]`;
|
|
6850
|
-
}
|
|
6851
|
-
const keepSet = new Set(
|
|
6852
|
-
[...segments].sort((a, b) => {
|
|
6853
|
-
if (a.hasTool !== b.hasTool) return a.hasTool ? -1 : 1;
|
|
6854
|
-
return b.tokens - a.tokens;
|
|
6855
|
-
}).slice(0, maxSegs)
|
|
6856
|
-
);
|
|
6857
|
-
const shown = segments.filter((s) => keepSet.has(s));
|
|
6858
|
-
const omitted = segments.filter((s) => !keepSet.has(s));
|
|
6859
|
-
const omittedTokens = omitted.reduce((sum, s) => sum + s.tokens, 0);
|
|
6860
|
-
const omittedMsgs = omitted.reduce((sum, s) => sum + s.count, 0);
|
|
6861
|
-
return `[Visible (top ${shown.length} of ${totalSegs} segments, ${totalMsgs} msgs): ${shown.map(formatSegment).join(", ")} | +${omitted.length} smaller segment${omitted.length === 1 ? "" : "s"} (~${fmt(omittedTokens)} tokens, ${omittedMsgs} msg${omittedMsgs === 1 ? "" : "s"}) omitted]`;
|
|
6862
|
-
}
|
|
6863
|
-
function injectVisibleIdRange(state, config, messages, target) {
|
|
6864
|
-
if (!target) return;
|
|
6865
|
-
const segments = buildVisibleSegments(state, messages);
|
|
6866
|
-
if (segments.length === 0) return;
|
|
6867
|
-
const maxSegs = config.compress?.maxVisibleSegments ?? 50;
|
|
6868
|
-
const rangeTag = "\n\n" + formatVisibleGuidance(segments, maxSegs);
|
|
6869
|
-
for (const part of target.parts) {
|
|
6870
|
-
if (part.type === "text") {
|
|
6871
|
-
appendToTextPart(part, rangeTag);
|
|
6872
|
-
return;
|
|
6873
|
-
}
|
|
6874
|
-
}
|
|
6875
|
-
target.parts.push(createSyntheticTextPart(target, rangeTag));
|
|
6876
|
-
}
|
|
6877
7085
|
var injectMessageIds = (state, config, messages, compressionPriorities) => {
|
|
6878
7086
|
if (compressPermission(state, config) === "deny") {
|
|
6879
7087
|
return;
|
|
@@ -7415,19 +7623,17 @@ ${content}`;
|
|
|
7415
7623
|
|
|
7416
7624
|
// lib/compress/status.ts
|
|
7417
7625
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
7418
|
-
var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview
|
|
7626
|
+
var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges by default.
|
|
7419
7627
|
|
|
7420
|
-
No args: Overview
|
|
7421
|
-
scope:"uncompressed":
|
|
7628
|
+
No args: Overview with totals, compressed blocks, and compressible ranges.
|
|
7629
|
+
scope:"uncompressed": Compressible ranges only (default view:"ranges"). Add view:"messages" for per-message listing with tool/sort filters.
|
|
7422
7630
|
scope:"compressed": Drill into compressed blocks \u2014 list each with full details (age, generation, consumed lineage).
|
|
7423
7631
|
|
|
7424
|
-
Sort options: "size" (default, largest first), "time" (chronological), "tool" (group by tool type).
|
|
7425
|
-
|
|
7426
7632
|
Use this tool to:
|
|
7427
|
-
- See what's consuming context (
|
|
7428
|
-
-
|
|
7429
|
-
-
|
|
7430
|
-
-
|
|
7633
|
+
- See what's consuming context + compressible ranges in one call (no args)
|
|
7634
|
+
- Focus on ranges only (scope:"uncompressed")
|
|
7635
|
+
- Find all messages of a specific tool type (scope:"uncompressed", view:"messages", tool:"bash")
|
|
7636
|
+
- Check block details before decompressing (scope:"compressed")`;
|
|
7431
7637
|
function formatTokens(n) {
|
|
7432
7638
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
7433
7639
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
@@ -7477,7 +7683,7 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
7477
7683
|
});
|
|
7478
7684
|
return { messages: result, summaryTokens };
|
|
7479
7685
|
}
|
|
7480
|
-
function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
|
|
7686
|
+
function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, rawMessages, ctx) {
|
|
7481
7687
|
const lines = [];
|
|
7482
7688
|
const toolTypeMap = /* @__PURE__ */ new Map();
|
|
7483
7689
|
for (const m of visibleMessages) {
|
|
@@ -7498,7 +7704,7 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
|
|
|
7498
7704
|
lines.push(
|
|
7499
7705
|
` ${formatTokens(total)} total | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
|
|
7500
7706
|
);
|
|
7501
|
-
const topTypes = Array.from(toolTypeMap.entries()).map(([
|
|
7707
|
+
const topTypes = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
|
|
7502
7708
|
if (topTypes.length > 0) {
|
|
7503
7709
|
lines.push(` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`);
|
|
7504
7710
|
}
|
|
@@ -7522,9 +7728,45 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
|
|
|
7522
7728
|
lines.push(` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`);
|
|
7523
7729
|
}
|
|
7524
7730
|
}
|
|
7731
|
+
if (!fetchFailed) {
|
|
7732
|
+
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
7733
|
+
const visibleRaw = rawMessages.filter((msg) => {
|
|
7734
|
+
const msgId = msg.info?.id || "";
|
|
7735
|
+
const entry = pruneMap.get(msgId);
|
|
7736
|
+
return !entry || entry.activeBlockIds.length === 0;
|
|
7737
|
+
});
|
|
7738
|
+
const ranges = buildCompressibleRanges(visibleRaw, ctx.state);
|
|
7739
|
+
if (ranges.length > 0) {
|
|
7740
|
+
lines.push("");
|
|
7741
|
+
lines.push(formatCompressibleRanges(ranges));
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7525
7744
|
lines.push("");
|
|
7526
7745
|
const hintTool = topToolName || "bash";
|
|
7527
|
-
lines.push(`Tip: acp_status({scope:"uncompressed", tool:"${hintTool}"
|
|
7746
|
+
lines.push(`Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`);
|
|
7747
|
+
return lines;
|
|
7748
|
+
}
|
|
7749
|
+
function renderUncompressedRanges(rawMessages, ctx) {
|
|
7750
|
+
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
7751
|
+
const visibleMessages = rawMessages.filter((msg) => {
|
|
7752
|
+
const msgId = msg.info?.id || "";
|
|
7753
|
+
const entry = pruneMap.get(msgId);
|
|
7754
|
+
return !entry || entry.activeBlockIds.length === 0;
|
|
7755
|
+
});
|
|
7756
|
+
const ranges = buildCompressibleRanges(visibleMessages, ctx.state);
|
|
7757
|
+
const totalTokens = ranges.reduce((s, r) => s + r.tokens, 0);
|
|
7758
|
+
const totalMsgs = ranges.reduce((s, r) => s + r.count, 0);
|
|
7759
|
+
const lines = [];
|
|
7760
|
+
lines.push(`UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${totalMsgs} msgs in ${ranges.length} ranges`);
|
|
7761
|
+
lines.push("");
|
|
7762
|
+
if (ranges.length === 0) {
|
|
7763
|
+
lines.push(" (no compressible ranges)");
|
|
7764
|
+
} else {
|
|
7765
|
+
lines.push(formatCompressibleRanges(ranges));
|
|
7766
|
+
}
|
|
7767
|
+
lines.push("");
|
|
7768
|
+
lines.push(`Per-message listing: acp_status({scope:"uncompressed", view:"messages"})`);
|
|
7769
|
+
lines.push(`Filter by tool: acp_status({scope:"uncompressed", view:"messages", tool:"bash"})`);
|
|
7528
7770
|
return lines;
|
|
7529
7771
|
}
|
|
7530
7772
|
function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
@@ -7608,12 +7850,14 @@ function createAcpStatusTool(ctx) {
|
|
|
7608
7850
|
description: ACP_STATUS_TOOL_DESCRIPTION,
|
|
7609
7851
|
args: {
|
|
7610
7852
|
scope: tool5.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
|
|
7611
|
-
|
|
7853
|
+
view: tool5.schema.string().optional().describe('Display format for scope:"uncompressed": "ranges" (default, grouped by turn \u2014 matches nudge format) or "messages" (per-message listing with sort/filter)'),
|
|
7854
|
+
tool: tool5.schema.string().optional().describe('Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'),
|
|
7612
7855
|
sort: tool5.schema.string().optional().describe('Sort order: "size" (default), "time", or "tool"'),
|
|
7613
7856
|
limit: tool5.schema.number().optional().describe("Max items to list (default 30)")
|
|
7614
7857
|
},
|
|
7615
7858
|
async execute(args, toolCtx) {
|
|
7616
7859
|
const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
|
|
7860
|
+
const view = args.view === "messages" ? "messages" : "ranges";
|
|
7617
7861
|
const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
|
|
7618
7862
|
const sort = args.sort === "time" || args.sort === "tool" || args.sort === "age" ? args.sort : "size";
|
|
7619
7863
|
const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
|
|
@@ -7628,8 +7872,9 @@ function createAcpStatusTool(ctx) {
|
|
|
7628
7872
|
let visibleMsgs = [];
|
|
7629
7873
|
let summaryTokens = 0;
|
|
7630
7874
|
let fetchFailed = false;
|
|
7875
|
+
let rawMessages = [];
|
|
7631
7876
|
try {
|
|
7632
|
-
|
|
7877
|
+
rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
7633
7878
|
const result = collectVisibleMessages(rawMessages, ctx);
|
|
7634
7879
|
visibleMsgs = result.messages;
|
|
7635
7880
|
summaryTokens = result.summaryTokens;
|
|
@@ -7638,17 +7883,80 @@ function createAcpStatusTool(ctx) {
|
|
|
7638
7883
|
}
|
|
7639
7884
|
if (scope === "uncompressed") {
|
|
7640
7885
|
if (fetchFailed) return "(unable to fetch messages)";
|
|
7641
|
-
|
|
7886
|
+
if (view === "messages") {
|
|
7887
|
+
lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
|
|
7888
|
+
} else {
|
|
7889
|
+
lines.push(...renderUncompressedRanges(rawMessages, ctx));
|
|
7890
|
+
}
|
|
7642
7891
|
} else {
|
|
7643
|
-
lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed));
|
|
7892
|
+
lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed, rawMessages, ctx));
|
|
7644
7893
|
}
|
|
7645
7894
|
return lines.join("\n");
|
|
7646
7895
|
}
|
|
7647
7896
|
});
|
|
7648
7897
|
}
|
|
7649
7898
|
|
|
7650
|
-
// lib/compress/
|
|
7899
|
+
// lib/compress/recap.ts
|
|
7651
7900
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
7901
|
+
function formatRange(startId, endId) {
|
|
7902
|
+
const start = (startId || "").trim();
|
|
7903
|
+
const end = (endId || "").trim();
|
|
7904
|
+
if (!start || !end) return "\u2014";
|
|
7905
|
+
if (start === end) return start;
|
|
7906
|
+
return `${start}\u2013${end}`;
|
|
7907
|
+
}
|
|
7908
|
+
var RECAP_TOOL_DESCRIPTION = `Read-only retrieval of compression block summaries.
|
|
7909
|
+
|
|
7910
|
+
This tool is primarily system-managed: ACP automatically injects compression summaries into context via this tool's result format. You can also call it directly to re-fetch a specific block's summary without decompressing the full original content.
|
|
7911
|
+
|
|
7912
|
+
Args:
|
|
7913
|
+
- blockId: optional block number (e.g., 5). If omitted, lists all active blocks with brief info.`;
|
|
7914
|
+
function createAcpContextRecapTool(ctx) {
|
|
7915
|
+
return tool6({
|
|
7916
|
+
description: RECAP_TOOL_DESCRIPTION,
|
|
7917
|
+
args: {
|
|
7918
|
+
blockId: tool6.schema.number().optional().describe("Block number to retrieve (e.g., 5). If omitted, lists all active blocks.")
|
|
7919
|
+
},
|
|
7920
|
+
async execute(args) {
|
|
7921
|
+
const msgState = ctx.state.prune.messages;
|
|
7922
|
+
const activeIds = Array.from(msgState.activeBlockIds).sort((a, b) => a - b);
|
|
7923
|
+
if (activeIds.length === 0) {
|
|
7924
|
+
return "No active compression blocks.";
|
|
7925
|
+
}
|
|
7926
|
+
if (args.blockId !== void 0) {
|
|
7927
|
+
const block = msgState.blocksById.get(args.blockId);
|
|
7928
|
+
if (!block) {
|
|
7929
|
+
return `Block b${args.blockId} not found. Active blocks: ${activeIds.map((id) => `b${id}`).join(", ")}`;
|
|
7930
|
+
}
|
|
7931
|
+
if (!block.active) {
|
|
7932
|
+
return `Block b${args.blockId} is inactive (deactivated by GC or nested compression).`;
|
|
7933
|
+
}
|
|
7934
|
+
const range = formatRange(block.startId, block.endId);
|
|
7935
|
+
return `[Compressed conversation section]
|
|
7936
|
+
${block.summary}
|
|
7937
|
+
|
|
7938
|
+
[Block b${args.blockId} | ${range} | topic: "${block.topic || "(none)"}"]`;
|
|
7939
|
+
}
|
|
7940
|
+
const lines = [];
|
|
7941
|
+
lines.push(`Active compression blocks (${activeIds.length}):`);
|
|
7942
|
+
for (const id of activeIds) {
|
|
7943
|
+
const block = msgState.blocksById.get(id);
|
|
7944
|
+
if (!block || !block.active) continue;
|
|
7945
|
+
const range = formatRange(block.startId, block.endId);
|
|
7946
|
+
const summaryPreview = block.summary.slice(0, 200);
|
|
7947
|
+
lines.push(`
|
|
7948
|
+
b${id} | ${range} | "${block.topic || "(none)"}"`);
|
|
7949
|
+
lines.push(` ${summaryPreview}${block.summary.length > 200 ? "..." : ""}`);
|
|
7950
|
+
}
|
|
7951
|
+
lines.push(`
|
|
7952
|
+
Call with blockId to get the full summary: acp_context_recap({ blockId: N })`);
|
|
7953
|
+
return lines.join("\n");
|
|
7954
|
+
}
|
|
7955
|
+
});
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
// lib/compress/prune-tool.ts
|
|
7959
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
7652
7960
|
var PRUNE_TOOL_DESCRIPTION = `Remove old tool outputs by tool type \u2014 frees context without compression.
|
|
7653
7961
|
|
|
7654
7962
|
Unlike compress (which creates summaries), prune directly strips tool call outputs from context. Use for disposable tool outputs where the content is no longer needed: old todowrite states, edit success echoes, repeated status checks.
|
|
@@ -7658,11 +7966,11 @@ Args:
|
|
|
7658
7966
|
- keepLatest: how many recent calls to keep visible (default 3)`;
|
|
7659
7967
|
function createPruneTool(ctx) {
|
|
7660
7968
|
ctx.prompts.reload();
|
|
7661
|
-
return
|
|
7969
|
+
return tool7({
|
|
7662
7970
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
7663
7971
|
args: {
|
|
7664
|
-
toolType:
|
|
7665
|
-
keepLatest:
|
|
7972
|
+
toolType: tool7.schema.string().describe('Tool name to prune (e.g., "todowrite", "bash", "edit")'),
|
|
7973
|
+
keepLatest: tool7.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
|
|
7666
7974
|
},
|
|
7667
7975
|
async execute(args, toolCtx) {
|
|
7668
7976
|
const keepLatest = args.keepLatest ?? 3;
|
|
@@ -7939,7 +8247,7 @@ You have five context-management tools:
|
|
|
7939
8247
|
- \`decompress\` \u2014 Restore a previously compressed block's full original content, optionally to a file for large blocks. Use when a summary lacks the exact detail you need. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\`.
|
|
7940
8248
|
- \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
|
|
7941
8249
|
- \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
|
|
7942
|
-
- \`acp_status\` \u2014 Context status with
|
|
8250
|
+
- \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.
|
|
7943
8251
|
|
|
7944
8252
|
COMPRESSION PHILOSOPHY
|
|
7945
8253
|
|
|
@@ -7947,9 +8255,9 @@ Two failure modes to avoid:
|
|
|
7947
8255
|
- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
|
|
7948
8256
|
- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
|
|
7949
8257
|
|
|
7950
|
-
Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, it
|
|
8258
|
+
Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, compress it. All ranges listed in the context breakdown should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.
|
|
7951
8259
|
|
|
7952
|
-
Be frugal with context. Compress obvious waste proactively \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full; that harms retrieval quality and risks overflow.
|
|
8260
|
+
Be frugal with context. Compress obvious waste proactively \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full; that harms retrieval quality and risks overflow. But never let the urge to compress distract from the actual task.
|
|
7953
8261
|
|
|
7954
8262
|
WHEN TO COMPRESS
|
|
7955
8263
|
|
|
@@ -7966,7 +8274,7 @@ WHEN NOT TO COMPRESS
|
|
|
7966
8274
|
|
|
7967
8275
|
- Content the current task step is actively reading or reasoning about.
|
|
7968
8276
|
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
|
|
7969
|
-
-
|
|
8277
|
+
- Protected tool outputs (default: \`skill\` only) \u2014 hard-excluded from compression ranges, survive intact in visible context.
|
|
7970
8278
|
|
|
7971
8279
|
${HOW_TO_COMPRESS_RULES}
|
|
7972
8280
|
|
|
@@ -7991,9 +8299,9 @@ Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text
|
|
|
7991
8299
|
- "code" = messages containing code blocks
|
|
7992
8300
|
- "text" = plain text messages
|
|
7993
8301
|
|
|
7994
|
-
Below the breakdown, the system lists
|
|
8302
|
+
Below the breakdown, the system lists compressible ranges grouped by conversation turn. All listed ranges should be compressed to summary format \u2014 the only exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct. Compress the largest ranges first when the current step no longer needs them.
|
|
7995
8303
|
|
|
7996
|
-
|
|
8304
|
+
Each compression creates a reusable summary block you can decompress later if needed.
|
|
7997
8305
|
`;
|
|
7998
8306
|
|
|
7999
8307
|
// lib/prompts/compress-range.ts
|
|
@@ -8033,6 +8341,23 @@ Rules:
|
|
|
8033
8341
|
|
|
8034
8342
|
BATCHING
|
|
8035
8343
|
When multiple independent ranges are ready and their boundaries do not overlap, include all of them as separate entries in the \`content\` array of a single tool call. Each entry should have its own \`startId\`, \`endId\`, and \`summary\`.
|
|
8344
|
+
|
|
8345
|
+
KEEP AND REF MARKERS
|
|
8346
|
+
When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
|
|
8347
|
+
|
|
8348
|
+
- \`[[KEEP:mNNNNN]]\` \u2014 Expands to the original message content inline (truncated to a max length). Use for critical content you want preserved verbatim in the summary without re-typing it: key function definitions, important error messages, essential file contents.
|
|
8349
|
+
- \`[[REF:mNNNNN|short description]]\` \u2014 Creates a compact link like \`[\u2192 m00065: key function definition]\`. Use for content the reader can decompress later if needed. Does not expand \u2014 saves space.
|
|
8350
|
+
|
|
8351
|
+
Example:
|
|
8352
|
+
\`\`\`
|
|
8353
|
+
Implemented the QuotaMonitor feature. Key design: observer pattern.
|
|
8354
|
+
|
|
8355
|
+
[[KEEP:m00065]]
|
|
8356
|
+
|
|
8357
|
+
The rest of the bash calls were repetitive export commands. See [[REF:m00078|test results]] for details.
|
|
8358
|
+
\`\`\`
|
|
8359
|
+
|
|
8360
|
+
Use KEEP sparingly \u2014 each expansion adds to the summary length. Prefer REF for content that is important but not immediately critical.
|
|
8036
8361
|
`;
|
|
8037
8362
|
|
|
8038
8363
|
// lib/prompts/compress-message.ts
|
|
@@ -8932,7 +9257,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
8932
9257
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
8933
9258
|
"Return after compress with a brief explanation of what content was compressed."
|
|
8934
9259
|
].join("\n\n");
|
|
8935
|
-
function getTriggerPrompt(
|
|
9260
|
+
function getTriggerPrompt(tool8, state, config, userFocus) {
|
|
8936
9261
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
8937
9262
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
8938
9263
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -8961,8 +9286,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
8961
9286
|
);
|
|
8962
9287
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
8963
9288
|
}
|
|
8964
|
-
async function handleManualTriggerCommand(ctx,
|
|
8965
|
-
return getTriggerPrompt(
|
|
9289
|
+
async function handleManualTriggerCommand(ctx, tool8, userFocus) {
|
|
9290
|
+
return getTriggerPrompt(tool8, ctx.state, ctx.config, userFocus);
|
|
8966
9291
|
}
|
|
8967
9292
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
8968
9293
|
const pending = state.pendingManualTrigger;
|
|
@@ -9808,7 +10133,9 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
9808
10133
|
saveSessionState(state, logger).catch(() => {
|
|
9809
10134
|
});
|
|
9810
10135
|
}
|
|
10136
|
+
const prePruneTokens = getCurrentTokenUsage(state, output.messages);
|
|
9811
10137
|
prune(state, logger, config, output.messages);
|
|
10138
|
+
stripStaleCompressCalls(output.messages);
|
|
9812
10139
|
assignMessageRefs(state, output.messages);
|
|
9813
10140
|
await injectExtendedSubAgentResults(
|
|
9814
10141
|
client,
|
|
@@ -9825,7 +10152,19 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
9825
10152
|
logger,
|
|
9826
10153
|
output.messages,
|
|
9827
10154
|
prompts.getRuntimePrompts(),
|
|
9828
|
-
compressionPriorities
|
|
10155
|
+
compressionPriorities,
|
|
10156
|
+
config.debug && state.sessionId ? (text) => {
|
|
10157
|
+
sendIgnoredMessage(
|
|
10158
|
+
client,
|
|
10159
|
+
state.sessionId,
|
|
10160
|
+
`[ACP Debug] Nudge injected:
|
|
10161
|
+
${text}`,
|
|
10162
|
+
{},
|
|
10163
|
+
logger
|
|
10164
|
+
).catch(() => {
|
|
10165
|
+
});
|
|
10166
|
+
} : void 0,
|
|
10167
|
+
prePruneTokens
|
|
9829
10168
|
);
|
|
9830
10169
|
injectMessageIds(state, config, output.messages, compressionPriorities);
|
|
9831
10170
|
applyPendingManualTrigger(state, output.messages, logger);
|
|
@@ -10231,7 +10570,8 @@ var server = (async (ctx) => {
|
|
|
10231
10570
|
decompress: createDecompressTool(compressToolContext),
|
|
10232
10571
|
prune: createPruneTool(compressToolContext),
|
|
10233
10572
|
search_context: createSearchContextTool(compressToolContext),
|
|
10234
|
-
acp_status: createAcpStatusTool(compressToolContext)
|
|
10573
|
+
acp_status: createAcpStatusTool(compressToolContext),
|
|
10574
|
+
acp_context_recap: createAcpContextRecapTool(compressToolContext)
|
|
10235
10575
|
}
|
|
10236
10576
|
},
|
|
10237
10577
|
config: async (opencodeConfig) => {
|