acp-kernel 0.0.8 → 0.0.10

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;AAcpB,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;CACpB;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;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAgN7D;AA8lBD,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;AAcpB,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;CACpB;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;AAED,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAoN7D;AAgpBD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"hide-consumed.d.ts","sourceRoot":"","sources":["../src/hide-consumed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIhE,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,WAAW,EAAE,GACxB,kBAAkB,CAwDpB"}
1
+ {"version":3,"file":"hide-consumed.d.ts","sourceRoot":"","sources":["../src/hide-consumed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQhE,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,WAAW,EAAE,GACxB,kBAAkB,CAwDpB"}
package/dist/index.js CHANGED
@@ -81,7 +81,8 @@ function createInitialState() {
81
81
  lastPerMessageNudgeTokens: 0,
82
82
  lastNudgeShownTokens: 0,
83
83
  baselineTokens: 0,
84
- anchors: {}
84
+ anchors: {},
85
+ lastShownByTier: {}
85
86
  },
86
87
  stats: { tokensCompressed: 0, compressionCount: 0 },
87
88
  nextBlockId: 1,
@@ -504,7 +505,7 @@ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, opt
504
505
  }
505
506
 
506
507
  // src/hide-consumed.ts
507
- var KEEP_LAST_ORPHANED = 2;
508
+ var KEEP_LAST_ORPHANED = 0;
508
509
  function hideConsumedCompressCalls(state, messages) {
509
510
  const activeCallIds = /* @__PURE__ */ new Set();
510
511
  const allBlockCallIds = /* @__PURE__ */ new Set();
@@ -1048,6 +1049,7 @@ function createCore(ports = {}) {
1048
1049
  if (blocksCreated > 0) {
1049
1050
  state.nudge.lastPerMessageNudgeTokens = 0;
1050
1051
  state.nudge.lastNudgeShownTokens = 0;
1052
+ state.nudge.lastShownByTier = {};
1051
1053
  }
1052
1054
  return { state, result: { blocksCreated, tokensCompressed, errors } };
1053
1055
  }
@@ -1183,7 +1185,8 @@ var nudgeNode = {
1183
1185
  config: ctx.config,
1184
1186
  state: io.state,
1185
1187
  messages: io.messages,
1186
- recommendation: io.effects.recommendation
1188
+ recommendation: io.effects.recommendation,
1189
+ countTokens: ctx.countTokens
1187
1190
  });
1188
1191
  const baseline = io.state.nudge.lastPerMessageNudgeTokens;
1189
1192
  const nudgeGrowthTokens = resolveAdaptiveGrowth(
@@ -1200,6 +1203,9 @@ var nudgeNode = {
1200
1203
  }
1201
1204
  if (nudge.shouldInject) {
1202
1205
  stamped.lastNudgeShownTokens = ctx.tokenCount;
1206
+ if (nudge.tier !== null) {
1207
+ stamped.lastShownByTier = { ...stamped.lastShownByTier, [nudge.tier]: ctx.tokenCount };
1208
+ }
1203
1209
  }
1204
1210
  return {
1205
1211
  ...io,
@@ -1427,8 +1433,19 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
1427
1433
  )
1428
1434
  );
1429
1435
  }
