opencode-acp 1.13.8-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 (46) hide show
  1. package/README.md +80 -11
  2. package/README.zh-CN.md +63 -8
  3. package/dist/index.js +593 -227
  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/message.d.ts.map +1 -1
  16. package/dist/lib/compress/pipeline.d.ts +11 -4
  17. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  18. package/dist/lib/compress/protected-content.d.ts +1 -1
  19. package/dist/lib/compress/protected-content.d.ts.map +1 -1
  20. package/dist/lib/compress/range.d.ts.map +1 -1
  21. package/dist/lib/compress/state.d.ts.map +1 -1
  22. package/dist/lib/compress/status.d.ts.map +1 -1
  23. package/dist/lib/config-validation.d.ts.map +1 -1
  24. package/dist/lib/config.d.ts +6 -0
  25. package/dist/lib/config.d.ts.map +1 -1
  26. package/dist/lib/hooks.d.ts.map +1 -1
  27. package/dist/lib/messages/index.d.ts +0 -1
  28. package/dist/lib/messages/index.d.ts.map +1 -1
  29. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  30. package/dist/lib/messages/inject/utils.d.ts +18 -0
  31. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  32. package/dist/lib/messages/sync.d.ts.map +1 -1
  33. package/dist/lib/prompts/system.d.ts +1 -1
  34. package/dist/lib/prompts/system.d.ts.map +1 -1
  35. package/dist/lib/state/persistence.d.ts +4 -0
  36. package/dist/lib/state/persistence.d.ts.map +1 -1
  37. package/dist/lib/state/state.d.ts.map +1 -1
  38. package/dist/lib/state/types.d.ts +12 -1
  39. package/dist/lib/state/types.d.ts.map +1 -1
  40. package/dist/lib/state/utils.d.ts +12 -1
  41. package/dist/lib/state/utils.d.ts.map +1 -1
  42. package/package.json +2 -2
  43. package/dist/lib/messages/inject/subagent-results.d.ts +0 -4
  44. package/dist/lib/messages/inject/subagent-results.d.ts.map +0 -1
  45. package/dist/lib/subagents/subagent-results.d.ts +0 -5
  46. package/dist/lib/subagents/subagent-results.d.ts.map +0 -1
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) {
@@ -2851,64 +2903,6 @@ function isToolNameProtected(toolName, patterns) {
2851
2903
  return globPatterns.some((pattern) => matchesGlob(toolName, pattern));
2852
2904
  }
2853
2905
 
2854
- // lib/subagents/subagent-results.ts
2855
- var SUB_AGENT_RESULT_BLOCK_REGEX = /(<task_result>\s*)([\s\S]*?)(\s*<\/task_result>)/i;
2856
- function getSubAgentId(part) {
2857
- const sessionId = part?.state?.metadata?.sessionId;
2858
- if (typeof sessionId !== "string") {
2859
- return null;
2860
- }
2861
- const value = sessionId.trim();
2862
- return value.length > 0 ? value : null;
2863
- }
2864
- function buildSubagentResultText(messages) {
2865
- const assistantMessages = messages.filter((message) => message.info.role === "assistant");
2866
- if (assistantMessages.length === 0) {
2867
- return "";
2868
- }
2869
- const lastAssistant = assistantMessages[assistantMessages.length - 1];
2870
- const lastText = getLastTextPart(lastAssistant);
2871
- if (assistantMessages.length < 2) {
2872
- return lastText;
2873
- }
2874
- const secondToLastAssistant = assistantMessages[assistantMessages.length - 2];
2875
- if (!assistantMessageHasCompressTool(secondToLastAssistant)) {
2876
- return lastText;
2877
- }
2878
- const secondToLastText = getLastTextPart(secondToLastAssistant);
2879
- return [secondToLastText, lastText].filter((text) => text.length > 0).join("\n\n");
2880
- }
2881
- function mergeSubagentResult(output, subAgentResultText) {
2882
- if (!subAgentResultText || typeof output !== "string") {
2883
- return output;
2884
- }
2885
- return output.replace(
2886
- SUB_AGENT_RESULT_BLOCK_REGEX,
2887
- (_match, openTag, _body, closeTag) => `${openTag}${subAgentResultText}${closeTag}`
2888
- );
2889
- }
2890
- function getLastTextPart(message) {
2891
- const parts = Array.isArray(message.parts) ? message.parts : [];
2892
- for (let index = parts.length - 1; index >= 0; index--) {
2893
- const part = parts[index];
2894
- if (part.type !== "text" || typeof part.text !== "string") {
2895
- continue;
2896
- }
2897
- const text = part.text.trim();
2898
- if (!text) {
2899
- continue;
2900
- }
2901
- return text;
2902
- }
2903
- return "";
2904
- }
2905
- function assistantMessageHasCompressTool(message) {
2906
- const parts = Array.isArray(message.parts) ? message.parts : [];
2907
- return parts.some(
2908
- (part) => part.type === "tool" && part.tool === "compress" && part.state?.status === "completed"
2909
- );
2910
- }
2911
-
2912
2906
  // lib/compress/protected-content.ts
2913
2907
  function appendProtectedUserMessages(summary, selection, searchContext, state, enabled) {
2914
2908
  if (!enabled) return summary;
@@ -2975,7 +2969,7 @@ function extractProtectedPromptInfo(text) {
2975
2969
  }
2976
2970
  return protectedTexts;
2977
2971
  }
2978
- async function appendProtectedTools(client, state, allowSubAgents, summary, selection, searchContext, protectedTools, protectedFilePatterns = []) {
2972
+ async function appendProtectedTools(client, state, summary, selection, searchContext, protectedTools, protectedFilePatterns = []) {
2979
2973
  const protectedOutputs = [];
2980
2974
  for (const messageId of selection.messageIds) {
2981
2975
  const existingCompressionEntry = state.prune.messages.byMessageId.get(messageId);
@@ -3000,38 +2994,6 @@ async function appendProtectedTools(client, state, allowSubAgents, summary, sele
3000
2994
  if (part.state?.status === "completed" && part.state?.output) {
3001
2995
  output = typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output);
3002
2996
  }
3003
- if (allowSubAgents && part.tool === "task" && part.state?.status === "completed" && typeof part.state?.output === "string") {
3004
- const cachedSubAgentResult = state.subAgentResultCache.get(part.callID);
3005
- if (cachedSubAgentResult !== void 0) {
3006
- if (cachedSubAgentResult) {
3007
- output = mergeSubagentResult(
3008
- part.state.output,
3009
- cachedSubAgentResult
3010
- );
3011
- }
3012
- } else {
3013
- const subAgentSessionId = getSubAgentId(part);
3014
- if (subAgentSessionId) {
3015
- let subAgentResultText = "";
3016
- try {
3017
- const subAgentMessages = await fetchSessionMessages(
3018
- client,
3019
- subAgentSessionId
3020
- );
3021
- subAgentResultText = buildSubagentResultText(subAgentMessages);
3022
- } catch {
3023
- subAgentResultText = "";
3024
- }
3025
- if (subAgentResultText) {
3026
- state.subAgentResultCache.set(part.callID, subAgentResultText);
3027
- output = mergeSubagentResult(
3028
- part.state.output,
3029
- subAgentResultText
3030
- );
3031
- }
3032
- }
3033
- }
3034
- }
3035
2997
  if (output) {
3036
2998
  protectedOutputs.push(`
3037
2999
  ### ${title}
@@ -3152,7 +3114,20 @@ ${footer}`;
3152
3114
  function applyCompressionState(state, input, selection, anchorMessageId, blockId, summary, consumedBlockIds, gcConfig) {
3153
3115
  const messagesState = state.prune.messages;
3154
3116
  const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))];
3155
- 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;
3156
3131
  const effectiveMessageIds = new Set(selection.messageIds);
3157
3132
  const effectiveToolIds = new Set(selection.toolIds);
3158
3133
  for (const consumedBlockId of consumed) {
@@ -3160,6 +3135,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3160
3135
  if (!consumedBlock) {
3161
3136
  continue;
3162
3137
  }
3138
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3139
+ continue;
3140
+ }
3163
3141
  for (const messageId of consumedBlock.effectiveMessageIds) {
3164
3142
  effectiveMessageIds.add(messageId);
3165
3143
  }
@@ -3184,7 +3162,6 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3184
3162
  initiallyActiveToolIds.add(toolId);
3185
3163
  }
3186
3164
  }
3187
- const createdAt = Date.now();
3188
3165
  const block = {
3189
3166
  blockId,
3190
3167
  runId: input.runId,
@@ -3194,6 +3171,7 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3194
3171
  summaryTokens: input.summaryTokens,
3195
3172
  durationMs: 0,
3196
3173
  mode: input.mode,
3174
+ tier: outputTier,
3197
3175
  topic: input.topic,
3198
3176
  batchTopic: input.batchTopic,
3199
3177
  startId: input.startId,
@@ -3201,8 +3179,11 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3201
3179
  anchorMessageId,
3202
3180
  compressMessageId: input.compressMessageId,
3203
3181
  compressCallId: input.compressCallId,
3204
- includedBlockIds: included,
3205
- 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
+ }),
3206
3187
  parentBlockIds: [],
3207
3188
  directMessageIds: [],
3208
3189
  directToolIds: [],
@@ -3231,6 +3212,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3231
3212
  if (!consumedBlock || !consumedBlock.active) {
3232
3213
  continue;
3233
3214
  }
3215
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3216
+ continue;
3217
+ }
3234
3218
  consumedBlock.active = false;
