opencode-acp 1.13.9-dev.1 → 1.14.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 +72 -11
  2. package/README.zh-CN.md +55 -8
  3. package/dist/index.js +592 -66
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/commands/compression-targets.d.ts.map +1 -1
  6. package/dist/lib/commands/recompress.d.ts.map +1 -1
  7. package/dist/lib/commands/stats.d.ts.map +1 -1
  8. package/dist/lib/compress/decompress-logic.d.ts +3 -1
  9. package/dist/lib/compress/decompress-logic.d.ts.map +1 -1
  10. package/dist/lib/compress/decompress.d.ts.map +1 -1
  11. package/dist/lib/compress/hide-consumed.d.ts +17 -0
  12. package/dist/lib/compress/hide-consumed.d.ts.map +1 -0
  13. package/dist/lib/compress/index.d.ts +1 -0
  14. package/dist/lib/compress/index.d.ts.map +1 -1
  15. package/dist/lib/compress/pipeline.d.ts +11 -4
  16. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  17. package/dist/lib/compress/state.d.ts.map +1 -1
  18. package/dist/lib/compress/status.d.ts.map +1 -1
  19. package/dist/lib/config-validation.d.ts.map +1 -1
  20. package/dist/lib/config.d.ts +6 -0
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/hooks.d.ts.map +1 -1
  23. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  24. package/dist/lib/messages/inject/utils.d.ts +18 -0
  25. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  26. package/dist/lib/messages/sync.d.ts.map +1 -1
  27. package/dist/lib/prompts/system.d.ts +1 -1
  28. package/dist/lib/prompts/system.d.ts.map +1 -1
  29. package/dist/lib/state/persistence.d.ts +4 -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 +12 -0
  33. package/dist/lib/state/types.d.ts.map +1 -1
  34. package/dist/lib/state/utils.d.ts +12 -1
  35. package/dist/lib/state/utils.d.ts.map +1 -1
  36. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -909,6 +909,10 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
909
909
  "compress.emergencyThresholdPercent",
910
910
  "compress.maxVisibleSegments",
911
911
  "compress.keepEmbedMaxChars",
912
+ "compress.lastSegmentSoftBlock",
913
+ "compress.preserveRecentMessages",
914
+ "compress.preserveRecentTokens",
915
+ "compress.preserveLastUserMessage",
912
916
  "gc",
913
917
  "gc.algorithm",
914
918
  "gc.promotionThreshold",
@@ -1277,6 +1281,48 @@ function validateConfigTypes(config) {
1277
1281
  actual: `${compress.keepEmbedMaxChars}`
1278
1282
  });
1279
1283
  }
1284
+ if (compress.lastSegmentSoftBlock !== void 0 && typeof compress.lastSegmentSoftBlock !== "boolean") {
1285
+ errors.push({
1286
+ key: "compress.lastSegmentSoftBlock",
1287
+ expected: "boolean",
1288
+ actual: typeof compress.lastSegmentSoftBlock
1289
+ });
1290
+ }
1291
+ if (compress.preserveRecentMessages !== void 0 && typeof compress.preserveRecentMessages !== "number") {
1292
+ errors.push({
1293
+ key: "compress.preserveRecentMessages",
1294
+ expected: "number",
1295
+ actual: typeof compress.preserveRecentMessages
1296
+ });
1297
+ }
1298
+ if (typeof compress.preserveRecentMessages === "number" && compress.preserveRecentMessages < 0) {
1299
+ errors.push({
1300
+ key: "compress.preserveRecentMessages",
1301
+ expected: "non-negative number (>= 0)",
1302
+ actual: `${compress.preserveRecentMessages}`
1303
+ });
1304
+ }
1305
+ if (compress.preserveRecentTokens !== void 0 && typeof compress.preserveRecentTokens !== "number") {
1306
+ errors.push({
1307
+ key: "compress.preserveRecentTokens",
1308
+ expected: "number",
1309
+ actual: typeof compress.preserveRecentTokens
1310
+ });
1311
+ }
1312
+ if (typeof compress.preserveRecentTokens === "number" && compress.preserveRecentTokens < 0) {
1313
+ errors.push({
1314
+ key: "compress.preserveRecentTokens",
1315
+ expected: "non-negative number (>= 0)",
1316
+ actual: `${compress.preserveRecentTokens}`
1317
+ });
1318
+ }
1319
+ if (compress.preserveLastUserMessage !== void 0 && typeof compress.preserveLastUserMessage !== "boolean") {
1320
+ errors.push({
1321
+ key: "compress.preserveLastUserMessage",
1322
+ expected: "boolean",
1323
+ actual: typeof compress.preserveLastUserMessage
1324
+ });
1325
+ }
1280
1326
  if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
