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/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(tool7, parameters) {
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 (tool7 === "apply_patch" && typeof params.patchText === "string") {
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 (tool7 === "multiedit") {
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(tool7, parameters) {
4581
+ function createToolSignature(tool8, parameters) {
4582
4582
  if (!parameters) {
4583
- return tool7;
4583
+ return tool8;
4584
4584
  }
4585
4585
  const normalized = normalizeParameters(parameters);
4586
4586
  const sorted = sortObjectKeys(normalized);
4587
- return `${tool7}::${JSON.stringify(sorted)}`;
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(tool7, parameters) {
4662
+ function extractParameterKey(tool8, parameters) {
4663
4663
  if (!parameters) return "";
4664
- if (tool7 === "read" && parameters.filePath) {
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 ((tool7 === "write" || tool7 === "edit" || tool7 === "multiedit") && parameters.filePath) {
4678
+ if ((tool8 === "write" || tool8 === "edit" || tool8 === "multiedit") && parameters.filePath) {
4679
4679
  return parameters.filePath;
4680
4680
  }
4681
- if (tool7 === "apply_patch" && typeof parameters.patchText === "string") {
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 (tool7 === "list") {
4698
+ if (tool8 === "list") {
4699
4699
  return parameters.path || "(current directory)";
4700
4700
  }
4701
- if (tool7 === "glob") {
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 (tool7 === "grep") {
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 (tool7 === "bash") {
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 (tool7 === "webfetch" && parameters.url) {
4721
+ if (tool8 === "webfetch" && parameters.url) {
4722
4722
  return parameters.url;
4723
4723
  }
4724
- if (tool7 === "websearch" && parameters.query) {
4724
+ if (tool8 === "websearch" && parameters.query) {
4725
4725
  return `"${parameters.query}"`;
4726
4726
  }
4727
- if (tool7 === "codesearch" && parameters.query) {
4727
+ if (tool8 === "codesearch" && parameters.query) {
4728
4728
  return `"${parameters.query}"`;
4729
4729
  }
4730
- if (tool7 === "todowrite") {
4730
+ if (tool8 === "todowrite") {
4731
4731
  return `${parameters.todos?.length || 0} todos`;
4732
4732
  }
4733
- if (tool7 === "todoread") {
4733
+ if (tool8 === "todoread") {
4734
4734
  return "read todo list";
4735
4735
  }
4736
- if (tool7 === "task" && parameters.description) {
4736
+ if (tool8 === "task" && parameters.description) {
4737
4737
  return parameters.description;
4738
4738
  }
4739
- if (tool7 === "skill" && parameters.name) {
4739
+ if (tool8 === "skill" && parameters.name) {
4740
4740
  return parameters.name;
4741
4741
  }
4742
- if (tool7 === "lsp") {
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 (tool7 === "question") {
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
- var DETAILED_NOTIFICATION_SUMMARY_MAX_CHARS = 1e4;
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
- const maxChars = config.pruneNotification === "detailed" ? DETAILED_NOTIFICATION_SUMMARY_MAX_CHARS : NOTIFICATION_SUMMARY_MAX_CHARS;
5049
- const displaySummary = summary.length > maxChars ? truncateToastSummary(summary, maxChars) : summary;
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(`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.`)
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
- for (const { plan, summaryWithTools } of preparedPlans) {
5265
- const blockId = allocateBlockId(ctx.state);
5266
- const storedSummary = wrapCompressedSummary(blockId, summaryWithTools);
5267
- const summaryTokens = countTokens2(storedSummary);
5268
- applyCompressionState(
5269
- ctx.state,
5270
- {
5271
- topic: plan.entry.topic,
5272
- batchTopic: input.topic,
5273
- startId: plan.entry.messageId,
5274
- endId: plan.entry.messageId,
5275
- mode: "message",
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
- compressMessageId: toolCtx.messageID,
5278
- compressCallId: callId,
5439
+ summary: resolvedSummary,
5279
5440
  summaryTokens
5280
- },
5281
- plan.selection,
5282
- plan.anchorMessageId,
5283
- blockId,
5284
- storedSummary,
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(`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.`)
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
- for (const preparedPlan of preparedPlans) {
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;
5572
- const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
5573
- const summaryTokens = countTokens2(storedSummary);
5574
- const applied = applyCompressionState(
5575
- ctx.state,
5576
- {
5577
- topic: input.topic,
5578
- batchTopic: input.topic,
5579
- startId: preparedPlan.entry.startId,
5580
- endId: preparedPlan.entry.endId,
5581
- mode: "range",
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
- compressMessageId: toolCtx.messageID,
5584
- compressCallId: callId,
5647
+ summary: preparedPlan.finalSummary,
5585
5648
  summaryTokens
5586
- },
5587
- preparedPlan.selection,
5588
- preparedPlan.anchorMessageId,
5589
- blockId,
5590
- storedSummary,
5591
- preparedPlan.consumedBlockIds,
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
- if (!messagesState.byMessageId.has(block.anchorMessageId)) {
5971
- block.active = false;
5972
- block.deactivatedAt = now;
5973
- block.deactivatedByBlockId = void 0;
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, tool7) => {
6086
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool7) : false;
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(([tool7, tokens]) => ({ tool: tool7, tokens })).sort((a, b) => b.tokens - a.tokens);
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 = currentTokens;
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(([tool7, tokens]) => ({ tool: tool7, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
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/prune-tool.ts
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 tool6({
7995
+ return tool7({
7912
7996
  description: PRUNE_TOOL_DESCRIPTION,
7913
7997
  args: {
7914
- toolType: tool6.schema.string().describe('Tool name to prune (e.g., "todowrite", "bash", "edit")'),
7915
- keepLatest: tool6.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
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(tool7, state, config, userFocus) {
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, tool7, userFocus) {
9232
- return getTriggerPrompt(tool7, ctx.state, ctx.config, userFocus);
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) => {