opencode-acp 1.12.0 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: {
@@ -5162,6 +5175,118 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
5162
5175
  );
5163
5176
  }
5164
5177
 
5178
+ // lib/compress/keep-markers.ts
5179
+ var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
5180
+ var REF_REGEX = /\[\[REF:(m\d+)\|([^\]]+)\]\]/g;
5181
+ function resolveKeepMarkers(summary, messages, state, config) {
5182
+ const msgByRef = /* @__PURE__ */ new Map();
5183
+ for (const msg of messages) {
5184
+ const ref = state.messageIds.byRawId.get(msg.info.id);
5185
+ if (ref) msgByRef.set(ref, msg);
5186
+ }
5187
+ const maxChars = config.compress?.keepEmbedMaxChars ?? 2e3;
5188
+ let expandedCount = 0;
5189
+ let refCount = 0;
5190
+ const unresolvedRefs = [];
5191
+ const expanded = summary.replace(KEEP_REGEX, (match, ref) => {
5192
+ const normalized = normalizeRef(ref);
5193
+ const msg = normalized ? msgByRef.get(normalized) : void 0;
5194
+ if (!msg) {
5195
+ unresolvedRefs.push(ref);
5196
+ return match;
5197
+ }
5198
+ expandedCount++;
5199
+ return formatKeptMessage(msg, normalized, maxChars);
5200
+ }).replace(REF_REGEX, (_match, ref, desc) => {
5201
+ const normalized = normalizeRef(ref);
5202
+ const msg = normalized ? msgByRef.get(normalized) : void 0;
5203
+ if (!msg) {
5204
+ unresolvedRefs.push(ref);
5205
+ return _match;
5206
+ }
5207
+ refCount++;
5208
+ return `[\u2192 ${normalized}: ${desc.trim()}]`;
5209
+ });
5210
+ return { summary: expanded, expandedCount, refCount, unresolvedRefs };
5211
+ }
5212
+ function normalizeRef(ref) {
5213
+ const idx = parseMessageRef(ref);
5214
+ if (idx === null) return null;
5215
+ return formatMessageRef(idx);
5216
+ }
5217
+ function formatKeptMessage(msg, ref, maxChars) {
5218
+ const formatted = formatByType(msg);
5219
+ const truncated = truncate2(formatted, maxChars);
5220
+ return `
5221
+ --- [${ref}: ${labelForMessage(msg)}] ---
5222
+ ${truncated}
5223
+ --- end ---
5224
+ `;
5225
+ }
5226
+ function formatByType(msg) {
5227
+ for (const part of msg.parts || []) {
5228
+ if (part.type === "text" && typeof part.text === "string") {
5229
+ return part.text;
5230
+ }
5231
+ if (part.type === "tool") {
5232
+ const tool8 = part.tool || "unknown";
5233
+ const state = part.state || {};
5234
+ const input = state.input || {};
5235
+ const output = state.output || "";
5236
+ switch (tool8) {
5237
+ case "bash":
5238
+ case "interactive_bash": {
5239
+ const cmd = typeof input === "string" ? input : input.command || JSON.stringify(input);
5240
+ return `$ ${cmd}
5241
+ ${output}`;
5242
+ }
5243
+ case "read": {
5244
+ const fp = input.filePath || input.path || input.file || "";
5245
+ return output;
5246
+ }
5247
+ case "write":
5248
+ case "edit": {
5249
+ const fp = input.filePath || input.path || "";
5250
+ const content = input.content || input.newString || "";
5251
+ return `${fp}:
5252
+ ${content}`;
5253
+ }
5254
+ case "reply": {
5255
+ return output || "[reply posted]";
5256
+ }
5257
+ case "grep":
5258
+ case "glob": {
5259
+ return output;
5260
+ }
5261
+ default: {
5262
+ if (output && typeof output === "string" && output.length > 0) {
5263
+ return output;
5264
+ }
5265
+ const compact = JSON.stringify({ tool: tool8, input }, null, 0);
5266
+ return compact.length > 500 ? compact.slice(0, 500) + "..." : compact;
5267
+ }
5268
+ }
5269
+ }
5270
+ }
5271
+ return "[empty message]";
5272
+ }
5273
+ function labelForMessage(msg) {
5274
+ for (const part of msg.parts || []) {
5275
+ if (part.type === "tool") {
5276
+ const tool8 = part.tool || "unknown";
5277
+ const input = part.state?.input || {};
5278
+ const fp = input.filePath || input.path || input.command || "";
5279
+ return fp ? `${tool8}: ${String(fp).slice(0, 60)}` : tool8;
5280
+ }
5281
+ }
5282
+ return msg.info.role === "user" ? "user" : "text";
5283
+ }
5284
+ function truncate2(text, maxChars) {
5285
+ if (text.length <= maxChars) return text;
5286
+ return text.slice(0, maxChars) + `
5287
+ ... [truncated, ${text.length} chars total]`;
5288
+ }
5289
+
5165
5290
  // lib/compress/message.ts