3235
3219
  consumedBlock.deactivatedAt = deactivatedAt;
3236
3220
  consumedBlock.deactivatedByBlockId = blockId;
@@ -3256,6 +3240,9 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3256
3240
  if (!consumedBlock) {
3257
3241
  continue;
3258
3242
  }
3243
+ if ((consumedBlock.tier ?? 1) !== targetTierForConsumption) {
3244
+ continue;
3245
+ }
3259
3246
  for (const messageId of consumedBlock.effectiveMessageIds) {
3260
3247
  const entry = messagesState.byMessageId.get(messageId);
3261
3248
  if (!entry) {
@@ -3321,6 +3308,14 @@ function applyCompressionState(state, input, selection, anchorMessageId, blockId
3321
3308
  block.directMessageIds = [...newlyCompressedMessageIds];
3322
3309
  block.directToolIds = [...newlyCompressedToolIds];
3323
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;
3324
3319
  state.stats.pruneTokenCounter += compressedTokens;
3325
3320
  state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
3326
3321
  state.stats.pruneTokenCounter = 0;
@@ -3683,6 +3678,7 @@ function loadPruneMessagesState(persisted) {
3683
3678
  runId: typeof block.runId === "number" && Number.isInteger(block.runId) && block.runId > 0 ? block.runId : blockId,
3684
3679
  active: block.active === true,
3685
3680
  deactivatedByUser: block.deactivatedByUser === true,
3681
+ deactivatedByUserDeep: block.deactivatedByUserDeep === true ? true : void 0,
3686
3682
  compressedTokens: typeof block.compressedTokens === "number" && Number.isFinite(block.compressedTokens) ? Math.max(0, block.compressedTokens) : 0,
3687
3683
  summaryTokens: typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens) ? Math.max(0, block.summaryTokens) : typeof block.summary === "string" ? countTokens2(block.summary) : 0,
3688
3684
  durationMs: typeof block.durationMs === "number" && Number.isFinite(block.durationMs) ? Math.max(0, block.durationMs) : 0,
@@ -3706,7 +3702,9 @@ function loadPruneMessagesState(persisted) {
3706
3702
  deactivatedByBlockId: typeof block.deactivatedByBlockId === "number" && Number.isInteger(block.deactivatedByBlockId) ? block.deactivatedByBlockId : void 0,
3707
3703
  summary: typeof block.summary === "string" ? block.summary : "",
3708
3704
  survivedCount: typeof block.survivedCount === "number" && Number.isFinite(block.survivedCount) ? Math.max(0, Math.floor(block.survivedCount)) : 0,
3709
- 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
3710
3708
  });
3711
3709
  }
3712
3710
  }
