billion-context-pi 0.1.37 → 0.1.39

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
@@ -76,6 +76,7 @@ function createInitialState() {
76
76
  return {
77
77
  blocks: [],
78
78
  messageRefs: { byRaw: {}, byRef: {} },
79
+ tokenSnapshot: {},
79
80
  nudge: {
80
81
  lastPerMessageNudgeTokens: 0,
81
82
  lastNudgeShownTokens: 0,
@@ -245,11 +246,23 @@ function syncBlocks(messages, state) {
245
246
  byRaw: { ...state.messageRefs.byRaw },
246
247
  byRef: { ...state.messageRefs.byRef }
247
248
  },
249
+ // Snapshot is keyed by ref with primitive values — shallow copy suffices.
250
+ tokenSnapshot: { ...state.tokenSnapshot ?? {} },
248
251
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
249
252
  stats: { ...state.stats },
250
253
  nextBlockId: state.nextBlockId,
251
254
  nextRunId: state.nextRunId
252
255
  };
256
+ const liveRefs = new Set(
257
+ messages.map((m) => result.messageRefs.byRaw[m.id]).filter((r) => typeof r === "string")
258
+ );
259
+ if (Object.keys(result.tokenSnapshot).length !== liveRefs.size) {
260
+ const pruned = {};
261
+ for (const [ref, n] of Object.entries(result.tokenSnapshot)) {
262
+ if (liveRefs.has(ref)) pruned[ref] = n;
263
+ }
264
+ result.tokenSnapshot = pruned;
265
+ }
253
266
  const consumedBlockIds = /* @__PURE__ */ new Set();
254
267
  for (const block of result.blocks) {
255
268
  for (const consumedId of block.directBlockIds) {
@@ -293,7 +306,8 @@ function defaultConfig(modelContextLimit, overrides = {}) {
293
306
  growthCap: 5e4,
294
307
  minGrowthFloor: 2e4,
295
308
  minGrowthRatio: 0.45,
296
- emergencyThresholdPct: 0.95
309
+ emergencyThresholdPct: 0.95,
310
+ tier2GrowthMultiplier: 1.5
297
311
  },
298
312
  promotionThreshold: 5,
299
313
  truncate: { threshold: 0.95 },
@@ -723,7 +737,7 @@ var TAG_CLOSE = LT + "/acp" + GT;
723
737
  function acpTag(ref, tokens, type) {
724
738
  return TAG_OPEN + 'tokens="' + formatTokens(tokens) + '" type="' + type + '"' + GT + ref + TAG_CLOSE;
725
739
  }
726
- function renderMessage(message, map, countTokens, strategy) {
740
+ function renderMessage(message, map, countTokens, strategy, snapshot = null) {
727
741
  const ref = refForRaw(map, message.id);
728
742
  if (!ref || ref === BLOCKED_REF) return message;
729
743
  if (strategy === "none") return message;
@@ -734,26 +748,33 @@ function renderMessage(message, map, countTokens, strategy) {
734
748
  "^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
735
749
  );
736
750
  const cleanText = (message.text || "").replace(ownTagRe, "");
737
- const tokens = countTokens(cleanText);
751
+ const tokens = snapshot ? snapshot[ref] ?? (snapshot[ref] = countTokens(cleanText)) : countTokens(cleanText);
738
752
  const type = classifyType(message);
739
753
  const prefix = acpTag(ref, tokens, type) + "\n";
740
754
  if (!cleanText) return { ...message, text: prefix };
741
755
  return { ...message, text: prefix + cleanText };
742
756
  }
743
- function renderVisibleRefs(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
757
+ function renderWithSnapshot(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
744
758
  const map = state.messageRefs;
745
- return messages.map(
746
- (message) => renderMessage(message, map, countTokens, strategy)
759
+ const snapshot = { ...state.tokenSnapshot ?? {} };
760
+ const rendered = messages.map(
761
+ (message) => renderMessage(message, map, countTokens, strategy, snapshot)
747
762
  );
763
+ return { messages: rendered, tokenSnapshot: snapshot };
748
764
  }
749
765
  function createRenderRefsNode(strategy) {
750
766
  return {
751
767
  name: "render-refs",
752
768
  run(io, ctx) {
753
- return {
754
- ...io,
755
- messages: renderVisibleRefs(io.messages, io.state, ctx.countTokens, strategy)
756
- };
769
+ const { messages, tokenSnapshot } = renderWithSnapshot(
770
+ io.messages,
771
+ io.state,
772
+ ctx.countTokens,
773
+ strategy
774
+ );
775
+ const prev = io.state.tokenSnapshot;
776
+ const changed = !prev || Object.keys(tokenSnapshot).length !== Object.keys(prev).length;
777
+ return changed ? { ...io, messages, state: { ...io.state, tokenSnapshot } } : { ...io, messages };
757
778
  }
758
779
  };
759
780
  }
@@ -949,6 +970,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
949
970
  ref,
950
971
  refNum: rn,
951
972
  tokens: countTokens(msg.text ?? ""),
973
+ chars: (msg.text ?? "").length,
952
974
  isTool: isToolMessage(msg),
953
975
  isUser: msg.role === "user"
954
976
  });
@@ -969,6 +991,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
969
991
  endRef: info.ref,
970
992
  count: 1,
971
993
  tokens: info.tokens,
994
+ chars: info.chars,
972
995
  toolPct: info.isTool ? 100 : 0,
973
996
  textPct: info.isTool ? 0 : 100
974
997
  };
@@ -976,6 +999,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
976
999
  cur.endRef = info.ref;
977
1000
  cur.count++;
978
1001
  cur.tokens += info.tokens;
1002
+ cur.chars = (cur.chars ?? 0) + info.chars;
979
1003
  if (info.isTool) {
980
1004
  cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
981
1005
  } else {
@@ -1018,6 +1042,51 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
1018
1042
  protected: protectedRanges
1019
1043
  };
1020
1044
  }
1045
+ function mergeBatch(batch) {
1046
+ const first = batch[0];
1047
+ const last = batch[batch.length - 1];
1048
+ const count = batch.reduce((s, r) => s + r.count, 0);
1049
+ const tokens = batch.reduce((s, r) => s + r.tokens, 0);
1050
+ const chars = batch.reduce((s, r) => s + rangeChars(r), 0);
1051
+ const toolPct = Math.round(
1052
+ batch.reduce((s, r) => s + r.toolPct * r.count, 0) / count
1053
+ );
1054
+ const merged = {
1055
+ startRef: first.startRef,
1056
+ endRef: last.endRef,
1057
+ count,
1058
+ tokens,
1059
+ chars,
1060
+ toolPct,
1061
+ textPct: 100 - toolPct
1062
+ };
1063
+ if (batch.some((r) => r.dangerous === true)) {
1064
+ merged.dangerous = true;
1065
+ }
1066
+ return merged;
1067
+ }
1068
+ function rangeChars(r) {
1069
+ return r.chars ?? r.tokens * 4;
1070
+ }
1071
+ function mergeRangesToThreshold(ranges, minChars) {
1072
+ if (minChars <= 0 || ranges.length === 0) return ranges;
1073
+ const result = [];
1074
+ let batch = [];
1075
+ let batchChars = 0;
1076
+ for (const r of ranges) {
1077
+ batch.push(r);
1078
+ batchChars += rangeChars(r);
1079
+ if (batchChars >= minChars) {
1080
+ result.push(mergeBatch(batch));
1081
+ batch = [];
1082
+ batchChars = 0;
1083
+ }
1084
+ }
1085
+ if (batch.length > 0) {
1086
+ result.push(mergeBatch(batch));
1087
+ }
1088
+ return result;
1089
+ }
1021
1090
  function runPipeline(nodes, initial, ctx) {
1022
1091
  let io = initial;
1023
1092
  for (const node of nodes) {
@@ -1297,7 +1366,10 @@ var recommendNode = {
1297
1366
  const nothingToCompress = contextRanges.compressible.length === 0;
1298
1367
  const recommendation = {
1299
1368
  contextRanges,
1300
- recommendedRanges: contextRanges.compressible,
1369
+ recommendedRanges: mergeRangesToThreshold(
1370
+ contextRanges.compressible,
1371
+ ctx.config.compress.minCompressRange
1372
+ ),
1301
1373
  nothingToCompress
1302
1374
  };
1303
1375
  return { ...io, effects: { ...io.effects, recommendation } };
@@ -1323,6 +1395,7 @@ var nudgeNode = {
1323
1395
  if (baseline > 0 && ctx.tokenCount < baseline - nudgeGrowthTokens) {
1324
1396
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
1325
1397
  stamped.lastNudgeShownTokens = 0;
1398
+ stamped.lastShownByTier = {};
1326
1399
  }
1327
1400
  if (stamped.lastPerMessageNudgeTokens === 0) {
1328
1401
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
@@ -1591,10 +1664,11 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
1591
1664
  )
1592
1665
  );
1593
1666
  }
1594
- function pendingByTier(state, recommendation, countTokens) {
1667
+ function pendingByTier(state, recommendation, countTokens, minCompressRange) {
1595
1668
  const out = {};
1596
- const compressible = recommendation?.contextRanges.compressible ?? [];
1597
- out[1] = { pending: compressible.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1669
+ const merged = recommendation?.recommendedRanges ?? [];
1670
+ const effective = minCompressRange > 0 ? merged.filter((r) => (r.chars ?? r.tokens * 4) >= minCompressRange) : merged;
1671
+ out[1] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1598
1672
  const active = activeBlocks(state);
1599
1673
  const t1 = active.filter((b) => b.tier === 1);
1600
1674
  const t2 = active.filter((b) => b.tier === 2);
@@ -1609,6 +1683,7 @@ function decideNudge(input) {
1609
1683
  const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
1610
1684
  const overLimit = usage >= config.nudge.maxContextLimitPct;
1611
1685
  const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
1686
+ const pressure = overLimit || emergencyOverride;
1612
1687
  const baseline = state.nudge.lastPerMessageNudgeTokens;
1613
1688
  const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
1614
1689
  const hasPendingNudge = hadPendingNudge;
@@ -1620,44 +1695,67 @@ function decideNudge(input) {
1620
1695
  );
1621
1696
  const growthSinceReference = tokenCount - growthReference;
1622
1697
  const rec = recommendation;
1623
- const tiers = pendingByTier(state, rec, countTokens);
1698
+ const tiers = pendingByTier(
1699
+ state,
1700
+ rec,
1701
+ countTokens,
1702
+ config.compress.minCompressRange
1703
+ );
1704
+ const tier2Threshold = Math.round(
1705
+ nudgeGrowthTokens * (config.nudge.tier2GrowthMultiplier ?? 1.5)
1706
+ );
1624
1707
  let injectedTier = null;
1625
1708
  let injectedReason = "";
1626
1709
  const growthReady = growthSinceReference >= growthFloor;
1627
- if (!overLimit && growthReady) {
1628
- for (const tier of [1, 2, 3]) {
1629
- if (!config.tiers.enabled && tier > 1) break;
1630
- const info = tiers[tier];
1631
- if (!info || info.pending < nudgeGrowthTokens) continue;
1632
- const lastShown = state.nudge.lastShownByTier[tier] ?? 0;
1633
- const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1634
- if (!cadenceMet) continue;
1635
- injectedTier = tier;
1636
- 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)}%`;
1637
- break;
1710
+ const t1Eff = tiers[1]?.pending ?? 0;
1711
+ const t2Pen = tiers[2]?.pending ?? 0;
1712
+ const t3Pen = tiers[3]?.pending ?? 0;
1713
+ if (pressure) {
1714
+ const candidates = [1];
1715
+ if (config.tiers.enabled) {
1716
+ candidates.push(2, 3);
1717
+ }
1718
+ let best = null;
1719
+ let bestPending = 0;
1720
+ for (const t of candidates) {
1721
+ const p = tiers[t]?.pending ?? 0;
1722
+ if (p > bestPending) {
1723
+ bestPending = p;
1724
+ best = t;
1725
+ }
1638
1726
  }
1639
- } else if (overLimit) {
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 < config.compress.minCompressRange) continue;
1644
- injectedTier = tier;
1645
- 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}`;
1646
- break;
1727
+ if (best !== null && bestPending > 0) {
1728
+ injectedTier = best;
1729
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1730
+ 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)}%`;
1731
+ }
1732
+ } else if (growthReady) {
1733
+ if (t1Eff >= nudgeGrowthTokens) {
1734
+ injectedTier = 1;
1735
+ injectedReason = `T1 effective ${t1Eff} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%`;
1736
+ } else if (config.tiers.enabled && t2Pen >= tier2Threshold && t2Pen > t1Eff) {
1737
+ const lastShown = state.nudge.lastShownByTier[2] ?? 0;
1738
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1739
+ if (cadenceMet) {
1740
+ injectedTier = 2;
1741
+ 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)}%`;
1742
+ }
1743
+ } else if (config.tiers.enabled && t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff) {
1744
+ const lastShown = state.nudge.lastShownByTier[3] ?? 0;
1745
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1746
+ if (cadenceMet) {
1747
+ injectedTier = 3;
1748
+ 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)}%`;
1749
+ }
1647
1750
  }
1648
1751
  }
1649
- const shouldInject = injectedTier !== null || overLimit && (rec?.recommendedRanges?.length ?? 0) > 0;
1752
+ const shouldInject = injectedTier !== null;
1650
1753
  let reason;
1651
- if (emergencyOverride && injectedTier !== null) {
1652
- reason = injectedReason;
1653
- } else if (emergencyOverride) {
1654
- reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}% (no compressible content)`;
1655
- } else if (overLimit && injectedTier !== null) {
1656
- reason = injectedReason;
1657
- } else if (overLimit) {
1658
- reason = `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}% (no compressible content)`;
1659
- } else if (injectedTier !== null) {
1754
+ if (injectedTier !== null) {
1660
1755
  reason = injectedReason;
1756
+ } else if (pressure) {
1757
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1758
+ 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`;
1661
1759
  } else {
1662
1760
  const tiersList = [1, 2, 3];
1663
1761
  const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
@@ -1731,6 +1829,7 @@ function cloneState(state) {
1731
1829
  byRaw: { ...state.messageRefs.byRaw },
1732
1830
  byRef: { ...state.messageRefs.byRef }
1733
1831
  },
1832
+ tokenSnapshot: { ...state.tokenSnapshot ?? {} },
1734
1833
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
1735
1834
  stats: { ...state.stats },
1736
1835
  nextBlockId: state.nextBlockId,
@@ -2006,20 +2105,23 @@ ${lines.join("\n")}`;
2006
2105
  function renderNudgeText(decision, prompts = defaultPrompts) {
2007
2106
  const breakdownStr = formatBreakdown(decision.contextBreakdown);
2008
2107
  const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
2108
+ const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
2009
2109
  if (decision.tier !== null && decision.tier >= 2) {
2010
2110
  const isT2 = decision.tier === 2;
2011
2111
  const targets = decision.tierTargetBlocks ?? [];
2012
2112
  const blockList = formatTierTargetBlocks(targets);
2013
2113
  const startId = targets[0]?.blockId ?? "b1";
2014
2114
  const endId = targets[targets.length - 1]?.blockId ?? "b5";
2115
+ const voice = isEmergency ? "emergency" : "gentle";
2116
+ 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]`;
2015
2117
  return {
2016
- voice: "gentle",
2118
+ voice,
2017
2119
  text: [
2018
2120
  efficiencyNote(prompts),
2019
2121
  "",
2020
2122
  breakdownStr,
2021
2123
  "",
2022
- `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
2124
+ triggerLine,
2023
2125
  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.`,
2024
2126
  blockList,
2025
2127
  `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
@@ -2030,7 +2132,6 @@ function renderNudgeText(decision, prompts = defaultPrompts) {
2030
2132
  ].join("\n")
2031
2133
  };
2032
2134
  }
2033
- const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
2034
2135
  if (isEmergency) {
2035
2136
  return {
2036
2137
  voice: "emergency",
@@ -2601,7 +2702,20 @@ function resolveDelegate(adapter) {
2601
2702
  displayUsage: adapter.displayUsage ?? "separate"
2602
2703
  };
2603
2704
  }
2604
- function resolveConfig(adapter, liveContextLimit) {
2705
+ function mergeCompress(global, provider, model) {
2706
+ return {
2707
+ maxContextLimit: model?.maxContextLimit ?? provider?.maxContextLimit ?? global?.maxContextLimit,
2708
+ emergencyThresholdPercent: model?.emergencyThresholdPercent ?? provider?.emergencyThresholdPercent ?? global?.emergencyThresholdPercent,
2709
+ nudgeGrowthTokens: model?.nudgeGrowthTokens ?? provider?.nudgeGrowthTokens ?? global?.nudgeGrowthTokens
2710
+ };
2711
+ }
2712
+ function resolveCompress(compress, provider, modelId) {
2713
+ if (!compress) return {};
2714
+ const prov = provider ? compress.providers?.[provider] : void 0;
2715
+ const model = prov && modelId ? prov.models?.[modelId] : void 0;
2716
+ return mergeCompress(compress, prov, model);
2717
+ }
2718
+ function resolveConfig(adapter, liveContextLimit, provider, modelId) {
2605
2719
  const envLimit = process.env.ACP_MODEL_CONTEXT_LIMIT;
2606
2720
  const envLimitNum = envLimit ? Number(envLimit) : NaN;
2607
2721
  const FALLBACK_LIMIT = 15e4;
@@ -2611,14 +2725,14 @@ function resolveConfig(adapter, liveContextLimit) {
2611
2725
  preserveRecentMessages: adapter.preserveRecentMessages ?? 5,
2612
2726
  ...adapter.coreOverrides
2613
2727
  });
2614
- const c = adapter.compress;
2615
- if (c?.maxContextLimit !== void 0) config.nudge.maxContextLimitPct = parsePercent(c.maxContextLimit);
2616
- if (c?.emergencyThresholdPercent !== void 0) {
2728
+ const c = resolveCompress(adapter.compress, provider, modelId);
2729
+ if (c.maxContextLimit !== void 0) config.nudge.maxContextLimitPct = parsePercent(c.maxContextLimit);
2730
+ if (c.emergencyThresholdPercent !== void 0) {
2617
2731
  const pct2 = parsePercent(c.emergencyThresholdPercent);
2618
2732
  config.nudge.emergencyThresholdPct = pct2;
2619
2733
  config.truncate.threshold = pct2;
2620
2734
  }
2621
- if (c?.nudgeGrowthTokens !== void 0) {
2735
+ if (c.nudgeGrowthTokens !== void 0) {
2622
2736
  config.nudge.growthFloor = c.nudgeGrowthTokens;
2623
2737
  config.nudge.growthCap = c.nudgeGrowthTokens;
2624
2738
  }
@@ -2631,6 +2745,89 @@ function parsePercent(v) {
2631
2745
  return Number(s);
2632
2746
  }
2633
2747
 
2748
+ // src/density.ts
2749
+ var DENSITY_MIN = 0.5;
2750
+ var DENSITY_MAX = 2.5;
2751
+ var MIN_DELTA_EST = 50;
2752
+ var CONFIRM_RATIO = 0.2;
2753
+ var INITIAL_DENSITY = 1;
2754
+ var DensityEstimator = class {
2755
+ models = /* @__PURE__ */ new Map();
2756
+ /** 重置指定模型(模型切换/会话开始时调用)。 */
2757
+ resetModel(modelId) {
2758
+ this.models.delete(modelId);
2759
+ }
2760
+ /** 返回当前密度系数(未知模型返回初始 1)。 */
2761
+ densityFor(modelId) {
2762
+ return this.models.get(modelId)?.density ?? INITIAL_DENSITY;
2763
+ }
2764
+ /**
2765
+ * 每轮 context 事件调用。realTotal 为 provider 真实 usage(可空),
2766
+ * estTotal 为本地估算总 token。postCompression 为压缩刚发生标志。
2767
+ */
2768
+ update(modelId, realTotal, estTotal, postCompression = false) {
2769
+ if (realTotal === null) return;
2770
+ let est = this.models.get(modelId);
2771
+ if (!est) {
2772
+ est = {
2773
+ density: INITIAL_DENSITY,
2774
+ anchorReal: null,
2775
+ anchorEst: null,
2776
+ pendingDensity: null,
2777
+ confirmCount: 0,
2778
+ postCompressionSkip: false
2779
+ };
2780
+ this.models.set(modelId, est);
2781
+ }
2782
+ if (postCompression) {
2783
+ est.postCompressionSkip = true;
2784
+ return;
2785
+ }
2786
+ if (est.postCompressionSkip) {
2787
+ est.postCompressionSkip = false;
2788
+ est.anchorReal = realTotal;
2789
+ est.anchorEst = estTotal;
2790
+ est.pendingDensity = null;
2791
+ est.confirmCount = 0;
2792
+ return;
2793
+ }
2794
+ if (est.anchorReal === null || est.anchorEst === null) {
2795
+ est.anchorReal = realTotal;
2796
+ est.anchorEst = estTotal;
2797
+ return;
2798
+ }
2799
+ const dReal = realTotal - est.anchorReal;
2800
+ const dEst = estTotal - est.anchorEst;
2801
+ if (dEst < MIN_DELTA_EST) return;
2802
+ est.anchorReal = realTotal;
2803
+ est.anchorEst = estTotal;
2804
+ const instant = clamp(dReal / dEst, DENSITY_MIN, DENSITY_MAX);
2805
+ if (est.pendingDensity === null) {
2806
+ est.pendingDensity = instant;
2807
+ est.confirmCount = 1;
2808
+ } else if (Math.abs(instant - est.pendingDensity) / est.pendingDensity <= CONFIRM_RATIO) {
2809
+ est.confirmCount += 1;
2810
+ } else {
2811
+ est.pendingDensity = instant;
2812
+ est.confirmCount = 1;
2813
+ }
2814
+ if (est.confirmCount >= 2) {
2815
+ est.density = est.pendingDensity;
2816
+ est.confirmCount = 0;
2817
+ est.pendingDensity = null;
2818
+ }
2819
+ }
2820
+ /** 注入器:估算文本 token = defaultCountTokens × density。 */
2821
+ estimateWithDensity(modelId, text) {
2822
+ const d = this.densityFor(modelId);
2823
+ if (d === 1) return defaultCountTokens(text);
2824
+ return Math.round(defaultCountTokens(text) * d);
2825
+ }
2826
+ };
2827
+ function clamp(v, lo, hi) {
2828
+ return v < lo ? lo : v > hi ? hi : v;
2829
+ }
2830
+
2634
2831
  // src/messages.ts
2635
2832
  var REF_TAG_SOURCE = "(?:<acp\\s[^>]*>m\\d{5}</acp>|\\[m\\d{1,5}\\])";
2636
2833
  var REF_TAG = new RegExp(`^${REF_TAG_SOURCE}\\s?\\n?`);
@@ -3136,6 +3333,7 @@ function mergeInitialState(parsed) {
3136
3333
  return {
3137
3334
  blocks: parsed.blocks ?? fresh.blocks,
3138
3335
  messageRefs: parsed.messageRefs ?? fresh.messageRefs,
3336
+ tokenSnapshot: parsed.tokenSnapshot ?? fresh.tokenSnapshot,
3139
3337
  nudge: { ...fresh.nudge, ...parsed.nudge ?? {} },
3140
3338
  stats: { ...fresh.stats, ...parsed.stats ?? {} },
3141
3339
  nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,
@@ -3358,8 +3556,14 @@ function pruneOrphanRefs(state, messages) {
3358
3556
  }
3359
3557
  }
3360
3558
  function createRuntime(adapter) {
3361
- const core = createCore({ countTokens: defaultCountTokens });
3559
+ const density = new DensityEstimator();
3560
+ let countModelId = "default";
3561
+ const core = createCore({
3562
+ // 密度校准版 countTokens(Phase 2):默认回落 defaultCountTokens(density=1)
3563
+ countTokens: (text) => density.estimateWithDensity(countModelId, text)
3564
+ });
3362
3565
  const store = new SessionStateStore();
3566
+ const lastActiveBlockIds = /* @__PURE__ */ new Map();
3363
3567
  const locks = /* @__PURE__ */ new Map();
3364
3568
  let adapterRef = adapter;
3365
3569
  let promptsRef = defaultPrompts;
@@ -3384,7 +3588,8 @@ function createRuntime(adapter) {
3384
3588
  return m?.contextWindow ?? 0;
3385
3589
  }
3386
3590
  function configFor(ctx) {
3387
- return resolveConfig(adapterRef, liveContextLimit(ctx));
3591
+ const m = ctx.model;
3592
+ return resolveConfig(adapterRef, liveContextLimit(ctx), m?.provider, m?.id);
3388
3593
  }
3389
3594
  async function stateFor(ctx, liveMessages) {
3390
3595
  const sm = ctx.sessionManager;
@@ -3407,7 +3612,19 @@ function createRuntime(adapter) {
3407
3612
  const sm = ctx.sessionManager;
3408
3613
  await store.save(state, sm.getSessionFile() ?? void 0, sm.getSessionId());
3409
3614
  }
3410
- return { core, store, get adapter() {
3615
+ function noteActiveBlocks(sid, activeBlockIds) {
3616
+ const current = new Set(activeBlockIds);
3617
+ const prev = lastActiveBlockIds.get(sid);
3618
+ const isNew = prev !== void 0 && activeBlockIds.some((id) => !prev.has(id));
3619
+ lastActiveBlockIds.set(sid, current);
3620
+ return isNew;
3621
+ }
3622
+ function clearSessionTracking(sid) {
3623
+ lastActiveBlockIds.delete(sid);
3624
+ }
3625
+ return { core, store, density, setCountModel: (m) => {
3626
+ countModelId = m;
3627
+ }, noteActiveBlocks, clearSessionTracking, get adapter() {
3411
3628
  return adapterRef;
3412
3629
  }, setAdapter: (a) => {
3413
3630
  adapterRef = a;
@@ -7834,6 +8051,9 @@ function estimateTokens(messages, coveredIds) {
7834
8051
  }
7835
8052
  return tokens;
7836
8053
  }
8054
+ function calibrateTokens(estimate, density) {
8055
+ return density === 1 ? estimate : Math.round(estimate * density);
8056
+ }
7837
8057
  function lastUserMessageId(entries) {
7838
8058
  for (let i = entries.length - 1; i >= 0; i--) {
7839
8059
  const e = entries[i];
@@ -7842,6 +8062,23 @@ function lastUserMessageId(entries) {
7842
8062
  return void 0;
7843
8063
  }
7844
8064
 
8065
+ // src/compat.ts
8066
+ function normalizeSystemPrompt(input) {
8067
+ if (input === void 0) return "";
8068
+ if (Array.isArray(input)) return input.join("\n");
8069
+ return input;
8070
+ }
8071
+ function formatSystemPromptForEvent(base, append) {
8072
+ const normalized = normalizeSystemPrompt(base);
8073
+ return `${normalized}
8074
+
8075
+ ${append}`;
8076
+ }
8077
+ function getSystemPromptText(ctx) {
8078
+ const result = ctx.getSystemPrompt?.();
8079
+ return normalizeSystemPrompt(result);
8080
+ }
8081
+
7845
8082
  // src/compress-tool.ts
7846
8083
  function formatK2(n) {
7847
8084
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
@@ -7887,21 +8124,26 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7887
8124
  if (ranges.length === 0) return "No ranges provided.";
7888
8125
  const { state: initialState, coreMessages } = await runtime.stateFor(ctx);
7889
8126
  const config = runtime.configFor(ctx);
7890
- const estimatedTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState));
7891
- const realUsage = ctx.getContextUsage?.();
8127
+ const modelId = ctx.model?.id ?? "default";
8128
+ const systemPromptText = getSystemPromptText(ctx);
8129
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
8130
+ const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState)) + systemPromptTokens;
7892
8131
  const turn = runtime.core.processTurn({
7893
8132
  messages: coreMessages,
7894
8133
  state: initialState,
7895
8134
  config,
7896
- tokenCount: realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : estimatedTokens
8135
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
7897
8136
  });
7898
8137
  const state = turn.state;
7899
8138
  const messages = turn.messages;
7900
- const beforeTokens = estimateTokens(messages, collectCoveredMessageIds(state));
8139
+ const density = runtime.density.densityFor(modelId);
8140
+ const beforeTokens = calibrateTokens(estimateTokens(messages, collectCoveredMessageIds(state)), density);
7901
8141
  const summaryMaxChars = args.summaryMaxChars;
7902
8142
  const topLevelTopic = args.topic;
7903
8143
  debug.event("compress-in", {
7904
8144
  sid: ctx.sessionManager.getSessionId(),
8145
+ modelId,
8146
+ density,
7905
8147
  ranges: ranges.length,
7906
8148
  spans: ranges.map((r) => ({ span: `${r.startId}..${r.endId}`, summaryLen: r.summary.length, summary: r.summary, topic: r.topic ?? topLevelTopic ?? null })),
7907
8149
  blocksBefore: state.blocks.length,
@@ -7958,7 +8200,8 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7958
8200
 
7959
8201
  // src/decompress-tool.ts
7960
8202
  import { writeFile, mkdir } from "fs/promises";
7961
- import { resolve, relative, isAbsolute, join as join3 } from "path";
8203
+ import { existsSync as existsSync2, lstatSync, readlinkSync, realpathSync } from "fs";
8204
+ import { resolve, relative, isAbsolute, join as join3, basename as basename2, dirname as dirname3 } from "path";
7962
8205
  import { tmpdir, homedir as homedir2 } from "os";
7963
8206
  var AUTO_DIR = join3(homedir2() || tmpdir(), ".cache", "pi", "acp-decompress");
7964
8207
  var PREVIEW_CHARS = 600;
@@ -8002,14 +8245,39 @@ var ALLOWED_DIRS = [
8002
8245
  function resolveToFilePath(targetPath) {
8003
8246
  const expanded = targetPath.startsWith("~/") ? join3(homedir2(), targetPath.slice(2)) : targetPath;
8004
8247
  const resolved = resolve(expanded);
8005
- const isAllowed = ALLOWED_DIRS.some((dir) => {
8006
- const rel = relative(dir, resolved);
8248
+ let probe = resolved;
8249
+ const suffix = [];
8250
+ while (!existsSync2(probe) && probe !== dirname3(probe)) {
8251
+ suffix.unshift(basename2(probe));
8252
+ probe = dirname3(probe);
8253
+ }
8254
+ const real = existsSync2(probe) ? realpathSync(probe) : probe;
8255
+ let checked = real;
8256
+ for (const part of suffix) {
8257
+ checked = join3(checked, part);
8258
+ try {
8259
+ if (lstatSync(checked).isSymbolicLink()) {
8260
+ const target = readlinkSync(checked);
8261
+ checked = isAbsolute(target) ? resolve(target) : resolve(dirname3(checked), target);
8262
+ }
8263
+ } catch {
8264
+ }
8265
+ }
8266
+ const allowed = ALLOWED_DIRS.map((d) => {
8267
+ try {
8268
+ return realpathSync(d);
8269
+ } catch {
8270
+ return d;
8271
+ }
8272
+ });
8273
+ const isAllowed = allowed.some((dir) => {
8274
+ const rel = relative(dir, checked);
8007
8275
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
8008
8276
  });
8009
8277
  if (!isAllowed) {
8010
8278
  return { error: `Error: toFile path must be under ${tmpdir()}, ~/.cache/opencode, or ~/.cache/pi. Got: ${targetPath}` };
8011
8279
  }
8012
- return resolved;
8280
+ return checked;
8013
8281
  }
8014
8282
  function autoFilePath(blockId) {
8015
8283
  return join3(AUTO_DIR, `${blockId}-${Date.now()}.txt`);
@@ -8075,7 +8343,7 @@ ${text}`;
8075
8343
  }
8076
8344
  async function handleDecompress(args, runtime, ctx) {
8077
8345
  const { state, coreMessages } = await runtime.stateFor(ctx);
8078
- const arg = args.blockId.trim();
8346
+ const arg = (args.blockId ?? "").trim();
8079
8347
  const owner = state.blocks.find((b) => b.effectiveMessageIds.includes(arg));
8080
8348
  if (owner) {
8081
8349
  return handleMessageRef(arg, owner.blockId, args, ctx);
@@ -8247,20 +8515,125 @@ function formatSize(tokens) {
8247
8515
  return `${(tokens / 1e6).toFixed(1)}M`;
8248
8516
  }
8249
8517
 
8518
+ // node_modules/billion-context-kit/dist/index.js
8519
+ var VIABLE_RANGE_MIN_TOKENS = 200;
8520
+ function viableRanges(ranges) {
8521
+ return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
8522
+ }
8523
+ function topicFallback(summary) {
8524
+ const first = summary.split(/[.\n]/)[0] ?? "";
8525
+ const t = first.trim().replace(/^["'`]+/, "").trim();
8526
+ return t.length <= 30 ? t : `${t.slice(0, 30).trimEnd()}\u2026`;
8527
+ }
8528
+ function formatCompactTokens(count) {
8529
+ if (count < 1e3) return count.toString();
8530
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
8531
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
8532
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
8533
+ return `${Math.round(count / 1e6)}M`;
8534
+ }
8535
+ function bar(value, total, width = 20) {
8536
+ if (total === 0) return "";
8537
+ const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
8538
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
8539
+ }
8540
+ function buildStatusPanel(input) {
8541
+ const { tokenCount, state, nudge, modelContextLimit } = input;
8542
+ const fmt2 = input.fmtTokens ?? formatCompactTokens;
8543
+ const bd = nudge?.contextBreakdown;
8544
+ const limit = modelContextLimit;
8545
+ const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
8546
+ const systemPromptTokens = input.systemPromptTokens;
8547
+ const sentTotal = classified + systemPromptTokens;
8548
+ const sessionOnly = input.unprunedTokens !== void 0 ? Math.max(0, input.unprunedTokens - sentTotal) : 0;
8549
+ const displayTotal = tokenCount;
8550
+ const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
8551
+ const sentPct = limit > 0 ? Math.round(sentTotal / limit * 100) : 0;
8552
+ const activeBlocksList = state.blocks.filter((b) => b.active);
8553
+ const totalBlocksList = state.blocks;
8554
+ const lines = [];
8555
+ lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
8556
+ lines.push("\u2502 ACP Context Analysis \u2502");
8557
+ lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
8558
+ if (input.version) lines.push(input.version);
8559
+ lines.push("");
8560
+ lines.push(`Context (session accounting, host footer scale): ${displayPct}% (${fmt2(displayTotal)} / ${fmt2(limit)}) \u2014 never shrinks; includes compressed originals`);
8561
+ if (nudge && bd) {
8562
+ const growth = bd.growth;
8563
+ if (growth > 0 && displayTotal > 0) {
8564
+ lines.push(`Growth: +${fmt2(growth)} since last nudge`);
8565
+ }
8566
+ lines.push("");
8567
+ lines.push(`Sent to LLM (after compression, est.): ${fmt2(sentTotal)}${limit > 0 ? ` (${sentPct}% of limit)` : ""}`);
8568
+ if (input.unprunedTokens !== void 0 && sessionOnly > 0) {
8569
+ lines.push(`Session-only (compressed originals, est.): ${fmt2(sessionOnly)} \u2014 pruned from every request; the footer/nudge still count them`);
8570
+ }
8571
+ lines.push("");
8572
+ lines.push("Token Breakdown (sent view):");
8573
+ const categories = [
8574
+ { label: "Tool", value: bd.tool },
8575
+ { label: "SysPrompt", value: systemPromptTokens },
8576
+ { label: "Text", value: bd.text },
8577
+ { label: "Code", value: bd.code },
8578
+ { label: "Summaries", value: bd.summaries }
8579
+ ];
8580
+ for (const cat of categories) {
8581
+ if (cat.value <= 0) continue;
8582
+ const pct2 = sentTotal > 0 ? Math.round(cat.value / sentTotal * 100) : 0;
8583
+ const b = bar(cat.value, sentTotal);
8584
+ lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmt2(cat.value)}`);
8585
+ }
8586
+ }
8587
+ lines.push("");
8588
+ if (nudge) {
8589
+ if (nudge.shouldInject) {
8590
+ const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
8591
+ lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
8592
+ } else {
8593
+ lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
8594
+ }
8595
+ }
8596
+ const ranges = viableRanges(nudge?.compressibleRanges ?? []);
8597
+ const protectedRanges = nudge?.protectedRanges ?? [];
8598
+ if (ranges.length > 0 || protectedRanges.length > 0) {
8599
+ lines.push("");
8600
+ lines.push(formatRanges(ranges, protectedRanges));
8601
+ }
8602
+ if (activeBlocksList.length > 0) {
8603
+ lines.push("");
8604
+ lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8605
+ for (const b of activeBlocksList) {
8606
+ const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
8607
+ const summaryTok = defaultCountTokens(b.summary || "");
8608
+ const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
8609
+ lines.push(` [${b.blockId}] T${b.tier} ${fmt2(origTok)}\u2192${fmt2(summaryTok)}${topic}`);
8610
+ }
8611
+ } else if (totalBlocksList.length > 0) {
8612
+ lines.push("");
8613
+ lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8614
+ } else {
8615
+ lines.push("");
8616
+ lines.push("Blocks: none (nothing compressed yet)");
8617
+ }
8618
+ lines.push("");
8619
+ lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
8620
+ return lines.join("\n");
8621
+ }
8622
+
8250
8623
  // src/delegate-tool.ts
8251
8624
  import {
8252
8625
  spawn
8253
8626
  } from "child_process";
8254
- import { createWriteStream, existsSync as existsSync2 } from "fs";
8627
+ import { createWriteStream, existsSync as existsSync3 } from "fs";
8255
8628
  import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
8256
8629
  import { tmpdir as tmpdir2 } from "os";
8257
- import { dirname as dirname3, join as join4, resolve as resolvePath } from "path";
8630
+ import { dirname as dirname4, join as join4, resolve as resolvePath } from "path";
8258
8631
 
8259
8632
  // src/footer-status.ts
8260
8633
  var FOOTER_STATUS_KEY = "billion-context-pi";
8261
8634
  var ui;
8262
8635
  var lastFooterText = "";
8263
- function formatCompactTokens(count) {
8636
+ function formatCompactTokens2(count) {
8264
8637
  if (count < 1e3) return count.toString();
8265
8638
  if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
8266
8639
  if (count < 1e6) return `${Math.round(count / 1e3)}k`;
@@ -8277,7 +8650,7 @@ function updateFooterStatus() {
8277
8650
  let text;
8278
8651
  if (usage && usage.totalTokens > 0) {
8279
8652
  const costStr = usage.cost.total > 0 ? ` ($${usage.cost.total.toFixed(4)})` : "";
8280
- text = `sub-agents \u2191${formatCompactTokens(usage.input)} \u2193${formatCompactTokens(usage.output)}${costStr}`;
8653
+ text = `sub-agents \u2191${formatCompactTokens2(usage.input)} \u2193${formatCompactTokens2(usage.output)}${costStr}`;
8281
8654
  }
8282
8655
  if ((text ?? "") === lastFooterText) return;
8283
8656
  lastFooterText = text ?? "";
@@ -8670,11 +9043,11 @@ function delegateSpawnOptions(cwd, env) {
8670
9043
  var PI_CLI_ENTRY_RE = /[\\/]pi-coding-agent[\\/]dist[\\/]cli\.js$/;
8671
9044
  var PI_PACKAGE_REL = join4("@earendil-works", "pi-coding-agent", "dist", "cli.js");
8672
9045
  function probeUpFromArgv(argv1) {
8673
- let dir = resolvePath(dirname3(argv1) || process.cwd());
9046
+ let dir = resolvePath(dirname4(argv1) || process.cwd());
8674
9047
  for (; ; ) {
8675
9048
  const candidate = join4(dir, "node_modules", PI_PACKAGE_REL);
8676
- if (existsSync2(candidate)) return candidate;
8677
- const parent = dirname3(dir);
9049
+ if (existsSync3(candidate)) return candidate;
9050
+ const parent = dirname4(dir);
8678
9051
  if (parent === dir) return null;
8679
9052
  dir = parent;
8680
9053
  }
@@ -8699,7 +9072,7 @@ function resolvePiCliEntry(argv1, env = process.env, piHost = true) {
8699
9072
  const probed = probeUpFromArgv(argv1);
8700
9073
  if (probed) return probed;
8701
9074
  for (const candidate of piCliGlobalCandidates(env)) {
8702
- if (existsSync2(candidate)) return candidate;
9075
+ if (existsSync3(candidate)) return candidate;
8703
9076
  }
8704
9077
  logWarn("delegate", { event: "cli-entry-unresolved", argv1, fallback: "argv[1]" });
8705
9078
  }
@@ -9499,13 +9872,16 @@ function makeStatusTool(runtime) {
9499
9872
  async function handleStatus(args, runtime, ctx) {
9500
9873
  const { state, coreMessages } = await runtime.stateFor(ctx);
9501
9874
  const config = runtime.configFor(ctx);
9502
- const tokenCount = estimateTokens(coreMessages, collectCoveredMessageIds(state));
9503
- const realUsage = ctx.getContextUsage?.();
9875
+ const coveredIds = collectCoveredMessageIds(state);
9876
+ const modelId = ctx.model?.id ?? "default";
9877
+ const systemPromptText = getSystemPromptText(ctx);
9878
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
9879
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
9504
9880
  const turn = runtime.core.processTurn({
9505
9881
  messages: coreMessages,
9506
9882
  state,
9507
9883
  config,
9508
- tokenCount: realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : tokenCount
9884
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
9509
9885
  });
9510
9886
  const processed = turn.messages;
9511
9887
  const base = buildStatusReport(turn.state, processed, defaultCountTokens, {
@@ -9517,7 +9893,7 @@ async function handleStatus(args, runtime, ctx) {
9517
9893
  });
9518
9894
  if (args.scope) return base;
9519
9895
  const nudge = turn.nudge;
9520
- const ranges = nudge?.compressibleRanges ?? [];
9896
+ const ranges = viableRanges(nudge?.compressibleRanges ?? []);
9521
9897
  const protectedRanges = nudge?.protectedRanges ?? [];
9522
9898
  const extra = [];
9523
9899
  if (nudge) {
@@ -9548,23 +9924,6 @@ async function handleStatus(args, runtime, ctx) {
9548
9924
  ${extra.join("\n")}` : base;
9549
9925
  }
9550
9926
 
9551
- // src/compat.ts
9552
- function normalizeSystemPrompt(input) {
9553
- if (input === void 0) return "";
9554
- if (Array.isArray(input)) return input.join("\n");
9555
- return input;
9556
- }
9557
- function formatSystemPromptForEvent(base, append) {
9558
- const normalized = normalizeSystemPrompt(base);
9559
- return `${normalized}
9560
-
9561
- ${append}`;
9562
- }
9563
- function getSystemPromptText(ctx) {
9564
- const result = ctx.getSystemPrompt?.();
9565
- return normalizeSystemPrompt(result);
9566
- }
9567
-
9568
9927
  // src/commands.ts
9569
9928
  function makeCommands(runtime) {
9570
9929
  return [
@@ -9632,106 +9991,35 @@ ${text}`);
9632
9991
  }
9633
9992
  ];
9634
9993
  }
9635
- function fmtTokens(n) {
9636
- return formatCompactTokens(n);
9637
- }
9638
- function bar(value, total, width = 20) {
9639
- if (total === 0) return "";
9640
- const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
9641
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
9642
- }
9643
9994
  async function statusReport(runtime, ctx) {
9644
9995
  const { state, coreMessages } = await runtime.stateFor(ctx);
9645
9996
  const config = runtime.configFor(ctx);
9646
9997
  const realUsage = ctx.getContextUsage?.();
9647
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
9648
- const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
9649
- const nudge = turn.nudge;
9650
- const bd = nudge?.contextBreakdown;
9651
- const limit = config.modelContextLimit;
9652
- const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
9653
9998
  const systemPromptText = getSystemPromptText(ctx);
9654
9999
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
9655
- const framework = bd ? Math.max(0, tokenCount - classified - systemPromptTokens) : 0;
9656
- const displayTotal = tokenCount;
9657
- const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
9658
- const activeBlocksList = state.blocks.filter((b) => b.active);
9659
- const totalBlocksList = state.blocks;
9660
- const lines = [];
9661
- const versionStr = "0.1.37" ? `billion-context-pi@${"0.1.37"}` : "";
9662
- lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
9663
- lines.push("\u2502 ACP Context Analysis \u2502");
9664
- lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
9665
- if (versionStr) lines.push(versionStr);
9666
- lines.push("");
9667
- lines.push(`Context: ${displayPct}% (${fmtTokens(displayTotal)} / ${fmtTokens(limit)})`);
9668
- if (nudge && bd) {
9669
- const growth = bd.growth;
9670
- if (growth > 0 && displayTotal > 0) {
9671
- lines.push(`Growth: +${fmtTokens(growth)} since last nudge`);
9672
- }
9673
- if (displayTotal > 0) {
9674
- lines.push("");
9675
- lines.push("Token Breakdown:");
9676
- const categories = [
9677
- { label: "Tool", value: bd.tool },
9678
- { label: "SysPrompt", value: systemPromptTokens },
9679
- { label: "Framework", value: framework },
9680
- { label: "Text", value: bd.text },
9681
- { label: "Code", value: bd.code },
9682
- { label: "Summaries", value: bd.summaries }
9683
- ];
9684
- for (const cat of categories) {
9685
- if (cat.value <= 0) continue;
9686
- const pct2 = displayTotal > 0 ? Math.round(cat.value / displayTotal * 100) : 0;
9687
- const b = bar(cat.value, displayTotal);
9688
- lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmtTokens(cat.value)}`);
9689
- }
9690
- }
9691
- }
9692
- lines.push("");
9693
- if (nudge) {
9694
- if (nudge.shouldInject) {
9695
- const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
9696
- lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
9697
- } else {
9698
- lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
9699
- }
9700
- }
9701
- const ranges = nudge?.compressibleRanges ?? [];
9702
- const protectedRanges = nudge?.protectedRanges ?? [];
9703
- if (ranges.length > 0 || protectedRanges.length > 0) {
9704
- lines.push("");
9705
- lines.push(formatRanges(ranges, protectedRanges));
9706
- }
9707
- if (activeBlocksList.length > 0) {
9708
- lines.push("");
9709
- lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
9710
- for (const b of activeBlocksList) {
9711
- const topic = b.topic ? `: ${b.topic}` : "";
9712
- const summaryTok = defaultCountTokens(b.summary || "");
9713
- const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
9714
- lines.push(` [${b.blockId}] T${b.tier} ${fmtTokens(origTok)}\u2192${fmtTokens(summaryTok)}${topic}`);
9715
- }
9716
- } else if (totalBlocksList.length > 0) {
9717
- lines.push("");
9718
- lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
9719
- } else {
9720
- lines.push("");
9721
- lines.push("Blocks: none (nothing compressed yet)");
9722
- }
9723
- lines.push("");
10000
+ const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
10001
+ const coveredIds = collectCoveredMessageIds(state);
10002
+ const modelId = ctx.model?.id ?? "default";
10003
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10004
+ const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId)) });
10005
+ const versionStr = "0.1.39" ? `billion-context-pi@${"0.1.39"}` : void 0;
10006
+ let text = buildStatusPanel({
10007
+ version: versionStr,
10008
+ tokenCount: sessionTokens,
10009
+ systemPromptTokens,
10010
+ state: turn.state,
10011
+ nudge: turn.nudge,
10012
+ modelContextLimit: config.modelContextLimit,
10013
+ unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0)
10014
+ });
9724
10015
  const delegateUsage = getDelegateUsage();
9725
10016
  if (delegateUsage && delegateUsage.totalTokens > 0) {
9726
- lines.push("");
9727
10017
  const cost = delegateUsage.cost.total;
9728
10018
  const costStr = cost > 0 ? ` ($${cost.toFixed(4)})` : "";
9729
- lines.push("\u2500\u2500 Session delegate usage (excluded from main totals) \u2500\u2500");
9730
- lines.push(`Tokens: ${delegateUsage.input.toLocaleString()} in, ${delegateUsage.output.toLocaleString()} out (${delegateUsage.totalTokens.toLocaleString()} total)${costStr}`);
10019
+ text += "\n\n\u2500\u2500 Session delegate usage (excluded from main totals) \u2500\u2500\n";
10020
+ text += `Tokens: ${delegateUsage.input.toLocaleString()} in, ${delegateUsage.output.toLocaleString()} out (${delegateUsage.totalTokens.toLocaleString()} total)${costStr}`;
9731
10021
  }
9732
- lines.push("");
9733
- lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
9734
- return lines.join("\n");
10022
+ return text;
9735
10023
  }
9736
10024
 
9737
10025
  // src/system-prompt.ts
@@ -9927,7 +10215,7 @@ function wireToolGuardrails(pi, runtime) {
9927
10215
 
9928
10216
  // src/update.ts
9929
10217
  import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
9930
- import { join as join5, dirname as dirname4 } from "path";
10218
+ import { join as join5, dirname as dirname5 } from "path";
9931
10219
  import { fileURLToPath } from "url";
9932
10220
  import { execFile } from "child_process";
9933
10221
  import { homedir as homedir3 } from "os";
@@ -9960,7 +10248,7 @@ async function readLastCheck() {
9960
10248
  }
9961
10249
  async function writeLastCheck(timestamp) {
9962
10250
  try {
9963
- await mkdir3(dirname4(THROTTLE_FILE), { recursive: true });
10251
+ await mkdir3(dirname5(THROTTLE_FILE), { recursive: true });
9964
10252
  await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
9965
10253
  } catch {
9966
10254
  }
@@ -9974,20 +10262,20 @@ async function readPackageJson(path4) {
9974
10262
  }
9975
10263
  }
9976
10264
  function findNpmRoot(extDir) {
9977
- let dir = dirname4(extDir);
10265
+ let dir = dirname5(extDir);
9978
10266
  for (; ; ) {
9979
- if (dir.endsWith("node_modules")) return dirname4(dir);
9980
- const parent = dirname4(dir);
10267
+ if (dir.endsWith("node_modules")) return dirname5(dir);
10268
+ const parent = dirname5(dir);
9981
10269
  if (parent === dir) return void 0;
9982
10270
  dir = parent;
9983
10271
  }
9984
10272
  }
9985
10273
  async function findExtensionDir() {
9986
- let dir = dirname4(fileURLToPath(import.meta.url));
10274
+ let dir = dirname5(fileURLToPath(import.meta.url));
9987
10275
  for (; ; ) {
9988
10276
  const pkg = await readPackageJson(join5(dir, "package.json"));
9989
10277
  if (pkg?.name === PACKAGE_NAME) return dir;
9990
- const parent = dirname4(dir);
10278
+ const parent = dirname5(dir);
9991
10279
  if (parent === dir) return void 0;
9992
10280
  dir = parent;
9993
10281
  }
@@ -10036,7 +10324,7 @@ async function checkForUpdate(autoUpdate, notify) {
10036
10324
  const data = await res.json();
10037
10325
  const latest = data.version;
10038
10326
  if (!latest) return;
10039
- const current = runtimeVersion ?? "0.1.37";
10327
+ const current = runtimeVersion ?? "0.1.39";
10040
10328
  const hasUpdate = isNewer(latest, current);
10041
10329
  debug.event("update-check", {
10042
10330
  current,
@@ -10072,7 +10360,7 @@ async function getRuntimeVersion() {
10072
10360
 
10073
10361
  // src/setup-subagent-tools.ts
10074
10362
  import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
10075
- import { existsSync as existsSync3 } from "fs";
10363
+ import { existsSync as existsSync4 } from "fs";
10076
10364
  import { homedir as homedir4 } from "os";
10077
10365
  import { join as join6 } from "path";
10078
10366
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
@@ -10137,7 +10425,7 @@ async function ensureSubagentAcpTools(settingsPath) {
10137
10425
  return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
10138
10426
  }
10139
10427
  const backupPath = `${path4}.acp-bak`;
10140
- if (!existsSync3(backupPath)) {
10428
+ if (!existsSync4(backupPath)) {
10141
10429
  try {
10142
10430
  await copyFile(path4, backupPath);
10143
10431
  } catch {
@@ -10284,10 +10572,14 @@ function wireSessionLifecycle(pi, runtime) {
10284
10572
  pi.on("session_start", async (_event, ctx) => {
10285
10573
  runtime.store.invalidate();
10286
10574
  runtime.clearNudgeTracking();
10575
+ const modelId = ctx.model?.id ?? "default";
10576
+ runtime.density.resetModel(modelId);
10287
10577
  resetDelegateUsage();
10288
10578
  setDelegateDisplayUsage("separate");
10289
10579
  const sid = ctx.sessionManager.getSessionId();
10290
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.37" : null });
10580
+ runtime.clearSessionTracking(sid);
10581
+ const modelInfo = ctx.model;
10582
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.39" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
10291
10583
  try {
10292
10584
  const user = await loadUserConfig(ctx.cwd);
10293
10585
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -10323,29 +10615,39 @@ function wireContextTransform(pi, runtime) {
10323
10615
  const sid = ctx.sessionManager.getSessionId();
10324
10616
  const release = await runtime.acquireLock(sid);
10325
10617
  try {
10618
+ const modelId = ctx.model?.id ?? "default";
10619
+ runtime.setCountModel(modelId);
10326
10620
  const { state, coreMessages, entries } = await runtime.stateFor(ctx, event.messages);
10327
10621
  const config = runtime.configFor(ctx);
10328
10622
  const coveredIds = collectCoveredMessageIds(state);
10329
10623
  const realUsage = ctx.getContextUsage?.();
10330
- const estimated = estimateTokens(coreMessages, coveredIds);
10331
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : estimated;
10624
+ const systemPromptText = getSystemPromptText(ctx);
10625
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10626
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10627
+ const tokenCount = calibrateTokens(sentTokens, runtime.density.densityFor(modelId));
10628
+ const postCompression = runtime.noteActiveBlocks(
10629
+ sid,
10630
+ state.blocks.filter((b) => b.active).map((b) => b.blockId)
10631
+ );
10332
10632
  debug.event("context-in", {
10333
10633
  sid,
10634
+ modelId,
10635
+ density: runtime.density.densityFor(modelId),
10334
10636
  eventMsgs: event.messages?.length ?? 0,
10335
10637
  entries: entries.length,
10336
10638
  coreMsgs: coreMessages.length,
10337
10639
  tokenCount,
10338
- estimatedTokens: estimated,
10339
- realTokens: realUsage?.tokens ?? null,
10340
- realPercent: realUsage?.percent ?? null,
10640
+ sessionTokens: realUsage?.tokens ?? null,
10341
10641
  limit: config.modelContextLimit,
10342
10642
  blocksBefore: state.blocks.length,
10343
10643
  activeBefore: state.blocks.filter((b) => b.active).length
10344
10644
  });
10345
10645
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
10346
10646
  await runtime.save(turn.state, ctx);
10647
+ runtime.density.update(modelId, realUsage?.tokens ?? null, sentTokens, postCompression);
10347
10648
  logInfo("turn", {
10348
10649
  sid,
10650
+ model: ctx.model?.id ?? null,
10349
10651
  inMsgs: coreMessages.length,
10350
10652
  outMsgs: turn.messages.length,
10351
10653
  tokens: tokenCount,
@@ -10357,6 +10659,8 @@ function wireContextTransform(pi, runtime) {
10357
10659
  activeBlocks: turn.state.blocks.filter((b) => b.active).length
10358
10660
  });
10359
10661
  debug.event("processTurn", {
10662
+ modelId,
10663
+ density: runtime.density.densityFor(modelId),
10360
10664
  outMsgs: turn.messages.length,
10361
10665
  summaryMsgs: turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
10362
10666
  prunedMsgs: coreMessages.length - turn.messages.length + turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
@@ -10376,6 +10680,7 @@ function wireContextTransform(pi, runtime) {
10376
10680
  const debugOn2 = debug.enabled;
10377
10681
  if (turn.nudge?.shouldInject) {
10378
10682
  const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
10683
+ turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
10379
10684
  const turnKey = lastUserMessageId(entries) ?? sid;
10380
10685
  const alreadyShown = !emergency && runtime.nudgeShownFor(turnKey);
10381
10686
  if (!alreadyShown) {