opencode-acp 1.12.2 → 1.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -904,6 +904,9 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
904
904
  "compress.protectUserMessages",
905
905
  "compress.maxSummaryLengthHard",
906
906
  "compress.minCompressRange",
907
+ "compress.minNudgeGrowthRatio",
908
+ "compress.minNudgeGrowthFloor",
909
+ "compress.emergencyThresholdPercent",
907
910
  "compress.maxVisibleSegments",
908
911
  "compress.keepEmbedMaxChars",
909
912
  "gc",
@@ -1187,6 +1190,61 @@ function validateConfigTypes(config) {
1187
1190
  actual: `${compress.minCompressRange}`
1188
1191
  });
1189
1192
  }
1193
+ if (compress.minNudgeGrowthRatio !== void 0 && typeof compress.minNudgeGrowthRatio !== "number") {
1194
+ errors.push({
1195
+ key: "compress.minNudgeGrowthRatio",
1196
+ expected: "number",
1197
+ actual: typeof compress.minNudgeGrowthRatio
1198
+ });
1199
+ }
1200
+ if (typeof compress.minNudgeGrowthRatio === "number" && (compress.minNudgeGrowthRatio < 0 || compress.minNudgeGrowthRatio > 1)) {
1201
+ errors.push({
1202
+ key: "compress.minNudgeGrowthRatio",
1203
+ expected: "number in range [0, 1]",
1204
+ actual: `${compress.minNudgeGrowthRatio}`
1205
+ });
1206
+ }
1207
+ if (compress.minNudgeGrowthFloor !== void 0 && typeof compress.minNudgeGrowthFloor !== "number") {
1208
+ errors.push({
1209
+ key: "compress.minNudgeGrowthFloor",
1210
+ expected: "number",
1211
+ actual: typeof compress.minNudgeGrowthFloor
1212
+ });
1213
+ }
1214
+ if (typeof compress.minNudgeGrowthFloor === "number" && compress.minNudgeGrowthFloor < 0) {
1215
+ errors.push({
1216
+ key: "compress.minNudgeGrowthFloor",
1217
+ expected: "non-negative number (>= 0)",
1218
+ actual: `${compress.minNudgeGrowthFloor}`
1219
+ });
1220
+ }
1221
+ const emergencyThreshold = compress.emergencyThresholdPercent;
1222
+ if (emergencyThreshold !== void 0) {
1223
+ if (typeof emergencyThreshold === "number") {
1224
+ if (emergencyThreshold < 0) {
1225
+ errors.push({
1226
+ key: "compress.emergencyThresholdPercent",
1227
+ expected: 'non-negative number or "${number}%" (0\u2013100)',
1228
+ actual: `${emergencyThreshold}`
1229
+ });
1230
+ }
1231
+ } else if (typeof emergencyThreshold === "string" && emergencyThreshold.endsWith("%")) {
1232
+ const parsed = parseFloat(emergencyThreshold.slice(0, -1));
1233
+ if (isNaN(parsed) || parsed < 0 || parsed > 100) {
1234
+ errors.push({
1235
+ key: "compress.emergencyThresholdPercent",
1236
+ expected: '"${number}%" with percentage in [0, 100]',
1237
+ actual: JSON.stringify(emergencyThreshold)
1238
+ });
1239
+ }
1240
+ } else {
1241
+ errors.push({
1242
+ key: "compress.emergencyThresholdPercent",
1243
+ expected: 'number | "${number}%"',
1244
+ actual: JSON.stringify(emergencyThreshold)
1245
+ });
1246
+ }
1247
+ }
1190
1248
  if (compress.maxVisibleSegments !== void 0 && typeof compress.maxVisibleSegments !== "number") {
1191
1249
  errors.push({
1192
1250
  key: "compress.maxVisibleSegments",
@@ -1529,7 +1587,10 @@ var defaultConfig = {
1529
1587
  protectTags: false,
1530
1588
  protectUserMessages: false,
1531
1589
  maxSummaryLengthHard: 1e4,
1532
- minCompressRange: 2e3,
1590
+ minCompressRange: 5e3,
1591
+ minNudgeGrowthRatio: 0.45,
1592
+ minNudgeGrowthFloor: 5e3,
1593
+ emergencyThresholdPercent: "98%",
1533
1594
  maxVisibleSegments: 50,
1534
1595
  keepEmbedMaxChars: 2e3
1535
1596
  },
@@ -1688,6 +1749,9 @@ function mergeCompress(base, override) {
1688
1749
  protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
1689
1750
  maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
1690
1751
  minCompressRange: override.minCompressRange ?? base.minCompressRange,
1752
+ minNudgeGrowthRatio: override.minNudgeGrowthRatio ?? base.minNudgeGrowthRatio,
1753
+ minNudgeGrowthFloor: override.minNudgeGrowthFloor ?? base.minNudgeGrowthFloor,
1754
+ emergencyThresholdPercent: override.emergencyThresholdPercent ?? base.emergencyThresholdPercent,
1691
1755
  maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments,
1692
1756
  keepEmbedMaxChars: override.keepEmbedMaxChars ?? base.keepEmbedMaxChars
1693
1757
  };
@@ -1949,27 +2013,22 @@ function getCurrentTokenUsage(state, messages) {
1949
2013
  continue;
1950
2014
  }
1951
2015
  const assistantInfo = msg.info;
1952
- if ((assistantInfo.tokens?.output || 0) <= 0) {
1953
- continue;
1954
- }
1955
- if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
1956
- return 0;
1957
- }
1958
2016
  const input = assistantInfo.tokens?.input || 0;
1959
2017
  const output = assistantInfo.tokens?.output || 0;
1960
2018
  const reasoning = assistantInfo.tokens?.reasoning || 0;
1961
2019
  const cacheRead = assistantInfo.tokens?.cache?.read || 0;
1962
2020
  const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
2021
+ if (input <= 0 && output <= 0) {
2022
+ continue;
2023
+ }
2024
+ if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
2025
+ return 0;
2026
+ }
1963
2027
  return input + cacheRead + cacheWrite + output + reasoning;
1964
2028
  }
