opencode-acp 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1485,7 +1485,7 @@ var defaultConfig = {
1485
1485
  maxOldGenSummaryLength: 3e3,
1486
1486
  majorGcThresholdPercent: "100%",
1487
1487
  batchCleanup: {
1488
- lowThreshold: "60%",
1488
+ lowThreshold: "55%",
1489
1489
  highThreshold: "75%",
1490
1490
  forceThreshold: "90%"
1491
1491
  }
@@ -5218,11 +5218,18 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
5218
5218
  const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
5219
5219
  const refs = activeBlockIds.map((id) => `b${id}`);
5220
5220
  const blockCount = refs.length;
5221
- const blockList = blockCount > 0 ? refs.join(", ") : "none";
5221
+ let blockList;
5222
+ if (blockCount <= 20) {
5223
+ blockList = blockCount > 0 ? refs.join(", ") : "none";
5224
+ } else {
5225
+ const recent = refs.slice(-20).join(", ");
5226
+ blockList = `${recent} (+${blockCount - 20} older, use decompress to access by ID)`;
5227
+ }
5222
5228
  const lines = [
5223
5229
  "Compressed block context:",
5224
5230
  `- Active compressed blocks: ${blockCount} (${blockList})`,
5225
- "- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`."
5231
+ "- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`.",
5232
+ "- \u{1F4A1} When you've finished using tool outputs, compress them \u2014 you can decompress later if needed. Lean context improves accuracy."
5226
5233
  ];
5227
5234
  const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
5228
5235
  if (gcConfig && usageRatio > 0.5) {
@@ -5575,14 +5582,14 @@ function buildContextUsageGuidance(config, currentTokens, modelContextLimit) {
5575
5582
  const formatK = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
5576
5583
  const minPct = resolveThresholdPercent(config.compress.minContextLimit, modelContextLimit) ?? 45;
5577
5584
  const maxPct = resolveThresholdPercent(config.compress.maxContextLimit, modelContextLimit) ?? 55;
5578
- const base = `Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%). ACP threshold: ${maxPct.toFixed(0)}%.`;
5585
+ const base = `Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%).`;
5579
5586
  let guidance;
5580
5587
  if (pct < minPct) {
5581
- guidance = " Context is ample \u2014 focus on your task. Only compress obvious waste (large terminal outputs, duplicated content).";
5588
+ guidance = " \u{1F4A1} Be frugal with context \u2014 compress tool outputs you've finished using into summaries. You can decompress later; nothing is permanently lost. Lean context means better accuracy. Extract and keep what matters: user intent, key decisions, file paths, and important findings \u2014 even if buried in large messages. Compress everything else, including verbose parts of any message.";
5582
5589
  } else if (pct < maxPct) {
5583
- guidance = " Context is moderate \u2014 compress completed sections and high-token waste. Preserve key details.";
5590
+ guidance = " \u26A0\uFE0F Context is growing \u2014 compress completed sections and high-token waste now. Preserve key details.";
5584
5591
  } else {
5585
- guidance = " Context is high \u2014 compress aggressively but selectively. Preserve only what is essential.";
5592
+ guidance = " \u{1F525} Context is high \u2014 compress aggressively but selectively. Preserve only what is essential.";
5586
5593
  }
5587
5594
  return `
5588
5595
 
@@ -6628,15 +6635,15 @@ COMPRESSION PHILOSOPHY
6628
6635
 
6629
6636
  Compression replaces raw conversation content with dense summaries. When used correctly, it keeps your context sharp and focused. When used carelessly, it destroys information you need.
6630
6637
 
6631
- The key principle: compress based on context pressure, not habit. When context is ample, compress rarely or not at all. When context is tight, compress aggressively but selectively. The runtime context usage indicator tells you the current pressure level.
6638
+ The key principle: compress proactively to keep context lean, but selectively. Large tool outputs (shell, diffs, logs) can be compressed into summaries at any time \u2014 you can decompress later if needed. Extract and keep what matters: user intent, key decisions, file paths, and important findings \u2014 even if buried in large messages. Compress everything else, including verbose parts of user messages, large code dumps, and long discussions.
6632
6639
 
6633
6640
  Target the largest UNCOMPRESSED content first. Savings scale with original size \u2014 compressing a 5000-token tool output frees far more than re-shrinking an already-summarized 300-token block.
6634
6641
 
6635
6642
  CONTEXT PRESSURE LEVELS
6636
6643
 
6637
- - Ample: Context is well below the threshold. Do NOT compress unless there is obvious waste (huge terminal dumps, duplicated content). Focus entirely on your task.
6638
- - Moderate: Context is approaching the threshold. Compress completed sections proactively. Prioritize high-token waste over minor cleanup.
6639
- - High: Context has exceeded the threshold. Compress aggressively. Every compression should free meaningful tokens. Preserve only what is essential for the current task.
6644
+ - Normal: Be frugal \u2014 compress tool outputs you've finished using into summaries. You can decompress later. Extract and keep what matters from any message; compress verbose parts \u2014 including large logs in user messages or generated code.
6645
+ - Elevated: Context is growing. Compress completed sections and high-token waste more urgently.
6646
+ - Critical: Compress aggressively now. Every compression should free meaningful tokens. Preserve only what is essential for the current task.
6640
6647
 
6641
6648
  WHAT TO COMPRESS FIRST (high value, low risk)
6642
6649
 
@@ -6798,9 +6805,9 @@ General cleanup should be done periodically between other normal compression too
6798
6805
  // lib/prompts/context-limit-nudge.ts
6799
6806
  var CONTEXT_LIMIT_NUDGE = `
6800
6807
  <system-reminder>
6801
- \u26A0\uFE0F CRITICAL: Context limit reached. You MUST use the \`compress\` tool NOW.
6808
+ \u26A0\uFE0F Context limit reached \u2014 time to compress the largest ranges you no longer need. Prioritize completed tool outputs and resolved work. You can decompress specific blocks later if you need details. Keeping context lean helps you stay accurate.
6802
6809
 
6803
- If mid-atomic-operation, finish that step first, then compress immediately.
6810
+ If mid-atomic-operation, finish that step first, then compress.
6804
6811
 
6805
6812
  HOW TO CALL COMPRESS:
6806
6813
  {
@@ -6815,7 +6822,7 @@ HOW TO CALL COMPRESS:
6815
6822
  }
6816
6823
 
6817
6824
  \u26A0\uFE0F ID RULES \u2014 MOST COMMON CAUSE OF ERRORS:
6818
- - ONLY use IDs you can see in <dcp-message-id> tags in the messages ABOVE.
6825
+ - ONLY use IDs you can see in tags in the messages ABOVE.
6819
6826
  - Do NOT copy IDs from this example. Do NOT invent IDs.
6820
6827
  - Do NOT use IDs from compressed block summaries \u2014 they are stale.
6821
6828
  - startId must appear BEFORE endId in the conversation.
@@ -6831,14 +6838,14 @@ SUMMARY RULES:
6831
6838
  // lib/prompts/turn-nudge.ts
6832
6839
  var TURN_NUDGE = `
6833
6840
  <system-reminder>
6834
- Context is getting full. Compress closed/older conversation ranges now.
6841
+ Context is getting full. If you've finished reading tool outputs or exploration results, compress them \u2014 you can decompress later if needed. This keeps your focus on the current task and improves accuracy.
6835
6842
 
6836
6843
  {
6837
6844
  "topic": "Short Label",
6838
6845
  "content": [{ "startId": "<visible message ID>", "endId": "<visible message ID>", "summary": "..." }]
6839
6846
  }
6840
6847
 
6841
- \u26A0\uFE0F ONLY use IDs from <dcp-message-id> tags visible above. Do NOT invent or copy example IDs.
6848
+ \u26A0\uFE0F ONLY use IDs from tags visible above. Do NOT invent or copy example IDs.
6842
6849
  </system-reminder>
6843
6850
  `;
6844
6851
 
@@ -8212,10 +8219,12 @@ function parseGcThreshold(limit, modelContextLimit) {
8212
8219
 
8213
8220
  // lib/gc/merge.ts
8214
8221
  var DEFAULT_BATCH_CLEANUP = {
8215
- lowThreshold: "60%",
8222
+ lowThreshold: "55%",
8216
8223
  highThreshold: "75%",
8217
8224
  forceThreshold: "90%"
8218
8225
  };
8226
+ var ESCALATE_MIN_MARKED = 3;
8227
+ var ESCALATE_MIN_RATIO = 0.4;
8219
8228
  function resolveBatchCleanup(gc) {
8220
8229
  return gc.batchCleanup ?? DEFAULT_BATCH_CLEANUP;
8221
8230
  }
@@ -8239,11 +8248,15 @@ function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
8239
8248
  return blocks;
8240
8249
  }
8241
8250
  function collectActiveMarkedBlocks(state) {
8242
- const ids = Array.from(state.prune.messages.markedForCleanup).sort((a, b) => a - b);
8251
+ const messagesState = state.prune.messages;
8252
+ const ids = Array.from(messagesState.markedForCleanup).sort((a, b) => a - b);
8243
8253
  const blocks = [];
8244
8254
  for (const id of ids) {
8245
- const block = state.prune.messages.blocksById.get(id);
8246
- if (!block || !block.active) continue;
8255
+ const block = messagesState.blocksById.get(id);
8256
+ if (!block || !block.active) {
8257
+ messagesState.markedForCleanup.delete(id);
8258
+ continue;
8259
+ }
8247
8260
  blocks.push(block);
8248
8261
  }
8249
8262
  return blocks;
@@ -8368,21 +8381,53 @@ function mergeMarkedBlocks(state, markedIds, maxMergedLength) {
8368
8381
  const savedTokens = Math.max(0, sourceTokens - newSummaryTokens);
8369
8382
  return { mergedCount: sourceBlocks.length, savedTokens };
8370
8383
  }
8371
- function buildNudgeText(state, maxMergedLength) {
8372
- const blocks = collectActiveMarkedBlocks(state);
8373
- if (blocks.length < 1) return void 0;
8374
- const refs = blocks.map((b) => formatBlockRef(b.blockId)).join(", ");
8375
- const sourceTokens = blocks.reduce(
8384
+ function estimateTokens(blocks) {
8385
+ return blocks.reduce(
8376
8386
  (sum, block) => sum + (block.summaryTokens || Math.round(block.summary.length / 4)),
8377
8387
  0
8378
8388
  );
8379
- const estimatedMergedTokens = Math.round(maxMergedLength / 4);
8380
- const estimatedSavings = Math.max(0, sourceTokens - estimatedMergedTokens);
8389
+ }
8390
+ function buildNudgeText(state, maxMergedLength) {
8391
+ const marked = collectActiveMarkedBlocks(state);
8392
+ const oldGen = collectActiveOldGenBlocks(state, maxMergedLength);
8393
+ if (oldGen.length === 0) return void 0;
8394
+ const oldGenIds = new Set(oldGen.map((b) => b.blockId));
8395
+ const markedOldGen = marked.filter((b) => oldGenIds.has(b.blockId));
8396
+ const markedOldGenCount = markedOldGen.length;
8397
+ const oldGenCount = oldGen.length;
8398
+ const ratio = markedOldGenCount / oldGenCount;
8399
+ const ratioPct = Math.round(ratio * 100);
8400
+ const escalateMinPct = Math.round(ESCALATE_MIN_RATIO * 100);
8401
+ if (markedOldGenCount >= ESCALATE_MIN_MARKED && ratio >= ESCALATE_MIN_RATIO) {
8402
+ const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
8403
+ const firstRef = formatBlockRef(marked[0].blockId);
8404
+ const lastRef = formatBlockRef(marked[marked.length - 1].blockId);
8405
+ const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
8406
+ return [
8407
+ `\u{1F525} ${markedOldGenCount}/${oldGenCount} old-gen blocks marked (${ratioPct}%) \u2014 ready for batch cleanup.`,
8408
+ `Compressing ${refs} (range ${firstRef}\u2013${lastRef}) would free ~${estimatedSavings} tokens in one cache break.`,
8409
+ `Call compress with this range now to consolidate them.`
8410
+ ].join(" ");
8411
+ }
8412
+ if (marked.length >= 1) {
8413
+ const refs = marked.map((b) => formatBlockRef(b.blockId)).join(", ");
8414
+ const estimatedSavings = Math.max(0, estimateTokens(marked) - Math.round(maxMergedLength / 4));
8415
+ return [
8416
+ `\u26A0\uFE0F ${marked.length} block(s) marked for batch cleanup (${refs}).`,
8417
+ `Merge-compressing them would free ~${estimatedSavings} tokens.`,
8418
+ marked.length >= 2 ? "They will auto-merge when context pressure reaches the high threshold." : "A single marked block won't auto-merge on its own \u2014 use compress to consolidate it, or unmark_block if no longer needed.",
8419
+ `Mark more old-gen blocks (need \u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}%) to trigger batch cleanup sooner.`,
8420
+ "To act now, use compress with a range covering these blocks."
8421
+ ].join(" ");
8422
+ }
8423
+ const shown = oldGen.slice(0, 5);
8424
+ const oldGenRefs = shown.map((b) => formatBlockRef(b.blockId)).join(", ");
8425
+ const more = oldGenCount > 5 ? ` (+${oldGenCount - 5} more)` : "";
8381
8426
  return [
8382
- `\u26A0\uFE0F ${blocks.length} block(s) marked for batch cleanup (${refs}).`,
8383
- `Merge-compressing them would free ~${estimatedSavings} tokens.`,
8384
- blocks.length >= 2 ? "They will auto-merge when context pressure reaches the high threshold." : "A single marked block won't auto-merge on its own \u2014 use compress to consolidate it, or unmark_block if no longer needed.",
8385
- "To act now, use compress with a range covering these blocks."
8427
+ `\u{1F4CB} Context pressure rising \u2014 ${oldGenCount} old-gen compressed block(s) occupy ~${estimateTokens(oldGen)} tokens (${oldGenRefs}${more}).`,
8428
+ `Review which blocks contain information you no longer need, and use mark_block to flag them.`,
8429
+ `Once enough are marked (\u2265${ESCALATE_MIN_MARKED} at \u2265${escalateMinPct}% of old-gen), they'll be batch-merged in one cache break to preserve cache hit rate.`,
8430
+ `Do NOT mark blocks you may still need.`
8386
8431
  ].join(" ");
8387
8432
  }