@@ -3765,17 +3763,38 @@ function collectTurnNudgeAnchors(messages) {
3765
3763
  }
3766
3764
  return anchors;
3767
3765
  }
3768
- function getActiveSummaryTokenUsage(state) {
3766
+ function getActiveSummaryTokenUsage(state, visibleMessageIds) {
3769
3767
  let total = 0;
3770
3768
  for (const blockId of state.prune.messages.activeBlockIds) {
3771
3769
  const block = state.prune.messages.blocksById.get(blockId);
3772
3770
  if (!block || !block.active) {
3773
3771
  continue;
3774
3772
  }
3773
+ if (visibleMessageIds && block.compressMessageId) {
3774
+ if (!visibleMessageIds.has(block.compressMessageId)) {
3775
+ continue;
3776
+ }
3777
+ }
3775
3778
  total += block.summaryTokens;
3776
3779
  }
3777
3780
  return total;
3778
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
+ }
3779
3798
  function resetOnCompaction(state) {
3780
3799
  state.toolParameters.clear();
3781
3800
  state.prune.tools = /* @__PURE__ */ new Map();
@@ -3787,6 +3806,8 @@ function resetOnCompaction(state) {
3787
3806
  lastPerMessageNudgeTokens: void 0,
3788
3807
  lastNudgeShownTokens: void 0,
3789
3808
  lastToolOutputNudgeTokens: void 0,
3809
+ lastTier2NudgeTokens: void 0,
3810
+ lastTier3NudgeTokens: void 0,
3790
3811
  shouldInjectThisTurn: void 0,
3791
3812
  compressBaselineSet: false
3792
3813
  };
@@ -3863,6 +3884,8 @@ async function saveSessionState(sessionState, logger, sessionName) {
3863
3884
  lastPerMessageNudgeTokens: sessionState.nudges.lastPerMessageNudgeTokens,
3864
3885
  lastNudgeShownTokens: sessionState.nudges.lastNudgeShownTokens,
3865
3886
  lastToolOutputNudgeTokens: sessionState.nudges.lastToolOutputNudgeTokens,
3887
+ lastTier2NudgeTokens: sessionState.nudges.lastTier2NudgeTokens,
3888
+ lastTier3NudgeTokens: sessionState.nudges.lastTier3NudgeTokens,
3866
3889
  compressBaselineSet: sessionState.nudges.compressBaselineSet
3867
3890
  },
3868
3891
  stats: sessionState.stats,
@@ -4475,6 +4498,8 @@ function createSessionState() {
4475
4498
  lastPerMessageNudgeTokens: void 0,
4476
4499
  lastNudgeShownTokens: void 0,
4477
4500
  lastToolOutputNudgeTokens: void 0,
4501
+ lastTier2NudgeTokens: void 0,
4502
+ lastTier3NudgeTokens: void 0,
4478
4503
  shouldInjectThisTurn: void 0,
4479
4504
  compressBaselineSet: false
4480
4505
  },
@@ -4487,7 +4512,6 @@ function createSessionState() {
4487
4512
  pendingByCallId: /* @__PURE__ */ new Map()
4488
4513
  },
4489
4514
  toolParameters: /* @__PURE__ */ new Map(),
4490
- subAgentResultCache: /* @__PURE__ */ new Map(),
4491
4515
  toolIdList: [],
4492
4516
  messageIds: {
4493
4517
  byRawId: /* @__PURE__ */ new Map(),
@@ -4519,6 +4543,8 @@ function resetSessionState(state) {
4519
4543
  lastPerMessageNudgeTokens: void 0,
4520
4544
  lastNudgeShownTokens: void 0,
4521
4545
  lastToolOutputNudgeTokens: void 0,
4546
+ lastTier2NudgeTokens: void 0,
4547
+ lastTier3NudgeTokens: void 0,
4522
4548
  shouldInjectThisTurn: void 0,
4523
4549
  compressBaselineSet: false
4524
4550
  };
@@ -4527,7 +4553,6 @@ function resetSessionState(state) {
4527
4553
  totalPruneTokens: 0
4528
4554
  };
4529
4555
  state.toolParameters.clear();
4530
- state.subAgentResultCache.clear();
4531
4556
  state.toolIdList = [];
4532
4557
  state.messageIds = {
4533
4558
  byRawId: /* @__PURE__ */ new Map(),
@@ -4576,6 +4601,8 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4576
4601
  state.nudges.lastPerMessageNudgeTokens = persisted.nudges.lastPerMessageNudgeTokens;
4577
4602
  state.nudges.lastNudgeShownTokens = persisted.nudges.lastNudgeShownTokens;
4578
4603
  state.nudges.lastToolOutputNudgeTokens = persisted.nudges.lastToolOutputNudgeTokens;
4604
+ state.nudges.lastTier2NudgeTokens = persisted.nudges.lastTier2NudgeTokens ?? persisted.nudges.lastTierNudgeTokens;
4605
+ state.nudges.lastTier3NudgeTokens = persisted.nudges.lastTier3NudgeTokens;
4579
4606
  state.nudges.compressBaselineSet = persisted.nudges.compressBaselineSet ?? false;
4580
4607
  state.stats = {
4581
4608
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
@@ -5139,7 +5166,7 @@ async function sendCompressNotification(client, logger, config, state, sessionId
5139
5166
  const logTopics = entries.map((e) => state.prune.messages.blocksById.get(e.blockId)?.topic ?? "?");
5140
5167
  const logCompressedTokens = entries.reduce((sum, e) => {
5141
5168
  const block = state.prune.messages.blocksById.get(e.blockId);
5142
- return sum + (block?.compressedTokens ?? 0);
5169
+ return sum + (block?.effectiveCompressedTokens ?? block?.compressedTokens ?? 0);
5143
5170
  }, 0);
5144
5171
  const logSummaryTokens = entries.reduce((sum, e) => sum + e.summaryTokens, 0);
5145
5172
  logger.info("Compression completed", {
@@ -6083,7 +6110,7 @@ function evaluatePreCommitQuality(rawMessages, messageIds, messageTokenById, sum
6083
6110
  }
6084
6111
  }
6085
6112
 
6086
- // node_modules/context-compress-algorithms/dist/chunk-ZRHPFN6B.js
6113
+ // node_modules/context-compress-algorithms/dist/chunk-EBKMI537.js
6087
6114
  var COMPRESS_PHILOSOPHY = `Compression Philosophy:
6088
6115
  - All compression serves the primary task, but be frugal.
6089
6116
  - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
@@ -6128,6 +6155,69 @@ PRIORITY \u2014 when the summary must be compact, preserve in this order:
6128
6155
  5. Lessons learned: what failed and why.
6129
6156
 
6130
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.`;
6131
6221
 
6132
6222
  // lib/compress/quality-gate/rejection.ts
6133
6223
  function formatMetric(result, name) {
@@ -6272,17 +6362,6 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
6272
6362
  contextTokensBefore
6273
6363
  );
6274
6364
  }
6275
- function getLastVisibleMessageId(rawMessages, state) {
6276
- for (let i = rawMessages.length - 1; i >= 0; i--) {
6277
- const msg = rawMessages[i];
6278
- const id = msg?.info?.id;
6279
- if (!id || typeof id !== "string") continue;
6280
- if (isSyntheticMessage(msg)) continue;
6281
- if (state.prune.messages.byMessageId.has(id)) continue;
6282
- return id;
6283
- }
6284
- return null;
6285
- }
6286
6365
  function checkPhantomBlock(state, plans) {
6287
6366
  for (let i = 0; i < plans.length; i++) {
6288
6367
  const plan = plans[i];
@@ -6300,6 +6379,16 @@ function checkPhantomBlock(state, plans) {
6300
6379
  return !entry || entry.activeBlockIds.length === 0;
6301
6380
  });
6302
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
+ }
6303
6392
  return new Error(
6304
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.`
6305
6394
  );
@@ -6307,16 +6396,71 @@ function checkPhantomBlock(state, plans) {
6307
6396
  }
6308
6397
  return null;
6309
6398
  }
6310
- 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) {
6311
6444
  if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
6312
- const lastVisibleId = getLastVisibleMessageId(rawMessages, ctx.state);
6313
- if (!lastVisibleId) return null;
6314
- const coversLast = allPlanMessageIds.some((ids) => ids.includes(lastVisibleId));
6315
- 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;
6316
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;
6317
6460
  return new Error(
6318
- `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.
6319
6462
 
6463
+ Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens + most recent user message.
6320
6464
  If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
6321
6465
  Otherwise, compress older ranges that do not include the tail of the conversation.`
6322
6466
  );
@@ -6515,7 +6659,7 @@ function createCompressMessageTool(factoryCtx) {
6515
6659
  }
6516
6660
  }
6517
6661
  const dangerous = args.dangerous === true;
6518
- const lastSegmentError = checkLastSegmentDangerous(
6662
+ const lastSegmentError = checkProtectedRange(
6519
6663
  ctx,
6520
6664
  plans.map((p) => p.selection.messageIds),
6521
6665
  rawMessages,
@@ -6535,7 +6679,6 @@ function createCompressMessageTool(factoryCtx) {
6535
6679
  const summaryWithTools = await appendProtectedTools(
6536
6680
  ctx.client,
6537
6681
  ctx.state,
6538
- ctx.config.experimental.allowSubAgents,
6539
6682
  summaryWithPromptInfo,
6540
6683
  plan.selection,
6541
6684
  searchContext,
@@ -6736,7 +6879,7 @@ function createCompressRangeTool(factoryCtx) {
6736
6879
  }
6737
6880
  }
6738
6881
  const dangerous = args.dangerous === true;
6739
- const lastSegmentError = checkLastSegmentDangerous(
6882
+ const lastSegmentError = checkProtectedRange(
6740
6883
  ctx,
6741
6884
  filteredPlans.map((p) => p.selection.messageIds),
6742
6885
  rawMessages,
@@ -6780,7 +6923,6 @@ function createCompressRangeTool(factoryCtx) {
6780
6923
  const summaryWithTools = await appendProtectedTools(
6781
6924
  ctx.client,
6782
6925
  ctx.state,
6783
- ctx.config.experimental.allowSubAgents,
6784
6926
  summaryWithPromptInfo,
6785
6927
  plan.selection,
6786
6928
  searchContext,
@@ -7009,10 +7151,9 @@ var syncCompressionBlocks = (state, logger, messages) => {
7009
7151
  messagesState.activeBlockIds.clear();
7010
7152
  messagesState.activeByAnchorMessageId.clear();
7011
7153
  const now = Date.now();
7012
- const missingOriginBlockIds = [];
7013
7154
  const orderedBlocks = Array.from(messagesState.blocksById.values()).sort(sortBlocksByCreation);
7014
7155
  for (const block of orderedBlocks) {
7015
- if (block.deactivatedByUser) {
7156
+ if (block.deactivatedByUser || block.deactivatedByUserDeep) {
7016
7157
  block.active = false;
7017
7158
  if (block.deactivatedAt === void 0) {
7018
7159
  block.deactivatedAt = now;
@@ -7020,12 +7161,6 @@ var syncCompressionBlocks = (state, logger, messages) => {
7020
7161
  block.deactivatedByBlockId = void 0;
7021
7162
  continue;
7022
7163
  }
7023
- if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
7024
- block.active = false;
7025
- block.deactivatedAt = now;
7026
- block.deactivatedByBlockId = void 0;
7027
- continue;
7028
- }
7029
7164
  for (const consumedBlockId of block.consumedBlockIds) {
7030
7165
  if (!messagesState.activeBlockIds.has(consumedBlockId)) {
7031
7166
  continue;
@@ -7070,9 +7205,8 @@ var syncCompressionBlocks = (state, logger, messages) => {
7070
7205
  reactivatedCount++;
7071
7206
  }
7072
7207
  }
7073
- if (missingOriginBlockIds.length > 0 || deactivatedCount > 0 || reactivatedCount > 0) {
7208
+ if (deactivatedCount > 0 || reactivatedCount > 0) {
7074
7209
  logger.info("Synced compress block state", {
7075
- missingOriginCount: missingOriginBlockIds.length,
7076
7210
  deactivatedCount,
7077
7211
  reactivatedCount
7078
7212
  });
@@ -7483,7 +7617,7 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
7483
7617
  return refs;
7484
7618
  }
7485
7619
 
7486
- // node_modules/context-compress-algorithms/dist/chunk-UX2UW5FY.js
7620
+ // node_modules/context-compress-algorithms/dist/chunk-BZYW3CH5.js
7487
7621
  var NUDGE_GROWTH_FLOOR = 6e3;
7488
7622
  var NUDGE_GROWTH_CAP = 5e4;
7489
7623
  var NUDGE_GROWTH_RATIO = 0.05;
@@ -7616,7 +7750,10 @@ function resolveContextTokenLimit(config, state, providerId, modelId, threshold)
7616
7750
  return parseLimitValue(globalLimit);
7617
7751
  }
7618
7752
  function isContextOverLimits(config, state, providerId, modelId, messages) {
7619
- 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;
7620
7757
  const resolvedMaxContextLimit = resolveContextTokenLimit(
7621
7758
  config,
7622
7759
  state,
@@ -8239,6 +8376,57 @@ ${lines2.join("\n")}`;
8239
8376
  return `Compressible ranges (oldest first):
8240
8377
  ${lines.join("\n")}`;
8241
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
+ }
8242
8430
 
8243
8431
  // lib/messages/inject/inject.ts
8244
8432
  var ACP_SUFFIX_SEED = "acp-dynamic-guidance";
@@ -8278,6 +8466,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8278
8466
  state.nudges.iterationNudgeAnchors.clear();
8279
8467
  state.nudges.lastNudgeShownTokens = void 0;
8280
8468
  state.nudges.lastToolOutputNudgeTokens = void 0;
8469
+ state.nudges.lastTier2NudgeTokens = void 0;
8470
+ state.nudges.lastTier3NudgeTokens = void 0;
8281
8471
  if (wasNudgeTriggered && !state.nudges.compressBaselineSet) {
8282
8472
  const baseline = state.nudges.lastPerMessageNudgeTokens;
8283
8473
  const postCompress = currentTokens;
@@ -8416,8 +8606,10 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8416
8606
  config.compress.protectedTools,
8417
8607
  config.protectedFilePatterns
8418
8608
  );
8609
+ const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8610
+ const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
8419
8611
  const recommendedRanges = filterRecommendedRanges(
8420
- contextRanges.compressible,
8612
+ unprotectedCompressible,
8421
8613
  contextRanges.protected,
8422
8614
  { modelContextLimit, growthRatio: 0.05, logger }
8423
8615
  );
@@ -8444,11 +8636,92 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8444
8636
  const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
8445
8637
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
8446
8638
  const nothingToCompress = filterSuppressed || allProtected;
8447
- const shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
8639
+ let shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
8448
8640
  if (nudgeAllowed && nothingToCompress && !emergencyOverride && currentTokens !== void 0) {
8449
8641
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
8450
8642
  state.nudges.lastNudgeShownTokens = void 0;
8451
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
+ }
8452
8725
  state.nudges.shouldInjectThisTurn = shouldInject;
8453
8726
  let tipsText = null;
8454
8727
  if (shouldInject) {
@@ -8608,65 +8881,6 @@ var injectMessageIds = (state, config, messages, compressionPriorities) => {
8608
8881
  }
8609
8882
  };
8610
8883
 
8611
- // lib/messages/inject/subagent-results.ts
8612
- async function fetchSubAgentMessages(client, sessionId) {
8613
- const response = await client.session.messages({
8614
- path: { id: sessionId }
8615
- });
8616
- return filterMessages(response?.data || response);
8617
- }
8618
- var injectExtendedSubAgentResults = async (client, state, logger, messages, allowSubAgents) => {
8619
- if (!allowSubAgents) {
8620
- return;
8621
- }
8622
- for (const message of messages) {
8623
- const parts = Array.isArray(message.parts) ? message.parts : [];
8624
- for (const part of parts) {
8625
- if (part.type !== "tool" || part.tool !== "task" || !part.callID) {
8626
- continue;
8627
- }
8628
- if (state.prune.tools.has(part.callID)) {
8629
- continue;
8630
- }
8631
- if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
8632
- continue;
8633
- }
8634
- const cachedResult = state.subAgentResultCache.get(part.callID);
8635
- if (cachedResult !== void 0) {
8636
- if (cachedResult) {
8637
- part.state.output = stripHallucinationsFromString(
8638
- mergeSubagentResult(part.state.output, cachedResult)
8639
- );
8640
- }
8641
- continue;
8642
- }
8643
- const subAgentSessionId = getSubAgentId(part);
8644
- if (!subAgentSessionId) {
8645
- continue;
8646
- }
8647
- let subAgentMessages = [];
8648
- try {
8649
- subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId);
8650
- } catch (error) {
8651
- logger.warn("Failed to fetch subagent session for output expansion", {
8652
- subAgentSessionId,
8653
- callID: part.callID,
8654
- error: error instanceof Error ? error.message : String(error)
8655
- });
8656
- continue;
8657
- }
8658
- const subAgentResultText = buildSubagentResultText(subAgentMessages);
8659
- if (!subAgentResultText) {
8660
- continue;
8661
- }
8662
- state.subAgentResultCache.set(part.callID, subAgentResultText);
8663
- part.state.output = stripHallucinationsFromString(
8664
- mergeSubagentResult(part.state.output, subAgentResultText)
8665
- );
8666
- }
8667
- }
8668
- };
8669
-
8670
8884
  // lib/messages/reasoning-strip.ts
8671
8885
  function stripStaleMetadata(messages) {
8672
8886
  const lastUserMessage = getLastUserMessage(messages);
@@ -8712,7 +8926,10 @@ function buildTarget(blocks) {
8712
8926
  displayId: first.blockId,
8713
8927
  runId: first.runId,
8714
8928
  topic: grouped ? first.batchTopic || first.topic : first.topic,
8715
- compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
8929
+ compressedTokens: ordered.reduce(
8930
+ (total, block) => total + (block.effectiveCompressedTokens ?? block.compressedTokens),
8931
+ 0
8932
+ ),
8716
8933
  durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
8717
8934
  grouped,
8718
8935
  blocks: ordered
@@ -8880,17 +9097,25 @@ function snapshotActiveMessages(messagesState) {
8880
9097
  }
8881
9098
  return activeMessages;
8882
9099
  }
8883
- function deactivateCompressionTarget(messagesState, target) {
9100
+ function deactivateCompressionTarget(messagesState, target, options) {
8884
9101
  const deactivatedAt = Date.now();
8885
9102
  for (const block of target.blocks) {
8886
9103
  block.active = false;
8887
9104
  block.deactivatedByUser = true;
8888
9105
  block.deactivatedAt = deactivatedAt;
8889
9106
  block.deactivatedByBlockId = void 0;
8890
- for (const consumedId of block.consumedBlockIds) {
8891
- const consumedBlock = messagesState.blocksById.get(consumedId);
8892
- if (consumedBlock) {
8893
- 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
+ }
8894
9119
  }
8895
9120
  }
8896
9121
  }
@@ -9106,14 +9331,18 @@ ARGUMENTS:
9106
9331
  IMPORTANT:
9107
9332
  - Decompressing inflates context. Check context usage before decompressing.
9108
9333
  - Message-mode blocks from the same batch (same runId) are restored together.
9109
- - 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.
9110
9338
  - Do NOT call this tool in parallel with compress \u2014 their state mutations may conflict.`;
9111
9339
  function buildSchema3() {
9112
9340
  return {
9113
9341
  blockId: tool4.schema.string().optional().describe('Block reference to decompress (e.g., "b0", "b2"). Mutually exclusive with startId/endId.'),
9114
9342
  startId: tool4.schema.string().optional().describe('Range start: message ref (e.g., "m00150") or block ref (e.g., "b2"). Used with endId.'),
9115
9343
  endId: tool4.schema.string().optional().describe('Range end: message ref (e.g., "m00200") or block ref (e.g., "b5"). Used with startId.'),
9116
- 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.")
9117
9346
  };
9118
9347
  }
9119
9348
  function extractMessageId(m) {
@@ -9183,7 +9412,7 @@ function createDecompressTool(factoryCtx) {
9183
9412
  const activeMessagesBefore = snapshotActiveMessages(messagesState);
9184
9413
  const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
9185
9414
  for (const target of targets) {
9186
- deactivateCompressionTarget(messagesState, target);
9415
+ deactivateCompressionTarget(messagesState, target, { full: args.full === true });
9187
9416
  }
9188
9417
  syncCompressionBlocks(ctx.state, ctx.logger, rawMessages);
9189
9418
  const { restoredMessageCount, restoredTokens } = computeRestoredMessages(
@@ -9262,13 +9491,54 @@ function formatIdRange(block) {
9262
9491
  const count = block.effectiveMessageIds?.length || 0;
9263
9492
  return count > 0 ? `${count} msg${count !== 1 ? "s" : ""}` : "\u2014";
9264
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
+ }
9265
9531
  function collectVisibleMessages(rawMessages, ctx) {
9266
9532
  const pruneMap = ctx.state.prune.messages.byMessageId;
9267
9533
  const byRawId = ctx.state.messageIds.byRawId;
9268
9534
  const result = [];
9269
9535
  let summaryTokens = 0;
9536
+ const visibleMessageIds = new Set(rawMessages.map((m) => m.info.id));
9270
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);
9271
9538
  for (const block of activeBlocks) {
9539
+ if (block.compressMessageId && !visibleMessageIds.has(block.compressMessageId)) {
9540
+ continue;
9541
+ }
9272
9542
  summaryTokens += block.summaryTokens || 0;
9273
9543
  }
9274
9544
  rawMessages.forEach((msg, idx) => {
@@ -9329,19 +9599,32 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
9329
9599
  lines.push("COMPRESSED BLOCKS");
9330
9600
  lines.push(" No compressed blocks.");
9331
9601
  } else {
9602
+ const blocksById = ctx.state.prune.messages.blocksById;
9332
9603
  const totalSummary = blocks.reduce((s, b) => s + (b.summaryTokens || 0), 0);
9333
- const totalCompressed = blocks.reduce((s, b) => s + (b.compressedTokens || 0), 0);
9334
- lines.push(
9335
- `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
9336
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
+ }
9337
9614
  lines.push("");
9338
- 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
+ });
9339
9620
  for (const b of sorted.slice(0, 30)) {
9340
9621
  const ageStr = formatAge(b.createdAt);
9341
9622
  const range = formatIdRange(b);
9342
9623
  const topic = b.topic || "(no topic)";
9624
+ const tier = tierLabel(b);
9625
+ const effTokens = getEffectiveCompressedTokens(b, blocksById);
9343
9626
  lines.push(
9344
- ` 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}"`
9345
9628
  );
9346
9629
  }
9347
9630
  }
@@ -9445,7 +9728,7 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
9445
9728
  }
9446
9729
  return lines;
9447
9730
  }
