acp-kernel 0.0.22 → 0.0.24

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.
@@ -1 +1 @@
1
- {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAapB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOvD,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAsBD,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAwQ7D;AA8sBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAapB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AASvD,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AAsBD,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAwQ7D;AAixBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,EACzB,SAAS,GAAE,OAAO,CAAC,MAAM,CAAM,GAC9B,MAAM,CAoCR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BvD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,EACzB,SAAS,GAAE,OAAO,CAAC,MAAM,CAAM,GAC9B,MAAM,CAqCR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BvD"}
package/dist/index.js CHANGED
@@ -142,9 +142,11 @@ function prune(messages, state, options = {}) {
142
142
  const indexById = /* @__PURE__ */ new Map();
143
143
  messages.forEach((message, index) => indexById.set(message.id, index));
144
144
  const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
145
- return stripOrphanedToolResults(
146
- stripOrphanedToolCalls(
147
- rebuildMessages(messages, covered, firstUserIndex, anchors)
145
+ return stripOrphanedReasoning(
146
+ stripOrphanedToolResults(
147
+ stripOrphanedToolCalls(
148
+ rebuildMessages(messages, covered, firstUserIndex, anchors)
149
+ )
148
150
  )
149
151
  );
150
152
  }
@@ -221,6 +223,24 @@ function stripOrphanedToolCalls(messages) {
221
223
  (m) => m.contentType !== "tool-call" || !m.toolCallId || m.toolName === "compress" || knownResultIds.has(m.toolCallId)
222
224
  );
223
225
  }
226
+ function stripOrphanedReasoning(messages) {
227
+ const drop = /* @__PURE__ */ new Set();
228
+ for (let i = 0; i < messages.length; i++) {
229
+ if (drop.has(i)) continue;
230
+ if (messages[i].contentType !== "reasoning") continue;
231
+ let j = i;
232
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
233
+ j++;
234
+ }
235
+ const companion = messages[j + 1];
236
+ const hasCompanion = companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call");
237
+ if (!hasCompanion) {
238
+ for (let k = i; k <= j; k++) drop.add(k);
239
+ }
240
+ }
241
+ if (drop.size === 0) return messages;
242
+ return messages.filter((_, i) => !drop.has(i));
243
+ }
224
244
 
225
245
  // src/sync.ts
226
246
  function syncBlocks(messages, state) {
@@ -310,7 +330,8 @@ function defaultConfig(modelContextLimit, overrides = {}) {
310
330
  growthCap: 5e4,
311
331
  minGrowthFloor: 2e4,
312
332
  minGrowthRatio: 0.45,
313
- emergencyThresholdPct: 0.95
333
+ emergencyThresholdPct: 0.95,
334
+ tier2GrowthMultiplier: 1.5
314
335
  },
315
336
  promotionThreshold: 5,
316
337
  truncate: { threshold: 0.95 },
@@ -889,6 +910,40 @@ function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan =
889
910
  return { startIndex: newStartIndex, endIndex: newEndIndex };
890
911
  }
891
912
 
913
+ // src/reasoning-pairs.ts
914
+ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
915
+ if (startIndex > endIndex) {
916
+ return { startIndex, endIndex };
917
+ }
918
+ let newStartIndex = startIndex;
919
+ let newEndIndex = endIndex;
920
+ for (let i = startIndex; i <= endIndex && i < messages.length; i++) {
921
+ const msg = messages[i];
922
+ if (!msg) continue;
923
+ if (msg.contentType === "reasoning") {
924
+ let j = i;
925
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
926
+ j++;
927
+ }
928
+ const companion = messages[j + 1];
929
+ if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call") && j + 1 > newEndIndex) {
930
+ newEndIndex = j + 1;
931
+ }
932
+ }
933
+ if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
934
+ let k = i - 1;
935
+ while (k >= 0 && messages[k].contentType === "reasoning") {
936
+ k--;
937
+ }
938
+ const runStart = k + 1;
939
+ if (runStart < i && runStart >= 0 && messages[runStart].contentType === "reasoning" && runStart < newStartIndex) {
940
+ newStartIndex = runStart;
941
+ }
942
+ }
943
+ }
944
+ return { startIndex: newStartIndex, endIndex: newEndIndex };
945
+ }
946
+
892
947
  // src/recommend.ts
893
948
  function refNum(ref) {
894
949
  const n = parseInt(ref.slice(1), 10);
@@ -1036,6 +1091,44 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
1036
1091
  protected: protectedRanges
1037
1092
  };
