opencode-acp 1.11.4 → 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.
Files changed (36) hide show
  1. package/README.md +18 -0
  2. package/README.zh-CN.md +18 -0
  3. package/dist/index.js +361 -83
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/compress/keep-markers.d.ts +11 -0
  6. package/dist/lib/compress/keep-markers.d.ts.map +1 -0
  7. package/dist/lib/compress/range.d.ts.map +1 -1
  8. package/dist/lib/compress/status.d.ts.map +1 -1
  9. package/dist/lib/config-validation.d.ts.map +1 -1
  10. package/dist/lib/config.d.ts +1 -0
  11. package/dist/lib/config.d.ts.map +1 -1
  12. package/dist/lib/hooks.d.ts.map +1 -1
  13. package/dist/lib/messages/inject/inject.d.ts +1 -1
  14. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  15. package/dist/lib/messages/inject/utils.d.ts +10 -0
  16. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  17. package/dist/lib/prompts/compress-range.d.ts +1 -1
  18. package/dist/lib/prompts/compress-range.d.ts.map +1 -1
  19. package/dist/lib/prompts/compression-rules.d.ts +7 -1
  20. package/dist/lib/prompts/compression-rules.d.ts.map +1 -1
  21. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  22. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  23. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  24. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  25. package/dist/lib/prompts/system.d.ts +1 -1
  26. package/dist/lib/prompts/system.d.ts.map +1 -1
  27. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  28. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  29. package/dist/lib/state/persistence.d.ts +2 -0
  30. package/dist/lib/state/persistence.d.ts.map +1 -1
  31. package/dist/lib/state/state.d.ts.map +1 -1
  32. package/dist/lib/state/types.d.ts +10 -0
  33. package/dist/lib/state/types.d.ts.map +1 -1
  34. package/dist/lib/state/utils.d.ts.map +1 -1
  35. package/dist/lib/ui/notification.d.ts.map +1 -1
  36. 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(),
@@ -3719,7 +3738,9 @@ async function saveSessionState(sessionState, logger, sessionName) {
3719
3738
  iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
3720
3739
  lastPerMessageNudgeTurn: sessionState.nudges.lastPerMessageNudgeTurn ?? 0,
3721
3740
  lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
3722
- lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens
3741
+ lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
3742
+ lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
3743
+ compressBaselineSet: sessionState.nudges.compressBaselineSet
3723
3744
  },
3724
3745
  stats: sessionState.stats,
3725
3746
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4291,8 +4312,10 @@ function createSessionState() {
4291
4312
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
4292
4313
  lastPerMessageNudgeTurn: 0,
4293
4314
  lastPerMessageNudgeTokens: void 0,
4315
+ lastNudgeShownTokens: void 0,
4294
4316
  lastToolOutputNudgeTokens: void 0,
4295
- shouldInjectThisTurn: void 0
4317
+ shouldInjectThisTurn: void 0,
4318
+ compressBaselineSet: false
4296
4319
  },