1436
+ function pendingByTier(state, recommendation, countTokens) {
1437
+ const out = {};
1438
+ const compressible = recommendation?.contextRanges.compressible ?? [];
1439
+ out[1] = { pending: compressible.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1440
+ const active = activeBlocks(state);
1441
+ const t1 = active.filter((b) => b.tier === 1);
1442
+ const t2 = active.filter((b) => b.tier === 2);
1443
+ out[2] = { pending: t1.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t1 };
1444
+ out[3] = { pending: t2.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t2 };
1445
+ return out;
1446
+ }
1430
1447
  function decideNudge(input) {
1431
- const { config, state, tokenCount, recommendation } = input;
1448
+ const { config, state, tokenCount, recommendation, countTokens } = input;
1432
1449
  const limit = config.modelContextLimit;
1433
1450
  const usage = limit > 0 ? tokenCount / limit : 0;
1434
1451
  const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
@@ -1443,40 +1460,45 @@ function decideNudge(input) {
1443
1460
  config.nudge.minGrowthRatio * nudgeGrowthTokens
1444
1461
  );
1445
1462
  const growthSinceReference = tokenCount - growthReference;
1446
- const growthTriggered = growthSinceReference >= effectiveThreshold;
1447
- let shouldContextNudge = emergencyOverride || growthTriggered;
1448
- if (shouldContextNudge && !emergencyOverride) {
1449
- shouldContextNudge = growthSinceReference >= growthFloor;
1450
- }
1451
- let tier = null;
1452
- let tierTargetBlocks = [];
1453
- if (config.tiers.enabled) {
1454
- const active = activeBlocks(state);
1455
- const activeT1 = active.filter((b) => b.tier === 1);
1456
- const activeT2 = active.filter((b) => b.tier === 2);
1457
- if (activeT2.length >= config.tiers.tier3Trigger) {
1458
- tier = 3;
1459
- tierTargetBlocks = activeT2;
1460
- } else if (activeT1.length >= config.tiers.tier2Trigger) {
1461
- tier = 2;
1462
- tierTargetBlocks = activeT1;
1463
+ const rec = recommendation;
1464
+ const tiers = pendingByTier(state, rec, countTokens);
1465
+ let injectedTier = null;
1466
+ let injectedReason = "";
1467
+ const growthReady = growthSinceReference >= growthFloor;
1468
+ if (!emergencyOverride && growthReady) {
1469
+ for (const tier of [1, 2, 3]) {
1470
+ if (!config.tiers.enabled && tier > 1) break;
1471
+ const info = tiers[tier];
1472
+ if (!info || info.pending < nudgeGrowthTokens) continue;
1473
+ const lastShown = state.nudge.lastShownByTier[tier] ?? 0;
1474
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1475
+ if (!cadenceMet) continue;
1476
+ injectedTier = tier;
1477
+ 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)}%`;
1478
+ break;
1463
1479
  }
1464
1480
  }
1465
- const rec = recommendation;
1466
- const nothingToCompress = rec ? rec.contextRanges.compressible.length === 0 : false;
1467
- const shouldInject = shouldContextNudge;
1481
+ const shouldInject = emergencyOverride || injectedTier !== null;
1468
1482
  let reason;
1469
- if (!shouldInject) {
1470
- const tierHint = tier !== null ? `, tier-${tier} ready (distill when growth reaches threshold)` : "";
1471
- reason = `growth ${growthSinceReference} < threshold ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""} (floor ${growthFloor})${tierHint}`;
1472
- } else if (emergencyOverride) {
1483
+ if (emergencyOverride) {
1473
1484
  reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%`;
1474
- } else if (tier !== null) {
1475
- reason = `growth ${growthSinceReference} >= ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""}, usage ${Math.round(usage * 100)}%, tier-${tier} distillation`;
1476
- } else if (nothingToCompress) {
1477
- reason = `growth ${growthSinceReference} >= ${effectiveThreshold} but no specific ranges worth compressing`;
1485
+ } else if (injectedTier !== null) {
1486
+ reason = injectedReason;
1478
1487
  } else {
1479
- reason = `growth ${growthSinceReference} >= ${effectiveThreshold}${hasPendingNudge ? " (halved)" : ""}, usage ${Math.round(usage * 100)}%`;
1488
+ const tiersList = [1, 2, 3];
1489
+ const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
1490
+ const ready = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens).map((t) => `T${t} ${tiers[t].pending}`);
1491
+ const readyHint = ready.length > 0 ? `, ready: ${ready.join(", ")}` : "";
1492
+ const blocked = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor).map((t) => `T${t} (cadence)`);
1493
+ const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
1494
+ const maxPending = Math.max(0, ...Object.values(tiers).map((t) => t.pending));
1495
+ const pendingShort = maxPending < nudgeGrowthTokens;
1496
+ const growthShort = growthSinceReference < growthFloor;
1497
+ const parts = [];
1498
+ if (pendingShort) parts.push(`max compressible ${maxPending} < threshold ${nudgeGrowthTokens}`);
1499
+ if (growthShort) parts.push(`growth ${growthSinceReference} < floor ${growthFloor}`);
1500
+ if (parts.length === 0) parts.push(`max compressible ${maxPending}, growth ${growthSinceReference}`);
1501
+ reason = `${parts.join("; ")}${readyHint}${blockedHint}`;
1480
1502
  }