9448
- function renderCompressedDrilldown(blocks, sort, limit) {
9731
+ function renderCompressedDrilldown(blocks, sort, limit, blocksById) {
9449
9732
  const lines = [];
9450
9733
  let sorted = [...blocks];
9451
9734
  if (sort === "time") {
@@ -9453,13 +9736,22 @@ function renderCompressedDrilldown(blocks, sort, limit) {
9453
9736
  } else if (sort === "age") {
9454
9737
  sorted.sort((a, b) => (b.survivedCount || 0) - (a.survivedCount || 0));
9455
9738
  } else {
9456
- 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
+ );
9457
9742
  }
9458
9743
  const totalSummary = sorted.reduce((s, b) => s + (b.summaryTokens || 0), 0);
9459
- 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
+ );
9460
9748
  lines.push(
9461
- `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`
9462
9750
  );
9751
+ const breakdown = tierBreakdown(sorted);
9752
+ if (breakdown) {
9753
+ lines.push(`Tier usage: ${breakdown}`);
9754
+ }
9463
9755
  lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
9464
9756
  lines.push("");
9465
9757
  const shown = sorted.slice(0, limit);
@@ -9467,10 +9759,12 @@ function renderCompressedDrilldown(blocks, sort, limit) {
9467
9759
  const survived = b.survivedCount ?? 0;
9468
9760
  const gen = b.generation ?? "young";
9469
9761
  const effCount = b.effectiveMessageIds?.length ?? 0;
9470
- 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(",")}]` : "";
9471
9763
  const topic = b.topic || "(no topic)";
9764
+ const tier = tierLabel(b);
9765
+ const effTokens = getEffectiveCompressedTokens(b, blocksById);
9472
9766
  lines.push(
9473
- ` 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}`
9474
9768
  );
9475
9769
  lines.push(` "${topic}"`);
9476
9770
  }
@@ -9511,7 +9805,14 @@ function createAcpStatusTool(factoryCtx) {
9511
9805
  const allBlocks = activeIds.map((id) => msgState.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
9512
9806
  const lines = [];
9513
9807
  if (scope === "compressed") {
9514
- lines.push(...renderCompressedDrilldown(allBlocks, sort, limit));
9808
+ lines.push(
9809
+ ...renderCompressedDrilldown(
9810
+ allBlocks,
9811
+ sort,
9812
+ limit,
9813
+ msgState.blocksById
9814
+ )
9815
+ );
9515
9816
  return lines.join("\n");
9516
9817
  }
9517
9818
  let visibleMsgs = [];
@@ -9670,6 +9971,53 @@ IMPORTANT: This was an automatic context pruning. You MUST continue your previou
9670
9971
  });
9671
9972
  }
9672
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
+
9673
10021
  // lib/logger.ts
9674
10022
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
9675
10023
  import { join as join3 } from "path";
@@ -9898,7 +10246,7 @@ TOOLS
9898
10246
  You have five context-management tools:
9899
10247
 
9900
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: "..." }] })\`.
9901
- - \`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 })\`.
9902
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" })\`.
9903
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 })\`.
9904
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.
@@ -9932,6 +10280,16 @@ WHEN NOT TO COMPRESS
9932
10280
 
9933
10281
  ${HOW_TO_COMPRESS_RULES}
9934
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
+
9935
10293
  PERIODIC CONTEXT STATUS
9936
10294
 
9937
10295
  Periodically, as context grows, the system appends a short status line in a synthetic suffix message. It looks like:
@@ -11109,8 +11467,21 @@ async function handleRecompressCommand(ctx) {
11109
11467
  const activeBlockIdsBefore = new Set(messagesState.activeBlockIds);
11110
11468
  for (const block of target.blocks) {
11111
11469
  block.deactivatedByUser = false;
11470
+ block.deactivatedByUserDeep = false;
11112
11471
  block.deactivatedAt = void 0;
11113
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
+ }
11114
11485
  }
11115
11486
  syncCompressionBlocks(state, logger, messages);
11116
11487
  let recompressedMessageCount = 0;
@@ -11197,8 +11568,9 @@ function formatCompressionTime(ms) {
11197
11568
  async function handleStatsCommand(ctx) {
11198
11569
  const { client, state, logger, sessionId, messages } = ctx;
11199
11570
  const sessionTokens = state.stats.totalPruneTokens;
11571
+ const visibleMessageIds = new Set(messages.map((m) => m.info.id));
11200
11572
  const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
11201
- (total, block) => block.active ? total + block.summaryTokens : total,
11573
+ (total, block) => block.active && visibleMessageIds.has(block.compressMessageId ?? "") ? total + block.summaryTokens : total,
11202
11574
  0
11203
11575
  );
11204
11576
  const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
@@ -11776,14 +12148,8 @@ function createChatMessageTransformHandler(client, registry3, logger, config, pr
11776
12148
  }
11777
12149
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
11778
12150
  prune(state, logger, config, output.messages);
12151
+ hideConsumedCompressCalls(state, output.messages);
11779
12152
  assignMessageRefs(state, output.messages);
11780
- await injectExtendedSubAgentResults(
11781
- client,
11782
- state,
11783
- logger,
11784
- output.messages,
11785
- config.experimental.allowSubAgents
11786
- );
11787
12153
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
11788
12154
  prompts.reload();
11789
12155
  injectCompressNudges(