1038
1093
  }
1094
+ function mergeBatch(batch) {
1095
+ const first = batch[0];
1096
+ const last = batch[batch.length - 1];
1097
+ const count = batch.reduce((s, r) => s + r.count, 0);
1098
+ const tokens = batch.reduce((s, r) => s + r.tokens, 0);
1099
+ const toolPct = Math.round(
1100
+ batch.reduce((s, r) => s + r.toolPct * r.count, 0) / count
1101
+ );
1102
+ const merged = {
1103
+ startRef: first.startRef,
1104
+ endRef: last.endRef,
1105
+ count,
1106
+ tokens,
1107
+ toolPct,
1108
+ textPct: 100 - toolPct
1109
+ };
1110
+ if (batch.some((r) => r.dangerous === true)) {
1111
+ merged.dangerous = true;
1112
+ }
1113
+ return merged;
1114
+ }
1115
+ function mergeRangesToThreshold(ranges, minChars) {
1116
+ if (minChars <= 0 || ranges.length === 0) return ranges;
1117
+ const result = [];
1118
+ let batch = [];
1119
+ for (const r of ranges) {
1120
+ batch.push(r);
1121
+ const batchTokens = batch.reduce((s, x) => s + x.tokens, 0);
1122
+ if (batchTokens * 4 >= minChars) {
1123
+ result.push(mergeBatch(batch));
1124
+ batch = [];
1125
+ }
1126
+ }
1127
+ if (batch.length > 0) {
1128
+ result.push(mergeBatch(batch));
1129
+ }
1130
+ return result;
1131
+ }
1039
1132
 
1040
1133
  // src/pipeline.ts
