opencode-acp 1.12.0 → 1.12.2
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 +16 -0
- package/README.zh-CN.md +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +403 -315
- 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.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +8 -1
- package/dist/lib/compress/pipeline.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/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/prune.d.ts +1 -0
- package/dist/lib/messages/prune.d.ts.map +1 -1
- package/dist/lib/messages/sync.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
|
@@ -2677,20 +2677,20 @@ function matchesGlob(inputPath, pattern) {
|
|
|
2677
2677
|
regex += "$";
|
|
2678
2678
|
return new RegExp(regex).test(input);
|
|
2679
2679
|
}
|
|
2680
|
-
function getFilePathsFromParameters(
|
|
2680
|
+
function getFilePathsFromParameters(tool8, parameters) {
|
|
2681
2681
|
if (typeof parameters !== "object" || parameters === null) {
|
|
2682
2682
|
return [];
|
|
2683
2683
|
}
|
|
2684
2684
|
const paths = [];
|
|
2685
2685
|
const params = parameters;
|
|
2686
|
-
if (
|
|
2686
|
+
if (tool8 === "apply_patch" && typeof params.patchText === "string") {
|
|
2687
2687
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
2688
2688
|
let match;
|
|
2689
2689
|
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
2690
2690
|
paths.push(match[1].trim());
|
|
2691
2691
|
}
|
|
2692
2692
|
}
|
|
2693
|
-
if (
|
|
2693
|
+
if (tool8 === "multiedit") {
|
|
2694
2694
|
if (typeof params.filePath === "string") {
|
|
2695
2695
|
paths.push(params.filePath);
|
|
2696
2696
|
}
|
|
@@ -4578,13 +4578,13 @@ var deduplicate = (state, logger, config, messages) => {
|
|
|
4578
4578
|
logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
|
|
4579
4579
|
}
|
|
4580
4580
|
};
|
|
4581
|
-
function createToolSignature(
|
|
4581
|
+
function createToolSignature(tool8, parameters) {
|
|
4582
4582
|
if (!parameters) {
|
|
4583
|
-
return
|
|
4583
|
+
return tool8;
|
|
4584
4584
|
}
|
|
4585
4585
|
const normalized = normalizeParameters(parameters);
|
|
4586
4586
|
const sorted = sortObjectKeys(normalized);
|
|
4587
|
-
return `${
|
|
4587
|
+
return `${tool8}::${JSON.stringify(sorted)}`;
|
|
4588
4588
|
}
|
|
4589
4589
|
function normalizeParameters(params) {
|
|
4590
4590
|
if (typeof params !== "object" || params === null) return params;
|
|
@@ -4659,9 +4659,9 @@ var purgeErrors = (state, logger, config, messages) => {
|
|
|
4659
4659
|
};
|
|
4660
4660
|
|
|
4661
4661
|
// lib/ui/utils.ts
|
|
4662
|
-
function extractParameterKey(
|
|
4662
|
+
function extractParameterKey(tool8, parameters) {
|
|
4663
4663
|
if (!parameters) return "";
|
|
4664
|
-
if (
|
|
4664
|
+
if (tool8 === "read" && parameters.filePath) {
|
|
4665
4665
|
const offset = parameters.offset;
|
|
4666
4666
|
const limit = parameters.limit;
|
|
4667
4667
|
if (offset !== void 0 && limit !== void 0) {
|
|
@@ -4675,10 +4675,10 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4675
4675
|
}
|
|
4676
4676
|
return parameters.filePath;
|
|
4677
4677
|
}
|
|
4678
|
-
if ((
|
|
4678
|
+
if ((tool8 === "write" || tool8 === "edit" || tool8 === "multiedit") && parameters.filePath) {
|
|
4679
4679
|
return parameters.filePath;
|
|
4680
4680
|
}
|
|
4681
|
-
if (
|
|
4681
|
+
if (tool8 === "apply_patch" && typeof parameters.patchText === "string") {
|
|
4682
4682
|
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
|
|
4683
4683
|
const paths = [];
|
|
4684
4684
|
let match;
|
|
@@ -4695,51 +4695,51 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4695
4695
|
}
|
|
4696
4696
|
return "patch";
|
|
4697
4697
|
}
|
|
4698
|
-
if (
|
|
4698
|
+
if (tool8 === "list") {
|
|
4699
4699
|
return parameters.path || "(current directory)";
|
|
4700
4700
|
}
|
|
4701
|
-
if (
|
|
4701
|
+
if (tool8 === "glob") {
|
|
4702
4702
|
if (parameters.pattern) {
|
|
4703
4703
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
4704
4704
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
4705
4705
|
}
|
|
4706
4706
|
return "(unknown pattern)";
|
|
4707
4707
|
}
|
|
4708
|
-
if (
|
|
4708
|
+
if (tool8 === "grep") {
|
|
4709
4709
|
if (parameters.pattern) {
|
|
4710
4710
|
const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
|
|
4711
4711
|
return `"${parameters.pattern}"${pathInfo}`;
|
|
4712
4712
|
}
|
|
4713
4713
|
return "(unknown pattern)";
|
|
4714
4714
|
}
|
|
4715
|
-
if (
|
|
4715
|
+
if (tool8 === "bash") {
|
|
4716
4716
|
if (parameters.description) return parameters.description;
|
|
4717
4717
|
if (parameters.command) {
|
|
4718
4718
|
return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
|
|
4719
4719
|
}
|
|
4720
4720
|
}
|
|
4721
|
-
if (
|
|
4721
|
+
if (tool8 === "webfetch" && parameters.url) {
|
|
4722
4722
|
return parameters.url;
|
|
4723
4723
|
}
|
|
4724
|
-
if (
|
|
4724
|
+
if (tool8 === "websearch" && parameters.query) {
|
|
4725
4725
|
return `"${parameters.query}"`;
|
|
4726
4726
|
}
|
|
4727
|
-
if (
|
|
4727
|
+
if (tool8 === "codesearch" && parameters.query) {
|
|
4728
4728
|
return `"${parameters.query}"`;
|
|
4729
4729
|
}
|
|
4730
|
-
if (
|
|
4730
|
+
if (tool8 === "todowrite") {
|
|
4731
4731
|
return `${parameters.todos?.length || 0} todos`;
|
|
4732
4732
|
}
|
|
4733
|
-
if (
|
|
4733
|
+
if (tool8 === "todoread") {
|
|
4734
4734
|
return "read todo list";
|
|
4735
4735
|
}
|
|
4736
|
-
if (
|
|
4736
|
+
if (tool8 === "task" && parameters.description) {
|
|
4737
4737
|
return parameters.description;
|
|
4738
4738
|
}
|
|
4739
|
-
if (
|
|
4739
|
+
if (tool8 === "skill" && parameters.name) {
|
|
4740
4740
|
return parameters.name;
|
|
4741
4741
|
}
|
|
4742
|
-
if (
|
|
4742
|
+
if (tool8 === "lsp") {
|
|
4743
4743
|
const op = parameters.operation || "lsp";
|
|
4744
4744
|
const path = parameters.filePath || "";
|
|
4745
4745
|
const line = parameters.line;
|
|
@@ -4752,7 +4752,7 @@ function extractParameterKey(tool7, parameters) {
|
|
|
4752
4752
|
}
|
|
4753
4753
|
return op;
|
|
4754
4754
|
}
|
|
4755
|
-
if (
|
|
4755
|
+
if (tool8 === "question") {
|
|
4756
4756
|
const questions = parameters.questions;
|
|
4757
4757
|
if (Array.isArray(questions) && questions.length > 0) {
|
|
4758
4758
|
const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
|
|
@@ -4896,7 +4896,22 @@ function formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) {
|
|
|
4896
4896
|
var TOAST_BODY_MAX_LINES = 12;
|
|
4897
4897
|
var TOAST_SUMMARY_MAX_CHARS = 600;
|
|
4898
4898
|
var NOTIFICATION_SUMMARY_MAX_CHARS = 1500;
|
|
4899
|
-
|
|
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
|
+
}
|
|
4900
4915
|
function truncateToastBody(body, maxLines = TOAST_BODY_MAX_LINES) {
|
|
4901
4916
|
const lines = body.split("\n");
|
|
4902
4917
|
if (lines.length <= maxLines) {
|
|
@@ -5014,6 +5029,7 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5014
5029
|
contextTokensBefore,
|
|
5015
5030
|
contextTokensAfter
|
|
5016
5031
|
)}`;
|
|
5032
|
+
let displaySummary = summary;
|
|
5017
5033
|
if (config.pruneNotification === "minimal") {
|
|
5018
5034
|
message = `${notificationHeader} \u2014 ${compressionLabel}`;
|
|
5019
5035
|
} else {
|
|
@@ -5035,6 +5051,11 @@ async function sendCompressNotification(client, logger, config, state, sessionId
|
|
|
5035
5051
|
${progressBar}`;
|
|
5036
5052
|
message += `
|
|
5037
5053
|
\u25A3 ${compressionLabel} ${formatCompressionMetrics(compressedTokens, summaryTokens)}`;
|
|
5054
|
+
const rangeStr = formatEntryRanges(entries, state);
|
|
5055
|
+
if (rangeStr) {
|
|
5056
|
+
message += `
|
|
5057
|
+
\u2192 Range: ${rangeStr}`;
|
|
5058
|
+
}
|
|
5038
5059
|
message += `
|
|
5039
5060
|
\u2192 Topic: ${topic}`;
|
|
5040
5061
|
message += `
|
|
@@ -5045,25 +5066,17 @@ ${progressBar}`;
|
|
|
5045
5066
|
message += ` compressed`;
|
|
5046
5067
|
}
|
|
5047
5068
|
if (config.compress.showCompression) {
|
|
5048
|
-
|
|
5049
|
-
|
|
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
|
+
}
|
|
5050
5074
|
message += `
|
|
5051
5075
|
\u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
|
|
5052
5076
|
}
|
|
5053
5077
|
}
|
|
5054
5078
|
if (config.pruneNotificationType === "toast") {
|
|
5055
5079
|
let toastMessage = message;
|
|
5056
|
-
if (config.compress.showCompression) {
|
|
5057
|
-
const truncatedSummary = truncateToastSummary(summary);
|
|
5058
|
-
if (truncatedSummary !== summary) {
|
|
5059
|
-
toastMessage = toastMessage.replace(
|
|
5060
|
-
`
|
|
5061
|
-
\u2192 Compression (~${summaryTokensStr}): ${truncateToastSummary(summary, NOTIFICATION_SUMMARY_MAX_CHARS)}`,
|
|
5062
|
-
`
|
|
5063
|
-
\u2192 Compression (~${summaryTokensStr}): ${truncatedSummary}`
|
|
5064
|
-
);
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
5080
|
toastMessage = config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage);
|
|
5068
5081
|
await client.tui.showToast({
|
|
5069
5082
|
body: {
|
|
@@ -5110,6 +5123,18 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
|
|
|
5110
5123
|
}
|
|
5111
5124
|
|
|
5112
5125
|
// lib/compress/pipeline.ts
|
|
5126
|
+
function snapshotCompressionState(state) {
|
|
5127
|
+
return {
|
|
5128
|
+
messages: structuredClone(state.prune.messages),
|
|
5129
|
+
stats: { ...state.stats },
|
|
5130
|
+
manualMode: state.manualMode
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
5133
|
+
function restoreCompressionState(state, snapshot) {
|
|
5134
|
+
state.prune.messages = structuredClone(snapshot.messages);
|
|
5135
|
+
state.stats = { ...snapshot.stats };
|
|
5136
|
+
state.manualMode = snapshot.manualMode;
|
|
5137
|
+
}
|
|
5113
5138
|
async function prepareSession(ctx, toolCtx, title) {
|
|
5114
5139
|
if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
|
|
5115
5140
|
throw new Error(
|
|
@@ -5162,6 +5187,118 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
|
|
|
5162
5187
|
);
|
|
5163
5188
|
}
|
|
5164
5189
|
|
|
5190
|
+
// lib/compress/keep-markers.ts
|
|
5191
|
+
var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
|
|
5192
|
+
var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
|
|
5193
|
+
function resolveKeepMarkers(summary, messages, state, config) {
|
|
5194
|
+
const msgByRef = /* @__PURE__ */ new Map();
|
|
5195
|
+
for (const msg of messages) {
|
|
5196
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
5197
|
+
if (ref) msgByRef.set(ref, msg);
|
|
5198
|
+
}
|
|
5199
|
+
const maxChars = config.compress?.keepEmbedMaxChars ?? 2e3;
|
|
5200
|
+
let expandedCount = 0;
|
|
5201
|
+
let refCount = 0;
|
|
5202
|
+
const unresolvedRefs = [];
|
|
5203
|
+
const expanded = summary.replace(KEEP_REGEX, (match, ref) => {
|
|
5204
|
+
const normalized = normalizeRef(ref);
|
|
5205
|
+
const msg = normalized ? msgByRef.get(normalized) : void 0;
|
|
5206
|
+
if (!msg) {
|
|
5207
|
+
unresolvedRefs.push(ref);
|
|
5208
|
+
return match;
|
|
5209
|
+
}
|
|
5210
|
+
expandedCount++;
|
|
5211
|
+
return formatKeptMessage(msg, normalized, maxChars);
|
|
5212
|
+
}).replace(REF_REGEX, (_match, ref, desc) => {
|
|
5213
|
+
const normalized = normalizeRef(ref);
|
|
5214
|
+
const msg = normalized ? msgByRef.get(normalized) : void 0;
|
|
5215
|
+
if (!msg) {
|
|
5216
|
+
unresolvedRefs.push(ref);
|
|
5217
|
+
return _match;
|
|
5218
|
+
}
|
|
5219
|
+
refCount++;
|
|
5220
|
+
return `[\u2192 ${normalized}: ${desc.trim()}]`;
|
|
5221
|
+
});
|
|
5222
|
+
return { summary: expanded, expandedCount, refCount, unresolvedRefs };
|
|
5223
|
+
}
|
|
5224
|
+
function normalizeRef(ref) {
|
|
5225
|
+
const idx = parseMessageRef(ref);
|
|
5226
|
+
if (idx === null) return null;
|
|
5227
|
+
return formatMessageRef(idx);
|
|
5228
|
+
}
|
|
5229
|
+
function formatKeptMessage(msg, ref, maxChars) {
|
|
5230
|
+
const formatted = formatByType(msg);
|
|
5231
|
+
const truncated = truncate2(formatted, maxChars);
|
|
5232
|
+
return `
|
|
5233
|
+
--- [${ref}: ${labelForMessage(msg)}] ---
|
|
5234
|
+
${truncated}
|
|
5235
|
+
--- end ---
|
|
5236
|
+
`;
|
|
5237
|
+
}
|
|
5238
|
+
function formatByType(msg) {
|
|
5239
|
+
for (const part of msg.parts || []) {
|
|
5240
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
5241
|
+
return part.text;
|
|
5242
|
+
}
|
|
5243
|
+
if (part.type === "tool") {
|
|
5244
|
+
const tool8 = part.tool || "unknown";
|
|
5245
|
+
const state = part.state || {};
|
|
5246
|
+
const input = state.input || {};
|
|
5247
|
+
const output = state.output || "";
|
|
5248
|
+
switch (tool8) {
|
|
5249
|
+
case "bash":
|
|
5250
|
+
case "interactive_bash": {
|
|
5251
|
+
const cmd = typeof input === "string" ? input : input.command || JSON.stringify(input);
|
|
5252
|
+
return `$ ${cmd}
|
|
5253
|
+
${output}`;
|
|
5254
|
+
}
|
|
5255
|
+
case "read": {
|
|
5256
|
+
const fp = input.filePath || input.path || input.file || "";
|
|
5257
|
+
return output;
|
|
5258
|
+
}
|
|
5259
|
+
case "write":
|
|
5260
|
+
case "edit": {
|
|
5261
|
+
const fp = input.filePath || input.path || "";
|
|
5262
|
+
const content = input.content || input.newString || "";
|
|
5263
|
+
return `${fp}:
|
|
5264
|
+
${content}`;
|
|
5265
|
+
}
|
|
5266
|
+
case "reply": {
|
|
5267
|
+
return output || "[reply posted]";
|
|
5268
|
+
}
|
|
5269
|
+
case "grep":
|
|
5270
|
+
case "glob": {
|
|
5271
|
+
return output;
|
|
5272
|
+
}
|
|
5273
|
+
default: {
|
|
5274
|
+
if (output && typeof output === "string" && output.length > 0) {
|
|
5275
|
+
return output;
|
|
5276
|
+
}
|
|
5277
|
+
const compact = JSON.stringify({ tool: tool8, input }, null, 0);
|
|
5278
|
+
return compact.length > 500 ? compact.slice(0, 500) + "..." : compact;
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return "[empty message]";
|
|
5284
|
+
}
|
|
5285
|
+
function labelForMessage(msg) {
|
|
5286
|
+
for (const part of msg.parts || []) {
|
|
5287
|
+
if (part.type === "tool") {
|
|
5288
|
+
const tool8 = part.tool || "unknown";
|
|
5289
|
+
const input = part.state?.input || {};
|
|
5290
|
+
const fp = input.filePath || input.path || input.command || "";
|
|
5291
|
+
return fp ? `${tool8}: ${String(fp).slice(0, 60)}` : tool8;
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
return msg.info.role === "user" ? "user" : "text";
|
|
5295
|
+
}
|
|
5296
|
+
function truncate2(text, maxChars) {
|
|
5297
|
+
if (text.length <= maxChars) return text;
|
|
5298
|
+
return text.slice(0, maxChars) + `
|
|
5299
|
+
... [truncated, ${text.length} chars total]`;
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5165
5302
|
// lib/compress/message.ts
|
|
5166
5303
|
function buildSchema(maxSummaryLengthHard) {
|
|
5167
5304
|
return {
|
|
@@ -5177,7 +5314,9 @@ function buildSchema(maxSummaryLengthHard) {
|
|
|
5177
5314
|
)
|
|
5178
5315
|
})
|
|
5179
5316
|
).describe("Batch of individual message summaries to create in one tool call"),
|
|
5180
|
-
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
5317
|
+
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
5318
|
+
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5319
|
+
)
|
|
5181
5320
|
};
|
|
5182
5321
|
}
|
|
5183
5322
|
function createCompressMessageTool(ctx) {
|
|
@@ -5260,39 +5399,52 @@ function createCompressMessageTool(ctx) {
|
|
|
5260
5399
|
summaryWithTools
|
|
5261
5400
|
});
|
|
5262
5401
|
}
|
|
5402
|
+
const snapshot = snapshotCompressionState(ctx.state);
|
|
5263
5403
|
const runId = allocateRunId(ctx.state);
|
|
5264
|
-
|
|
5265
|
-
const
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5404
|
+
try {
|
|
5405
|
+
for (const { plan, summaryWithTools } of preparedPlans) {
|
|
5406
|
+
const blockId = allocateBlockId(ctx.state);
|
|
5407
|
+
const keepResult = resolveKeepMarkers(
|
|
5408
|
+
summaryWithTools,
|
|
5409
|
+
rawMessages,
|
|
5410
|
+
ctx.state,
|
|
5411
|
+
ctx.config
|
|
5412
|
+
);
|
|
5413
|
+
const resolvedSummary = keepResult.summary;
|
|
5414
|
+
const storedSummary = wrapCompressedSummary(blockId, resolvedSummary);
|
|
5415
|
+
const summaryTokens = countTokens2(storedSummary);
|
|
5416
|
+
applyCompressionState(
|
|
5417
|
+
ctx.state,
|
|
5418
|
+
{
|
|
5419
|
+
topic: plan.entry.topic,
|
|
5420
|
+
batchTopic: input.topic,
|
|
5421
|
+
startId: plan.entry.messageId,
|
|
5422
|
+
endId: plan.entry.messageId,
|
|
5423
|
+
mode: "message",
|
|
5424
|
+
runId,
|
|
5425
|
+
compressMessageId: toolCtx.messageID,
|
|
5426
|
+
compressCallId: callId,
|
|
5427
|
+
summaryTokens
|
|
5428
|
+
},
|
|
5429
|
+
plan.selection,
|
|
5430
|
+
plan.anchorMessageId,
|
|
5431
|
+
blockId,
|
|
5432
|
+
storedSummary,
|
|
5433
|
+
[],
|
|
5434
|
+
ctx.config.gc
|
|
5435
|
+
);
|
|
5436
|
+
notifications.push({
|
|
5437
|
+
blockId,
|
|
5276
5438
|
runId,
|
|
5277
|
-
|
|
5278
|
-
compressCallId: callId,
|
|
5439
|
+
summary: resolvedSummary,
|
|
5279
5440
|
summaryTokens
|
|
5280
|
-
}
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
ctx.config.gc
|
|
5287
|
-
);
|
|
5288
|
-
notifications.push({
|
|
5289
|
-
blockId,
|
|
5290
|
-
runId,
|
|
5291
|
-
summary: summaryWithTools,
|
|
5292
|
-
summaryTokens
|
|
5293
|
-
});
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5444
|
+
} catch (error) {
|
|
5445
|
+
restoreCompressionState(ctx.state, snapshot);
|
|
5446
|
+
throw error;
|
|
5294
5447
|
}
|
|
5295
|
-
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5296
5448
|
return formatResult(plans.length, skippedIssues, skippedCount);
|
|
5297
5449
|
}
|
|
5298
5450
|
});
|
|
@@ -5300,113 +5452,6 @@ function createCompressMessageTool(ctx) {
|
|
|
5300
5452
|
|
|
5301
5453
|
// lib/compress/range.ts
|
|
5302
5454
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
5303
|
-
|
|
5304
|
-
// lib/compress/keep-markers.ts
|
|
5305
|
-
var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
|
|
5306
|
-
var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
|
|
5307
|
-
function resolveKeepMarkers(summary, messages, state, config) {
|
|
5308
|
-
const msgByRef = /* @__PURE__ */ new Map();
|
|
5309
|
-
for (const msg of messages) {
|
|
5310
|
-
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
5311
|
-
if (ref) msgByRef.set(ref, msg);
|
|
5312
|
-
}
|
|
5313
|
-
const maxChars = config.compress?.keepEmbedMaxChars ?? 2e3;
|
|
5314
|
-
let expandedCount = 0;
|
|
5315
|
-
let refCount = 0;
|
|
5316
|
-
const unresolvedRefs = [];
|
|
5317
|
-
const expanded = summary.replace(KEEP_REGEX, (match, ref) => {
|
|
5318
|
-
const msg = msgByRef.get(ref);
|
|
5319
|
-
if (!msg) {
|
|
5320
|
-
unresolvedRefs.push(ref);
|
|
5321
|
-
return match;
|
|
5322
|
-
}
|
|
5323
|
-
expandedCount++;
|
|
5324
|
-
return formatKeptMessage(msg, ref, maxChars);
|
|
5325
|
-
}).replace(REF_REGEX, (_match, ref, desc) => {
|
|
5326
|
-
const msg = msgByRef.get(ref);
|
|
5327
|
-
if (!msg) {
|
|
5328
|
-
unresolvedRefs.push(ref);
|
|
5329
|
-
return _match;
|
|
5330
|
-
}
|
|
5331
|
-
refCount++;
|
|
5332
|
-
return `[\u2192 ${ref}: ${desc.trim()}]`;
|
|
5333
|
-
});
|
|
5334
|
-
return { summary: expanded, expandedCount, refCount, unresolvedRefs };
|
|
5335
|
-
}
|
|
5336
|
-
function formatKeptMessage(msg, ref, maxChars) {
|
|
5337
|
-
const formatted = formatByType(msg);
|
|
5338
|
-
const truncated = truncate2(formatted, maxChars);
|
|
5339
|
-
return `
|
|
5340
|
-
--- [${ref}: ${labelForMessage(msg)}] ---
|
|
5341
|
-
${truncated}
|
|
5342
|
-
--- end ---
|
|
5343
|
-
`;
|
|
5344
|
-
}
|
|
5345
|
-
function formatByType(msg) {
|
|
5346
|
-
for (const part of msg.parts || []) {
|
|
5347
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
5348
|
-
return part.text;
|
|
5349
|
-
}
|
|
5350
|
-
if (part.type === "tool") {
|
|
5351
|
-
const tool7 = part.tool || "unknown";
|
|
5352
|
-
const state = part.state || {};
|
|
5353
|
-
const input = state.input || {};
|
|
5354
|
-
const output = state.output || "";
|
|
5355
|
-
switch (tool7) {
|
|
5356
|
-
case "bash":
|
|
5357
|
-
case "interactive_bash": {
|
|
5358
|
-
const cmd = typeof input === "string" ? input : input.command || JSON.stringify(input);
|
|
5359
|
-
return `$ ${cmd}
|
|
5360
|
-
${output}`;
|
|
5361
|
-
}
|
|
5362
|
-
case "read": {
|
|
5363
|
-
const fp = input.filePath || input.path || input.file || "";
|
|
5364
|
-
return output;
|
|
5365
|
-
}
|
|
5366
|
-
case "write":
|
|
5367
|
-
case "edit": {
|
|
5368
|
-
const fp = input.filePath || input.path || "";
|
|
5369
|
-
const content = input.content || input.newString || "";
|
|
5370
|
-
return `${fp}:
|
|
5371
|
-
${content}`;
|
|
5372
|
-
}
|
|
5373
|
-
case "reply": {
|
|
5374
|
-
return output || "[reply posted]";
|
|
5375
|
-
}
|
|
5376
|
-
case "grep":
|
|
5377
|
-
case "glob": {
|
|
5378
|
-
return output;
|
|
5379
|
-
}
|
|
5380
|
-
default: {
|
|
5381
|
-
if (output && typeof output === "string" && output.length > 0) {
|
|
5382
|
-
return output;
|
|
5383
|
-
}
|
|
5384
|
-
const compact = JSON.stringify({ tool: tool7, input }, null, 0);
|
|
5385
|
-
return compact.length > 500 ? compact.slice(0, 500) + "..." : compact;
|
|
5386
|
-
}
|
|
5387
|
-
}
|
|
5388
|
-
}
|
|
5389
|
-
}
|
|
5390
|
-
return "[empty message]";
|
|
5391
|
-
}
|
|
5392
|
-
function labelForMessage(msg) {
|
|
5393
|
-
for (const part of msg.parts || []) {
|
|
5394
|
-
if (part.type === "tool") {
|
|
5395
|
-
const tool7 = part.tool || "unknown";
|
|
5396
|
-
const input = part.state?.input || {};
|
|
5397
|
-
const fp = input.filePath || input.path || input.command || "";
|
|
5398
|
-
return fp ? `${tool7}: ${String(fp).slice(0, 60)}` : tool7;
|
|
5399
|
-
}
|
|
5400
|
-
}
|
|
5401
|
-
return msg.info.role === "user" ? "user" : "text";
|
|
5402
|
-
}
|
|
5403
|
-
function truncate2(text, maxChars) {
|
|
5404
|
-
if (text.length <= maxChars) return text;
|
|
5405
|
-
return text.slice(0, maxChars) + `
|
|
5406
|
-
... [truncated, ${text.length} chars total]`;
|
|
5407
|
-
}
|
|
5408
|
-
|
|
5409
|
-
// lib/compress/range.ts
|
|
5410
5455
|
function buildSchema2(maxSummaryLengthHard) {
|
|
5411
5456
|
return {
|
|
5412
5457
|
topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
@@ -5423,7 +5468,9 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
5423
5468
|
).describe(
|
|
5424
5469
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
5425
5470
|
),
|
|
5426
|
-
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5471
|
+
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5472
|
+
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5473
|
+
)
|
|
5427
5474
|
};
|
|
5428
5475
|
}
|
|
5429
5476
|
function createCompressRangeTool(ctx) {
|
|
@@ -5559,47 +5606,53 @@ function createCompressRangeTool(ctx) {
|
|
|
5559
5606
|
consumedBlockIds: mergeConsumedBlockIds
|
|
5560
5607
|
});
|
|
5561
5608
|
}
|
|
5609
|
+
const snapshot = snapshotCompressionState(ctx.state);
|
|
5562
5610
|
const runId = allocateRunId(ctx.state);
|
|
5563
|
-
|
|
5564
|
-
const
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5611
|
+
try {
|
|
5612
|
+
for (const preparedPlan of preparedPlans) {
|
|
5613
|
+
const blockId = allocateBlockId(ctx.state);
|
|
5614
|
+
const keepResult = resolveKeepMarkers(
|
|
5615
|
+
preparedPlan.finalSummary,
|
|
5616
|
+
rawMessages,
|
|
5617
|
+
ctx.state,
|
|
5618
|
+
ctx.config
|
|
5619
|
+
);
|
|
5620
|
+
preparedPlan.finalSummary = keepResult.summary;
|
|
5621
|
+
const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
|
|
5622
|
+
const summaryTokens = countTokens2(storedSummary);
|
|
5623
|
+
const applied = applyCompressionState(
|
|
5624
|
+
ctx.state,
|
|
5625
|
+
{
|
|
5626
|
+
topic: input.topic,
|
|
5627
|
+
batchTopic: input.topic,
|
|
5628
|
+
startId: preparedPlan.entry.startId,
|
|
5629
|
+
endId: preparedPlan.entry.endId,
|
|
5630
|
+
mode: "range",
|
|
5631
|
+
runId,
|
|
5632
|
+
compressMessageId: toolCtx.messageID,
|
|
5633
|
+
compressCallId: callId,
|
|
5634
|
+
summaryTokens
|
|
5635
|
+
},
|
|
5636
|
+
preparedPlan.selection,
|
|
5637
|
+
preparedPlan.anchorMessageId,
|
|
5638
|
+
blockId,
|
|
5639
|
+
storedSummary,
|
|
5640
|
+
preparedPlan.consumedBlockIds,
|
|
5641
|
+
ctx.config.gc
|
|
5642
|
+
);
|
|
5643
|
+
totalCompressedMessages += applied.messageIds.length;
|
|
5644
|
+
notifications.push({
|
|
5645
|
+
blockId,
|
|
5582
5646
|
runId,
|
|
5583
|
-
|
|
5584
|
-
compressCallId: callId,
|
|
5647
|
+
summary: preparedPlan.finalSummary,
|
|
5585
5648
|
summaryTokens
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
ctx.config.gc
|
|
5593
|
-
);
|
|
5594
|
-
totalCompressedMessages += applied.messageIds.length;
|
|
5595
|
-
notifications.push({
|
|
5596
|
-
blockId,
|
|
5597
|
-
runId,
|
|
5598
|
-
summary: preparedPlan.finalSummary,
|
|
5599
|
-
summaryTokens
|
|
5600
|
-
});
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5652
|
+
} catch (error) {
|
|
5653
|
+
restoreCompressionState(ctx.state, snapshot);
|
|
5654
|
+
throw error;
|
|
5601
5655
|
}
|
|
5602
|
-
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5603
5656
|
return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.
|
|
5604
5657
|
IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
|
|
5605
5658
|
\u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
|
|
@@ -5934,6 +5987,40 @@ var filterCompressedRanges = (state, logger, config, messages) => {
|
|
|
5934
5987
|
messages.length = 0;
|
|
5935
5988
|
messages.push(...result);
|
|
5936
5989
|
};
|
|
5990
|
+
function stripStaleCompressCalls(messages) {
|
|
5991
|
+
const lastUserIdx = messages.findLastIndex(
|
|
5992
|
+
(m) => m.info.role === "user" && !isIgnoredUserMessage(m)
|
|
5993
|
+
);
|
|
5994
|
+
if (lastUserIdx < 0) return 0;
|
|
5995
|
+
let stripped = 0;
|
|
5996
|
+
const result = [];
|
|
5997
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5998
|
+
const msg = messages[i];
|
|
5999
|
+
if (i >= lastUserIdx) {
|
|
6000
|
+
result.push(msg);
|
|
6001
|
+
continue;
|
|
6002
|
+
}
|
|
6003
|
+
const hasCompress = msg.parts.some(
|
|
6004
|
+
(p) => p.type === "tool" && p.tool === "compress"
|
|
6005
|
+
);
|
|
6006
|
+
if (!hasCompress) {
|
|
6007
|
+
result.push(msg);
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
6010
|
+
const remaining = msg.parts.filter(
|
|
6011
|
+
(p) => !(p.type === "tool" && p.tool === "compress")
|
|
6012
|
+
);
|
|
6013
|
+
stripped++;
|
|
6014
|
+
if (remaining.length > 0) {
|
|
6015
|
+
result.push({ ...msg, parts: remaining });
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
if (stripped > 0) {
|
|
6019
|
+
messages.length = 0;
|
|
6020
|
+
messages.push(...result);
|
|
6021
|
+
}
|
|
6022
|
+
return stripped;
|
|
6023
|
+
}
|
|
5937
6024
|
|
|
5938
6025
|
// lib/messages/sync.ts
|
|
5939
6026
|
function sortBlocksByCreation(a, b) {
|
|
@@ -5967,12 +6054,10 @@ var syncCompressionBlocks = (state, logger, messages) => {
|
|
|
5967
6054
|
continue;
|
|
5968
6055
|
}
|
|
5969
6056
|
if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
continue;
|
|
5975
|
-
}
|
|
6057
|
+
block.active = false;
|
|
6058
|
+
block.deactivatedAt = now;
|
|
6059
|
+
block.deactivatedByBlockId = void 0;
|
|
6060
|
+
continue;
|
|
5976
6061
|
}
|
|
5977
6062
|
for (const consumedBlockId of block.consumedBlockIds) {
|
|
5978
6063
|
if (!messagesState.activeBlockIds.has(consumedBlockId)) {
|
|
@@ -6082,8 +6167,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
|
|
|
6082
6167
|
agentName ? hostPermissions.agents[agentName] : void 0
|
|
6083
6168
|
) ? "deny" : basePermission;
|
|
6084
6169
|
};
|
|
6085
|
-
var hasExplicitToolPermission = (permissionConfig,
|
|
6086
|
-
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig,
|
|
6170
|
+
var hasExplicitToolPermission = (permissionConfig, tool8) => {
|
|
6171
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool8) : false;
|
|
6087
6172
|
};
|
|
6088
6173
|
|
|
6089
6174
|
// lib/compress-permission.ts
|
|
@@ -6660,7 +6745,7 @@ function estimateContextComposition(messages, state) {
|
|
|
6660
6745
|
perTool.sort((a, b) => b.tokens - a.tokens);
|
|
6661
6746
|
perCode.sort((a, b) => b.tokens - a.tokens);
|
|
6662
6747
|
perText.sort((a, b) => b.tokens - a.tokens);
|
|
6663
|
-
const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([
|
|
6748
|
+
const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens);
|
|
6664
6749
|
return {
|
|
6665
6750
|
toolTokens,
|
|
6666
6751
|
codeTokens,
|
|
@@ -6795,7 +6880,7 @@ function createSuffixMessage(messages) {
|
|
|
6795
6880
|
messages.push(synthetic);
|
|
6796
6881
|
return synthetic;
|
|
6797
6882
|
}
|
|
6798
|
-
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify) => {
|
|
6883
|
+
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify, preCompressTokens) => {
|
|
6799
6884
|
if (compressPermission(state, config) === "deny") {
|
|
6800
6885
|
return;
|
|
6801
6886
|
}
|
|
@@ -6818,13 +6903,30 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6818
6903
|
const currentTurnStart = lastUserIdx >= 0 ? lastUserIdx + 1 : 0;
|
|
6819
6904
|
const currentTurnHasCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
|
|
6820
6905
|
if (currentTurnHasCompress) {
|
|
6906
|
+
const wasNudgeTriggered = state.nudges.lastNudgeShownTokens !== void 0;
|
|
6821
6907
|
state.nudges.contextLimitAnchors.clear();
|
|
6822
6908
|
state.nudges.turnNudgeAnchors.clear();
|
|
6823
6909
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6824
6910
|
state.nudges.lastNudgeShownTokens = void 0;
|
|
6825
6911
|
state.nudges.lastToolOutputNudgeTokens = void 0;
|
|
6826
|
-
if (!state.nudges.compressBaselineSet) {
|
|
6827
|
-
state.nudges.lastPerMessageNudgeTokens
|
|
6912
|
+
if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
|
|
6913
|
+
const baseline = state.nudges.lastPerMessageNudgeTokens;
|
|
6914
|
+
const postCompress = currentTokens;
|
|
6915
|
+
const preCompress = preCompressTokens;
|
|
6916
|
+
if (baseline !== void 0 && postCompress !== void 0 && preCompress !== void 0 && preCompress > postCompress) {
|
|
6917
|
+
const growth = preCompress - baseline;
|
|
6918
|
+
const compressed = preCompress - postCompress;
|
|
6919
|
+
if (growth > 0 && compressed > 0) {
|
|
6920
|
+
const ratio = Math.min(1, compressed / growth);
|
|
6921
|
+
const adjustment = Math.min(1, ratio * 2);
|
|
6922
|
+
const newBaseline = baseline + Math.round((postCompress - baseline) * adjustment);
|
|
6923
|
+
state.nudges.lastPerMessageNudgeTokens = newBaseline;
|
|
6924
|
+
} else {
|
|
6925
|
+
state.nudges.lastPerMessageNudgeTokens = postCompress;
|
|
6926
|
+
}
|
|
6927
|
+
} else {
|
|
6928
|
+
state.nudges.lastPerMessageNudgeTokens = postCompress;
|
|
6929
|
+
}
|
|
6828
6930
|
state.nudges.compressBaselineSet = true;
|
|
6829
6931
|
}
|
|
6830
6932
|
saveSessionState(state, logger).catch(() => {
|
|
@@ -6938,7 +7040,11 @@ Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%
|
|
|
6938
7040
|
breakdown += `
|
|
6939
7041
|
|
|
6940
7042
|
${formatCompressibleRanges(ranges)}`;
|
|
7043
|
+
breakdown += `
|
|
7044
|
+
\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
|
|
6941
7045
|
}
|
|
7046
|
+
breakdown += `
|
|
7047
|
+
Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
|
|
6942
7048
|
if (decision.tipsVariant !== "maxLimit") {
|
|
6943
7049
|
breakdown += `
|
|
6944
7050
|
|
|
@@ -6967,7 +7073,6 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6967
7073
|
if (tipsText && suffixMessage) {
|
|
6968
7074
|
appendToLastTextPart(suffixMessage, tipsText);
|
|
6969
7075
|
}
|
|
6970
|
-
injectVisibleIdRange(state, config, messages, suffixMessage);
|
|
6971
7076
|
}
|
|
6972
7077
|
if (suffixMessage) {
|
|
6973
7078
|
if (hasContent(suffixMessage)) {
|
|
@@ -7003,86 +7108,6 @@ function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
|
7003
7108
|
}
|
|
7004
7109
|
target.parts.push(createSyntheticTextPart(target, usageTag));
|
|
7005
7110
|
}
|
|
7006
|
-
function refNumber(ref) {
|
|
7007
|
-
const n = parseInt(ref.slice(1), 10);
|
|
7008
|
-
return Number.isNaN(n) ? -1 : n;
|
|
7009
|
-
}
|
|
7010
|
-
function buildVisibleSegments(state, messages) {
|
|
7011
|
-
const refInfo = /* @__PURE__ */ new Map();
|
|
7012
|
-
for (const msg of messages) {
|
|
7013
|
-
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
7014
|
-
if (!ref) continue;
|
|
7015
|
-
let tokens = 0;
|
|
7016
|
-
let hasTool = false;
|
|
7017
|
-
for (const part of msg.parts || []) {
|
|
7018
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
7019
|
-
tokens += Math.round(part.text.length / 4);
|
|
7020
|
-
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
7021
|
-
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
7022
|
-
hasTool = true;
|
|
7023
|
-
}
|
|
7024
|
-
}
|
|
7025
|
-
refInfo.set(ref, { tokens, hasTool });
|
|
7026
|
-
}
|
|
7027
|
-
if (refInfo.size === 0) return [];
|
|
7028
|
-
const refs = Array.from(refInfo.keys()).sort((a, b) => refNumber(a) - refNumber(b));
|
|
7029
|
-
const segments = [];
|
|
7030
|
-
let cur = null;
|
|
7031
|
-
let prevNum = -2;
|
|
7032
|
-
for (const ref of refs) {
|
|
7033
|
-
const num = refNumber(ref);
|
|
7034
|
-
const info = refInfo.get(ref);
|
|
7035
|
-
if (cur && num === prevNum + 1) {
|
|
7036
|
-
cur.endRef = ref;
|
|
7037
|
-
cur.count++;
|
|
7038
|
-
cur.tokens += info.tokens;
|
|
7039
|
-
if (info.hasTool) cur.hasTool = true;
|
|
7040
|
-
} else {
|
|
7041
|
-
if (cur) segments.push(cur);
|
|
7042
|
-
cur = { startRef: ref, endRef: ref, count: 1, tokens: info.tokens, hasTool: info.hasTool };
|
|
7043
|
-
}
|
|
7044
|
-
prevNum = num;
|
|
7045
|
-
}
|
|
7046
|
-
if (cur) segments.push(cur);
|
|
7047
|
-
return segments;
|
|
7048
|
-
}
|
|
7049
|
-
function formatSegment(seg) {
|
|
7050
|
-
return seg.startRef === seg.endRef ? seg.startRef : `${seg.startRef}\u2013${seg.endRef}`;
|
|
7051
|
-
}
|
|
7052
|
-
function formatVisibleGuidance(segments, maxSegs) {
|
|
7053
|
-
if (segments.length === 0) return "";
|
|
7054
|
-
const totalMsgs = segments.reduce((s, seg) => s + seg.count, 0);
|
|
7055
|
-
const totalSegs = segments.length;
|
|
7056
|
-
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
7057
|
-
if (totalSegs <= maxSegs) {
|
|
7058
|
-
return `[Visible: ${segments.map(formatSegment).join(", ")} (${totalMsgs} msg${totalMsgs === 1 ? "" : "s"}, ${totalSegs} segment${totalSegs === 1 ? "" : "s"})]`;
|
|
7059
|
-
}
|
|
7060
|
-
const keepSet = new Set(
|
|
7061
|
-
[...segments].sort((a, b) => {
|
|
7062
|
-
if (a.hasTool !== b.hasTool) return a.hasTool ? -1 : 1;
|
|
7063
|
-
return b.tokens - a.tokens;
|
|
7064
|
-
}).slice(0, maxSegs)
|
|
7065
|
-
);
|
|
7066
|
-
const shown = segments.filter((s) => keepSet.has(s));
|
|
7067
|
-
const omitted = segments.filter((s) => !keepSet.has(s));
|
|
7068
|
-
const omittedTokens = omitted.reduce((sum, s) => sum + s.tokens, 0);
|
|
7069
|
-
const omittedMsgs = omitted.reduce((sum, s) => sum + s.count, 0);
|
|
7070
|
-
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]`;
|
|
7071
|
-
}
|
|
7072
|
-
function injectVisibleIdRange(state, config, messages, target) {
|
|
7073
|
-
if (!target) return;
|
|
7074
|
-
const segments = buildVisibleSegments(state, messages);
|
|
7075
|
-
if (segments.length === 0) return;
|
|
7076
|
-
const maxSegs = config.compress?.maxVisibleSegments ?? 50;
|
|
7077
|
-
const rangeTag = "\n\n" + formatVisibleGuidance(segments, maxSegs);
|
|
7078
|
-
for (const part of target.parts) {
|
|
7079
|
-
if (part.type === "text") {
|
|
7080
|
-
appendToTextPart(part, rangeTag);
|
|
7081
|
-
return;
|
|
7082
|
-
}
|
|
7083
|
-
}
|
|
7084
|
-
target.parts.push(createSyntheticTextPart(target, rangeTag));
|
|
7085
|
-
}
|
|
7086
7111
|
var injectMessageIds = (state, config, messages, compressionPriorities) => {
|
|
7087
7112
|
if (compressPermission(state, config) === "deny") {
|
|
7088
7113
|
return;
|
|
@@ -7705,7 +7730,7 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
|
|
|
7705
7730
|
lines.push(
|
|
7706
7731
|
` ${formatTokens(total)} total | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
|
|
7707
7732
|
);
|
|
7708
|
-
const topTypes = Array.from(toolTypeMap.entries()).map(([
|
|
7733
|
+
const topTypes = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
|
|
7709
7734
|
if (topTypes.length > 0) {
|
|
7710
7735
|
lines.push(` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`);
|
|
7711
7736
|
}
|
|
@@ -7897,8 +7922,67 @@ function createAcpStatusTool(ctx) {
|
|
|
7897
7922
|
});
|
|
7898
7923
|
}
|
|
7899
7924
|
|
|
7900
|
-
// lib/compress/
|
|
7925
|
+
// lib/compress/recap.ts
|
|
7901
7926
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
7927
|
+
function formatRange(startId, endId) {
|
|
7928
|
+
const start = (startId || "").trim();
|
|
7929
|
+
const end = (endId || "").trim();
|
|
7930
|
+
if (!start || !end) return "\u2014";
|
|
7931
|
+
if (start === end) return start;
|
|
7932
|
+
return `${start}\u2013${end}`;
|
|
7933
|
+
}
|
|
7934
|
+
var RECAP_TOOL_DESCRIPTION = `Read-only retrieval of compression block summaries.
|
|
7935
|
+
|
|
7936
|
+
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.
|
|
7937
|
+
|
|
7938
|
+
Args:
|
|
7939
|
+
- blockId: optional block number (e.g., 5). If omitted, lists all active blocks with brief info.`;
|
|
7940
|
+
function createAcpContextRecapTool(ctx) {
|
|
7941
|
+
return tool6({
|
|
7942
|
+
description: RECAP_TOOL_DESCRIPTION,
|
|
7943
|
+
args: {
|
|
7944
|
+
blockId: tool6.schema.number().optional().describe("Block number to retrieve (e.g., 5). If omitted, lists all active blocks.")
|
|
7945
|
+
},
|
|
7946
|
+
async execute(args) {
|
|
7947
|
+
const msgState = ctx.state.prune.messages;
|
|
7948
|
+
const activeIds = Array.from(msgState.activeBlockIds).sort((a, b) => a - b);
|
|
7949
|
+
if (activeIds.length === 0) {
|
|
7950
|
+
return "No active compression blocks.";
|
|
7951
|
+
}
|
|
7952
|
+
if (args.blockId !== void 0) {
|
|
7953
|
+
const block = msgState.blocksById.get(args.blockId);
|
|
7954
|
+
if (!block) {
|
|
7955
|
+
return `Block b${args.blockId} not found. Active blocks: ${activeIds.map((id) => `b${id}`).join(", ")}`;
|
|
7956
|
+
}
|
|
7957
|
+
if (!block.active) {
|
|
7958
|
+
return `Block b${args.blockId} is inactive (deactivated by GC or nested compression).`;
|
|
7959
|
+
}
|
|
7960
|
+
const range = formatRange(block.startId, block.endId);
|
|
7961
|
+
return `[Compressed conversation section]
|
|
7962
|
+
${block.summary}
|
|
7963
|
+
|
|
7964
|
+
[Block b${args.blockId} | ${range} | topic: "${block.topic || "(none)"}"]`;
|
|
7965
|
+
}
|
|
7966
|
+
const lines = [];
|
|
7967
|
+
lines.push(`Active compression blocks (${activeIds.length}):`);
|
|
7968
|
+
for (const id of activeIds) {
|
|
7969
|
+
const block = msgState.blocksById.get(id);
|
|
7970
|
+
if (!block || !block.active) continue;
|
|
7971
|
+
const range = formatRange(block.startId, block.endId);
|
|
7972
|
+
const summaryPreview = block.summary.slice(0, 200);
|
|
7973
|
+
lines.push(`
|
|
7974
|
+
b${id} | ${range} | "${block.topic || "(none)"}"`);
|
|
7975
|
+
lines.push(` ${summaryPreview}${block.summary.length > 200 ? "..." : ""}`);
|
|
7976
|
+
}
|
|
7977
|
+
lines.push(`
|
|
7978
|
+
Call with blockId to get the full summary: acp_context_recap({ blockId: N })`);
|
|
7979
|
+
return lines.join("\n");
|
|
7980
|
+
}
|
|
7981
|
+
});
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// lib/compress/prune-tool.ts
|
|
7985
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
7902
7986
|
var PRUNE_TOOL_DESCRIPTION = `Remove old tool outputs by tool type \u2014 frees context without compression.
|
|
7903
7987
|
|
|
7904
7988
|
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.
|
|
@@ -7908,11 +7992,11 @@ Args:
|
|
|
7908
7992
|
- keepLatest: how many recent calls to keep visible (default 3)`;
|
|
7909
7993
|
function createPruneTool(ctx) {
|
|
7910
7994
|
ctx.prompts.reload();
|
|
7911
|
-
return
|
|
7995
|
+
return tool7({
|
|
7912
7996
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
7913
7997
|
args: {
|
|
7914
|
-
toolType:
|
|
7915
|
-
keepLatest:
|
|
7998
|
+
toolType: tool7.schema.string().describe('Tool name to prune (e.g., "todowrite", "bash", "edit")'),
|
|
7999
|
+
keepLatest: tool7.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
|
|
7916
8000
|
},
|
|
7917
8001
|
async execute(args, toolCtx) {
|
|
7918
8002
|
const keepLatest = args.keepLatest ?? 3;
|
|
@@ -9199,7 +9283,7 @@ var COMPRESS_TRIGGER_PROMPT = [
|
|
|
9199
9283
|
"Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
|
|
9200
9284
|
"Return after compress with a brief explanation of what content was compressed."
|
|
9201
9285
|
].join("\n\n");
|
|
9202
|
-
function getTriggerPrompt(
|
|
9286
|
+
function getTriggerPrompt(tool8, state, config, userFocus) {
|
|
9203
9287
|
const base = COMPRESS_TRIGGER_PROMPT;
|
|
9204
9288
|
const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
|
|
9205
9289
|
const sections = [base, compressedBlockGuidance];
|
|
@@ -9228,8 +9312,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
|
|
|
9228
9312
|
);
|
|
9229
9313
|
logger.info("Manual mode toggled", { manualMode: state.manualMode });
|
|
9230
9314
|
}
|
|
9231
|
-
async function handleManualTriggerCommand(ctx,
|
|
9232
|
-
return getTriggerPrompt(
|
|
9315
|
+
async function handleManualTriggerCommand(ctx, tool8, userFocus) {
|
|
9316
|
+
return getTriggerPrompt(tool8, ctx.state, ctx.config, userFocus);
|
|
9233
9317
|
}
|
|
9234
9318
|
function applyPendingManualTrigger(state, messages, logger) {
|
|
9235
9319
|
const pending = state.pendingManualTrigger;
|
|
@@ -10075,7 +10159,9 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
10075
10159
|
saveSessionState(state, logger).catch(() => {
|
|
10076
10160
|
});
|
|
10077
10161
|
}
|
|
10162
|
+
const prePruneTokens = getCurrentTokenUsage(state, output.messages);
|
|
10078
10163
|
prune(state, logger, config, output.messages);
|
|
10164
|
+
stripStaleCompressCalls(output.messages);
|
|
10079
10165
|
assignMessageRefs(state, output.messages);
|
|
10080
10166
|
await injectExtendedSubAgentResults(
|
|
10081
10167
|
client,
|
|
@@ -10103,7 +10189,8 @@ ${text}`,
|
|
|
10103
10189
|
logger
|
|
10104
10190
|
).catch(() => {
|
|
10105
10191
|
});
|
|
10106
|
-
} : void 0
|
|
10192
|
+
} : void 0,
|
|
10193
|
+
prePruneTokens
|
|
10107
10194
|
);
|
|
10108
10195
|
injectMessageIds(state, config, output.messages, compressionPriorities);
|
|
10109
10196
|
applyPendingManualTrigger(state, output.messages, logger);
|
|
@@ -10509,7 +10596,8 @@ var server = (async (ctx) => {
|
|
|
10509
10596
|
decompress: createDecompressTool(compressToolContext),
|
|
10510
10597
|
prune: createPruneTool(compressToolContext),
|
|
10511
10598
|
search_context: createSearchContextTool(compressToolContext),
|
|
10512
|
-
acp_status: createAcpStatusTool(compressToolContext)
|
|
10599
|
+
acp_status: createAcpStatusTool(compressToolContext),
|
|
10600
|
+
acp_context_recap: createAcpContextRecapTool(compressToolContext)
|
|
10513
10601
|
}
|
|
10514
10602
|
},
|
|
10515
10603
|
config: async (opencodeConfig) => {
|