1965
2029
  let estimated = 0;
1966
2030
  for (const m of messages) {
1967
- const parts = Array.isArray(m.parts) ? m.parts : [];
1968
- for (const part of parts) {
1969
- if (part.type === "text" && typeof part.text === "string") {
1970
- estimated += countTokens2(part.text);
1971
- }
1972
- }
2031
+ estimated += countAllMessageTokens(m);
1973
2032
  }
1974
2033
  return estimated;
1975
2034
  }
@@ -5678,9 +5737,9 @@ import { tool as tool4 } from "@opencode-ai/plugin";
5678
5737
  import { createHash } from "crypto";
5679
5738
  var SUMMARY_ID_HASH_LENGTH = 16;
5680
5739
  var ACP_RECAP_TOOL_NAME = "acp_context_recap";
5681
- var DCP_BLOCK_ID_TAG_REGEX = /(<dcp-message-id(?=[\s>])[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
5682
- var DCP_MESSAGE_REF_TAG_REGEX = /<dcp-message-id>m\d+<\/(?:dcp|acp)-message-id>/g;
5683
- var DCP_PAIRED_TAG_REGEX = /<dcp[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
5740
+ var DCP_BLOCK_ID_TAG_REGEX = /(<(?:dcp|acp)-message-id[^>]*>)b\d+(<\/(?:dcp|acp)-message-id>)/g;
5741
+ var DCP_MESSAGE_REF_TAG_REGEX = /<(?:dcp|acp)-message-id[^>]*>m\d+<\/(?:dcp|acp)-message-id>/g;
5742
+ var DCP_PAIRED_TAG_REGEX = /<(?:dcp|acp)[^>]*>[\s\S]*?<\/(?:dcp|acp)[^>]*>/gi;
5684
5743
  var DCP_UNPAIRED_TAG_REGEX = /<\/?(?:dcp|acp)[^>]*>/gi;
5685
5744
  var generateStableId = (prefix, seed) => {
5686
5745
  const hash = createHash("sha256").update(seed).digest("hex").slice(0, SUMMARY_ID_HASH_LENGTH);
@@ -6600,20 +6659,41 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
6600
6659
  const nudgeParts = [];
6601
6660
  if (config.compress.mode === "message") {
6602
6661
  if (state.nudges.contextLimitAnchors.size > 0) {
6603
- for (const { index } of collectAnchoredMessages(state.nudges.contextLimitAnchors, messages)) {
6604
- const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
6662
+ for (const { index } of collectAnchoredMessages(
6663
+ state.nudges.contextLimitAnchors,
6664
+ messages
6665
+ )) {
6666
+ const guidance = buildMessagePriorityGuidance(
6667
+ messages,
6668
+ compressionPriorities,
6669
+ index,
6670
+ MESSAGE_MODE_NUDGE_PRIORITY
6671
+ );
6605
6672
  nudgeParts.push(appendGuidanceToDcpTag(prompts.contextLimitNudge, guidance));
6606
6673
  }
6607
6674
  }
6608
6675
  if (turnNudgeAnchors.size > 0) {
6609
6676
  for (const { index } of collectAnchoredMessages(turnNudgeAnchors, messages)) {
6610
- const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
6677
+ const guidance = buildMessagePriorityGuidance(
6678
+ messages,
6679
+ compressionPriorities,
6680
+ index,
6681
+ MESSAGE_MODE_NUDGE_PRIORITY
6682
+ );
6611
6683
  nudgeParts.push(appendGuidanceToDcpTag(prompts.turnNudge, guidance));
6612
6684
  }
6613
6685
  }
6614
6686
  if (state.nudges.iterationNudgeAnchors.size > 0) {
6615
- for (const { index } of collectAnchoredMessages(state.nudges.iterationNudgeAnchors, messages)) {
6616
- const guidance = buildMessagePriorityGuidance(messages, compressionPriorities, index, MESSAGE_MODE_NUDGE_PRIORITY);
6687
+ for (const { index } of collectAnchoredMessages(
6688
+ state.nudges.iterationNudgeAnchors,
6689
+ messages
6690
+ )) {
6691
+ const guidance = buildMessagePriorityGuidance(
6692
+ messages,
6693
+ compressionPriorities,
6694
+ index,
6695
+ MESSAGE_MODE_NUDGE_PRIORITY
6696
+ );
6617
6697
  nudgeParts.push(appendGuidanceToDcpTag(prompts.iterationNudge, guidance));
6618
6698
  }
6619
6699
  }
@@ -6661,12 +6741,7 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
6661
6741
  prompts.contextLimitNudge,
6662
6742
  ""
6663
6743
  );
6664
- applyRangeModeAnchoredNudge(
6665
- turnNudgeAnchors,
6666
- messages,
6667
- prompts.turnNudge,
6668
- ""
6669
- );
6744
+ applyRangeModeAnchoredNudge(turnNudgeAnchors, messages, prompts.turnNudge, "");
6670
6745
  applyRangeModeAnchoredNudge(
6671
6746
  state.nudges.iterationNudgeAnchors,
6672
6747
  messages,
@@ -6687,11 +6762,12 @@ function estimateCodeTokens(text) {
6687
6762
  }
6688
6763
  return Math.round(codeChars / 4);
6689
6764
  }
6690
- function estimateContextComposition(messages, state) {
6765
+ function estimateContextComposition(messages, state, protectedTools = [], protectedFilePatterns = []) {
6691
6766
  let toolTokens = 0;
6692
6767
  let codeTokens = 0;
6693
6768
  let summaryTokens = 0;
6694
6769
  let messageTokens = 0;
6770
+ let protectedTokens = 0;
6695
6771
  const perMessage = [];
6696
6772
  const perTool = [];
6697
6773
  const perCode = [];
@@ -6701,6 +6777,7 @@ function estimateContextComposition(messages, state) {
6701
6777
  const text = (msg.parts || []).filter((p) => p.type === "text").map((p) => p.text || "").join("");
6702
6778
  const msgId = msg.info?.id || "";
6703
6779
  const isSummary = msgId.startsWith("msg_dcp_summary") || text.includes("[Compressed conversation section]");
6780
+ const isProtected = (protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns);
6704
6781
  let msgTotal = 0;
6705
6782
  let msgTool = 0;
6706
6783
  let msgCode = 0;
@@ -6733,6 +6810,9 @@ function estimateContextComposition(messages, state) {
6733
6810
  if (!msgToolName) msgToolName = toolName;
6734
6811
  }
6735
6812
  }
6813
+ if (isProtected && !isSummary) {
6814
+ protectedTokens += msgTotal;
6815
+ }
6736
6816
  if (!isSummary) {
6737
6817
  const ref = state?.messageIds?.byRawId?.get(msgId) || "?";
6738
6818
  if (msgTotal > 500) perMessage.push({ ref, tokens: msgTotal });
@@ -6752,6 +6832,7 @@ function estimateContextComposition(messages, state) {
6752
6832
  summaryTokens,
6753
6833
  messageTokens,
6754
6834
  textTokens: Math.max(0, messageTokens - codeTokens),
6835
+ protectedTokens,
6755
6836
  total: toolTokens + summaryTokens + messageTokens,
6756
6837
  largestRanges: perMessage.slice(0, 15),
6757
6838
  largestToolRanges: perTool.slice(0, 15),
@@ -6760,12 +6841,33 @@ function estimateContextComposition(messages, state) {
6760
6841
  toolTypeBreakdown
6761
6842
  };
6762
6843
  }
6763
- function buildCompressibleRanges(messages, state) {
6844
+ function refNum(ref) {
6845
+ const n = parseInt(ref.slice(1), 10);
6846
+ return Number.isNaN(n) ? -1 : n;
6847
+ }
6848
+ function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = []) {
6764
6849
  const msgInfo = [];
6850
+ const protectedMsgInfo = [];
6765
6851
  for (const msg of messages) {
6766
6852
  if (isSyntheticMessage(msg)) continue;
6767
6853
  const ref = state.messageIds.byRawId.get(msg.info.id);
6768
6854
  if (!ref) continue;
6855
+ const rn = parseInt(ref.slice(1), 10);
6856
+ if ((protectedTools.length > 0 || protectedFilePatterns.length > 0) && messageContainsProtectedTool(msg, protectedTools, protectedFilePatterns)) {
6857
+ let tokens2 = 0;
6858
+ const tools = /* @__PURE__ */ new Set();
6859
+ for (const part of msg.parts || []) {
6860
+ if (part.type === "text" && typeof part.text === "string") {
6861
+ tokens2 += Math.round(part.text.length / 4);
6862
+ } else if (part.type !== "text" && part.type !== "reasoning") {
6863
+ tokens2 += Math.round(JSON.stringify(part).length / 4);
6864
+ const toolName = part?.tool;
6865
+ if (toolName) tools.add(toolName);
6866
+ }
6867
+ }
6868
+ protectedMsgInfo.push({ ref, refNum: rn, tokens: tokens2, tools: [...tools] });
6869
+ continue;
6870
+ }
6769
6871
  let tokens = 0;
6770
6872
  let isTool = false;
6771
6873
  for (const part of msg.parts || []) {
@@ -6776,10 +6878,8 @@ function buildCompressibleRanges(messages, state) {
6776
6878
  isTool = true;
6777
6879
  }
6778
6880
  }
6779
- const refNum = parseInt(ref.slice(1), 10);
6780
- msgInfo.push({ ref, refNum, tokens, isTool, isUser: msg.info.role === "user" });
6881
+ msgInfo.push({ ref, refNum: rn, tokens, isTool, isUser: msg.info.role === "user" });
6781
6882
  }
6782
- if (msgInfo.length === 0) return [];
6783
6883
  const groups = [];
6784
6884
  let cur = null;
6785
6885
  let prevRefNum = -2;
@@ -6812,14 +6912,114 @@ function buildCompressibleRanges(messages, state) {
6812
6912
  }
6813
6913
  }
6814
6914
  if (cur) groups.push(cur);
6815
- return groups.filter((g) => g.tokens > 0);
6915
+ const protectedGroups = [];
6916
+ let pcur = null;
6917
+ let pPrevRefNum = -2;
6918
+ for (const info of protectedMsgInfo) {
6919
+ const hasGap = info.refNum > pPrevRefNum + 1;
6920
+ if (pcur && hasGap) {
6921
+ protectedGroups.push(pcur);
6922
+ pcur = null;
6923
+ }
6924
+ pPrevRefNum = info.refNum;
6925
+ if (!pcur) {
6926
+ pcur = {
6927
+ startRef: info.ref,
6928
+ endRef: info.ref,
6929
+ count: 1,
6930
+ tokens: info.tokens,
6931
+ tools: [...info.tools]
6932
+ };
6933
+ } else {
6934
+ pcur.endRef = info.ref;
6935
+ pcur.count++;
6936
+ pcur.tokens += info.tokens;
6937
+ for (const t of info.tools) {
6938
+ if (!pcur.tools.includes(t)) pcur.tools.push(t);
6939
+ }
6940
+ }
6941
+ }
6942
+ if (pcur) protectedGroups.push(pcur);
6943
+ return {
6944
+ compressible: groups.filter((g) => g.tokens > 0),
6945
+ protected: protectedGroups
6946
+ };
6816
6947
  }
6817
- function formatCompressibleRanges(ranges) {
6818
- if (ranges.length === 0) return "";
6948
+ function formatCompressibleRanges(ranges, protectedRanges) {
6819
6949
  const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
6820
- const lines = ranges.map((r, i) => {
6821
- const suffix = i === ranges.length - 1 ? " (recent \u2014 may still be in active use)" : "";
6822
- return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
6950
+ if (!protectedRanges || protectedRanges.length === 0) {
6951
+ if (ranges.length === 0) return "";
6952
+ const lines2 = ranges.map((r, i) => {
6953
+ const suffix = i === ranges.length - 1 ? " (recent \u2014 may still be in active use)" : "";
6954
+ return ` ${r.startRef}\u2013${r.endRef} ${r.count} msgs ${fmt(r.tokens)} [tool ${r.toolPct}% | text ${r.textPct}%]${suffix}`;
6955
+ });
6956
+ return `Compressible ranges (oldest first):
6957
+ ${lines2.join("\n")}`;
6958
+ }
6959
+ const entries = [];
6960
+ for (const r of ranges) {
6961
+ entries.push({
6962
+ startRef: r.startRef,
6963
+ endRef: r.endRef,
6964
+ startNum: refNum(r.startRef),
6965
+ endNum: refNum(r.endRef),
6966
+ count: r.count,
6967
+ tokens: r.tokens,
6968
+ toolPct: r.toolPct,
6969
+ textPct: r.textPct,
6970
+ compressibleTokens: r.tokens,
6971
+ compressibleCount: r.count,
6972
+ protectedTokens: 0,
6973
+ protectedCount: 0,
6974
+ protectedTools: []
6975
+ });
6976
+ }
6977
+ for (const r of protectedRanges) {
6978
+ entries.push({
6979
+ startRef: r.startRef,
6980
+ endRef: r.endRef,
6981
+ startNum: refNum(r.startRef),
6982
+ endNum: refNum(r.endRef),
6983
+ count: r.count,
6984
+ tokens: r.tokens,
6985
+ toolPct: 0,
6986
+ textPct: 0,
6987
+ compressibleTokens: 0,
6988
+ compressibleCount: 0,
6989
+ protectedTokens: r.tokens,
6990
+ protectedCount: r.count,
6991
+ protectedTools: [...r.tools]
6992
+ });
6993
+ }
6994
+ entries.sort((a, b) => a.startNum - b.startNum);
6995
+ const merged = [];
6996
+ for (const entry of entries) {
6997
+ const last = merged[merged.length - 1];
6998
+ if (last && entry.startNum <= last.endNum + 1) {
6999
+ last.endRef = entry.endRef;
7000
+ last.endNum = Math.max(last.endNum, entry.endNum);
7001
+ last.count += entry.count;
7002
+ last.tokens += entry.tokens;
7003
+ last.compressibleTokens += entry.compressibleTokens;
7004
+ last.compressibleCount += entry.compressibleCount;
7005
+ last.protectedTokens += entry.protectedTokens;
7006
+ last.protectedCount += entry.protectedCount;
7007
+ for (const t of entry.protectedTools) {
7008
+ if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
7009
+ }
7010
+ } else {
7011
+ merged.push({ ...entry });
7012
+ }
7013
+ }
7014
+ const lines = merged.map((e, i) => {
7015
+ const suffix = i === merged.length - 1 && e.compressibleTokens > 0 ? " (recent \u2014 may still be in active use)" : "";
7016
+ if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
7017
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
7018
+ }
7019
+ if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
7020
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [${fmt(e.compressibleTokens)} compressible | ${fmt(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
7021
+ }
7022
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${fmt(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
6823
7023
  });
6824
7024
  return `Compressible ranges (oldest first):
6825
7025
  ${lines.join("\n")}`;
@@ -6995,8 +7195,13 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6995
7195
  }
6996
7196
  }
6997
7197
  const suffixMessage = createSuffixMessage(messages);
6998
- applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
6999
7198
  const nudgeGrowthTokens = config.compress?.nudgeGrowthTokens ?? resolveAdaptiveNudgeGrowth(modelContextLimit);
7199
+ const growthFloor = Math.max(
7200
+ config.compress?.minNudgeGrowthFloor ?? 5e3,
7201
+ (config.compress?.minNudgeGrowthRatio ?? 0.45) * nudgeGrowthTokens
7202
+ );
7203
+ const emergencyThreshold = resolveEmergencyThreshold(config, modelContextLimit);
7204
+ const emergencyOverride = emergencyThreshold !== void 0 && currentTokens !== void 0 && currentTokens >= emergencyThreshold;
7000
7205
  if (currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 && currentTokens < state.nudges.lastPerMessageNudgeTokens - nudgeGrowthTokens) {
7001
7206
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
7002
7207
  state.nudges.lastNudgeShownTokens = void 0;
@@ -7014,14 +7219,25 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7014
7219
  minNudgeContextPercent: config.compress?.minNudgeContextPercent ?? 15,
7015
7220
  nudgeGrowthTokens: effectiveThreshold
7016
7221
  });
7017
- state.nudges.shouldInjectThisTurn = decision.shouldNudge;
7222
+ const growthSinceBaseline = currentTokens !== void 0 && growthReference !== void 0 ? currentTokens - growthReference : void 0;
7223
+ const nudgeAllowed = emergencyOverride || growthSinceBaseline !== void 0 && growthSinceBaseline >= growthFloor;
7224
+ state.nudges.shouldInjectThisTurn = nudgeAllowed;
7225
+ const effectiveTipsVariant = emergencyOverride ? "maxLimit" : decision.tipsVariant;
7226
+ if (nudgeAllowed) {
7227
+ applyAnchoredNudges(state, config, messages, prompts, compressionPriorities, currentTokens, modelContextLimit, suffixMessage);
7228
+ }
7018
7229
  if (state.nudges.lastPerMessageNudgeTokens === void 0 && currentTokens !== void 0) {
7019
7230
  state.nudges.lastPerMessageNudgeTokens = currentTokens;
7020
7231
  baselineReEstablished = true;
7021
7232
  }
7022
- const composition = estimateContextComposition(messages, state);
7233
+ const composition = estimateContextComposition(
7234
+ messages,
7235
+ state,
7236
+ config.compress.protectedTools,
7237
+ config.protectedFilePatterns
7238
+ );
7023
7239
  let tipsText = null;
7024
- if (decision.shouldNudge) {
7240
+ if (nudgeAllowed) {
7025
7241
  injectContextUsage(suffixMessage, config, currentTokens, modelContextLimit);
7026
7242
  if (suffixMessage && composition.total > 0) {
7027
7243
  const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
@@ -7029,30 +7245,40 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
7029
7245
  const growth = currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 ? currentTokens - state.nudges.lastPerMessageNudgeTokens : 0;
7030
7246
  const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
7031
7247
  const plainTextTokens = composition.textTokens;
7032
- const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
7248
+ const efficiencyNote = effectiveTipsVariant !== "maxLimit" ? `
7033
7249
  This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
7034
7250
 
7035
7251
  ${COMPRESS_PHILOSOPHY}` : "";
7036
7252
  let breakdown = `${efficiencyNote}
7037
7253
  Breakdown: ${fmt(composition.toolTokens)} tool (${pct2(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct2(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct2(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct2(plainTextTokens)}%)${growthStr}`;
7038
- const ranges = buildCompressibleRanges(messages, state);
7039
- if (ranges.length > 0) {
7254
+ const compressibleTokens = composition.total - composition.protectedTokens - composition.summaryTokens;
7255
+ if (composition.protectedTokens > 0) {
7256
+ breakdown += `
7257
+ \u26A0\uFE0F ${fmt(composition.protectedTokens)} tokens are protected (environment-managed tools) \u2014 not compressible. Effective compressible: ~${fmt(compressibleTokens)}.`;
7258
+ }
7259
+ const contextRanges = buildCompressibleRanges(
7260
+ messages,
7261
+ state,
7262
+ config.compress.protectedTools,
7263
+ config.protectedFilePatterns
7264
+ );
7265
+ if (contextRanges.compressible.length > 0) {
7040
7266
  breakdown += `
7041
7267
 
7042
- ${formatCompressibleRanges(ranges)}`;
7268
+ ${formatCompressibleRanges(contextRanges.compressible, contextRanges.protected)}`;
7043
7269
  breakdown += `
7044
7270
  \u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
7045
7271
  }
7046
7272
  breakdown += `
7047
7273
  Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after compressing, or \`acp_status\` for compressed block details.`;
7048
- if (decision.tipsVariant !== "maxLimit") {
7274
+ if (effectiveTipsVariant !== "maxLimit") {
7049
7275
  breakdown += `
7050
7276
 
7051
7277
  ${HOW_TO_COMPRESS_RULES}`;
7052
7278
  }
7053
7279
  appendToLastTextPart(suffixMessage, breakdown);
7054
7280
  }
7055
- if (decision.tipsVariant === "maxLimit") {
7281
+ if (effectiveTipsVariant === "maxLimit") {
7056
7282
  tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
7057
7283
  }
7058
7284
  state.nudges.lastNudgeShownTokens = currentTokens;
@@ -7090,11 +7316,21 @@ ${HOW_TO_COMPRESS_RULES}`;
7090
7316
  }
7091
7317
  }
7092
7318
  }
7093
- if (anchorsChanged || decision.shouldNudge || baselineReEstablished || baselineCorrected) {
7319
+ if (anchorsChanged || nudgeAllowed || baselineReEstablished || baselineCorrected) {
7094
7320
  saveSessionState(state, logger).catch(() => {
7095
7321
  });
7096
7322
  }
7097
7323
  };
7324
+ function resolveEmergencyThreshold(config, modelContextLimit) {
7325
+ const threshold = config.compress?.emergencyThresholdPercent;
7326
+ if (threshold === void 0 || modelContextLimit === void 0) return void 0;
7327
+ if (typeof threshold === "number") return threshold;
7328
+ if (!threshold.endsWith("%")) return void 0;
7329
+ const parsedPercent = parseFloat(threshold.slice(0, -1));
7330
+ if (isNaN(parsedPercent)) return void 0;
7331
+ const clampedPercent = Math.max(0, Math.min(100, Math.round(parsedPercent)));
7332
+ return Math.round(clampedPercent / 100 * modelContextLimit);
7333
+ }
7098
7334
  function injectContextUsage(target, config, currentTokens, modelContextLimit) {
7099
7335
  if (!target) return;
7100
7336
  const rawUsage = buildContextUsageGuidance(config, currentTokens, modelContextLimit);
@@ -7124,14 +7360,11 @@ var injectMessageIds = (state, config, messages, compressionPriorities) => {
7124
7360
  const priority = config.compress.mode === "message" && !isBlockedMessage ? compressionPriorities?.get(message.info.id)?.priority : void 0;
7125
7361
  const msgType = classifyMessageType(message.parts);
7126
7362
  const msgTokens = Math.round(countMessageCharacters(message) / 4);
7127
- const tag = formatMessageIdTag(
7128
- isBlockedMessage ? "BLOCKED" : messageRef,
7129
- {
7130
- priority: priority ?? void 0,
7131
- type: msgType,
7132
- tokens: formatTokenSize(msgTokens)
7133
- }
7134
- );
7363
+ const tag = formatMessageIdTag(isBlockedMessage ? "BLOCKED" : messageRef, {
7364
+ priority: priority ?? void 0,
7365
+ type: msgType,
7366
+ tokens: formatTokenSize(msgTokens)
7367
+ });
7135
7368
  if (message.info.role === "user") {
7136
7369
  let injected = false;
7137
7370
  for (const part of message.parts) {
@@ -7732,7 +7965,9 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
7732
7965
  );
7733
7966
  const topTypes = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
7734
7967
  if (topTypes.length > 0) {
7735
- lines.push(` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`);
7968
+ lines.push(
7969
+ ` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`
7970
+ );
7736
7971
  }
7737
7972
  }
7738
7973
  lines.push("");
@@ -7751,7 +7986,9 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
7751
7986
  const ageStr = formatAge(b.createdAt);
7752
7987
  const range = formatIdRange(b);
7753
7988
  const topic = b.topic || "(no topic)";
7754
- lines.push(` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`);
7989
+ lines.push(
7990
+ ` b${b.blockId} ${formatTokens(b.compressedTokens)}\u2192${formatTokens(b.summaryTokens)} ${ageStr} ${range} "${topic}"`
7991
+ );
7755
7992
  }
7756
7993
  }
7757
7994
  if (!fetchFailed) {
@@ -7761,15 +7998,24 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
7761
7998
  const entry = pruneMap.get(msgId);
7762
7999
  return !entry || entry.activeBlockIds.length === 0;
7763
8000
  });
7764
- const ranges = buildCompressibleRanges(visibleRaw, ctx.state);
7765
- if (ranges.length > 0) {
8001
+ const contextRanges = buildCompressibleRanges(
8002
+ visibleRaw,
8003
+ ctx.state,
8004
+ ctx.config?.compress?.protectedTools ?? [],
8005
+ ctx.config?.protectedFilePatterns ?? []
8006
+ );
8007
+ if (contextRanges.compressible.length > 0 || contextRanges.protected.length > 0) {
7766
8008
  lines.push("");
7767
- lines.push(formatCompressibleRanges(ranges));
8009
+ lines.push(
8010
+ formatCompressibleRanges(contextRanges.compressible, contextRanges.protected)
8011
+ );
7768
8012
  }
7769
8013
  }
7770
8014
  lines.push("");
7771
8015
  const hintTool = topToolName || "bash";
7772
- lines.push(`Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`);
8016
+ lines.push(
8017
+ `Tip: acp_status({scope:"uncompressed", view:"messages", tool:"${hintTool}"}) for per-message listing`
8018
+ );
7773
8019
  return lines;
7774
8020
  }
7775
8021
  function renderUncompressedRanges(rawMessages, ctx) {
@@ -7779,16 +8025,24 @@ function renderUncompressedRanges(rawMessages, ctx) {
7779
8025
  const entry = pruneMap.get(msgId);
7780
8026
  return !entry || entry.activeBlockIds.length === 0;
7781
8027
  });
7782
- const ranges = buildCompressibleRanges(visibleMessages, ctx.state);
7783
- const totalTokens = ranges.reduce((s, r) => s + r.tokens, 0);
7784
- const totalMsgs = ranges.reduce((s, r) => s + r.count, 0);
8028
+ const contextRanges = buildCompressibleRanges(
8029
+ visibleMessages,
8030
+ ctx.state,
8031
+ ctx.config?.compress?.protectedTools ?? [],
8032
+ ctx.config?.protectedFilePatterns ?? []
8033
+ );
8034
+ const compressible = contextRanges.compressible;
8035
+ const totalTokens = compressible.reduce((s, r) => s + r.tokens, 0);
8036
+ const totalMsgs = compressible.reduce((s, r) => s + r.count, 0);
7785
8037
  const lines = [];
7786
- lines.push(`UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${totalMsgs} msgs in ${ranges.length} ranges`);
8038
+ lines.push(
8039
+ `UNCOMPRESSED \u2014 ${formatTokens(totalTokens)} | ${totalMsgs} msgs in ${compressible.length} ranges`
8040
+ );
7787
8041
  lines.push("");
7788
- if (ranges.length === 0) {
8042
+ if (compressible.length === 0 && contextRanges.protected.length === 0) {
7789
8043
  lines.push(" (no compressible ranges)");
7790
8044
  } else {
7791
- lines.push(formatCompressibleRanges(ranges));
8045
+ lines.push(formatCompressibleRanges(compressible, contextRanges.protected));
7792
8046
  }
7793
8047
  lines.push("");
7794
8048
  lines.push(`Per-message listing: acp_status({scope:"uncompressed", view:"messages"})`);
@@ -7820,7 +8074,9 @@ function renderUncompressedDrilldown(visibleMessages, toolFilter, sort, limit) {
7820
8074
  }
7821
8075
  if (filtered.length > shown.length) {
7822
8076
  lines.push("");
7823
- lines.push(`${shown.length} of ${filtered.length} shown (${filtered.length - shown.length} hidden).`);
8077
+ lines.push(
8078
+ `${shown.length} of ${filtered.length} shown (${filtered.length - shown.length} hidden).`
8079
+ );
7824
8080
  }
7825
8081
  if (filtered.length > 1 && sort !== "time") {
7826
8082
  const refs = filtered.map((m) => m.index);
@@ -7847,7 +8103,9 @@ function renderCompressedDrilldown(blocks, sort, limit) {
7847
8103
  }
7848
8104
  const totalSummary = sorted.reduce((s, b) => s + (b.summaryTokens || 0), 0);
7849
8105
  const totalCompressed = sorted.reduce((s, b) => s + (b.compressedTokens || 0), 0);
7850
- lines.push(`COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalCompressed)} original \u2192 ${formatTokens(totalSummary)} summary`);
8106
+ lines.push(
8107
+ `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens(totalCompressed)} original \u2192 ${formatTokens(totalSummary)} summary`
8108
+ );
7851
8109
  lines.push(`Sorted by ${sort === "time" ? "time" : sort === "age" ? "age" : "size"}`);
7852
8110
  lines.push("");
7853
8111
  const shown = sorted.slice(0, limit);
@@ -7867,7 +8125,9 @@ function renderCompressedDrilldown(blocks, sort, limit) {
7867
8125
  lines.push(`${shown.length} of ${sorted.length} shown.`);
7868
8126
  }
7869
8127
  lines.push("");
7870
- lines.push("Use decompress to restore a block's content, or search_context to search within blocks.");
8128
+ lines.push(
8129
+ "Use decompress to restore a block's content, or search_context to search within blocks."
8130
+ );
7871
8131
  return lines;
7872
8132
  }
7873
8133
  function createAcpStatusTool(ctx) {
@@ -7876,8 +8136,12 @@ function createAcpStatusTool(ctx) {
7876
8136
  description: ACP_STATUS_TOOL_DESCRIPTION,
7877
8137
  args: {
7878
8138
  scope: tool5.schema.string().optional().describe('Drill down: "compressed" or "uncompressed". No arg = overview of both.'),
7879
- view: tool5.schema.string().optional().describe('Display format for scope:"uncompressed": "ranges" (default, grouped by turn \u2014 matches nudge format) or "messages" (per-message listing with sort/filter)'),
7880
- tool: tool5.schema.string().optional().describe('Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'),
8139
+ view: tool5.schema.string().optional().describe(
8140
+ 'Display format for scope:"uncompressed": "ranges" (default, grouped by turn \u2014 matches nudge format) or "messages" (per-message listing with sort/filter)'
8141
+ ),
8142
+ tool: tool5.schema.string().optional().describe(
8143
+ 'Filter by tool type (only with scope:"uncompressed", view:"messages"). e.g., "bash", "todowrite", "write"'
8144
+ ),
7881
8145
  sort: tool5.schema.string().optional().describe('Sort order: "size" (default), "time", or "tool"'),
7882
8146
  limit: tool5.schema.number().optional().describe("Max items to list (default 30)")
7883
8147
  },
@@ -7915,7 +8179,16 @@ function createAcpStatusTool(ctx) {
7915
8179
  lines.push(...renderUncompressedRanges(rawMessages, ctx));
7916
8180
  }
7917
8181
  } else {
7918
- lines.push(...renderOverview(visibleMsgs, summaryTokens, allBlocks, fetchFailed, rawMessages, ctx));
8182
+ lines.push(
8183
+ ...renderOverview(
8184
+ visibleMsgs,
8185
+ summaryTokens,
8186
+ allBlocks,
8187
+ fetchFailed,
8188
+ rawMessages,
8189
+ ctx
8190
+ )
8191
+ );
7919
8192
  }
7920
8193
  return lines.join("\n");
7921
8194
  }