1281
1327
  errors.push({
1282
1328
  key: "compress.iterationNudgeThreshold",
@@ -1601,7 +1647,10 @@ var defaultConfig = {
1601
1647
  emergencyThresholdPercent: "98%",
1602
1648
  maxVisibleSegments: 50,
1603
1649
  keepEmbedMaxChars: 2e3,
1604
- lastSegmentSoftBlock: true
1650
+ lastSegmentSoftBlock: true,
1651
+ preserveRecentMessages: 20,
1652
+ preserveRecentTokens: 2e4,
1653
+ preserveLastUserMessage: true
1605
1654
  },
1606
1655
  strategies: {
1607
1656
  deduplication: {
@@ -1776,7 +1825,10 @@ function mergeCompress(base, override) {
1776
1825
  emergencyThresholdPercent: override.emergencyThresholdPercent ?? base.emergencyThresholdPercent,
1777
1826
  maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments,
1778
1827
  keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars,
1779
- lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock
1828
+ lastSegmentSoftBlock: override.lastSegmentSoftBlock ?? base.lastSegmentSoftBlock,
1829
+ preserveRecentMessages: override.preserveRecentMessages ?? base.preserveRecentMessages,
1830
+ preserveRecentTokens: override.preserveRecentTokens ?? base.preserveRecentTokens,
1831
+ preserveLastUserMessage: override.preserveLastUserMessage ?? base.preserveLastUserMessage
1780
1832
  };
1781
1833
  }
1782
1834
  function mergeCommands(base, override) {
@@ -3062,7 +3114,20 @@ ${footer}`;
3062
3114
  function applyCompressionState(state, input, selection, anchorMessageId, blockId, summary, consumedBlockIds, gcConfig) {
3063
3115
  const messagesState = state.prune.messages;
3064
3116
  const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))];
3065
- const included = [...consumed];
3117
+ const createdAt = Date.now();
3118
+ let minConsumedTier;
3119
+ for (const consumedBlockId of consumed) {
3120
+ const cb = messagesState.blocksById.get(consumedBlockId);
3121
+ if (cb) {
3122
+ const cbTier = cb.tier ?? 1;
3123
+ if (minConsumedTier === void 0 || cbTier < minConsumedTier) {
3124
+ minConsumedTier = cbTier;
3125
+ }
3126
+ }
3127
+ }
3128
+ const effectiveMinTier = minConsumedTier ?? 0;
3129
+ const outputTier = Math.min(3, effectiveMinTier + 1);
3130
+ const targetTierForConsumption = effectiveMinTier;
3066
3131
  const effectiveMessageIds = new Set(selection.messageIds);
3067
3132
  const effectiveToolIds = new Set(selection.toolIds);
3068
3133
  for (const consumedBlockId of consumed) {
@@ -3070,6 +3135,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3070
3135
  if (!consumedBlock) {
3071
3136
  continue;
3072
3137
  }
3138
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3139
+ continue;
3140
+ }
3073
3141
  for (const messageId of consumedBlock.effectiveMessageIds) {
3074
3142
  effectiveMessageIds.add(messageId);
3075
3143
  }
@@ -3094,7 +3162,6 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3094
3162
  initiallyActiveToolIds.add(toolId);
3095
3163
  }
3096
3164
  }
3097
- const createdAt = Date.now();
3098
3165
  const block = {
3099
3166
  blockId,
3100
3167
  runId: input.runId,
@@ -3104,6 +3171,7 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3104
3171
  summaryTokens: input.summaryTokens,
3105
3172
  durationMs: 0,
3106
3173
  mode: input.mode,
3174
+ tier: outputTier,
3107
3175
  topic: input.topic,
3108
3176
  batchTopic: input.batchTopic,
3109
3177
  startId: input.startId,
@@ -3111,8 +3179,11 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3111
3179
  anchorMessageId,
3112
3180
  compressMessageId: input.compressMessageId,
3113
3181
  compressCallId: input.compressCallId,
3114
- includedBlockIds: included,
3115
- consumedBlockIds: consumed,
3182
+ includedBlockIds: [...consumed],
3183
+ consumedBlockIds: consumed.filter((id) => {
3184
+ const cb = messagesState.blocksById.get(id);
3185
+ return cb && (cb.tier ?? 1) === targetTierForConsumption;
3186
+ }),
3116
3187
  parentBlockIds: [],
3117
3188
  directMessageIds: [],
3118
3189
  directToolIds: [],
@@ -3141,6 +3212,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3141
3212
  if (!consumedBlock || !consumedBlock.active) {
3142
3213
  continue;
3143
3214
  }
3215
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3216
+ continue;
3217
+ }
3144
3218
  consumedBlock.active = false;
3145
3219
  consumedBlock.deactivatedAt = deactivatedAt;
3146
3220
  consumedBlock.deactivatedByBlockId = blockId;
@@ -3166,6 +3240,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3166
3240
  if (!consumedBlock) {
3167
3241
  continue;
3168
3242
  }
3243
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3244
+ continue;
3245
+ }
3169
3246
  for (const messageId of consumedBlock.effectiveMessageIds) {
3170
3247
  const entry = messagesState.byMessageId.get(messageId);
3171
3248
  if (!entry) {
@@ -3231,6 +3308,14 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3231
3308
  block.directMessageIds = [...newlyCompressedMessageIds];
3232
3309
  block.directToolIds = [...newlyCompressedToolIds];
3233
3310
  block.compressedTokens = compressedTokens;
3311
+ let effectiveTokens = compressedTokens;
3312
+ for (const consumedBlockId of consumed) {
3313
+ const cb = messagesState.blocksById.get(consumedBlockId);
3314
+ if (cb && (cb.tier ?? 1) === targetTierForConsumption) {
3315
+ effectiveTokens += cb.effectiveCompressedTokens ?? cb.compressedTokens;
3316
+ }
3317
+ }
3318
+ block.effectiveCompressedTokens = effectiveTokens;
3234
3319
  state.stats.pruneTokenCounter += compressedTokens;
3235
3320
  state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
3236
3321
  state.stats.pruneTokenCounter = 0;
@@ -3593,6 +3678,7 @@ function loadPruneMessagesState(persisted) {
3593
3678
  runId: typeof block.runId === "number" && Number.isInteger(block.runId) && block.runId > 0 ? block.runId : blockId,
3594
3679
  active: block.active === true,
3595
3680
  deactivatedByUser: block.deactivatedByUser === true,
3681
+ deactivatedByUserDeep: block.deactivatedByUserDeep === true ? true : void 0,
3596
3682
  compressedTokens: typeof block.compressedTokens === "number" && Number.isFinite(block.compressedTokens) ? Math.max(0, block.compressedTokens) : 0,
3597
3683
  summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens2(block.summary) : 0,
3598
3684
  durationMs: typeof block.durationMs === "number" && Number.isFinite(block.durationMs) ? Math.max(0, block.durationMs) : 0,
@@ -3616,7 +3702,9 @@ function loadPruneMessagesState(persisted) {
3616
3702
  deactivatedByBlockId: typeof block.deactivatedByBlockId === "number" && Number.isInteger(block.deactivatedByBlockId) ? block.deactivatedByBlockId : void 0,
3617
3703
  summary: typeof block.summary === "string" ? block.summary : "",
3618
3704
  survivedCount: typeof block.survivedCount === "number" && Number.isFinite(block.survivedCount) ? Math.max(0, Math.floor(block.survivedCount)) : 0,
3619
- generation: block.generation === "young" || block.generation === "old" ? block.generation : void 0
3705
+ generation: block.generation === "young" || block.generation === "old" ? block.generation : void 0,
3706
+ tier: block.tier === 1 || block.tier === 2 || block.tier === 3 ? block.tier : void 0,
3707
+ effectiveCompressedTokens: typeof block.effectiveCompressedTokens === "number" && Number.isFinite(block.effectiveCompressedTokens) ? Math.max(0, block.effectiveCompressedTokens) : void 0
3620
3708
  });
3621
3709
  }
3622
3710
  }
@@ -3675,17 +3763,38 @@ function collectTurnNudgeAnchors(messages) {
3675
3763
  }
3676
3764
  return anchors;
3677
3765
  }
3678
- function getActiveSummaryTokenUsage(state) {
3766
+ function getActiveSummaryTokenUsage(state, visibleMessageIds) {
3679
3767
  let total = 0;
3680
3768
  for (const blockId of state.prune.messages.activeBlockIds) {
3681
3769
  const block = state.prune.messages.blocksById.get(blockId);
3682
3770
  if (!block || !block.active) {
3683
3771
  continue;
3684
3772
  }
3773
+ if (visibleMessageIds && block.compressMessageId) {
3774
+ if (!visibleMessageIds.has(block.compressMessageId)) {
3775
+ continue;
3776
+ }
3777
+ }
3685
3778
  total += block.summaryTokens;
3686
3779
  }
3687
3780
  return total;
3688
3781
  }
3782
+ function getTierTokenUsage(state) {
3783
+ let tier1Tokens = 0;
3784
+ let tier2Tokens = 0;
3785
+ let tier3Tokens = 0;
3786
+ for (const blockId of state.prune.messages.activeBlockIds) {
3787
+ const block = state.prune.messages.blocksById.get(blockId);
3788
+ if (!block || !block.active) {
3789
+ continue;
3790
+ }
3791
+ const tier = block.tier ?? 1;
3792
+ if (tier === 1) tier1Tokens += block.summaryTokens;
3793
+ else if (tier === 2) tier2Tokens += block.summaryTokens;
3794
+ else tier3Tokens += block.summaryTokens;
3795
+ }
3796
+ return { tier1Tokens, tier2Tokens, tier3Tokens };
3797
+ }
3689
3798
  function resetOnCompaction(state) {
3690
3799
  state.toolParameters.clear();
3691
3800
  state.prune.tools = /* @__PURE__ */ new Map();
@@ -3697,6 +3806,8 @@ function resetOnCompaction(state) {
3697
3806
  lastPerMessageNudgeTokens: void 0,
3698
3807
  lastNudgeShownTokens: void 0,
3699
3808
  lastToolOutputNudgeTokens: void 0,
3809
+ lastTier2NudgeTokens: void 0,
3810
+ lastTier3NudgeTokens: void 0,
3700
3811
  shouldInjectThisTurn: void 0,
3701
3812
  compressBaselineSet: false
3702
3813
  };
@@ -3773,6 +3884,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
3773
3884
  lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
3774
3885
  lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
3775
3886
  lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
3887
+ lastTier2NudgeTokens: sessionState.nudges.lastTier2NudgeTokens,
3888
+ lastTier3NudgeTokens: sessionState.nudges.lastTier3NudgeTokens,
3776
3889
  compressBaselineSet: sessionState.nudges.compressBaselineSet
3777
3890
  },
3778
3891
  stats: sessionState.stats,
@@ -4385,6 +4498,8 @@ function createSessionState() {
4385
4498
  lastPerMessageNudgeTokens: void 0,
4386
4499
  lastNudgeShownTokens: void 0,
4387
4500
  lastToolOutputNudgeTokens: void 0,
4501
+ lastTier2NudgeTokens: void 0,
4502
+ lastTier3NudgeTokens: void 0,
4388
4503
  shouldInjectThisTurn: void 0,
4389
4504
  compressBaselineSet: false
4390
4505
  },
@@ -4428,6 +4543,8 @@ function resetSessionState(state) {
4428
4543
  lastPerMessageNudgeTokens: void 0,
4429
4544
  lastNudgeShownTokens: void 0,
4430
4545
  lastToolOutputNudgeTokens: void 0,
4546
+ lastTier2NudgeTokens: void 0,
4547
+ lastTier3NudgeTokens: void 0,
4431
4548
  shouldInjectThisTurn: void 0,
4432
4549
  compressBaselineSet: false
4433
4550
  };
@@ -4484,6 +4601,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4484
4601
  state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
4485
4602
  state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
4486
4603
  state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
4604
+ state.nudges.lastTier2NudgeTokens = persisted.nudges.lastTier2NudgeTokens ?? persisted.nudges.lastTierNudgeTokens;
4605
+ state.nudges.lastTier3NudgeTokens = persisted.nudges.lastTier3NudgeTokens;
4487
4606
  state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
4488
4607
  state.stats = {
4489
4608
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
@@ -5047,7 +5166,7 @@ async function sendCompressNotification(client, logger, config, state, sessionId
5047
5166
  const logTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic ?? "?");
5048
5167
  const logCompressedTokens = entries.reduce((sum, e) => {
5049
5168
  const block = state.prune.messages.blocksById.get(e.blockId);
5050
- return sum + (block?.compressedTokens ?? 0);
5169
+ return sum + (block?.effectiveCompressedTokens ?? block?.compressedTokens ?? 0);
5051
5170
  }, 0);
5052
5171
  const logSummaryTokens = entries.reduce((sum, e) => sum + e.summaryTokens, 0);
5053
5172
  logger.info("Compression completed", {
@@ -5991,7 +6110,7 @@ function evaluatePreCommitQuality(rawMessages, messageIds, messageTokenById, sum
5991
6110
  }
5992
6111
  }
5993
6112
 
5994
- // node_modules/context-compress-algorithms/dist/chunk-ZRHPFN6B.js
6113
+ // node_modules/context-compress-algorithms/dist/chunk-EBKMI537.js
5995
6114
  var COMPRESS_PHILOSOPHY = `Compression Philosophy:
5996
6115
  - All compression serves the primary task, but be frugal.
5997
6116
  - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
@@ -6036,6 +6155,69 @@ PRIORITY \u2014 when the summary must be compact, preserve in this order:
6036
6155
  5. Lessons learned: what failed and why.
6037
6156
 
6038
6157
  Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
6158
+ var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
6159
+
6160
+ You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
6161
+
6162
+ KEEP \u2014 these are the only things that survive distillation:
6163
+ - Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
6164
+ - Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
6165
+ - Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
6166
+ - Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
6167
+ - Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
6168
+ - Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
6169
+ - Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
6170
+ - Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
6171
+
6172
+ DROP \u2014 these were useful during the work but are no longer needed:
6173
+ - Exact line numbers, diffs, verbose function signatures, full code listings.
6174
+ - Build/deploy process details, test execution steps.
6175
+ - Review process details (who reviewed, what rounds, test counts).
6176
+ - Verbose logs, command output, intermediate debugging steps.
6177
+
6178
+ FORMAT:
6179
+ - Start each distilled block with a source header line:
6180
+ \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
6181
+ Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
6182
+ - 3-5 bullet points per source block, each a self-contained fact.
6183
+ - Dense, scannable \u2014 no narrative prose.
6184
+ - Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
6185
+ - Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
6186
+
6187
+ SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]".`;
6188
+ var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
6189
+
6190
+ You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
6191
+
6192
+ PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
6193
+ 1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
6194
+ 2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
6195
+ 3. Key decisions with architectural impact ("chose X over Y because Z").
6196
+ 4. Critical constraints ("must support Node 22").
6197
+ Drop everything else. Tier 3 is a lookup index, not a knowledge base.
6198
+
6199
+ FORMAT:
6200
+ - Start with a source header line:
6201
+ \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
6202
+ - Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
6203
+ - No explanations, no rationale, no process \u2014 just the fact.
6204
+ - Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
6205
+ - Merge related facts from different source blocks if they concern the same topic.
6206
+
6207
+ EXAMPLES:
6208
+ - "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
6209
+ - "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
6210
+ - "Bug 1214 fixed \u2014 compress consumed all user messages"
6211
+ - "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
6212
+ - "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
6213
+
6214
+ DROP:
6215
+ - Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
6216
+ - Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
6217
+ - Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
6218
+ - Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
6219
+
6220
+ SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
6039
6221
 
6040
6222
  // lib/compress/quality-gate/rejection.ts
6041
6223
  function formatMetric(result, name) {
@@ -6180,17 +6362,6 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
6180
6362
  contextTokensBefore
6181
6363
  );
6182
6364
  }
6183
- function getLastVisibleMessageId(rawMessages, state) {
6184
- for (let i = rawMessages.length - 1; i >= 0; i--) {
6185
- const msg = rawMessages[i];
6186
- const id = msg?.info?.id;
6187
- if (!id || typeof id !== "string") continue;
6188
- if (isSyntheticMessage(msg)) continue;
6189
- if (state.prune.messages.byMessageId.has(id)) continue;
6190
- return id;
6191
- }
6192
- return null;
6193
- }
6194
6365
  function checkPhantomBlock(state, plans) {
6195
6366
  for (let i = 0; i < plans.length; i++) {
6196
6367
  const plan = plans[i];
@@ -6208,6 +6379,16 @@ function checkPhantomBlock(state, plans) {
6208
6379
  return !entry || entry.activeBlockIds.length === 0;
6209
6380
  });
6210
6381
  if (!hasNew) {
6382
+ if (plan.consumedBlockIds.length >= 2) {
6383
+ const tiers = new Set(
6384
+ plan.consumedBlockIds.map(
6385
+ (id) => state.prune.messages.blocksById.get(id)?.tier ?? 1
6386
+ )
6387
+ );
6388
+ if (tiers.size === 1) {
6389
+ continue;
6390
+ }
6391
+ }
6211
6392
  return new Error(
6212
6393
  `Compression range ${i + 1} contains only already-compressed messages (0 new direct messages, 0 tokens saved). Nothing to compress \u2014 pick a range with visible, uncompressed content. Use \`acp_status({scope:"uncompressed"})\` to see which ranges are still compressible.`
6213
6394
  );
@@ -6215,16 +6396,71 @@ function checkPhantomBlock(state, plans) {
6215
6396
  }
6216
6397
  return null;
6217
6398
  }
6218
- function checkLastSegmentDangerous(ctx, allPlanMessageIds, rawMessages, dangerous) {
6399
+ function computeProtectedRawIds(rawMessages, state, compress) {
6400
+ const preserveN = compress.preserveRecentMessages ?? 20;
6401
+ const preserveTokens = compress.preserveRecentTokens ?? 2e4;
6402
+ const preserveLastUser = compress.preserveLastUserMessage ?? true;
6403
+ const result = /* @__PURE__ */ new Set();
6404
+ const visible = [];
6405
+ for (const msg of rawMessages) {
6406
+ const id = msg?.info?.id;
6407
+ if (!id || typeof id !== "string") continue;
6408
+ if (isSyntheticMessage(msg)) continue;
6409
+ if (isIgnoredUserMessage(msg)) continue;
6410
+ if (state.prune.messages.byMessageId.has(id)) continue;
6411
+ let tokens = 0;
6412
+ for (const part of msg.parts || []) {
6413
+ if (part.type === "text" && typeof part.text === "string") {
6414
+ tokens += Math.round(part.text.length / 4);
6415
+ } else if (part.type !== "text" && part.type !== "reasoning") {
6416
+ tokens += Math.round(JSON.stringify(part).length / 4);
6417
+ }
6418
+ }
6419
+ visible.push({ id, tokens, isUser: msg.info.role === "user" });
6420
+ }
6421
+ if (preserveN > 0) {
6422
+ for (const m of visible.slice(-preserveN)) {
6423
+ result.add(m.id);
6424
+ }
6425
+ }
6426
+ if (preserveTokens > 0) {
6427
+ let tokenAccum = 0;
6428
+ for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
6429
+ result.add(visible[i].id);
6430
+ tokenAccum += visible[i].tokens;
6431
+ }
6432
+ }
6433
+ if (preserveLastUser) {
6434
+ for (let i = visible.length - 1; i >= 0; i--) {
6435
+ if (visible[i].isUser) {
6436
+ result.add(visible[i].id);
6437
+ break;
6438
+ }
6439
+ }
6440
+ }
6441
+ return result;
6442
+ }
6443
+ function checkProtectedRange(ctx, allPlanMessageIds, rawMessages, dangerous) {
6219
6444
  if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
6220
- const lastVisibleId = getLastVisibleMessageId(rawMessages, ctx.state);
6221
- if (!lastVisibleId) return null;
6222
- const coversLast = allPlanMessageIds.some((ids) => ids.includes(lastVisibleId));
6223
- if (!coversLast) return null;
6445
+ const protectedIds = computeProtectedRawIds(rawMessages, ctx.state, ctx.config.compress);
6446
+ if (protectedIds.size === 0) return null;
6447
+ const coveredProtected = [];
6448
+ for (const ids of allPlanMessageIds) {
6449
+ for (const id of ids) {
6450
+ if (protectedIds.has(id)) {
6451
+ coveredProtected.push(id);
6452
+ }
6453
+ }
6454
+ }
6455
+ if (coveredProtected.length === 0) return null;
6224
6456
  if (dangerous) return null;
6457
+ const sample = coveredProtected.slice(0, 3).join(", ");
6458
+ const nMsgs = ctx.config.compress.preserveRecentMessages ?? 20;
6459
+ const nToks = ctx.config.compress.preserveRecentTokens ?? 2e4;
6225
6460
  return new Error(
6226
- `This range includes the most recent message (${lastVisibleId}), which is likely still needed for the current task step.
6461
+ `This range includes ${coveredProtected.length} protected recent message(s) (${sample}), which are likely still needed for the current task step.
6227
6462
 
6463
+ Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens + most recent user message.
6228
6464
  If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
6229
6465
  Otherwise, compress older ranges that do not include the tail of the conversation.`
6230
6466
  );
@@ -6423,7 +6659,7 @@ function createCompressMessageTool(factoryCtx) {
6423
6659
  }
6424
6660
  }
6425
6661
  const dangerous = args.dangerous === true;
6426
- const lastSegmentError = checkLastSegmentDangerous(
6662
+ const lastSegmentError = checkProtectedRange(
6427
6663
  ctx,
6428
6664
  plans.map((p) => p.selection.messageIds),
6429
6665
  rawMessages,
@@ -6643,7 +6879,7 @@ function createCompressRangeTool(factoryCtx) {
6643
6879
  }
6644
6880
  }
6645
6881
  const dangerous = args.dangerous === true;
6646
- const lastSegmentError = checkLastSegmentDangerous(
6882
+ const lastSegmentError = checkProtectedRange(
6647
6883
  ctx,
6648
6884
  filteredPlans.map((p) => p.selection.messageIds),
6649
6885
  rawMessages,
@@ -6915,10 +7151,9 @@ var syncCompressionBlocks = (state, logger, messages) => {
6915
7151
  messagesState.activeBlockIds.clear();
6916
7152
  messagesState.activeByAnchorMessageId.clear();
6917
7153
  const now = Date.now();
6918
- const missingOriginBlockIds = [];
6919
7154
  const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
6920
7155
  for (const block of orderedBlocks) {
6921
- if (block.deactivatedByUser) {
7156
+ if (block.deactivatedByUser || block.deactivatedByUserDeep) {
6922
7157
  block.active = false;
6923
7158
  if (block.deactivatedAt === void 0) {
6924
7159
  block.deactivatedAt = now;
@@ -6926,12 +7161,6 @@ var syncCompressionBlocks = (state, logger, messages) => {
6926
7161
  block.deactivatedByBlockId = void 0;
6927
7162
  continue;
6928
7163
  }
6929
- if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
6930
- block.active = false;
6931
- block.deactivatedAt = now;
6932
- block.deactivatedByBlockId = void 0;
6933
- continue;
6934
- }
6935
7164
  for (const consumedBlockId of block.consumedBlockIds) {
6936
7165
  if (!messagesState.activeBlockIds.has(consumedBlockId)) {
6937
7166
  continue;
@@ -6976,9 +7205,8 @@ var syncCompressionBlocks = (state, logger, messages) => {
6976
7205
  reactivatedCount++;
6977
7206
  }
6978
7207
  }
6979
- if (missingOriginBlockIds.length > 0 || deactivatedCount > 0 || reactivatedCount > 0) {
7208
+ if (deactivatedCount > 0 || reactivatedCount > 0) {
6980
7209
  logger.info("Synced compress block state", {
6981
- missingOriginCount: missingOriginBlockIds.length,
6982
7210
  deactivatedCount,
6983
7211
  reactivatedCount
6984
7212
  });
@@ -7389,7 +7617,7 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
7389
7617
  return refs;
7390
7618
  }
7391
7619
 
7392
- // node_modules/context-compress-algorithms/dist/chunk-UX2UW5FY.js
7620
+ // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
7393
7621
  var NUDGE_GROWTH_FLOOR = 6e3;
7394
7622
  var NUDGE_GROWTH_CAP = 5e4;
7395
7623
  var NUDGE_GROWTH_RATIO = 0.05;
@@ -7522,7 +7750,10 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
7522
7750
  return parseLimitValue(globalLimit);
7523
7751
  }
7524
7752
  function isContextOverLimits(config, state, providerId, modelId, messages) {
7525
- const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(state) : 0;
7753
+ const summaryTokenExtension = config.compress.summaryBuffer ? getActiveSummaryTokenUsage(
7754
+ state,
7755
+ new Set(messages.map((m) => m.info.id))
7756
+ ) : 0;
7526
7757
  const resolvedMaxContextLimit = resolveContextTokenLimit(
7527
7758
  config,
7528
7759
  state,
@@ -8145,6 +8376,57 @@ ${lines2.join("\n")}`;
8145
8376
  return `Compressible ranges (oldest first):
8146
8377
  ${lines.join("\n")}`;
8147
8378
  }
8379
+ function computeProtectedRefs(messages, state, compress) {
8380
+ if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
8381
+ const preserveN = compress.preserveRecentMessages ?? 20;
8382
+ const preserveTokens = compress.preserveRecentTokens ?? 2e4;
8383
+ const preserveLastUser = compress.preserveLastUserMessage ?? true;
8384
+ const result = /* @__PURE__ */ new Set();
8385
+ const visible = [];
8386
+ for (const msg of messages) {
8387
+ if (isSyntheticMessage(msg)) continue;
8388
+ if (isIgnoredUserMessage(msg)) continue;
8389
+ const ref = state.messageIds.byRawId.get(msg.info.id);
8390
+ if (!ref) continue;
8391
+ if (state.prune.messages.byMessageId.has(msg.info.id)) continue;
8392
+ let tokens = 0;
8393
+ for (const part of msg.parts || []) {
8394
+ if (part.type === "text" && typeof part.text === "string") {
8395
+ tokens += Math.round(part.text.length / 4);
8396
+ } else if (part.type !== "text" && part.type !== "reasoning") {
8397
+ tokens += Math.round(JSON.stringify(part).length / 4);
8398
+ }
8399
+ }
8400
+ visible.push({ ref, tokens, isUser: msg.info.role === "user" });
8401
+ }
8402
+ if (preserveN > 0) {
8403
+ for (const m of visible.slice(-preserveN)) {
8404
+ result.add(m.ref);
8405
+ }
8406
+ }
8407
+ if (preserveTokens > 0) {
8408
+ let tokenAccum = 0;
8409
+ for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
8410
+ result.add(visible[i].ref);
8411
+ tokenAccum += visible[i].tokens;
8412
+ }
8413
+ }
8414
+ if (preserveLastUser) {
8415
+ for (let i = visible.length - 1; i >= 0; i--) {
8416
+ if (visible[i].isUser) {
8417
+ result.add(visible[i].ref);
8418
+ break;
8419
+ }
8420
+ }
8421
+ }
8422
+ return result;
8423
+ }
8424
+ function excludeProtectedRanges(ranges, protectedRefs) {
8425
+ if (protectedRefs.size === 0) return ranges;
8426
+ return ranges.filter(
8427
+ (r) => !protectedRefs.has(r.startRef) && !protectedRefs.has(r.endRef)
8428
+ );
8429
+ }
8148
8430
 
8149
8431
  // lib/messages/inject/inject.ts
8150
8432
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
@@ -8184,6 +8466,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8184
8466
  state.nudges.iterationNudgeAnchors.clear();
8185
8467
  state.nudges.lastNudgeShownTokens = void 0;
8186
8468
  state.nudges.lastToolOutputNudgeTokens = void 0;
8469
+ state.nudges.lastTier2NudgeTokens = void 0;
8470
+ state.nudges.lastTier3NudgeTokens = void 0;
8187
8471
  if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
8188
8472
  const baseline = state.nudges.lastPerMessageNudgeTokens;
8189
8473
  const postCompress = currentTokens;
@@ -8322,8 +8606,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8322
8606
  config.compress.protectedTools,
8323
8607
  config.protectedFilePatterns
8324
8608
  );
8609
+ const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8610
+ const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
8325
8611
  const recommendedRanges = filterRecommendedRanges(
8326
- contextRanges.compressible,
8612
+ unprotectedCompressible,
8327
8613
  contextRanges.protected,
8328
8614
  { modelContextLimit, growthRatio: 0.05, logger }
8329
8615
  );
@@ -8350,11 +8636,92 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8350
8636
  const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
8351
8637
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
8352
8638
  const nothingToCompress = filterSuppressed || allProtected;
8353
- const shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
8639
+ let shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
8354
8640
  if (nudgeAllowed && nothingToCompress && !emergencyOverride && currentTokens !== void 0) {
8355
8641
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
8356
8642
  state.nudges.lastNudgeShownTokens = void 0;
8357
8643
  }
8644
+ if (suffixMessage && !shouldInject) {
8645
+ const tierUsage = getTierTokenUsage(state);
8646
+ const tierChecks = [
8647
+ { triggerTier: 2, targetTier: 1, tokens: tierUsage.tier1Tokens, lastNudge: state.nudges.lastTier2NudgeTokens },
8648
+ { triggerTier: 3, targetTier: 2, tokens: tierUsage.tier2Tokens, lastNudge: state.nudges.lastTier3NudgeTokens }
8649
+ ];
8650
+ for (const tc of tierChecks) {
8651
+ if (tc.tokens < nudgeGrowthTokens) continue;
8652
+ const cadenceMet = tc.lastNudge === void 0 || currentTokens !== void 0 && currentTokens - tc.lastNudge >= growthFloor;
8653
+ if (!cadenceMet) continue;
8654
+ let candidates = [...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active && (b.tier ?? 1) === tc.targetTier).sort((a, b) => a.blockId - b.blockId);
8655
+ if (candidates.length >= 2) {
8656
+ const firstId = candidates[0].blockId;
8657
+ const lastId = candidates[candidates.length - 1].blockId;
8658
+ const nonTargetInIdRange = new Set(
8659
+ [...state.prune.messages.activeBlockIds].map((id) => state.prune.messages.blocksById.get(id)).filter(
8660
+ (b) => b !== void 0 && b.active && (b.tier ?? 1) !== tc.targetTier && b.blockId > firstId && b.blockId < lastId
8661
+ ).map((b) => b.blockId)
8662
+ );
8663
+ if (nonTargetInIdRange.size > 0) {
8664
+ let bestStart = 0;
8665
+ let bestLen = 1;
8666
+ let curStart = 0;
8667
+ for (let i = 1; i < candidates.length; i++) {
8668
+ const prevId = candidates[i - 1].blockId;
8669
+ const currId = candidates[i].blockId;
8670
+ let hasGap = false;
8671
+ for (const nid of nonTargetInIdRange) {
8672
+ if (nid > prevId && nid < currId) {
8673
+ hasGap = true;
8674
+ break;
8675
+ }
8676
+ }
8677
+ if (hasGap) {
8678
+ const curLen = i - curStart;
8679
+ if (curLen > bestLen) {
8680
+ bestLen = curLen;
8681
+ bestStart = curStart;
8682
+ }
8683
+ curStart = i;
8684
+ }
8685
+ }
8686
+ const finalLen = candidates.length - curStart;
8687
+ if (finalLen > bestLen) {
8688
+ bestLen = finalLen;
8689
+ bestStart = curStart;
8690
+ }
8691
+ candidates = candidates.slice(bestStart, bestStart + bestLen);
8692
+ }
8693
+ }
8694
+ if (candidates.length < 2) continue;
8695
+ const rules = tc.triggerTier === 2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES;
8696
+ const candidateTokens = candidates.reduce((s, b) => s + b.summaryTokens, 0);
8697
+ const firstBlock = candidates[0];
8698
+ const lastBlock = candidates[candidates.length - 1];
8699
+ const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
8700
+ const sourceTier = tc.triggerTier === 2 ? "Tier 1" : "Tier 2";
8701
+ const action = tc.triggerTier === 2 ? "Distill" : "Condense";
8702
+ const blockList = candidates.slice(0, 10).map((b) => `b${b.blockId} (age=${b.survivedCount}, ${fmt(b.summaryTokens)}tok): "${b.topic}"`).join("\n");
8703
+ const extraCount = candidates.length > 10 ? `
8704
+ ...and ${candidates.length - 10} more` : "";
8705
+ const tierText = `
8706
+
8707
+ [Tier ${tc.triggerTier} Trigger] ${sourceTier} summaries accumulated (${fmt(candidateTokens)} tokens across ${candidates.length} blocks). ${action} them to free context.
8708
+
8709
+ Target blocks (oldest first):
8710
+ ${blockList}${extraCount}
8711
+
8712
+ Compress range: \`content: [{ startId: "b${firstBlock.blockId}", endId: "b${lastBlock.blockId}", summary: "..." }]\`
8713
+
8714
+ ${rules}`;
8715
+ appendToLastTextPart(suffixMessage, tierText);
8716
+ shouldInject = true;
8717
+ if (tc.triggerTier === 2) {
8718
+ state.nudges.lastTier2NudgeTokens = currentTokens;
8719
+ } else {
8720
+ state.nudges.lastTier3NudgeTokens = currentTokens;
8721
+ }
8722
+ break;
8723
+ }
8724
+ }
8358
8725
  state.nudges.shouldInjectThisTurn = shouldInject;
8359
8726
  let tipsText = null;
8360
8727
  if (shouldInject) {
@@ -8559,7 +8926,10 @@ function buildTarget(blocks) {
8559
8926
  displayId: first.blockId,
8560
8927
  runId: first.runId,
8561
8928
  topic: grouped ? first.batchTopic || first.topic : first.topic,
8562
- compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
8929
+ compressedTokens: ordered.reduce(
8930
+ (total, block) => total + (block.effectiveCompressedTokens ?? block.compressedTokens),
8931
+ 0
8932
+ ),
8563
8933
  durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
8564
8934
  grouped,
8565
8935
  blocks: ordered
@@ -8727,17 +9097,25 @@ function snapshotActiveMessages(messagesState) {
8727
9097
  }
8728
9098
  return activeMessages;
8729
9099
  }
8730
- function deactivateCompressionTarget(messagesState, target) {
9100
+ function deactivateCompressionTarget(messagesState, target, options) {
8731
9101
  const deactivatedAt = Date.now();
8732
9102
  for (const block of target.blocks) {
8733
9103
  block.active = false;
8734
9104
  block.deactivatedByUser = true;
8735
9105
  block.deactivatedAt = deactivatedAt;
8736
9106
  block.deactivatedByBlockId = void 0;
8737
- for (const consumedId of block.consumedBlockIds) {
8738
- const consumedBlock = messagesState.blocksById.get(consumedId);
8739
- if (consumedBlock) {
8740
- consumedBlock.deactivatedByUser = true;
9107
+ if (options?.full) {
9108
+ const visited = /* @__PURE__ */ new Set();
9109
+ const queue = [...block.consumedBlockIds];
9110
+ while (queue.length > 0) {
9111
+ const consumedId = queue.shift();
9112
+ if (visited.has(consumedId)) continue;
9113
+ visited.add(consumedId);
9114
+ const consumedBlock = messagesState.blocksById.get(consumedId);
9115
+ if (consumedBlock) {
9116
+ consumedBlock.deactivatedByUserDeep = true;
9117
+ queue.push(...consumedBlock.consumedBlockIds);
9118
+ }
8741
9119
  }
8742
9120
  }
8743
9121
  }
@@ -8953,14 +9331,18 @@ ARGUMENTS:
8953
9331
  IMPORTANT:
8954
9332
  - Decompressing inflates context. Check context usage before decompressing.
8955
9333
  - Message-mode blocks from the same batch (same runId) are restored together.
8956
- - After decompression, the restored messages will appear in full in your next context window.
9334
+ - TIER-AWARE: by default, decompressing a multi-tier block restores the PREVIOUS tier's
9335
+ summaries (e.g., decompress T2 \u2192 T1 summaries visible, not raw messages). Use full:true
9336
+ to restore all the way to original messages (can be very expensive for T2/T3 blocks).
9337
+ - After decompression, the restored content will appear in full in your next context window.
8957
9338
  - Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
8958
9339
  function buildSchema3() {
8959
9340
  return {
8960
9341
  blockId: tool4.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
8961
9342
  startId: tool4.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
8962
9343
  endId: tool4.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
8963
- toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'")
9344
+ toFile: tool4.schema.string().optional().describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Path must be under /tmp or ~/.cache/opencode/. Example: '/tmp/block52.txt'"),
9345
+ full: tool4.schema.boolean().optional().describe("If true, restores ALL content down to original messages (multi-level decompress). Default: false \u2014 restores one tier up (e.g., decompressing a T2 block restores T1 summaries, not raw messages). Use full:true only when you need the exact original content and have context budget for it.")
8964
9346
  };
8965
9347
  }
8966
9348
  function extractMessageId(m) {
@@ -9030,7 +9412,7 @@ function createDecompressTool(factoryCtx) {
9030
9412
  const activeMessagesBefore = snapshotActiveMessages(messagesState);
9031
9413
  const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
9032
9414
  for (const target of targets) {
9033
- deactivateCompressionTarget(messagesState, target);
9415
+ deactivateCompressionTarget(messagesState, target, { full: args.full === true });
9034
9416
  }
9035
9417
  syncCompressionBlocks(ctx.state, ctx.logger, rawMessages);
9036
9418
  const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
@@ -9109,13 +9491,54 @@ function formatIdRange(block) {
9109
9491
  const count = block.effectiveMessageIds?.length || 0;
9110
9492
  return count > 0 ? `${count} msg${count !== 1 ? "s" : ""}` : "\u2014";
9111
9493
  }
9494
+ function getEffectiveCompressedTokens(block, blocksById, visited = /* @__PURE__ */ new Set()) {
9495
+ if (block.effectiveCompressedTokens !== void 0) {
9496
+ return block.effectiveCompressedTokens;
9497
+ }
9498
+ if (visited.has(block.blockId)) return 0;
9499
+ visited.add(block.blockId);
9500
+ let total = block.compressedTokens || 0;
9501
+ for (const consumedId of block.consumedBlockIds || []) {
9502
+ const consumed = blocksById.get(consumedId);
9503
+ if (consumed) {
9504
+ total += getEffectiveCompressedTokens(consumed, blocksById, visited);
9505
+ }
9506
+ }
9507
+ return total;
9508
+ }
9509
+ function tierLabel(block) {
9510
+ const tier = block.tier ?? 1;
9511
+ return `T${tier}`;
9512
+ }
9513
+ function tierBreakdown(blocks) {
9514
+ const tierTokens = {};
9515
+ for (const b of blocks) {
9516
+ const t = b.tier ?? 1;
9517
+ tierTokens[t] = (tierTokens[t] || 0) + (b.summaryTokens || 0);
9518
+ }
9519
+ const tiers = Object.keys(tierTokens).map(Number);
9520
+ if (tiers.length <= 1 && (!tierTokens[2] || tierTokens[2] === 0) && (!tierTokens[3] || tierTokens[3] === 0)) {
9521
+ return null;
9522
+ }
9523
+ const parts = [];
9524
+ for (const t of [1, 2, 3]) {
9525
+ if (tierTokens[t]) {
9526
+ parts.push(`T${t}: ${formatTokens(tierTokens[t])}`);
9527
+ }
9528
+ }
9529
+ return parts.join(" | ");
9530
+ }
9112
9531
  function collectVisibleMessages(rawMessages, ctx) {
9113
9532
  const pruneMap = ctx.state.prune.messages.byMessageId;
9114
9533
  const byRawId = ctx.state.messageIds.byRawId;
9115
9534
  const result = [];
9116
9535
  let summaryTokens = 0;
9536
+ const visibleMessageIds = new Set(rawMessages.map((m) => m.info.id));
9117
9537
  const activeBlocks = Array.from(ctx.state.prune.messages.activeBlockIds).map((id) => ctx.state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
9118
9538
  for (const block of activeBlocks) {
9539
+ if (block.compressMessageId && !visibleMessageIds.has(block.compressMessageId)) {
9540
+ continue;
9541
+ }
9119
9542
  summaryTokens += block.summaryTokens || 0;
9120
9543
  }
9121
9544
  rawMessages.forEach((msg, idx) => {
@@ -9176,19 +9599,32 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
9176
9599
  lines.push("COMPRESSED BLOCKS");
9177
9600
  lines.push(" No compressed blocks.");
9178
9601
  } else {
9602
+ const blocksById = ctx.state.prune.messages.blocksById;
9179
9603
  const totalSummary = blocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
9180
- const totalCompressed = blocks.reduce((s, b) => s + (b.compressedTokens || 0), 0);
9181
- lines.push(
9182
- `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens(totalSummary)} summary, ${formatTokens(totalCompressed)} original)`
9604
+ const totalEffective = blocks.reduce(
9605
+ (s, b) => s + getEffectiveCompressedTokens(b, blocksById),
9606
+ 0
9183
9607
  );
9608
+ const header = `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens(totalSummary)} summary, ${formatTokens(totalEffective)} original)`;
9609
+ lines.push(header);
9610
+ const breakdown = tierBreakdown(blocks);
9611
+ if (breakdown) {
9612
+ lines.push(` Tier usage: ${breakdown}`);
9613
+ }
9184
9614
  lines.push("");
9185
- const sorted = [...blocks].sort((a, b) => b.createdAt - a.createdAt);
9615
+ const sorted = [...blocks].sort((a, b) => {
9616
+ const effA = getEffectiveCompressedTokens(a, blocksById);
9617
+ const effB = getEffectiveCompressedTokens(b, blocksById);
9618
+ return effB - effA || b.createdAt - a.createdAt;
9619
+ });
9186
9620
  for (const b of sorted.slice(0, 30)) {
9187
9621
  const ageStr = formatAge(b.createdAt);
9188
9622
  const range = formatIdRange(b);
9189
9623
  const topic = b.topic || "(no topic)";
9624
+ const tier = tierLabel(b);
9625
+ const effTokens = getEffectiveCompressedTokens(b, blocksById);
9190
9626
  lines.push(
9191
- ` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
9627
+ ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
9192
9628
  );
9193
9629
  }
9194
9630
  }
@@ -9292,7 +9728,7 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9292
9728
  }
9293
9729
  return lines;
9294
9730
  }
9295
- function renderCompressedDrilldown(blocks, sort, limit) {
9731
+ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
9296
9732
  const lines = [];
9297
9733
  let sorted = [...blocks];
9298
9734
  if (sort === "time") {
@@ -9300,13 +9736,22 @@ function renderCompressedDrilldown(blocks, sort, limit) {
9300
9736
  } else if (sort === "age") {
9301
9737
  sorted.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
9302
9738
  } else {
9303
- sorted.sort((a, b) => (b.compressedTokens || 0) - (a.compressedTokens || 0));
9739
+ sorted.sort(
9740
+ (a, b) => getEffectiveCompressedTokens(b, blocksById) - getEffectiveCompressedTokens(a, blocksById) || b.createdAt - a.createdAt
9741
+ );
9304
9742
  }
9305
9743
  const totalSummary = sorted.reduce((s, b) => s + (b.summaryTokens || 0), 0);
9306
- const totalCompressed = sorted.reduce((s, b) => s + (b.compressedTokens || 0), 0);
9744
+ const totalEffective = sorted.reduce(
9745
+ (s, b) => s + getEffectiveCompressedTokens(b, blocksById),
9746
+ 0
9747
+ );
9307
9748
  lines.push(
9308
- `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalCompressed)} original \u2192 ${formatTokens(totalSummary)} summary`
9749
+ `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalEffective)} original \u2192 ${formatTokens(totalSummary)} summary`
9309
9750
  );
9751
+ const breakdown = tierBreakdown(sorted);
9752
+ if (breakdown) {
9753
+ lines.push(`Tier usage: ${breakdown}`);
9754
+ }
9310
9755
  lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
9311
9756
  lines.push("");
9312
9757
  const shown = sorted.slice(0, limit);
@@ -9314,10 +9759,12 @@ function renderCompressedDrilldown(blocks, sort, limit) {
9314
9759
  const survived = b.survivedCount ?? 0;
9315
9760
  const gen = b.generation ?? "young";
9316
9761
  const effCount = b.effectiveMessageIds?.length ?? 0;
9317
- const consumed = b.consumedBlockIds && b.consumedBlockIds.length > 0 ? ` nested=[${b.consumedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
9762
+ const consumed = b.includedBlockIds && b.includedBlockIds.length > 0 ? ` nested=[${b.includedBlockIds.map((n) => `b${n}`).join(",")}]` : "";
9318
9763
  const topic = b.topic || "(no topic)";
9764
+ const tier = tierLabel(b);
9765
+ const effTokens = getEffectiveCompressedTokens(b, blocksById);
9319
9766
  lines.push(
9320
- ` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
9767
+ ` b${b.blockId} (${tier}) ${formatTokens(effTokens)}\u2192${formatTokens(b.summaryTokens)} ${formatAge(b.createdAt)} ${formatIdRange(b)} age=${survived} ${gen} eff=${effCount}${consumed}`
9321
9768
  );
9322
9769
  lines.push(` "${topic}"`);
9323
9770
  }
@@ -9358,7 +9805,14 @@ function createAcpStatusTool(factoryCtx) {
9358
9805
  const allBlocks = activeIds.map((id) => msgState.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
9359
9806
  const lines = [];
9360
9807
  if (scope === "compressed") {
9361
- lines.push(...renderCompressedDrilldown(allBlocks, sort, limit));
9808
+ lines.push(
9809
+ ...renderCompressedDrilldown(
9810
+ allBlocks,
9811
+ sort,
9812
+ limit,
9813
+ msgState.blocksById
9814
+ )
9815
+ );
9362
9816
  return lines.join("\n");
9363
9817
  }
9364
9818
  let visibleMsgs = [];
@@ -9517,6 +9971,53 @@ IMPORTANT: This was an automatic context pruning. You MUST continue your previou
9517
9971
  });
9518
9972
  }
9519
9973
 
9974
+ // lib/compress/hide-consumed.ts
9975
+ function hideConsumedCompressCalls(state, messages) {
9976
+ if (state.prune.messages.blocksById.size === 0) {
9977
+ return 0;
9978
+ }
9979
+ const consumedMessageIds = /* @__PURE__ */ new Set();
9980
+ for (const block of state.prune.messages.blocksById.values()) {
9981
+ if (block.active) continue;
9982
+ if (block.deactivatedByUser) continue;
9983
+ if (block.deactivatedByUserDeep) continue;
9984
+ if (block.deactivatedByBlockId === void 0) continue;
9985
+ if (!block.compressMessageId) continue;
9986
+ consumedMessageIds.add(block.compressMessageId);
9987
+ }
9988
+ if (consumedMessageIds.size === 0) {
9989
+ return 0;
9990
+ }
9991
+ const lastUserIdx = messages.findLastIndex(
9992
+ (m) => m.info.role === "user" && !isIgnoredUserMessage(m)
9993
+ );
9994
+ let hidden = 0;
9995
+ for (let i = 0; i < messages.length; i++) {
9996
+ if (lastUserIdx >= 0 && i >= lastUserIdx) break;
9997
+ const msg = messages[i];
9998
+ if (!consumedMessageIds.has(msg.info.id)) continue;
9999
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10000
+ let changed = false;
10001
+ const remaining = parts.filter((p) => {
10002
+ if (p.type === "tool" && p.tool === "compress") {
10003
+ hidden++;
10004
+ changed = true;
10005
+ return false;
10006
+ }
10007
+ return true;
10008
+ });
10009
+ if (changed) {
10010
+ if (remaining.length > 0) {
10011
+ messages[i] = { ...msg, parts: remaining };
10012
+ } else {
10013
+ messages.splice(i, 1);
10014
+ i--;
10015
+ }
10016
+ }
10017
+ }
10018
+ return hidden;
10019
+ }
10020
+
9520
10021
  // lib/logger.ts
9521
10022
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9522
10023
  import { join as join3 } from "path";
@@ -9745,7 +10246,7 @@ TOOLS
9745
10246
  You have five context-management tools:
9746
10247
 
9747
10248
  - \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
9748
- - \`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" })\`.
10249
+ - \`decompress\` \u2014 Restore a previously compressed block's content. By default restores one tier up (T2\u2192T1 summaries, not raw messages). Use \`full: true\` to restore all the way to original messages. Use \`toFile\` to write to file instead of inflating context. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\` or \`decompress({ blockId: "b5", full: true })\`.
9749
10250
  - \`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" })\`.
9750
10251
  - \`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 })\`.
9751
10252
  - \`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.
@@ -9779,6 +10280,16 @@ WHEN NOT TO COMPRESS
9779
10280
 
9780
10281
  ${HOW_TO_COMPRESS_RULES}
9781
10282
 
10283
+ MULTI-TIER COMPRESSION
10284
+
10285
+ Summaries accumulate as the session grows. When tier-1 summaries pile up, the system injects a [Tier 2 Trigger] prompting you to DISTILL old blocks into a single tier-2 summary. If tier-2 summaries also accumulate, a [Tier 3 Trigger] asks you to CONDENSE them further.
10286
+
10287
+ - Tier 1 (default): Full-detail compression of conversation ranges. Uses HOW TO COMPRESS rules above.
10288
+ - Tier 2: Distillation of old tier-1 block summaries. Uses TIER 2 DISTILLATION rules (decisions/outcomes only, drop paths/code/process).
10289
+ - Tier 3: Ultra-condensation of tier-2 summaries. Uses TIER 3 CONDENSATION rules (bare facts, 1-3 lines per block).
10290
+
10291
+ To compress blocks: use block IDs as boundaries: \`compress({ content: [{ startId: "b3", endId: "b15", summary: "..." }] })\`. This deactivates the consumed blocks and creates a new higher-tier block. The system prompt at the trigger tells you which rules to follow.
10292
+
9782
10293
  PERIODIC CONTEXT STATUS
9783
10294
 
9784
10295
  Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
@@ -10956,8 +11467,21 @@ async function handleRecompressCommand(ctx) {
10956
11467
  const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
10957
11468
  for (const block of target.blocks) {
10958
11469
  block.deactivatedByUser = false;
11470
+ block.deactivatedByUserDeep = false;
10959
11471
  block.deactivatedAt = void 0;
10960
11472
  block.deactivatedByBlockId = void 0;
11473
+ const queue = [...block.consumedBlockIds];
11474
+ const visited = /* @__PURE__ */ new Set();
11475
+ while (queue.length > 0) {
11476
+ const consumedId = queue.shift();
11477
+ if (visited.has(consumedId)) continue;
11478
+ visited.add(consumedId);
11479
+ const consumedBlock = messagesState.blocksById.get(consumedId);
11480
+ if (consumedBlock) {
11481
+ consumedBlock.deactivatedByUserDeep = false;
11482
+ queue.push(...consumedBlock.consumedBlockIds);
11483
+ }
11484
+ }
10961
11485
  }
10962
11486
  syncCompressionBlocks(state, logger, messages);
10963
11487
  let recompressedMessageCount = 0;
@@ -11044,8 +11568,9 @@ function formatCompressionTime(ms) {
11044
11568
  async function handleStatsCommand(ctx) {
11045
11569
  const { client, state, logger, sessionId, messages } = ctx;
11046
11570
  const sessionTokens = state.stats.totalPruneTokens;
11571
+ const visibleMessageIds = new Set(messages.map((m) => m.info.id));
11047
11572
  const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
11048
- (total, block) => block.active ? total + block.summaryTokens : total,
11573
+ (total, block) => block.active && visibleMessageIds.has(block.compressMessageId ?? "") ? total + block.summaryTokens : total,
11049
11574
  0
11050
11575
  );
11051
11576
  const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
@@ -11623,6 +12148,7 @@ function createChatMessageTransformHandler(client, registry3, logger, config, pr
11623
12148
  }
11624
12149
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11625
12150
  prune(state, logger, config, output.messages);
12151
+ hideConsumedCompressCalls(state, output.messages);
11626
12152
  assignMessageRefs(state, output.messages);
11627
12153
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
11628
12154
  prompts.reload();