opencode-acp 1.11.2 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/README.zh-CN.md +38 -0
- package/dist/index.js +368 -91
- package/dist/index.js.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/range.d.ts.map +1 -1
- 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/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/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) {
|
|
@@ -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(),
|
|
@@ -3686,19 +3705,16 @@ function migrateFromLegacyIfNeeded(logger) {
|
|
|
3686
3705
|
logger.warn(`[ACP] Storage migration failed: ${e.message}`);
|
|
3687
3706
|
}
|
|
3688
3707
|
}
|
|
3689
|
-
async function ensureStorageDir(logger) {
|
|
3690
|
-
const storageDir = getStorageDir();
|
|
3691
|
-
if (!existsSync2(storageDir)) {
|
|
3692
|
-
migrateFromLegacyIfNeeded(logger);
|
|
3693
|
-
await fs.mkdir(storageDir, { recursive: true });
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
3708
|
function getSessionFilePath(sessionId) {
|
|
3697
3709
|
return join2(getStorageDir(), `${sessionId}.json`);
|
|
3698
3710
|
}
|
|
3699
3711
|
async function writePersistedSessionState(sessionId, state, logger) {
|
|
3700
|
-
await ensureStorageDir(logger);
|
|
3701
3712
|
const filePath = getSessionFilePath(sessionId);
|
|
3713
|
+
const storageDir = getStorageDir();
|
|
3714
|
+
if (!existsSync2(storageDir)) {
|
|
3715
|
+
migrateFromLegacyIfNeeded(logger);
|
|
3716
|
+
await fs.mkdir(storageDir, { recursive: true });
|
|
3717
|
+
}
|
|
3702
3718
|
const content = JSON.stringify(state, null, 2);
|
|
3703
3719
|
await fs.writeFile(filePath, content, "utf-8");
|
|
3704
3720
|
logger.info("Saved session state to disk", {
|
|
@@ -3722,7 +3738,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
|
|
|
3722
3738
|
iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
|
|
3723
3739
|
lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
|
|
3724
3740
|
lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
|
|
3725
|
-
|
|
3741
|
+
lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
|
|
3742
|
+
lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
|
|
3743
|
+
compressBaselineSet: sessionState.nudges.compressBaselineSet
|
|
3726
3744
|
},
|
|
3727
3745
|
stats: sessionState.stats,
|
|
3728
3746
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -4294,8 +4312,10 @@ function createSessionState() {
|
|
|
4294
4312
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
4295
4313
|
lastPerMessageNudgeTurn: 0,
|
|
4296
4314
|
lastPerMessageNudgeTokens: void 0,
|
|
4315
|
+
lastNudgeShownTokens: void 0,
|
|
4297
4316
|
lastToolOutputNudgeTokens: void 0,
|
|
4298
|
-
shouldInjectThisTurn: void 0
|
|
4317
|
+
shouldInjectThisTurn: void 0,
|
|
4318
|
+
compressBaselineSet: false
|
|
4299
4319
|
},
|
|
4300
4320
|
stats: {
|
|
4301
4321
|
pruneTokenCounter: 0,
|
|
@@ -4335,8 +4355,10 @@ function resetSessionState(state) {
|
|
|
4335
4355
|
iterationNudgeAnchors: /* @__PURE__ */ new Set(),
|
|
4336
4356
|
lastPerMessageNudgeTurn: 0,
|
|
4337
4357
|
lastPerMessageNudgeTokens: void 0,
|
|
4358
|
+
lastNudgeShownTokens: void 0,
|
|
4338
4359
|
lastToolOutputNudgeTokens: void 0,
|
|
4339
|
-
shouldInjectThisTurn: void 0
|
|
4360
|
+
shouldInjectThisTurn: void 0,
|
|
4361
|
+
compressBaselineSet: false
|
|
4340
4362
|
};
|
|
4341
4363
|
state.stats = {
|
|
4342
4364
|
pruneTokenCounter: 0,
|
|
@@ -4389,7 +4411,9 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
|
|
|
4389
4411
|
);
|
|
4390
4412
|
state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
|
|
4391
4413
|
state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
|
|
4414
|
+
state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
|
|
4392
4415
|
state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
|
|
4416
|
+
state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
|
|
4393
4417
|
state.stats = {
|
|
4394
4418
|
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
|
|
4395
4419
|
totalPruneTokens: persisted.stats?.totalPruneTokens || 0
|
|
@@ -4872,6 +4896,7 @@ function formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) {
|
|
|
4872
4896
|
var TOAST_BODY_MAX_LINES = 12;
|
|
4873
4897
|
var TOAST_SUMMARY_MAX_CHARS = 600;
|
|
4874
4898
|
var NOTIFICATION_SUMMARY_MAX_CHARS = 1500;
|
|
4899
|
+
var DETAILED_NOTIFICATION_SUMMARY_MAX_CHARS = 1e4;
|
|
4875
4900
|
function truncateToastBody(body, maxLines = TOAST_BODY_MAX_LINES) {
|
|
4876
4901
|
const lines = body.split("\n");
|
|
4877
4902
|
if (lines.length <= maxLines) {
|
|
@@ -4892,18 +4917,24 @@ function buildCompressionSummary(entries, state) {
|
|
|
4892
4917
|
if (entries.length === 1) {
|
|
4893
4918
|
return entries[0]?.summary ?? "";
|
|
4894
4919
|
}
|
|
4920
|
+
const perEntryMax = Math.floor(NOTIFICATION_SUMMARY_MAX_CHARS / entries.length);
|
|
4895
4921
|
let result = "";
|
|
4896
|
-
|
|
4922
|
+
let shown = 0;
|
|
4923
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4924
|
+
const entry = entries[i];
|
|
4897
4925
|
const topic = state.prune.messages.blocksById.get(entry.blockId)?.topic ?? "(unknown topic)";
|
|
4926
|
+
const truncated = entry.summary.length > perEntryMax ? entry.summary.slice(0, perEntryMax - 3) + "..." : entry.summary;
|
|
4898
4927
|
const section = `### ${topic}
|
|
4899
|
-
${
|
|
4928
|
+
${truncated}`;
|
|
4900
4929
|
if (result.length + section.length + 2 > NOTIFICATION_SUMMARY_MAX_CHARS) {
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4930
|
+
const remaining = entries.length - shown;
|
|
4931
|
+
if (remaining > 0) {
|
|
4932
|
+
result += (result ? "\n\n" : "") + `... and ${remaining} more`;
|
|
4933
|
+
}
|
|
4904
4934
|
break;
|
|
4905
4935
|
}
|
|
4906
4936
|
result += (result ? "\n\n" : "") + section;
|
|
4937
|
+
shown++;
|
|
4907
4938
|
}
|
|
4908
4939
|
return result;
|
|
4909
4940
|
}
|
|
@@ -5014,7 +5045,8 @@ ${progressBar}`;
|
|
|
5014
5045
|
message += ` compressed`;
|
|
5015
5046
|
}
|
|
5016
5047
|
if (config.compress.showCompression) {
|
|
5017
|
-
const
|
|
5048
|
+
const maxChars = config.pruneNotification === "detailed" ? DETAILED_NOTIFICATION_SUMMARY_MAX_CHARS : NOTIFICATION_SUMMARY_MAX_CHARS;
|
|
5049
|
+
const displaySummary = summary.length > maxChars ? truncateToastSummary(summary, maxChars) : summary;
|
|
5018
5050
|
message += `
|
|
5019
5051
|
\u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
|
|
5020
5052
|
}
|
|
@@ -5268,6 +5300,113 @@ function createCompressMessageTool(ctx) {
|
|
|
5268
5300
|
|
|
5269
5301
|
// lib/compress/range.ts
|
|
5270
5302
|
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
|
|
5271
5410
|
function buildSchema2(maxSummaryLengthHard) {
|
|
5272
5411
|
return {
|
|
5273
5412
|
topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
|
|
@@ -5423,6 +5562,13 @@ function createCompressRangeTool(ctx) {
|
|
|
5423
5562
|
const runId = allocateRunId(ctx.state);
|
|
5424
5563
|
for (const preparedPlan of preparedPlans) {
|
|
5425
5564
|
const blockId = allocateBlockId(ctx.state);
|
|
5565
|
+
const keepResult = resolveKeepMarkers(
|
|
5566
|
+
preparedPlan.finalSummary,
|
|
5567
|
+
rawMessages,
|
|
5568
|
+
ctx.state,
|
|
5569
|
+
ctx.config
|
|
5570
|
+
);
|
|
5571
|
+
preparedPlan.finalSummary = keepResult.summary;
|
|
5426
5572
|
const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
|
|
5427
5573
|
const summaryTokens = countTokens2(storedSummary);
|
|
5428
5574
|
const applied = applyCompressionState(
|
|
@@ -6361,8 +6507,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
|
|
|
6361
6507
|
const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6362
6508
|
return `
|
|
6363
6509
|
|
|
6364
|
-
Context: ${formatK(currentTokens)} tokens
|
|
6365
|
-
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.`;
|
|
6510
|
+
Context: ${formatK(currentTokens)} tokens.`;
|
|
6366
6511
|
}
|
|
6367
6512
|
function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
|
|
6368
6513
|
const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
|
|
@@ -6530,8 +6675,78 @@ function estimateContextComposition(messages, state) {
|
|
|
6530
6675
|
toolTypeBreakdown
|
|
6531
6676
|
};
|
|
6532
6677
|
}
|
|
6678
|
+
function buildCompressibleRanges(messages, state) {
|
|
6679
|
+
const msgInfo = [];
|
|
6680
|
+
for (const msg of messages) {
|
|
6681
|
+
if (isSyntheticMessage(msg)) continue;
|
|
6682
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
6683
|
+
if (!ref) continue;
|
|
6684
|
+
let tokens = 0;
|
|
6685
|
+
let isTool = false;
|
|
6686
|
+
for (const part of msg.parts || []) {
|
|
6687
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6688
|
+
tokens += Math.round(part.text.length / 4);
|
|
6689
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6690
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6691
|
+
isTool = true;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
const refNum = parseInt(ref.slice(1), 10);
|
|
6695
|
+
msgInfo.push({ ref, refNum, tokens, isTool, isUser: msg.info.role === "user" });
|
|
6696
|
+
}
|
|
6697
|
+
if (msgInfo.length === 0) return [];
|
|
6698
|
+
const groups = [];
|
|
6699
|
+
let cur = null;
|
|
6700
|
+
let prevRefNum = -2;
|
|
6701
|
+
for (const info of msgInfo) {
|
|
6702
|
+
const hasGap = info.refNum > prevRefNum + 1;
|
|
6703
|
+
if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
|
|
6704
|
+
groups.push(cur);
|
|
6705
|
+
cur = null;
|
|
6706
|
+
}
|
|
6707
|
+
prevRefNum = info.refNum;
|
|
6708
|
+
if (!cur) {
|
|
6709
|
+
cur = {
|
|
6710
|
+
startRef: info.ref,
|
|
6711
|
+
endRef: info.ref,
|
|
6712
|
+
count: 1,
|
|
6713
|
+
tokens: info.tokens,
|
|
6714
|
+
toolPct: info.isTool ? 100 : 0,
|
|
6715
|
+
textPct: info.isTool ? 0 : 100
|
|
6716
|
+
};
|
|
6717
|
+
} else {
|
|
6718
|
+
cur.endRef = info.ref;
|
|
6719
|
+
cur.count++;
|
|
6720
|
+
cur.tokens += info.tokens;
|
|
6721
|
+
if (info.isTool) {
|
|
6722
|
+
cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
|
|
6723
|
+
} else {
|
|
6724
|
+
cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
|
|
6725
|
+
}
|
|
6726
|
+
cur.textPct = 100 - cur.toolPct;
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
if (cur) groups.push(cur);
|
|
6730
|
+
return groups.filter((g) => g.tokens > 0);
|
|
6731
|
+
}
|
|
6732
|
+
function formatCompressibleRanges(ranges) {
|
|
6733
|
+
if (ranges.length === 0) return "";
|
|
6734
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6735
|
+
const lines = ranges.map((r, i) => {
|
|
6736
|
+
const suffix = i === ranges.length - 1 ? " (recent \u2014 may still be in active use)" : "";
|
|
6737
|
+
return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
|
|
6738
|
+
});
|
|
6739
|
+
return `Compressible ranges (oldest first):
|
|
6740
|
+
${lines.join("\n")}`;
|
|
6741
|
+
}
|
|
6533
6742
|
|
|
6534
6743
|
// lib/prompts/compression-rules.ts
|
|
6744
|
+
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
6745
|
+
- All compression serves the primary task, but be frugal.
|
|
6746
|
+
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
6747
|
+
- Compress by need, not by percentage.
|
|
6748
|
+
- 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.
|
|
6749
|
+
- 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).`;
|
|
6535
6750
|
var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
|
|
6536
6751
|
|
|
6537
6752
|
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.
|
|
@@ -6560,6 +6775,8 @@ DROP \u2014 extract the signal, discard the vessel:
|
|
|
6560
6775
|
|
|
6561
6776
|
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.
|
|
6562
6777
|
|
|
6778
|
+
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.
|
|
6779
|
+
|
|
6563
6780
|
PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
6564
6781
|
1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
|
|
6565
6782
|
2. Decisions and rationale.
|
|
@@ -6578,7 +6795,7 @@ function createSuffixMessage(messages) {
|
|
|
6578
6795
|
messages.push(synthetic);
|
|
6579
6796
|
return synthetic;
|
|
6580
6797
|
}
|
|
6581
|
-
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
|
|
6798
|
+
var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify) => {
|
|
6582
6799
|
if (compressPermission(state, config) === "deny") {
|
|
6583
6800
|
return;
|
|
6584
6801
|
}
|
|
@@ -6595,17 +6812,29 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6595
6812
|
modelId,
|
|
6596
6813
|
messages
|
|
6597
6814
|
);
|
|
6598
|
-
|
|
6815
|
+
const lastUserIdx = messages.findLastIndex(
|
|
6816
|
+
(m) => m.info.role === "user" && !isIgnoredUserMessage(m)
|
|
6817
|
+
);
|
|
6818
|
+
const currentTurnStart = lastUserIdx >= 0 ? lastUserIdx + 1 : 0;
|
|
6819
|
+
const currentTurnHasCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
|
|
6820
|
+
if (currentTurnHasCompress) {
|
|
6599
6821
|
state.nudges.contextLimitAnchors.clear();
|
|
6600
6822
|
state.nudges.turnNudgeAnchors.clear();
|
|
6601
6823
|
state.nudges.iterationNudgeAnchors.clear();
|
|
6602
|
-
state.nudges.
|
|
6824
|
+
state.nudges.lastNudgeShownTokens = void 0;
|
|
6603
6825
|
state.nudges.lastToolOutputNudgeTokens = void 0;
|
|
6826
|
+
if (!state.nudges.compressBaselineSet) {
|
|
6827
|
+
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6828
|
+
state.nudges.compressBaselineSet = true;
|
|
6829
|
+
}
|
|
6604
6830
|
saveSessionState(state, logger).catch(() => {
|
|
6605
6831
|
});
|
|
6606
6832
|
return;
|
|
6607
6833
|
}
|
|
6834
|
+
state.nudges.compressBaselineSet = false;
|
|
6608
6835
|
let anchorsChanged = false;
|
|
6836
|
+
let baselineReEstablished = false;
|
|
6837
|
+
let baselineCorrected = false;
|
|
6609
6838
|
if (!overMinLimit) {
|
|
6610
6839
|
const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
|
|
6611
6840
|
const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
|
|
@@ -6668,39 +6897,27 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6668
6897
|
const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
|
|
6669
6898
|
if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
|
|
6670
6899
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6900
|
+
state.nudges.lastNudgeShownTokens = void 0;
|
|
6901
|
+
baselineCorrected = true;
|
|
6671
6902
|
}
|
|
6903
|
+
const hasPendingNudge = state.nudges.lastNudgeShownTokens !== void 0;
|
|
6904
|
+
const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
|
|
6905
|
+
const growthReference = state.nudges.lastNudgeShownTokens ?? state.nudges.lastPerMessageNudgeTokens;
|
|
6672
6906
|
const decision = computeShouldNudge({
|
|
6673
6907
|
currentTokens,
|
|
6674
6908
|
modelContextLimit,
|
|
6675
6909
|
overMinLimit,
|
|
6676
6910
|
overMaxLimit,
|
|
6677
|
-
lastNudgeTokens:
|
|
6911
|
+
lastNudgeTokens: growthReference,
|
|
6678
6912
|
minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
|
|
6679
|
-
nudgeGrowthTokens
|
|
6913
|
+
nudgeGrowthTokens: effectiveThreshold
|
|
6680
6914
|
});
|
|
6681
6915
|
state.nudges.shouldInjectThisTurn = decision.shouldNudge;
|
|
6682
6916
|
if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
|
|
6683
6917
|
state.nudges.lastPerMessageNudgeTokens = currentTokens;
|
|
6918
|
+
baselineReEstablished = true;
|
|
6684
6919
|
}
|
|
6685
6920
|
const composition = estimateContextComposition(messages, state);
|
|
6686
|
-
const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? nudgeGrowthTokens;
|
|
6687
|
-
let toolOutputReminder = null;
|
|
6688
|
-
if (composition.toolTokens > 0) {
|
|
6689
|
-
if (state.nudges.lastToolOutputNudgeTokens === void 0) {
|
|
6690
|
-
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6691
|
-
} else {
|
|
6692
|
-
const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
|
|
6693
|
-
if (toolGrowth >= toolOutputThreshold) {
|
|
6694
|
-
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6695
|
-
const topRanges = composition.largestRanges.slice(0, 15).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
|
|
6696
|
-
toolOutputReminder = `
|
|
6697
|
-
|
|
6698
|
-
\u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
|
|
6699
|
-
state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
|
|
6700
|
-
anchorsChanged = true;
|
|
6701
|
-
}
|
|
6702
|
-
}
|
|
6703
|
-
}
|
|
6704
6921
|
let tipsText = null;
|
|
6705
6922
|
if (decision.shouldNudge) {
|
|
6706
6923
|
injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
|
|
@@ -6711,28 +6928,17 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6711
6928
|
const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
|
|
6712
6929
|
const plainTextTokens = composition.textTokens;
|
|
6713
6930
|
const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
|
|
6714
|
-
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
|
|
6931
|
+
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.
|
|
6932
|
+
|
|
6933
|
+
${COMPRESS_PHILOSOPHY}` : "";
|
|
6715
6934
|
let breakdown = `${efficiencyNote}
|
|
6716
6935
|
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}`;
|
|
6717
|
-
const
|
|
6718
|
-
if (
|
|
6719
|
-
breakdown += `
|
|
6720
|
-
Top tools: ${topToolTypes.map((t) => `${t.tool} (${pct2(t.tokens)}%)`).join(", ")}`;
|
|
6721
|
-
}
|
|
6722
|
-
if (composition.largestToolRanges.length > 0) {
|
|
6723
|
-
breakdown += `
|
|
6724
|
-
Largest tool outputs: ${composition.largestToolRanges.slice(0, 10).map((r) => `${r.ref} (${fmt(r.tokens)})${r.tool ? " " + r.tool : ""}`).join(", ")}`;
|
|
6725
|
-
}
|
|
6726
|
-
if (composition.largestCodeRanges.length > 0) {
|
|
6727
|
-
breakdown += `
|
|
6728
|
-
Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6729
|
-
}
|
|
6730
|
-
if (composition.largestMessageRanges.length > 0) {
|
|
6936
|
+
const ranges = buildCompressibleRanges(messages, state);
|
|
6937
|
+
if (ranges.length > 0) {
|
|
6731
6938
|
breakdown += `
|
|
6732
|
-
|
|
6939
|
+
|
|
6940
|
+
${formatCompressibleRanges(ranges)}`;
|
|
6733
6941
|
}
|
|
6734
|
-
breakdown += `
|
|
6735
|
-
\u{1F4A1} Compress incrementally: target the ranges above whose content you have already extracted for this step. Size alone is not a reason to compress \u2014 if a large range is still needed in full, keep it.`;
|
|
6736
6942
|
if (decision.tipsVariant !== "maxLimit") {
|
|
6737
6943
|
breakdown += `
|
|
6738
6944
|
|
|
@@ -6743,8 +6949,7 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6743
6949
|
if (decision.tipsVariant === "maxLimit") {
|
|
6744
6950
|
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.';
|
|
6745
6951
|
}
|
|
6746
|
-
state.nudges.
|
|
6747
|
-
state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
|
|
6952
|
+
state.nudges.lastNudgeShownTokens = currentTokens;
|
|
6748
6953
|
if (config.compress.mode !== "message") {
|
|
6749
6954
|
const visibleMessageIds = new Set(
|
|
6750
6955
|
messages.map((message) => message.info.id)
|
|
@@ -6764,12 +6969,15 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6764
6969
|
}
|
|
6765
6970
|
injectVisibleIdRange(state, config, messages, suffixMessage);
|
|
6766
6971
|
}
|
|
6767
|
-
if (toolOutputReminder && suffixMessage) {
|
|
6768
|
-
appendToLastTextPart(suffixMessage, toolOutputReminder);
|
|
6769
|
-
}
|
|
6770
6972
|
if (suffixMessage) {
|
|
6771
6973
|
if (hasContent(suffixMessage)) {
|
|
6772
6974
|
appendToLastTextPart(suffixMessage, "\n");
|
|
6975
|
+
if (debugNotify) {
|
|
6976
|
+
const text = suffixMessage.parts.filter((p) => p.type === "text").map((p) => p.text || "").join("\n").trim();
|
|
6977
|
+
if (text) {
|
|
6978
|
+
debugNotify(text);
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6773
6981
|
} else {
|
|
6774
6982
|
const idx = messages.lastIndexOf(suffixMessage);
|
|
6775
6983
|
if (idx !== -1) {
|
|
@@ -6777,7 +6985,7 @@ ${HOW_TO_COMPRESS_RULES}`;
|
|
|
6777
6985
|
}
|
|
6778
6986
|
}
|
|
6779
6987
|
}
|
|
6780
|
-
if (anchorsChanged || decision.shouldNudge) {
|
|
6988
|
+
if (anchorsChanged || decision.shouldNudge || baselineReEstablished || baselineCorrected) {
|
|
6781
6989
|
saveSessionState(state, logger).catch(() => {
|
|
6782
6990
|
});
|
|
6783
6991
|
}
|
|
@@ -7416,19 +7624,17 @@ ${content}`;
|
|
|
7416
7624
|
|
|
7417
7625
|
// lib/compress/status.ts
|
|
7418
7626
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
7419
|
-
var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview
|
|
7627
|
+
var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges by default.
|
|
7420
7628
|
|
|
7421
|
-
No args: Overview
|
|
7422
|
-
scope:"uncompressed":
|
|
7629
|
+
No args: Overview with totals, compressed blocks, and compressible ranges.
|
|
7630
|
+
scope:"uncompressed": Compressible ranges only (default view:"ranges"). Add view:"messages" for per-message listing with tool/sort filters.
|
|
7423
7631
|
scope:"compressed": Drill into compressed blocks \u2014 list each with full details (age, generation, consumed lineage).
|
|
7424
7632
|
|
|
7425
|
-
Sort options: "size" (default, largest first), "time" (chronological), "tool" (group by tool type).
|
|
7426
|
-
|
|
7427
7633
|
Use this tool to:
|
|
7428
|
-
- See what's consuming context (
|
|
7429
|
-
-
|
|
7430
|
-
-
|
|
7431
|
-
-
|
|
7634
|
+
- See what's consuming context + compressible ranges in one call (no args)
|
|
7635
|
+
- Focus on ranges only (scope:"uncompressed")
|
|
7636
|
+
- Find all messages of a specific tool type (scope:"uncompressed", view:"messages", tool:"bash")
|
|
7637
|
+
- Check block details before decompressing (scope:"compressed")`;
|
|
7432
7638
|
function formatTokens(n) {
|
|
7433
7639
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
7434
7640
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
@@ -7478,7 +7684,7 @@ function collectVisibleMessages(rawMessages, ctx) {
|
|
|
7478
7684
|
});
|
|
7479
7685
|
return { messages: result, summaryTokens };
|
|
7480
7686
|
}
|
|
7481
|
-
function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
|
|
7687
|
+
function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, rawMessages, ctx) {
|
|
7482
7688
|
const lines = [];
|
|
7483
7689
|
const toolTypeMap = /* @__PURE__ */ new Map();
|
|
7484
7690
|
for (const m of visibleMessages) {
|
|
@@ -7523,9 +7729,45 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
|
|
|
7523
7729
|
lines.push(` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`);
|
|
7524
7730
|
}
|
|
7525
7731
|
}
|
|
7732
|
+
if (!fetchFailed) {
|
|
7733
|
+
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
7734
|
+
const visibleRaw = rawMessages.filter((msg) => {
|
|
7735
|
+
const msgId = msg.info?.id || "";
|
|
7736
|
+
const entry = pruneMap.get(msgId);
|
|
7737
|
+
return !entry || entry.activeBlockIds.length === 0;
|
|
7738
|
+
});
|
|
7739
|
+
const ranges = buildCompressibleRanges(visibleRaw, ctx.state);
|
|
7740
|
+
if (ranges.length > 0) {
|
|
7741
|
+
lines.push("");
|
|
7742
|
+
lines.push(formatCompressibleRanges(ranges));
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7526
7745
|
lines.push("");
|
|
7527
7746
|
const hintTool = topToolName || "bash";
|
|
7528
|
-
lines.push(`Tip: acp_status({scope:"uncompressed", tool:"${hintTool}"
|
|
7747
|
+
lines.push(`Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`);
|
|
7748
|
+
return lines;
|
|
7749
|
+
}
|
|
7750
|
+
function renderUncompressedRanges(rawMessages, ctx) {
|
|
7751
|
+
const pruneMap = ctx.state.prune.messages.byMessageId;
|
|
7752
|
+
const visibleMessages = rawMessages.filter((msg) => {
|
|
7753
|
+
const msgId = msg.info?.id || "";
|
|
7754
|
+
const entry = pruneMap.get(msgId);
|
|
7755
|
+
return !entry || entry.activeBlockIds.length === 0;
|
|
7756
|
+
});
|
|
7757
|
+
const ranges = buildCompressibleRanges(visibleMessages, ctx.state);
|
|
7758
|
+
const totalTokens = ranges.reduce((s, r) => s + r.tokens, 0);
|
|
7759
|
+
const totalMsgs = ranges.reduce((s, r) => s + r.count, 0);
|
|
7760
|
+
const lines = [];
|
|
7761
|
+
lines.push(`UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${totalMsgs} msgs in ${ranges.length} ranges`);
|
|
7762
|
+
lines.push("");
|
|
7763
|
+
if (ranges.length === 0) {
|
|
7764
|
+
lines.push(" (no compressible ranges)");
|
|
7765
|
+
} else {
|
|
7766
|
+
lines.push(formatCompressibleRanges(ranges));
|
|
7767
|
+
}
|
|
7768
|
+
lines.push("");
|
|
7769
|
+
lines.push(`Per-message listing: acp_status({scope:"uncompressed", view:"messages"})`);
|
|
7770
|
+
lines.push(`Filter by tool: acp_status({scope:"uncompressed", view:"messages", tool:"bash"})`);
|
|
7529
7771
|
return lines;
|
|
7530
7772
|
}
|
|
7531
7773
|
function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
|
|
@@ -7609,12 +7851,14 @@ function createAcpStatusTool(ctx) {
|
|
|
7609
7851
|
description: ACP_STATUS_TOOL_DESCRIPTION,
|
|
7610
7852
|
args: {
|
|
7611
7853
|
scope: tool5.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
|
|
7612
|
-
|
|
7854
|
+
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)'),
|
|
7855
|
+
tool: tool5.schema.string().optional().describe('Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'),
|
|
7613
7856
|
sort: tool5.schema.string().optional().describe('Sort order: "size" (default), "time", or "tool"'),
|
|
7614
7857
|
limit: tool5.schema.number().optional().describe("Max items to list (default 30)")
|
|
7615
7858
|
},
|
|
7616
7859
|
async execute(args, toolCtx) {
|
|
7617
7860
|
const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
|
|
7861
|
+
const view = args.view === "messages" ? "messages" : "ranges";
|
|
7618
7862
|
const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
|
|
7619
7863
|
const sort = args.sort === "time" || args.sort === "tool" || args.sort === "age" ? args.sort : "size";
|
|
7620
7864
|
const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
|
|
@@ -7629,8 +7873,9 @@ function createAcpStatusTool(ctx) {
|
|
|
7629
7873
|
let visibleMsgs = [];
|
|
7630
7874
|
let summaryTokens = 0;
|
|
7631
7875
|
let fetchFailed = false;
|
|
7876
|
+
let rawMessages = [];
|
|
7632
7877
|
try {
|
|
7633
|
-
|
|
7878
|
+
rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
|
|
7634
7879
|
const result = collectVisibleMessages(rawMessages, ctx);
|
|
7635
7880
|
visibleMsgs = result.messages;
|
|
7636
7881
|
summaryTokens = result.summaryTokens;
|
|
@@ -7639,9 +7884,13 @@ function createAcpStatusTool(ctx) {
|
|
|
7639
7884
|
}
|
|
7640
7885
|
if (scope === "uncompressed") {
|
|
7641
7886
|
if (fetchFailed) return "(unable to fetch messages)";
|
|
7642
|
-
|
|
7887
|
+
if (view === "messages") {
|
|
7888
|
+
lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
|
|
7889
|
+
} else {
|
|
7890
|
+
lines.push(...renderUncompressedRanges(rawMessages, ctx));
|
|
7891
|
+
}
|
|
7643
7892
|
} else {
|
|
7644
|
-
lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed));
|
|
7893
|
+
lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed, rawMessages, ctx));
|
|
7645
7894
|
}
|
|
7646
7895
|
return lines.join("\n");
|
|
7647
7896
|
}
|
|
@@ -7940,7 +8189,7 @@ You have five context-management tools:
|
|
|
7940
8189
|
- \`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" })\`.
|
|
7941
8190
|
- \`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" })\`.
|
|
7942
8191
|
- \`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 })\`.
|
|
7943
|
-
- \`acp_status\` \u2014 Context status with
|
|
8192
|
+
- \`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.
|
|
7944
8193
|
|
|
7945
8194
|
COMPRESSION PHILOSOPHY
|
|
7946
8195
|
|
|
@@ -7948,9 +8197,9 @@ Two failure modes to avoid:
|
|
|
7948
8197
|
- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
|
|
7949
8198
|
- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
|
|
7950
8199
|
|
|
7951
|
-
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
|
|
8200
|
+
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.
|
|
7952
8201
|
|
|
7953
|
-
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.
|
|
8202
|
+
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.
|
|
7954
8203
|
|
|
7955
8204
|
WHEN TO COMPRESS
|
|
7956
8205
|
|
|
@@ -7967,7 +8216,7 @@ WHEN NOT TO COMPRESS
|
|
|
7967
8216
|
|
|
7968
8217
|
- Content the current task step is actively reading or reasoning about.
|
|
7969
8218
|
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
|
|
7970
|
-
-
|
|
8219
|
+
- Protected tool outputs (default: \`skill\` only) \u2014 hard-excluded from compression ranges, survive intact in visible context.
|
|
7971
8220
|
|
|
7972
8221
|
${HOW_TO_COMPRESS_RULES}
|
|
7973
8222
|
|
|
@@ -7992,9 +8241,9 @@ Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text
|
|
|
7992
8241
|
- "code" = messages containing code blocks
|
|
7993
8242
|
- "text" = plain text messages
|
|
7994
8243
|
|
|
7995
|
-
Below the breakdown, the system lists
|
|
8244
|
+
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.
|
|
7996
8245
|
|
|
7997
|
-
|
|
8246
|
+
Each compression creates a reusable summary block you can decompress later if needed.
|
|
7998
8247
|
`;
|
|
7999
8248
|
|
|
8000
8249
|
// lib/prompts/compress-range.ts
|
|
@@ -8034,6 +8283,23 @@ Rules:
|
|
|
8034
8283
|
|
|
8035
8284
|
BATCHING
|
|
8036
8285
|
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\`.
|
|
8286
|
+
|
|
8287
|
+
KEEP AND REF MARKERS
|
|
8288
|
+
When writing a summary, you may embed markers that reference specific messages in the compressed range. The system resolves them automatically:
|
|
8289
|
+
|
|
8290
|
+
- \`[[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.
|
|
8291
|
+
- \`[[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.
|
|
8292
|
+
|
|
8293
|
+
Example:
|
|
8294
|
+
\`\`\`
|
|
8295
|
+
Implemented the QuotaMonitor feature. Key design: observer pattern.
|
|
8296
|
+
|
|
8297
|
+
[[KEEP:m00065]]
|
|
8298
|
+
|
|
8299
|
+
The rest of the bash calls were repetitive export commands. See [[REF:m00078|test results]] for details.
|
|
8300
|
+
\`\`\`
|
|
8301
|
+
|
|
8302
|
+
Use KEEP sparingly \u2014 each expansion adds to the summary length. Prefer REF for content that is important but not immediately critical.
|
|
8037
8303
|
`;
|
|
8038
8304
|
|
|
8039
8305
|
// lib/prompts/compress-message.ts
|
|
@@ -9826,7 +10092,18 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
|
|
|
9826
10092
|
logger,
|
|
9827
10093
|
output.messages,
|
|
9828
10094
|
prompts.getRuntimePrompts(),
|
|
9829
|
-
compressionPriorities
|
|
10095
|
+
compressionPriorities,
|
|
10096
|
+
config.debug && state.sessionId ? (text) => {
|
|
10097
|
+
sendIgnoredMessage(
|
|
10098
|
+
client,
|
|
10099
|
+
state.sessionId,
|
|
10100
|
+
`[ACP Debug] Nudge injected:
|
|
10101
|
+
${text}`,
|
|
10102
|
+
{},
|
|
10103
|
+
logger
|
|
10104
|
+
).catch(() => {
|
|
10105
|
+
});
|
|
10106
|
+
} : void 0
|
|
9830
10107
|
);
|
|
9831
10108
|
injectMessageIds(state, config, output.messages, compressionPriorities);
|
|
9832
10109
|
applyPendingManualTrigger(state, output.messages, logger);
|