1041
1134
  function makeIO(messages, state, effects = {}) {
@@ -1322,7 +1415,10 @@ var recommendNode = {
1322
1415
  const nothingToCompress = contextRanges.compressible.length === 0;
1323
1416
  const recommendation = {
1324
1417
  contextRanges,
1325
- recommendedRanges: contextRanges.compressible,
1418
+ recommendedRanges: mergeRangesToThreshold(
1419
+ contextRanges.compressible,
1420
+ ctx.config.compress.minCompressRange
1421
+ ),
1326
1422
  nothingToCompress
1327
1423
  };
1328
1424
  return { ...io, effects: { ...io.effects, recommendation } };
@@ -1348,6 +1444,7 @@ var nudgeNode = {
1348
1444
  if (baseline > 0 && ctx.tokenCount < baseline - nudgeGrowthTokens) {
1349
1445
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
1350
1446
  stamped.lastNudgeShownTokens = 0;
1447
+ stamped.lastShownByTier = {};
1351
1448
  }
1352
1449
  if (stamped.lastPerMessageNudgeTokens === 0) {
1353
1450
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
@@ -1392,7 +1489,7 @@ function applySingleRange(input) {
1392
1489
  messages: input.messages,
1393
1490
  state: input.state
1394
1491
  });
1395
- const rangeMessageIds = applyToolPairAdjustment(
1492
+ const rangeMessageIds = applyPairBoundaryAdjustments(
1396
1493
  resolved,
1397
1494
  input.messages
1398
1495
  );
@@ -1506,20 +1603,33 @@ function applySingleRange(input) {
1506
1603
  }
1507
1604
  return { tokens: compressedTokens, warnings };
1508
1605
  }
1509
- function applyToolPairAdjustment(resolved, messages) {
1606
+ function applyPairBoundaryAdjustments(resolved, messages) {
1510
1607
  if (resolved.boundaryKind === "block") {
1511
1608
  return resolved.messageIds;
1512
1609
  }
1513
- const adjusted = adjustBoundariesForToolPairs(
1514
- resolved.startIndex,
1515
- resolved.endIndex,
1516
- messages
1517
- );
1518
- if (adjusted.startIndex === resolved.startIndex && adjusted.endIndex === resolved.endIndex) {
1610
+ let startIndex = resolved.startIndex;
1611
+ let endIndex = resolved.endIndex;
1612
+ for (let pass = 0; pass < 2; pass++) {
1613
+ const reasoningAdjusted = adjustBoundariesForReasoningPairs(
1614
+ startIndex,
1615
+ endIndex,
1616
+ messages
1617
+ );
1618
+ const toolAdjusted = adjustBoundariesForToolPairs(
1619
+ reasoningAdjusted.startIndex,
1620
+ reasoningAdjusted.endIndex,
1621
+ messages
1622
+ );
1623
+ const changed = toolAdjusted.startIndex !== startIndex || toolAdjusted.endIndex !== endIndex;
1624
+ startIndex = toolAdjusted.startIndex;
1625
+ endIndex = toolAdjusted.endIndex;
1626
+ if (!changed) break;
1627
+ }
1628
+ if (startIndex === resolved.startIndex && endIndex === resolved.endIndex) {
1519
1629
  return resolved.messageIds;
1520
1630
  }
1521
1631
  const ids = [];
1522
- for (let i = adjusted.startIndex; i <= adjusted.endIndex; i++) {
1632
+ for (let i = startIndex; i <= endIndex; i++) {
1523
1633
  const msg = messages[i];
1524
1634
  if (msg) ids.push(msg.id);
1525
1635
  }
@@ -1603,10 +1713,11 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
1603
1713
  )
1604
1714
  );
1605
1715
  }
1606
- function pendingByTier(state, recommendation, countTokens) {
1716
+ function pendingByTier(state, recommendation, countTokens, minCompressRange) {
1607
1717
  const out = {};
1608
- const compressible = recommendation?.contextRanges.compressible ?? [];
1609
- out[1] = { pending: compressible.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1718
+ const merged = recommendation?.recommendedRanges ?? [];
1719
+ const effective = minCompressRange > 0 ? merged.filter((r) => r.tokens * 4 >= minCompressRange) : merged;
1720
+ out[1] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1610
1721
  const active = activeBlocks(state);
1611
1722
  const t1 = active.filter((b) => b.tier === 1);
1612
1723
  const t2 = active.filter((b) => b.tier === 2);
@@ -1621,6 +1732,7 @@ function decideNudge(input) {
1621
1732
  const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
1622
1733
  const overLimit = usage >= config.nudge.maxContextLimitPct;
1623
1734
  const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
1735
+ const pressure = overLimit || emergencyOverride;
1624
1736
  const baseline = state.nudge.lastPerMessageNudgeTokens;
1625
1737
  const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
1626
1738
  const hasPendingNudge = hadPendingNudge;
@@ -1632,44 +1744,67 @@ function decideNudge(input) {
1632
1744
  );
1633
1745
  const growthSinceReference = tokenCount - growthReference;
1634
1746
  const rec = recommendation;
1635
- const tiers = pendingByTier(state, rec, countTokens);
1747
+ const tiers = pendingByTier(
1748
+ state,
1749
+ rec,
1750
+ countTokens,
1751
+ config.compress.minCompressRange
1752
+ );
1753
+ const tier2Threshold = Math.round(
1754
+ nudgeGrowthTokens * (config.nudge.tier2GrowthMultiplier ?? 1.5)
1755
+ );
1636
1756
  let injectedTier = null;
1637
1757
  let injectedReason = "";
1638
1758
  const growthReady = growthSinceReference >= growthFloor;
1639
- if (!overLimit && growthReady) {
1640
- for (const tier of [1, 2, 3]) {
1641
- if (!config.tiers.enabled && tier > 1) break;
1642
- const info = tiers[tier];
1643
- if (!info || info.pending < nudgeGrowthTokens) continue;
1644
- const lastShown = state.nudge.lastShownByTier[tier] ?? 0;
1645
- const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1646
- if (!cadenceMet) continue;
1647
- injectedTier = tier;
1648
- injectedReason = tier === 1 ? `T1 compressible ${info.pending} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%` : `T${tier} distill ready: ${info.targetBlocks.length} tier-${tier - 1} blocks (${info.pending} tokens) >= ${nudgeGrowthTokens}, usage ${Math.round(usage * 100)}%`;
1649
- break;
1759
+ const t1Eff = tiers[1]?.pending ?? 0;
1760
+ const t2Pen = tiers[2]?.pending ?? 0;
1761
+ const t3Pen = tiers[3]?.pending ?? 0;
1762
+ if (pressure) {
1763
+ const candidates = [1];
1764
+ if (config.tiers.enabled) {
1765
+ candidates.push(2, 3);
1766
+ }
1767
+ let best = null;
1768
+ let bestPending = 0;
1769
+ for (const t of candidates) {
1770
+ const p = tiers[t]?.pending ?? 0;
1771
+ if (p > bestPending) {
1772
+ bestPending = p;
1773
+ best = t;
1774
+ }
1650
1775
  }
1651
- } else if (overLimit) {
1652
- for (const tier of [1, 2, 3]) {
1653
- if (!config.tiers.enabled && tier > 1) break;
1654
- const info = tiers[tier];
1655
- if (!info || info.pending < config.compress.minCompressRange) continue;
1656
- injectedTier = tier;
1657
- injectedReason = emergencyOverride ? `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%, T${tier} pending ${info.pending}` : `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}%, T${tier} pending ${info.pending}`;
1658
- break;
1776
+ if (best !== null && bestPending > 0) {
1777
+ injectedTier = best;
1778
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1779
+ injectedReason = best === 1 ? `${label} T1: max effective pending ${bestPending}, usage ${Math.round(usage * 100)}%` : `${label} T${best} distill: max pending ${bestPending} (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}), usage ${Math.round(usage * 100)}%`;
1780
+ }
1781
+ } else if (growthReady) {
1782
+ if (t1Eff >= nudgeGrowthTokens) {
1783
+ injectedTier = 1;
1784
+ injectedReason = `T1 effective ${t1Eff} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%`;
1785
+ } else if (config.tiers.enabled && t2Pen >= tier2Threshold && t2Pen > t1Eff) {
1786
+ const lastShown = state.nudge.lastShownByTier[2] ?? 0;
1787
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1788
+ if (cadenceMet) {
1789
+ injectedTier = 2;
1790
+ injectedReason = `T2 distill ready: ${tiers[2].targetBlocks.length} tier-1 blocks (${t2Pen} tokens) >= ${tier2Threshold} (1.5x) and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
1791
+ }
1792
+ } else if (config.tiers.enabled && t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff) {
1793
+ const lastShown = state.nudge.lastShownByTier[3] ?? 0;
1794
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1795
+ if (cadenceMet) {
1796
+ injectedTier = 3;
1797
+ injectedReason = `T3 condense ready: ${tiers[3].targetBlocks.length} tier-2 blocks (${t3Pen} tokens) >= ${tier2Threshold} (1.5x) and > T2 ${t2Pen} and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
1798
+ }
1659
1799
  }
1660
1800
  }
1661
- const shouldInject = injectedTier !== null || overLimit && (rec?.recommendedRanges?.length ?? 0) > 0;
1801
+ const shouldInject = injectedTier !== null;
1662
1802
  let reason;
1663
- if (emergencyOverride && injectedTier !== null) {
1664
- reason = injectedReason;
1665
- } else if (emergencyOverride) {
1666
- reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}% (no compressible content)`;
1667
- } else if (overLimit && injectedTier !== null) {
1668
- reason = injectedReason;
1669
- } else if (overLimit) {
1670
- reason = `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}% (no compressible content)`;
1671
- } else if (injectedTier !== null) {
1803
+ if (injectedTier !== null) {
1672
1804
  reason = injectedReason;
1805
+ } else if (pressure) {
1806
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1807
+ reason = `${label}: usage ${Math.round(usage * 100)}% but no tier has effective compressible content (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 nudge suppressed to avoid offering ranges below minCompressRange`;
1673
1808
  } else {
1674
1809
  const tiersList = [1, 2, 3];
1675
1810
  const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
@@ -2024,20 +2159,23 @@ ${lines.join("\n")}`;
2024
2159
  function renderNudgeText(decision, prompts = defaultPrompts) {
2025
2160
  const breakdownStr = formatBreakdown(decision.contextBreakdown);
2026
2161
  const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
2162
+ const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
2027
2163
  if (decision.tier !== null && decision.tier >= 2) {
2028
2164
  const isT2 = decision.tier === 2;
2029
2165
  const targets = decision.tierTargetBlocks ?? [];
2030
2166
  const blockList = formatTierTargetBlocks(targets);
2031
2167
  const startId = targets[0]?.blockId ?? "b1";
2032
2168
  const endId = targets[targets.length - 1]?.blockId ?? "b5";
2169
+ const voice = isEmergency ? "emergency" : "gentle";
2170
+ const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
2033
2171
  return {
2034
- voice: "gentle",
2172
+ voice,
2035
2173
  text: [
2036
2174
  efficiencyNote(prompts),
2037
2175
  "",
2038
2176
  breakdownStr,
2039
2177
  "",
2040
- `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
2178
+ triggerLine,
2041
2179
  isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
2042
2180
  blockList,
2043
2181
  `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
@@ -2048,7 +2186,6 @@ function renderNudgeText(decision, prompts = defaultPrompts) {
2048
2186
  ].join("\n")
2049
2187
  };
2050
2188
  }
2051
- const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
2052
2189
  if (isEmergency) {
2053
2190
  return {
2054
2191
  voice: "emergency",