1481
1503
  const ctxBreakdown = computeContextBreakdown(input.messages, tokenCount, growthSinceReference);
1482
1504
  return {
@@ -1484,9 +1506,9 @@ function decideNudge(input) {
1484
1506
  reason,
1485
1507
  compressibleRanges: rec?.recommendedRanges ?? [],
1486
1508
  protectedRanges: rec?.contextRanges.protected ?? [],
1487
- tierTargetBlocks,
1509
+ tierTargetBlocks: injectedTier ? tiers[injectedTier].targetBlocks : [],
1488
1510
  contextUsage: usage,
1489
- tier,
1511
+ tier: injectedTier,
1490
1512
  breakdown: {
1491
1513
  usage,
1492
1514
  growth: growthSinceReference,
@@ -1495,7 +1517,10 @@ function decideNudge(input) {
1495
1517
  nudgeGrowthTokens,
1496
1518
  growthFloor,
1497
1519
  hasPendingNudge: hasPendingNudge ? 1 : 0,
1498
- emergencyOverride: emergencyOverride ? 1 : 0
1520
+ emergencyOverride: emergencyOverride ? 1 : 0,
1521
+ pendingT1: tiers[1].pending,
1522
+ pendingT2: tiers[2].pending,
1523
+ pendingT3: tiers[3].pending
1499
1524
  },
1500
1525
  contextBreakdown: ctxBreakdown
1501
1526
  };
@@ -1691,76 +1716,6 @@ function formatBreakdown(bd) {
1691
1716
  +${formatK(bd.growth)} since last nudge` : "";
1692
1717
  return `Context breakdown: ${parts.join(" | ")}${growth}`;
1693
1718
  }
1694
- function refToNum(ref) {
1695
- if (!ref) return 0;
1696
- const m = ref.match(/^m0*(\d+)$/);
1697
- return m && m[1] ? parseInt(m[1], 10) : 0;
1698
- }
1699
- function mergeRanges(compressible, protected_) {
1700
- const entries = [];
1701
- for (const r of compressible) {
1702
- const sNum = refToNum(r.startRef);
1703
- const eNum = refToNum(r.endRef);
1704
- entries.push({
1705
- startRef: r.startRef,
1706
- endRef: r.endRef,
1707
- startNum: sNum,
1708
- endNum: eNum,
1709
- count: r.count,
1710
- tokens: r.tokens,
1711
- compressibleTokens: r.tokens,
1712
- compressibleCount: r.count,
1713
- protectedTokens: 0,
1714
- protectedCount: 0,
1715
- protectedTools: [],
1716
- toolPct: r.toolPct,
1717
- textPct: r.textPct,
1718
- dangerous: r.dangerous ?? false
1719
- });
1720
- }
1721
- for (const r of protected_) {
1722
- const sNum = refToNum(r.startRef);
1723
- const eNum = refToNum(r.endRef);
1724
- entries.push({
1725
- startRef: r.startRef,
1726
- endRef: r.endRef,
1727
- startNum: sNum,
1728
- endNum: eNum,
1729
- count: r.count,
1730
- tokens: r.tokens,
1731
- compressibleTokens: 0,
1732
- compressibleCount: 0,
1733
- protectedTokens: r.tokens,
1734
- protectedCount: r.count,
1735
- protectedTools: [...r.tools],
1736
- toolPct: 0,
1737
- textPct: 0,
1738
- dangerous: false
1739
- });
1740
- }
1741
- entries.sort((a, b) => a.startNum - b.startNum);
1742
- const merged = [];
1743
- for (const e of entries) {
1744
- const last = merged[merged.length - 1];
1745
- if (last && last.endNum + 1 >= e.startNum) {
1746
- last.endRef = e.endRef;
1747
- last.endNum = Math.max(last.endNum, e.endNum);
1748
- last.count += e.count;
1749
- last.tokens += e.tokens;
1750
- last.compressibleTokens += e.compressibleTokens;
1751
- last.compressibleCount += e.compressibleCount;
1752
- last.protectedTokens += e.protectedTokens;
1753
- last.protectedCount += e.protectedCount;
1754
- if (e.dangerous) last.dangerous = true;
1755
- for (const t of e.protectedTools) {
1756
- if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
1757
- }
1758
- } else {
1759
- merged.push({ ...e });
1760
- }
1761
- }
1762
- return merged;
1763
- }
1764
1719
  function formatTierTargetBlocks(blocks) {
1765
1720
  if (blocks.length === 0) {
1766
1721
  return "Target blocks: (none \u2014 no tier blocks found)";
@@ -1773,27 +1728,25 @@ function formatTierTargetBlocks(blocks) {
1773
1728
  return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
1774
1729
  ${lines.join("\n")}`;
1775
1730
  }
1776
- function formatRanges(compressible, protected_) {
1777
- const merged = mergeRanges(compressible, protected_);
1778
- if (merged.length === 0) {
1731
+ function formatRanges(compressible) {
1732
+ if (compressible.length === 0) {
1779
1733
  return "[No specific ranges detected \u2014 compress any consumed content.]";
1780
1734
  }
1781
- const lines = merged.slice(0, 10).map((e) => {
1782
- const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
1783
- if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
1784
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
1785
- }
1786
- if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
1787
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
1788
- }
1735
+ const refNum2 = (ref) => {
1736
+ const m = ref.match(/\d+/);
1737
+ return m ? parseInt(m[0], 10) : 0;
1738
+ };
1739
+ const sorted = [...compressible].sort((a, b) => refNum2(a.startRef) - refNum2(b.startRef));
1740
+ const lines = sorted.map((e) => {
1741
+ const suffix = e.dangerous ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
1789
1742
  return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
1790
1743
  });
1791
- return `Compressible ranges (oldest first):
1744
+ return `Compressible ranges (${compressible.length}, oldest first):
1792
1745
  ${lines.join("\n")}`;
1793
1746
  }
1794
1747
  function renderNudgeText(decision) {
1795
1748
  const breakdownStr = formatBreakdown(decision.contextBreakdown);
1796
- const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
1749
+ const rangesStr = formatRanges(decision.compressibleRanges);
1797
1750
  if (decision.tier !== null && decision.tier >= 2) {
1798
1751
  const isT2 = decision.tier === 2;
1799
1752
  const targets = decision.tierTargetBlocks ?? [];
@@ -2150,11 +2103,31 @@ function renderUncompressedRanges(visible) {
2150
2103
  lines.push("");
2151
2104
  if (visible.length === 0) {
2152
2105
  lines.push(" (no uncompressed messages)");
2153
- } else {
2154
- for (const message of visible.slice(0, 30)) {
2155
- lines.push(` ${message.ref} (${formatTokens2(message.tokens)}) ${message.tool}`);
2106
+ return lines.join("\n");
2107
+ }
2108
+ const refNum2 = (ref) => {
2109
+ const m = ref.match(/\d+/);
2110
+ return m ? parseInt(m[0], 10) : 0;
2111
+ };
2112
+ const merged = [];
2113
+ for (const m of visible) {
2114
+ const num = refNum2(m.ref);
2115
+ const last = merged[merged.length - 1];
2116
+ if (last && num === last.startNum + last.count) {
2117
+ last.endRef = m.ref;
2118
+ last.count += 1;
2119
+ last.tokens += m.tokens;
2120
+ } else {
2121
+ merged.push({ startRef: m.ref, endRef: m.ref, startNum: num, count: 1, tokens: m.tokens, tool: m.tool });
2156
2122
  }
2157
2123
  }
2124
+ for (const r of merged.slice(0, 30)) {
2125
+ const range = r.count === 1 ? r.startRef : `${r.startRef}\u2013${r.endRef}`;
2126
+ lines.push(` ${range} (${r.count} msgs, ${formatTokens2(r.tokens)}${r.count > 1 ? ` (${Math.round(r.tokens / r.count)}/msg)` : ""}) ${r.tool}`);
2127
+ }
2128
+ if (merged.length > 30) {
2129
+ lines.push(` ... and ${merged.length - 30} more ranges`);
2130
+ }
2158
2131
  return lines.join("\n");
2159
2132
  }
2160
2133
  function renderMessageDrilldown(visible, toolFilter, sort, limit) {