8388
8433
  function runBatchCleanup(state, config, logger, messages) {
@@ -8427,26 +8472,24 @@ function runBatchCleanup(state, config, logger, messages) {
8427
8472
  }
8428
8473
  if (currentTokens >= highTokens) {
8429
8474
  const marked = collectActiveMarkedBlocks(state);
8430
- if (marked.length < 2) {
8431
- return noop;
8432
- }
8433
- const ids = marked.map((b) => b.blockId);
8434
- const result = mergeMarkedBlocks(state, ids, maxMergedLength);
8435
- if (result.mergedCount === 0) {
8436
- return noop;
8475
+ if (marked.length >= 2) {
8476
+ const ids = marked.map((b) => b.blockId);
8477
+ const result = mergeMarkedBlocks(state, ids, maxMergedLength);
8478
+ if (result.mergedCount > 0) {
8479
+ logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
8480
+ mergedCount: result.mergedCount,
8481
+ savedTokens: result.savedTokens,
8482
+ currentTokens,
8483
+ highThreshold: batchCleanup.highThreshold
8484
+ });
8485
+ return {
8486
+ tier: 2,
8487
+ action: "merge",
8488
+ mergedCount: result.mergedCount,
8489
+ savedTokens: result.savedTokens
8490
+ };
8491
+ }
8437
8492
  }
8438
- logger.info("Batch cleanup tier 2 (high): merged marked blocks", {
8439
- mergedCount: result.mergedCount,
8440
- savedTokens: result.savedTokens,
8441
- currentTokens,
8442
- highThreshold: batchCleanup.highThreshold
8443
- });
8444
- return {
8445
- tier: 2,
8446
- action: "merge",
8447
- mergedCount: result.mergedCount,
8448
- savedTokens: result.savedTokens
8449
- };
8450
8493
  }
8451
8494
  if (currentTokens >= lowTokens) {
8452
8495
  const nudgeText = buildNudgeText(state, maxMergedLength);