4297
4320
  stats: {
4298
4321
  pruneTokenCounter: 0,
@@ -4332,8 +4355,10 @@ function resetSessionState(state) {
4332
4355
  iterationNudgeAnchors: /* @__PURE__ */ new Set(),
4333
4356
  lastPerMessageNudgeTurn: 0,
4334
4357
  lastPerMessageNudgeTokens: void 0,
4358
+ lastNudgeShownTokens: void 0,
4335
4359
  lastToolOutputNudgeTokens: void 0,
4336
- shouldInjectThisTurn: void 0
4360
+ shouldInjectThisTurn: void 0,
4361
+ compressBaselineSet: false
4337
4362
  };
4338
4363
  state.stats = {
4339
4364
  pruneTokenCounter: 0,
@@ -4386,7 +4411,9 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4386
4411
  );
4387
4412
  state.nudges.lastPerMessageNudgeTurn = persisted.nudges.lastPerMessageNudgeTurn ?? 0;
4388
4413
  state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
4414
+ state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
4389
4415
  state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
4416
+ state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
4390
4417
  state.stats = {
4391
4418
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
4392
4419
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0
@@ -4869,6 +4896,7 @@ function formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) {
4869
4896
  var TOAST_BODY_MAX_LINES = 12;
4870
4897
  var TOAST_SUMMARY_MAX_CHARS = 600;
4871
4898
  var NOTIFICATION_SUMMARY_MAX_CHARS = 1500;
4899
+ var DETAILED_NOTIFICATION_SUMMARY_MAX_CHARS = 1e4;
4872
4900
  function truncateToastBody(body, maxLines = TOAST_BODY_MAX_LINES) {
4873
4901
  const lines = body.split("\n");
4874
4902
  if (lines.length <= maxLines) {
@@ -4889,18 +4917,24 @@ function buildCompressionSummary(entries, state) {
4889
4917
  if (entries.length === 1) {
4890
4918
  return entries[0]?.summary ?? "";
4891
4919
  }
4920
+ const perEntryMax = Math.floor(NOTIFICATION_SUMMARY_MAX_CHARS / entries.length);
4892
4921
  let result = "";
4893
- for (const entry of entries) {
4922
+ let shown = 0;
4923
+ for (let i = 0; i < entries.length; i++) {
4924
+ const entry = entries[i];
4894
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;
4895
4927
  const section = `### ${topic}
4896
- ${entry.summary}`;
4928
+ ${truncated}`;
4897
4929
  if (result.length + section.length + 2 > NOTIFICATION_SUMMARY_MAX_CHARS) {
4898
- result += `
4899
-
4900
- ... and ${entries.length - entries.indexOf(entry)} more`;
4930
+ const remaining = entries.length - shown;
4931
+ if (remaining > 0) {
4932
+ result += (result ? "\n\n" : "") + `... and ${remaining} more`;
4933
+ }
4901
4934
  break;
4902
4935
  }
4903
4936
  result += (result ? "\n\n" : "") + section;
4937
+ shown++;
4904
4938
  }
4905
4939
  return result;
4906
4940
  }
@@ -5011,7 +5045,8 @@ ${progressBar}`;
5011
5045
  message += ` compressed`;
5012
5046
  }
5013
5047
  if (config.compress.showCompression) {
5014
- const displaySummary = summary.length > NOTIFICATION_SUMMARY_MAX_CHARS ? truncateToastSummary(summary, NOTIFICATION_SUMMARY_MAX_CHARS) : summary;
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;
5015
5050
  message += `
5016
5051
  \u2192 Compression (~${summaryTokensStr}): ${displaySummary}`;
5017
5052
  }
@@ -5265,6 +5300,113 @@ function createCompressMessageTool(ctx) {
5265
5300
 
5266
5301
  // lib/compress/range.ts
5267
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
5268
5410
  function buildSchema2(maxSummaryLengthHard) {
5269
5411
  return {
5270
5412
  topic: tool3.schema.string().describe("Short label (3-5 words) for display - e.g., 'Auth System Exploration'"),
@@ -5420,6 +5562,13 @@ function createCompressRangeTool(ctx) {
5420
5562
  const runId = allocateRunId(ctx.state);
5421
5563
  for (const preparedPlan of preparedPlans) {
5422
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;
5423
5572
  const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
5424
5573
  const summaryTokens = countTokens2(storedSummary);
5425
5574
  const applied = applyCompressionState(
@@ -6358,8 +6507,7 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
6358
6507
  const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6359
6508
  return `
6360
6509
 
6361
- Context: ${formatK(currentTokens)} tokens.
6362
- All compression serves the primary task, but be frugal. Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools. Compress by need, not by percentage.`;
6510
+ Context: ${formatK(currentTokens)} tokens.`;
6363
6511
  }
6364
6512
  function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage) {
6365
6513
  const turnNudgeAnchors = collectTurnNudgeAnchors2(state, config, messages);
@@ -6527,8 +6675,78 @@ function estimateContextComposition(messages, state) {
6527
6675
  toolTypeBreakdown
6528
6676
  };
6529
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
+ }
6530
6742
 
6531
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).`;
6532
6750
  var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
6533
6751
 
6534
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.
@@ -6557,6 +6775,8 @@ DROP \u2014 extract the signal, discard the vessel:
6557
6775
 
6558
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.
6559
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
+
6560
6780
  PRIORITY \u2014 when the summary must be compact, preserve in this order:
6561
6781
  1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
6562
6782
  2. Decisions and rationale.
@@ -6575,7 +6795,7 @@ function createSuffixMessage(messages) {
6575
6795
  messages.push(synthetic);
6576
6796
  return synthetic;
6577
6797
  }
6578
- var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
6798
+ var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities, debugNotify) => {
6579
6799
  if (compressPermission(state, config) === "deny") {
6580
6800
  return;
6581
6801
  }
@@ -6592,18 +6812,29 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6592
6812
  modelId,
6593
6813
  messages
6594
6814
  );
6595
- if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
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) {
6596
6821
  state.nudges.contextLimitAnchors.clear();
6597
6822
  state.nudges.turnNudgeAnchors.clear();
6598
6823
  state.nudges.iterationNudgeAnchors.clear();
6599
- state.nudges.lastPerMessageNudgeTokens = void 0;
6824
+ state.nudges.lastNudgeShownTokens = void 0;
6600
6825
  state.nudges.lastToolOutputNudgeTokens = void 0;
6826
+ if (!state.nudges.compressBaselineSet) {
6827
+ state.nudges.lastPerMessageNudgeTokens = currentTokens;
6828
+ state.nudges.compressBaselineSet = true;
6829
+ }
6601
6830
  saveSessionState(state, logger).catch(() => {
6602
6831
  });
6603
6832
  return;
6604
6833
  }
6834
+ state.nudges.compressBaselineSet = false;
6605
6835
  let anchorsChanged = false;
6606
6836
  let baselineReEstablished = false;
6837
+ let baselineCorrected = false;
6607
6838
  if (!overMinLimit) {
6608
6839
  const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6609
6840
  const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
@@ -6666,15 +6897,20 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6666
6897
  const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
6667
6898
  if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
6668
6899
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
6900
+ state.nudges.lastNudgeShownTokens = void 0;
6901
+ baselineCorrected = true;
6669
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;
6670
6906
  const decision = computeShouldNudge({
6671
6907
  currentTokens,
6672
6908
  modelContextLimit,
6673
6909
  overMinLimit,
6674
6910
  overMaxLimit,
6675
- lastNudgeTokens: state.nudges.lastPerMessageNudgeTokens,
6911
+ lastNudgeTokens: growthReference,
6676
6912
  minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
6677
- nudgeGrowthTokens
6913
+ nudgeGrowthTokens: effectiveThreshold
6678
6914
  });
6679
6915
  state.nudges.shouldInjectThisTurn = decision.shouldNudge;
6680
6916
  if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
@@ -6682,24 +6918,6 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6682
6918
  baselineReEstablished = true;
6683
6919
  }
6684
6920
  const composition = estimateContextComposition(messages, state);
6685
- const toolOutputThreshold = config.compress?.toolOutputNudgeThreshold ?? nudgeGrowthTokens;
6686
- let toolOutputReminder = null;
6687
- if (composition.toolTokens > 0) {
6688
- if (state.nudges.lastToolOutputNudgeTokens === void 0) {
6689
- state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
6690
- } else {
6691
- const toolGrowth = composition.toolTokens - state.nudges.lastToolOutputNudgeTokens;
6692
- if (toolGrowth >= toolOutputThreshold) {
6693
- const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6694
- const topRanges = composition.largestRanges.slice(0, 15).map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ");
6695
- toolOutputReminder = `
6696
-
6697
- \u26A0\uFE0F ${fmt(toolGrowth)} new tool outputs accumulated (${fmt(composition.toolTokens)} total). Largest: ${topRanges}. Use compress tool to compress these ranges now.`;
6698
- state.nudges.lastToolOutputNudgeTokens = composition.toolTokens;
6699
- anchorsChanged = true;
6700
- }
6701
- }
6702
- }
6703
6921
  let tipsText = null;
6704
6922
  if (decision.shouldNudge) {
6705
6923
  injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
@@ -6710,28 +6928,17 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6710
6928
  const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
6711
6929
  const plainTextTokens = composition.textTokens;
6712
6930
  const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
6713
- This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.` : "";
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}` : "";
6714
6934
  let breakdown = `${efficiencyNote}
6715
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}`;
6716
- const topToolTypes = composition.toolTypeBreakdown.slice(0, 3);
6717
- if (topToolTypes.length > 0) {
6718
- breakdown += `
6719
- Top tools: ${topToolTypes.map((t) => `${t.tool} (${pct2(t.tokens)}%)`).join(", ")}`;
6720
- }
6721
- if (composition.largestToolRanges.length > 0) {
6722
- breakdown += `
6723
- Largest tool outputs: ${composition.largestToolRanges.slice(0, 10).map((r) => `${r.ref} (${fmt(r.tokens)})${r.tool ? " " + r.tool : ""}`).join(", ")}`;
6724
- }
6725
- if (composition.largestCodeRanges.length > 0) {
6726
- breakdown += `
6727
- Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
6728
- }
6729
- if (composition.largestMessageRanges.length > 0) {
6936
+ const ranges = buildCompressibleRanges(messages, state);
6937
+ if (ranges.length > 0) {
6730
6938
  breakdown += `
6731
- Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
6939
+
6940
+ ${formatCompressibleRanges(ranges)}`;
6732
6941
  }
6733
- breakdown += `
6734
- \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.`;
6735
6942
  if (decision.tipsVariant !== "maxLimit") {
6736
6943
  breakdown += `
6737
6944
 
@@ -6742,8 +6949,7 @@ ${HOW_TO_COMPRESS_RULES}`;
6742
6949
  if (decision.tipsVariant === "maxLimit") {
6743
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.';
6744
6951
  }
6745
- state.nudges.lastPerMessageNudgeTokens = currentTokens;
6746
- state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0;
6952
+ state.nudges.lastNudgeShownTokens = currentTokens;
6747
6953
  if (config.compress.mode !== "message") {
6748
6954
  const visibleMessageIds = new Set(
6749
6955
  messages.map((message) => message.info.id)
@@ -6763,12 +6969,15 @@ ${HOW_TO_COMPRESS_RULES}`;
6763
6969
  }
6764
6970
  injectVisibleIdRange(state, config, messages, suffixMessage);
6765
6971
  }
6766
- if (toolOutputReminder && suffixMessage) {
6767
- appendToLastTextPart(suffixMessage, toolOutputReminder);
6768
- }
6769
6972
  if (suffixMessage) {
6770
6973
  if (hasContent(suffixMessage)) {
6771
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
+ }
6772
6981
  } else {
6773
6982
  const idx = messages.lastIndexOf(suffixMessage);
6774
6983
  if (idx !== -1) {
@@ -6776,7 +6985,7 @@ ${HOW_TO_COMPRESS_RULES}`;
6776
6985
  }
6777
6986
  }
6778
6987
  }
6779
- if (anchorsChanged || decision.shouldNudge || baselineReEstablished) {
6988
+ if (anchorsChanged || decision.shouldNudge || baselineReEstablished || baselineCorrected) {
6780
6989
  saveSessionState(state, logger).catch(() => {
6781
6990
  });
6782
6991
  }
@@ -7415,19 +7624,17 @@ ${content}`;
7415
7624
 
7416
7625
  // lib/compress/status.ts
7417
7626
  import { tool as tool5 } from "@opencode-ai/plugin";
7418
- var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview or drill down into compressed/uncompressed sections.
7627
+ var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges by default.
7419
7628
 
7420
- No args: Overview of both visible (uncompressed) context and compressed blocks.
7421
- scope:"uncompressed": Drill into all visible messages \u2014 list each with ref, tokens, tool type. Add tool:"bash" to filter by tool type.
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.
7422
7631
  scope:"compressed": Drill into compressed blocks \u2014 list each with full details (age, generation, consumed lineage).
7423
7632
 
7424
- Sort options: "size" (default, largest first), "time" (chronological), "tool" (group by tool type).
7425
-
7426
7633
  Use this tool to:
7427
- - See what's consuming context (overview)
7428
- - Find all messages of a specific tool type to batch-compress
7429
- - Check block details before decompressing
7430
- - Find compression candidates when context grows`;
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")`;
7431
7638
  function formatTokens(n) {
7432
7639
  if (!Number.isFinite(n) || n <= 0) return "0";
7433
7640
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
@@ -7477,7 +7684,7 @@ function collectVisibleMessages(rawMessages, ctx) {
7477
7684
  });
7478
7685
  return { messages: result, summaryTokens };
7479
7686
  }
7480
- function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
7687
+ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, rawMessages, ctx) {
7481
7688
  const lines = [];
7482
7689
  const toolTypeMap = /* @__PURE__ */ new Map();
7483
7690
  for (const m of visibleMessages) {
@@ -7522,9 +7729,45 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed) {
7522
7729
  lines.push(` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`);
7523
7730
  }
7524
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
+ }
7525
7745
  lines.push("");
7526
7746
  const hintTool = topToolName || "bash";
7527
- lines.push(`Tip: acp_status({scope:"uncompressed", tool:"${hintTool}", sort:"size"}) \u2014 mix any params freely`);
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"})`);
7528
7771
  return lines;
7529
7772
  }
7530
7773
  function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
@@ -7608,12 +7851,14 @@ function createAcpStatusTool(ctx) {
7608
7851
  description: ACP_STATUS_TOOL_DESCRIPTION,
7609
7852
  args: {
7610
7853
  scope: tool5.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
7611
- tool: tool5.schema.string().optional().describe('Filter by tool type (only with scope:"uncompressed"). e.g., "bash", "todowrite", "write"'),
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"'),
7612
7856
  sort: tool5.schema.string().optional().describe('Sort order: "size" (default), "time", or "tool"'),
7613
7857
  limit: tool5.schema.number().optional().describe("Max items to list (default 30)")
7614
7858
  },
7615
7859
  async execute(args, toolCtx) {
7616
7860
  const scope = args.scope === "compressed" || args.scope === "uncompressed" ? args.scope : void 0;
7861
+ const view = args.view === "messages" ? "messages" : "ranges";
7617
7862
  const toolFilter = typeof args.tool === "string" ? args.tool : void 0;
7618
7863
  const sort = args.sort === "time" || args.sort === "tool" || args.sort === "age" ? args.sort : "size";
7619
7864
  const limit = Number.isFinite(args.limit) && args.limit > 0 ? Math.min(args.limit, 200) : 30;
@@ -7628,8 +7873,9 @@ function createAcpStatusTool(ctx) {
7628
7873
  let visibleMsgs = [];
7629
7874
  let summaryTokens = 0;
7630
7875
  let fetchFailed = false;
7876
+ let rawMessages = [];
7631
7877
  try {
7632
- const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
7878
+ rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
7633
7879
  const result = collectVisibleMessages(rawMessages, ctx);
7634
7880
  visibleMsgs = result.messages;
7635
7881
  summaryTokens = result.summaryTokens;
@@ -7638,9 +7884,13 @@ function createAcpStatusTool(ctx) {
7638
7884
  }
7639
7885
  if (scope === "uncompressed") {
7640
7886
  if (fetchFailed) return "(unable to fetch messages)";
7641
- lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
7887
+ if (view === "messages") {
7888
+ lines.push(...renderUncompressedDrilldown(visibleMsgs, toolFilter, sort, limit));
7889
+ } else {
7890
+ lines.push(...renderUncompressedRanges(rawMessages, ctx));
7891
+ }
7642
7892
  } else {
7643
- lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed));
7893
+ lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed, rawMessages, ctx));
7644
7894
  }
7645
7895
  return lines.join("\n");
7646
7896
  }
@@ -7939,7 +8189,7 @@ You have five context-management tools:
7939
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" })\`.
7940
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" })\`.
7941
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 })\`.
7942
- - \`acp_status\` \u2014 Context status with drilldown. No args = overview. \`scope:"uncompressed"\` lists all visible messages; add \`tool:"bash"\` to filter by tool type. \`scope:"compressed"\` shows block details. Example: \`acp_status({scope:"uncompressed", tool:"todowrite"})\`.
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.
7943
8193
 
7944
8194
  COMPRESSION PHILOSOPHY
7945
8195
 
@@ -7947,9 +8197,9 @@ Two failure modes to avoid:
7947
8197
  - Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality.
7948
8198
  - Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work.
7949
8199
 
7950
- Balance is key. The single test for whether to compress is: "Is this content still needed by the current task step?" If yes, keep it. If no, it is a candidate. When uncertain, lean toward keeping content.
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.
7951
8201
 
7952
- Be frugal with context. Compress obvious waste proactively \u2014 verbose outputs you have already used, duplicate reads, abandoned explorations. Do not wait until context is critically full; that harms retrieval quality and risks overflow. When compressing, cover the largest range you can in a single call \u2014 aim for 20+ messages. Compressing 3-5 messages at a time creates many small summaries that collectively waste more tokens than they save. But never let the urge to compress distract from the actual task.
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.
7953
8203
 
7954
8204
  WHEN TO COMPRESS
7955
8205
 
@@ -7966,7 +8216,7 @@ WHEN NOT TO COMPRESS
7966
8216
 
7967
8217
  - Content the current task step is actively reading or reasoning about.
7968
8218
  - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria verbatim, not just the most recent one.
7969
- - Outputs from protected tools (e.g. \`task\`, \`skill\`, \`todowrite\`, \`write\`, \`edit\`) \u2014 these are appended to summaries automatically, not compressed away.
8219
+ - Protected tool outputs (default: \`skill\` only) \u2014 hard-excluded from compression ranges, survive intact in visible context.
7970
8220
 
7971
8221
  ${HOW_TO_COMPRESS_RULES}
7972
8222
 
@@ -7991,9 +8241,9 @@ Breakdown: 12.3K tool (40%) | 3.1K summaries (10%) | 8.5K code (28%) | 6.5K text
7991
8241
  - "code" = messages containing code blocks
7992
8242
  - "text" = plain text messages
7993
8243
 
7994
- Below the breakdown, the system lists the largest ranges in each category (e.g. \`Largest tool outputs: m00175 (20.7K), m00200 (8.1K)\`). These are high-value compression candidates \u2014 compress those whose content you have already consumed (extracted the facts you need). Keep any you still need to reference.
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.
7995
8245
 
7996
- Compress incrementally: target one large consumed range per compress call (e.g. m00150\u2013m00200), not the entire context at once. Each compression creates a reusable summary block you can decompress later if needed.
8246
+ Each compression creates a reusable summary block you can decompress later if needed.
7997
8247
  `;
7998
8248
 
7999
8249
  // lib/prompts/compress-range.ts
@@ -8033,6 +8283,23 @@ Rules:
8033
8283
 
8034
8284
  BATCHING
8035
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.
8036
8303
  `;
8037
8304
 
8038
8305
  // lib/prompts/compress-message.ts
@@ -9825,7 +10092,18 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
9825
10092
  logger,
9826
10093
  output.messages,
9827
10094
  prompts.getRuntimePrompts(),
9828
- 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
9829
10107
  );
9830
10108
  injectMessageIds(state, config, output.messages, compressionPriorities);
9831
10109
  applyPendingManualTrigger(state, output.messages, logger);