5166
5291
  function buildSchema(maxSummaryLengthHard) {
5167
5292
  return {
@@ -5263,7 +5388,14 @@ function createCompressMessageTool(ctx) {
5263
5388
  const runId = allocateRunId(ctx.state);
5264
5389
  for (const { plan, summaryWithTools } of preparedPlans) {
5265
5390
  const blockId = allocateBlockId(ctx.state);
5266
- const storedSummary = wrapCompressedSummary(blockId, summaryWithTools);
5391
+ const keepResult = resolveKeepMarkers(
5392
+ summaryWithTools,
5393
+ rawMessages,
5394
+ ctx.state,
5395
+ ctx.config
5396
+ );
5397
+ const resolvedSummary = keepResult.summary;
5398
+ const storedSummary = wrapCompressedSummary(blockId, resolvedSummary);
5267
5399
  const summaryTokens = countTokens2(storedSummary);
5268
5400
  applyCompressionState(
5269
5401
  ctx.state,
@@ -5288,7 +5420,7 @@ function createCompressMessageTool(ctx) {
5288
5420
  notifications.push({
5289
5421
  blockId,
5290
5422
  runId,
5291
- summary: summaryWithTools,
5423
+ summary: resolvedSummary,
5292
5424
  summaryTokens
5293
5425
  });
5294
5426
  }
@@ -5300,113 +5432,6 @@ function createCompressMessageTool(ctx) {
5300
5432
 
5301
5433
  // lib/compress/range.ts
5302
5434
  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
5435
  function buildSchema2(maxSummaryLengthHard) {
5411
5436
  return {
5412
5437
  topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
@@ -5934,6 +5959,40 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5934
5959
  messages.length = 0;
5935
5960
  messages.push(...result);
5936
5961
  };
5962
+ function stripStaleCompressCalls(messages) {
5963
+ const lastUserIdx = messages.findLastIndex(
5964
+ (m) => m.info.role === "user" && !isIgnoredUserMessage(m)
5965
+ );
5966
+ if (lastUserIdx < 0) return 0;
5967
+ let stripped = 0;
5968
+ const result = [];
5969
+ for (let i = 0; i < messages.length; i++) {
5970
+ const msg = messages[i];
5971
+ if (i >= lastUserIdx) {
5972
+ result.push(msg);
5973
+ continue;
5974
+ }
5975
+ const hasCompress = msg.parts.some(
5976
+ (p) => p.type === "tool" && p.tool === "compress"
5977
+ );
5978
+ if (!hasCompress) {
5979
+ result.push(msg);
5980
+ continue;
5981
+ }
5982
+ const remaining = msg.parts.filter(
5983
+ (p) => !(p.type === "tool" && p.tool === "compress")
5984
+ );
5985
+ stripped++;
5986
+ if (remaining.length > 0) {
5987
+ result.push({ ...msg, parts: remaining });
5988
+ }
5989
+ }
5990
+ if (stripped > 0) {
5991
+ messages.length = 0;
5992
+ messages.push(...result);
5993
+ }
5994
+ return stripped;
5995
+ }
5937
5996
 
5938
5997
  // lib/messages/sync.ts
5939
5998
  function sortBlocksByCreation(a, b) {
@@ -6082,8 +6141,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
6082
6141
  agentName ? hostPermissions.agents[agentName] : void 0
6083
6142
  ) ? "deny" : basePermission;
6084
6143
  };
6085
- var hasExplicitToolPermission = (permissionConfig, tool7) => {
6086
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool7) : false;
6144
+ var hasExplicitToolPermission = (permissionConfig, tool8) => {
6145
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool8) : false;
6087
6146
  };
6088
6147
 
6089
6148
  // lib/compress-permission.ts
@@ -6660,7 +6719,7 @@ function estimateContextComposition(messages, state) {
6660
6719
  perTool.sort((a, b) => b.tokens - a.tokens);
6661
6720
  perCode.sort((a, b) => b.tokens - a.tokens);
6662
6721
  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);
6722
+ const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens);
6664
6723
  return {
6665
6724
  toolTokens,
6666
6725
  codeTokens,
@@ -6795,7 +6854,7 @@ function createSuffixMessage(messages) {
6795
6854
  messages.push(synthetic);
6796
6855
  return synthetic;
6797
6856
  }
6798
- var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify) => {
6857
+ var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify, preCompressTokens) => {
6799
6858
  if (compressPermission(state, config) === "deny") {
6800
6859
  return;
6801
6860
  }
@@ -6818,13 +6877,30 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6818
6877
  const currentTurnStart = lastUserIdx >= 0 ? lastUserIdx + 1 : 0;
6819
6878
  const currentTurnHasCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
6820
6879
  if (currentTurnHasCompress) {
6880
+ const wasNudgeTriggered = state.nudges.lastNudgeShownTokens !== void 0;
6821
6881
  state.nudges.contextLimitAnchors.clear();
6822
6882
  state.nudges.turnNudgeAnchors.clear();
6823
6883
  state.nudges.iterationNudgeAnchors.clear();
6824
6884
  state.nudges.lastNudgeShownTokens = void 0;
6825
6885
  state.nudges.lastToolOutputNudgeTokens = void 0;
6826
- if (!state.nudges.compressBaselineSet) {
6827
- state.nudges.lastPerMessageNudgeTokens = currentTokens;
6886
+ if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
6887
+ const baseline = state.nudges.lastPerMessageNudgeTokens;
6888
+ const postCompress = currentTokens;
6889
+ const preCompress = preCompressTokens;
6890
+ if (baseline !== void 0 && postCompress !== void 0 && preCompress !== void 0 && preCompress > postCompress) {
6891
+ const growth = preCompress - baseline;
6892
+ const compressed = preCompress - postCompress;
6893
+ if (growth > 0 && compressed > 0) {
6894
+ const ratio = Math.min(1, compressed / growth);
6895
+ const adjustment = Math.min(1, ratio * 2);
6896
+ const newBaseline = baseline + Math.round((postCompress - baseline) * adjustment);
6897
+ state.nudges.lastPerMessageNudgeTokens = newBaseline;
6898
+ } else {
6899
+ state.nudges.lastPerMessageNudgeTokens = postCompress;
6900
+ }
6901
+ } else {
6902
+ state.nudges.lastPerMessageNudgeTokens = postCompress;
6903
+ }
6828
6904
  state.nudges.compressBaselineSet = true;
6829
6905
  }
6830
6906
  saveSessionState(state, logger).catch(() => {
@@ -6938,7 +7014,11 @@ Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%
6938
7014
  breakdown += `
6939
7015
 
6940
7016
  ${formatCompressibleRanges(ranges)}`;
7017
+ breakdown += `
7018
+ \u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
6941
7019
  }
7020
+ breakdown += `
7021
+ Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
6942
7022
  if (decision.tipsVariant !== "maxLimit") {
6943
7023
  breakdown += `
6944
7024
 
@@ -6967,7 +7047,6 @@ ${HOW_TO_COMPRESS_RULES}`;
6967
7047
  if (tipsText && suffixMessage) {
6968
7048
  appendToLastTextPart(suffixMessage, tipsText);
6969
7049
  }
6970
- injectVisibleIdRange(state, config, messages, suffixMessage);
6971
7050
  }
6972
7051
  if (suffixMessage) {
6973
7052
  if (hasContent(suffixMessage)) {
@@ -7003,86 +7082,6 @@ function injectContextUsage(target, config, currentTokens, modelContextLimit) {
7003
7082
  }
7004
7083
  target.parts.push(createSyntheticTextPart(target, usageTag));
7005
7084
  }
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
7085
  var injectMessageIds = (state, config, messages, compressionPriorities) => {
7087
7086
  if (compressPermission(state, config) === "deny") {
7088
7087
  return;
@@ -7705,7 +7704,7 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
7705
7704
  lines.push(
7706
7705
  ` ${formatTokens(total)} total | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
7707
7706
  );
7708
- const topTypes = Array.from(toolTypeMap.entries()).map(([tool7, tokens]) => ({ tool: tool7, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
7707
+ const topTypes = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
7709
7708
  if (topTypes.length > 0) {
7710
7709
  lines.push(` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`);
7711
7710
  }
@@ -7897,8 +7896,67 @@ function createAcpStatusTool(ctx) {
7897
7896
  });
7898
7897
  }
7899
7898
 
7900
- // lib/compress/prune-tool.ts
7899
+ // lib/compress/recap.ts
7901
7900
  import { tool as tool6 } from "@opencode-ai/plugin";
7901
+ function formatRange(startId, endId) {
7902
+ const start = (startId || "").trim();
7903
+ const end = (endId || "").trim();
7904
+ if (!start || !end) return "\u2014";
7905
+ if (start === end) return start;
7906
+ return `${start}\u2013${end}`;
7907
+ }
7908
+ var RECAP_TOOL_DESCRIPTION = `Read-only retrieval of compression block summaries.
7909
+
7910
+ This tool is primarily system-managed: ACP automatically injects compression summaries into context via this tool's result format. You can also call it directly to re-fetch a specific block's summary without decompressing the full original content.
7911
+
7912
+ Args:
7913
+ - blockId: optional block number (e.g., 5). If omitted, lists all active blocks with brief info.`;
7914
+ function createAcpContextRecapTool(ctx) {
7915
+ return tool6({
7916
+ description: RECAP_TOOL_DESCRIPTION,
7917
+ args: {
7918
+ blockId: tool6.schema.number().optional().describe("Block number to retrieve (e.g., 5). If omitted, lists all active blocks.")
7919
+ },
7920
+ async execute(args) {
7921
+ const msgState = ctx.state.prune.messages;
7922
+ const activeIds = Array.from(msgState.activeBlockIds).sort((a, b) => a - b);
7923
+ if (activeIds.length === 0) {
7924
+ return "No active compression blocks.";
7925
+ }
7926
+ if (args.blockId !== void 0) {
7927
+ const block = msgState.blocksById.get(args.blockId);
7928
+ if (!block) {
7929
+ return `Block b${args.blockId} not found. Active blocks: ${activeIds.map((id) => `b${id}`).join(", ")}`;
7930
+ }
7931
+ if (!block.active) {
7932
+ return `Block b${args.blockId} is inactive (deactivated by GC or nested compression).`;
7933
+ }
7934
+ const range = formatRange(block.startId, block.endId);
7935
+ return `[Compressed conversation section]
7936
+ ${block.summary}
7937
+
7938
+ [Block b${args.blockId} | ${range} | topic: "${block.topic || "(none)"}"]`;
7939
+ }
7940
+ const lines = [];
7941
+ lines.push(`Active compression blocks (${activeIds.length}):`);
7942
+ for (const id of activeIds) {
7943
+ const block = msgState.blocksById.get(id);
7944
+ if (!block || !block.active) continue;
7945
+ const range = formatRange(block.startId, block.endId);
7946
+ const summaryPreview = block.summary.slice(0, 200);
7947
+ lines.push(`
7948
+ b${id} | ${range} | "${block.topic || "(none)"}"`);
7949
+ lines.push(` ${summaryPreview}${block.summary.length > 200 ? "..." : ""}`);
7950
+ }
7951
+ lines.push(`
7952
+ Call with blockId to get the full summary: acp_context_recap({ blockId: N })`);
7953
+ return lines.join("\n");
7954
+ }
7955
+ });
7956
+ }
7957
+
7958
+ // lib/compress/prune-tool.ts
7959
+ import { tool as tool7 } from "@opencode-ai/plugin";
7902
7960
  var PRUNE_TOOL_DESCRIPTION = `Remove old tool outputs by tool type \u2014 frees context without compression.
7903
7961
 
7904
7962
  Unlike compress (which creates summaries), prune directly strips tool call outputs from context. Use for disposable tool outputs where the content is no longer needed: old todowrite states, edit success echoes, repeated status checks.
@@ -7908,11 +7966,11 @@ Args:
7908
7966
  - keepLatest: how many recent calls to keep visible (default 3)`;
7909
7967
  function createPruneTool(ctx) {
7910
7968
  ctx.prompts.reload();
7911
- return tool6({
7969
+ return tool7({
7912
7970
  description: PRUNE_TOOL_DESCRIPTION,
7913
7971
  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)")
7972
+ toolType: tool7.schema.string().describe('Tool name to prune (e.g., "todowrite", "bash", "edit")'),
7973
+ keepLatest: tool7.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
7916
7974
  },
7917
7975
  async execute(args, toolCtx) {
7918
7976
  const keepLatest = args.keepLatest ?? 3;
@@ -9199,7 +9257,7 @@ var COMPRESS_TRIGGER_PROMPT = [
9199
9257
  "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
9200
9258
  "Return after compress with a brief explanation of what content was compressed."
9201
9259
  ].join("\n\n");
9202
- function getTriggerPrompt(tool7, state, config, userFocus) {
9260
+ function getTriggerPrompt(tool8, state, config, userFocus) {
9203
9261
  const base = COMPRESS_TRIGGER_PROMPT;
9204
9262
  const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
9205
9263
  const sections = [base, compressedBlockGuidance];
@@ -9228,8 +9286,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
9228
9286
  );
9229
9287
  logger.info("Manual mode toggled", { manualMode: state.manualMode });
9230
9288
  }
9231
- async function handleManualTriggerCommand(ctx, tool7, userFocus) {
9232
- return getTriggerPrompt(tool7, ctx.state, ctx.config, userFocus);
9289
+ async function handleManualTriggerCommand(ctx, tool8, userFocus) {
9290
+ return getTriggerPrompt(tool8, ctx.state, ctx.config, userFocus);
9233
9291
  }
9234
9292
  function applyPendingManualTrigger(state, messages, logger) {
9235
9293
  const pending = state.pendingManualTrigger;
@@ -10075,7 +10133,9 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
10075
10133
  saveSessionState(state, logger).catch(() => {
10076
10134
  });
10077
10135
  }
10136
+ const prePruneTokens = getCurrentTokenUsage(state, output.messages);
10078
10137
  prune(state, logger, config, output.messages);
10138
+ stripStaleCompressCalls(output.messages);
10079
10139
  assignMessageRefs(state, output.messages);
10080
10140
  await injectExtendedSubAgentResults(
10081
10141
  client,
@@ -10103,7 +10163,8 @@ ${text}`,
10103
10163
  logger
10104
10164
  ).catch(() => {
10105
10165
  });
10106
- } : void 0
10166
+ } : void 0,
10167
+ prePruneTokens
10107
10168
  );
10108
10169
  injectMessageIds(state, config, output.messages, compressionPriorities);
10109
10170
  applyPendingManualTrigger(state, output.messages, logger);
@@ -10509,7 +10570,8 @@ var server = (async (ctx) => {
10509
10570
  decompress: createDecompressTool(compressToolContext),
10510
10571
  prune: createPruneTool(compressToolContext),
10511
10572
  search_context: createSearchContextTool(compressToolContext),
10512
- acp_status: createAcpStatusTool(compressToolContext)
10573
+ acp_status: createAcpStatusTool(compressToolContext),
10574
+ acp_context_recap: createAcpContextRecapTool(compressToolContext)
10513
10575
  }
10514
10576
  },
10515
10577
  config: async (opencodeConfig) => {