billion-context-pi 0.1.38 → 0.1.40

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) {
@@ -724,7 +737,7 @@ var TAG_CLOSE = LT + "/acp" + GT;
724
737
  function acpTag(ref, tokens, type) {
725
738
  return TAG_OPEN + 'tokens="' + formatTokens(tokens) + '" type="' + type + '"' + GT + ref + TAG_CLOSE;
726
739
  }
727
- function renderMessage(message, map, countTokens, strategy) {
740
+ function renderMessage(message, map, countTokens, strategy, snapshot = null) {
728
741
  const ref = refForRaw(map, message.id);
729
742
  if (!ref || ref === BLOCKED_REF) return message;
730
743
  if (strategy === "none") return message;
@@ -735,26 +748,33 @@ function renderMessage(message, map, countTokens, strategy) {
735
748
  "^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
736
749
  );
737
750
  const cleanText = (message.text || "").replace(ownTagRe, "");
738
- const tokens = countTokens(cleanText);
751
+ const tokens = snapshot ? snapshot[ref] ?? (snapshot[ref] = countTokens(cleanText)) : countTokens(cleanText);
739
752
  const type = classifyType(message);
740
753
  const prefix = acpTag(ref, tokens, type) + "\n";
741
754
  if (!cleanText) return { ...message, text: prefix };
742
755
  return { ...message, text: prefix + cleanText };
743
756
  }
744
- 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") {
745
758
  const map = state.messageRefs;
746
- return messages.map(
747
- (message) => renderMessage(message, map, countTokens, strategy)
759
+ const snapshot = { ...state.tokenSnapshot ?? {} };
760
+ const rendered = messages.map(
761
+ (message) => renderMessage(message, map, countTokens, strategy, snapshot)
748
762
  );
763
+ return { messages: rendered, tokenSnapshot: snapshot };
749
764
  }
750
765
  function createRenderRefsNode(strategy) {
751
766
  return {
752
767
  name: "render-refs",
753
768
  run(io, ctx) {
754
- return {
755
- ...io,
756
- messages: renderVisibleRefs(io.messages, io.state, ctx.countTokens, strategy)
757
- };
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 };
758
778
  }
759
779
  };
760
780
  }
@@ -950,6 +970,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
950
970
  ref,
951
971
  refNum: rn,
952
972
  tokens: countTokens(msg.text ?? ""),
973
+ chars: (msg.text ?? "").length,
953
974
  isTool: isToolMessage(msg),
954
975
  isUser: msg.role === "user"
955
976
  });
@@ -970,6 +991,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
970
991
  endRef: info.ref,
971
992
  count: 1,
972
993
  tokens: info.tokens,
994
+ chars: info.chars,
973
995
  toolPct: info.isTool ? 100 : 0,
974
996
  textPct: info.isTool ? 0 : 100
975
997
  };
@@ -977,6 +999,7 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
977
999
  cur.endRef = info.ref;
978
1000
  cur.count++;
979
1001
  cur.tokens += info.tokens;
1002
+ cur.chars = (cur.chars ?? 0) + info.chars;
980
1003
  if (info.isTool) {
981
1004
  cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
982
1005
  } else {
@@ -1024,6 +1047,7 @@ function mergeBatch(batch) {
1024
1047
  const last = batch[batch.length - 1];
1025
1048
  const count = batch.reduce((s, r) => s + r.count, 0);
1026
1049
  const tokens = batch.reduce((s, r) => s + r.tokens, 0);
1050
+ const chars = batch.reduce((s, r) => s + rangeChars(r), 0);
1027
1051
  const toolPct = Math.round(
1028
1052
  batch.reduce((s, r) => s + r.toolPct * r.count, 0) / count
1029
1053
  );
@@ -1032,6 +1056,7 @@ function mergeBatch(batch) {
1032
1056
  endRef: last.endRef,
1033
1057
  count,
1034
1058
  tokens,
1059
+ chars,
1035
1060
  toolPct,
1036
1061
  textPct: 100 - toolPct
1037
1062
  };
@@ -1040,16 +1065,21 @@ function mergeBatch(batch) {
1040
1065
  }
1041
1066
  return merged;
1042
1067
  }
1068
+ function rangeChars(r) {
1069
+ return r.chars ?? r.tokens * 4;
1070
+ }
1043
1071
  function mergeRangesToThreshold(ranges, minChars) {
1044
1072
  if (minChars <= 0 || ranges.length === 0) return ranges;
1045
1073
  const result = [];
1046
1074
  let batch = [];
1075
+ let batchChars = 0;
1047
1076
  for (const r of ranges) {
1048
1077
  batch.push(r);
1049
- const batchTokens = batch.reduce((s, x) => s + x.tokens, 0);
1050
- if (batchTokens * 4 >= minChars) {
1078
+ batchChars += rangeChars(r);
1079
+ if (batchChars >= minChars) {
1051
1080
  result.push(mergeBatch(batch));
1052
1081
  batch = [];
1082
+ batchChars = 0;
1053
1083
  }
1054
1084
  }
1055
1085
  if (batch.length > 0) {
@@ -1637,7 +1667,7 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
1637
1667
  function pendingByTier(state, recommendation, countTokens, minCompressRange) {
1638
1668
  const out = {};
1639
1669
  const merged = recommendation?.recommendedRanges ?? [];
1640
- const effective = minCompressRange > 0 ? merged.filter((r) => r.tokens * 4 >= minCompressRange) : merged;
1670
+ const effective = minCompressRange > 0 ? merged.filter((r) => (r.chars ?? r.tokens * 4) >= minCompressRange) : merged;
1641
1671
  out[1] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1642
1672
  const active = activeBlocks(state);
1643
1673
  const t1 = active.filter((b) => b.tier === 1);
@@ -1799,6 +1829,7 @@ function cloneState(state) {
1799
1829
  byRaw: { ...state.messageRefs.byRaw },
1800
1830
  byRef: { ...state.messageRefs.byRef }
1801
1831
  },
1832
+ tokenSnapshot: { ...state.tokenSnapshot ?? {} },
1802
1833
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
1803
1834
  stats: { ...state.stats },
1804
1835
  nextBlockId: state.nextBlockId,
@@ -2464,9 +2495,9 @@ function charBigrams(text) {
2464
2495
  }
2465
2496
  return grams;
2466
2497
  }
2467
- function tfMap(text, stem22) {
2498
+ function tfMap(text, stem2) {
2468
2499
  const m = /* @__PURE__ */ new Map();
2469
- for (const t of tokenize(text, { stem: stem22 })) m.set(t, (m.get(t) ?? 0) + 1);
2500
+ for (const t of tokenize(text, { stem: stem2 })) m.set(t, (m.get(t) ?? 0) + 1);
2470
2501
  return m;
2471
2502
  }
2472
2503
  var bm25Algorithm = {
@@ -2671,7 +2702,20 @@ function resolveDelegate(adapter) {
2671
2702
  displayUsage: adapter.displayUsage ?? "separate"
2672
2703
  };
2673
2704
  }
2674
- 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) {
2675
2719
  const envLimit = process.env.ACP_MODEL_CONTEXT_LIMIT;
2676
2720
  const envLimitNum = envLimit ? Number(envLimit) : NaN;
2677
2721
  const FALLBACK_LIMIT = 15e4;
@@ -2681,14 +2725,14 @@ function resolveConfig(adapter, liveContextLimit) {
2681
2725
  preserveRecentMessages: adapter.preserveRecentMessages ?? 5,
2682
2726
  ...adapter.coreOverrides
2683
2727
  });
2684
- const c = adapter.compress;
2685
- if (c?.maxContextLimit !== void 0) config.nudge.maxContextLimitPct = parsePercent(c.maxContextLimit);
2686
- 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) {
2687
2731
  const pct2 = parsePercent(c.emergencyThresholdPercent);
2688
2732
  config.nudge.emergencyThresholdPct = pct2;
2689
2733
  config.truncate.threshold = pct2;
2690
2734
  }
2691
- if (c?.nudgeGrowthTokens !== void 0) {
2735
+ if (c.nudgeGrowthTokens !== void 0) {
2692
2736
  config.nudge.growthFloor = c.nudgeGrowthTokens;
2693
2737
  config.nudge.growthCap = c.nudgeGrowthTokens;
2694
2738
  }
@@ -2701,6 +2745,89 @@ function parsePercent(v) {
2701
2745
  return Number(s);
2702
2746
  }
2703
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
+
2704
2831
  // src/messages.ts
2705
2832
  var REF_TAG_SOURCE = "(?:<acp\\s[^>]*>m\\d{5}</acp>|\\[m\\d{1,5}\\])";
2706
2833
  var REF_TAG = new RegExp(`^${REF_TAG_SOURCE}\\s?\\n?`);
@@ -3206,6 +3333,7 @@ function mergeInitialState(parsed) {
3206
3333
  return {
3207
3334
  blocks: parsed.blocks ?? fresh.blocks,
3208
3335
  messageRefs: parsed.messageRefs ?? fresh.messageRefs,
3336
+ tokenSnapshot: parsed.tokenSnapshot ?? fresh.tokenSnapshot,
3209
3337
  nudge: { ...fresh.nudge, ...parsed.nudge ?? {} },
3210
3338
  stats: { ...fresh.stats, ...parsed.stats ?? {} },
3211
3339
  nextBlockId: parsed.nextBlockId ?? fresh.nextBlockId,
@@ -3213,6 +3341,64 @@ function mergeInitialState(parsed) {
3213
3341
  };
3214
3342
  }
3215
3343
 
3344
+ // src/user-config.ts
3345
+ import { promises as fs2 } from "fs";
3346
+ import * as path3 from "path";
3347
+ import { homedir as homedir2 } from "os";
3348
+ import { CONFIG_DIR_NAME as CONFIG_DIR_NAME2 } from "@earendil-works/pi-coding-agent";
3349
+ async function loadUserConfig(cwd) {
3350
+ const home = homedir2();
3351
+ const merged = {};
3352
+ for (const base of [join4(home, CONFIG_DIR_NAME2), join4(cwd, CONFIG_DIR_NAME2)]) {
3353
+ const file = join4(base, "acp.json");
3354
+ try {
3355
+ const raw = await fs2.readFile(file, "utf8");
3356
+ const parsed = JSON.parse(raw);
3357
+ if (parsed && typeof parsed === "object") {
3358
+ Object.assign(merged, pickKnown(parsed));
3359
+ debug.event("config-loaded", { file });
3360
+ }
3361
+ } catch (e) {
3362
+ const code = e.code;
3363
+ if (code !== "ENOENT") {
3364
+ logWarn("config", { event: "load-failed", file, error: e instanceof Error ? e.message : String(e) });
3365
+ }
3366
+ }
3367
+ }
3368
+ return merged;
3369
+ }
3370
+ function join4(...parts) {
3371
+ return path3.join(...parts);
3372
+ }
3373
+ var KNOWN = /* @__PURE__ */ new Set([
3374
+ "debug",
3375
+ "autoUpdate",
3376
+ "modelContextLimit",
3377
+ "toolBashDefaultTimeout",
3378
+ "toolOutputMaxBytes",
3379
+ "delegate",
3380
+ "compress",
3381
+ "displayUsage",
3382
+ "prompts",
3383
+ "acknowledgePromptsRisk"
3384
+ ]);
3385
+ function pickKnown(parsed) {
3386
+ const out = {};
3387
+ for (const [k, v] of Object.entries(parsed)) {
3388
+ if (KNOWN.has(k)) out[k] = v;
3389
+ }
3390
+ return out;
3391
+ }
3392
+ function applyUserConfig(adapter, user) {
3393
+ return {
3394
+ ...adapter,
3395
+ ...user,
3396
+ coreOverrides: adapter.coreOverrides,
3397
+ protectedTools: adapter.protectedTools,
3398
+ preserveRecentMessages: adapter.preserveRecentMessages
3399
+ };
3400
+ }
3401
+
3216
3402
  // src/sequence-match.ts
3217
3403
  function findUniqueLongestRun(candidates, live) {
3218
3404
  if (candidates.length === 0 || live.length === 0) return void 0;
@@ -3428,10 +3614,18 @@ function pruneOrphanRefs(state, messages) {
3428
3614
  }
3429
3615
  }
3430
3616
  function createRuntime(adapter) {
3431
- const core = createCore({ countTokens: defaultCountTokens });
3617
+ const density = new DensityEstimator();
3618
+ let countModelId = "default";
3619
+ const core = createCore({
3620
+ // 密度校准版 countTokens(Phase 2):默认回落 defaultCountTokens(density=1)
3621
+ countTokens: (text) => density.estimateWithDensity(countModelId, text)
3622
+ });
3432
3623
  const store = new SessionStateStore();
3624
+ const lastActiveBlockIds = /* @__PURE__ */ new Map();
3433
3625
  const locks = /* @__PURE__ */ new Map();
3626
+ const factoryAdapter = adapter;
3434
3627
  let adapterRef = adapter;
3628
+ let lastUserConfigKey;
3435
3629
  let promptsRef = defaultPrompts;
3436
3630
  const nudgeShownTurns = /* @__PURE__ */ new Set();
3437
3631
  async function acquireLock(sid) {
@@ -3454,7 +3648,27 @@ function createRuntime(adapter) {
3454
3648
  return m?.contextWindow ?? 0;
3455
3649
  }
3456
3650
  function configFor(ctx) {
3457
- return resolveConfig(adapterRef, liveContextLimit(ctx));
3651
+ const m = ctx.model;
3652
+ return resolveConfig(adapterRef, liveContextLimit(ctx), m?.provider, m?.id);
3653
+ }
3654
+ async function reloadConfig(cwd) {
3655
+ let user;
3656
+ try {
3657
+ user = await loadUserConfig(cwd);
3658
+ } catch (e) {
3659
+ logWarn("runtime", { event: "config-reload-failed", error: e instanceof Error ? e.message : String(e) });
3660
+ return;
3661
+ }
3662
+ try {
3663
+ const key = JSON.stringify(user);
3664
+ if (key === lastUserConfigKey) return;
3665
+ lastUserConfigKey = key;
3666
+ adapterRef = applyUserConfig(factoryAdapter, user);
3667
+ if (adapterRef.debug !== void 0) setDebugEnabled(adapterRef.debug);
3668
+ logInfo("runtime", { event: "config-reloaded", limit: adapterRef.modelContextLimit ?? null });
3669
+ } catch (e) {
3670
+ logWarn("runtime", { event: "config-reload-failed", error: e instanceof Error ? e.message : String(e) });
3671
+ }
3458
3672
  }
3459
3673
  async function stateFor(ctx, liveMessages) {
3460
3674
  const sm = ctx.sessionManager;
@@ -3477,10 +3691,20 @@ function createRuntime(adapter) {
3477
3691
  const sm = ctx.sessionManager;
3478
3692
  await store.save(state, sm.getSessionFile() ?? void 0, sm.getSessionId());
3479
3693
  }
3480
- return { core, store, get adapter() {
3694
+ function noteActiveBlocks(sid, activeBlockIds) {
3695
+ const current = new Set(activeBlockIds);
3696
+ const prev = lastActiveBlockIds.get(sid);
3697
+ const isNew = prev !== void 0 && activeBlockIds.some((id) => !prev.has(id));
3698
+ lastActiveBlockIds.set(sid, current);
3699
+ return isNew;
3700
+ }
3701
+ function clearSessionTracking(sid) {
3702
+ lastActiveBlockIds.delete(sid);
3703
+ }
3704
+ return { core, store, density, setCountModel: (m) => {
3705
+ countModelId = m;
3706
+ }, noteActiveBlocks, clearSessionTracking, get adapter() {
3481
3707
  return adapterRef;
3482
- }, setAdapter: (a) => {
3483
- adapterRef = a;
3484
3708
  }, get prompts() {
3485
3709
  return promptsRef;
3486
3710
  }, setPrompts: (p) => {
@@ -3489,7 +3713,7 @@ function createRuntime(adapter) {
3489
3713
  nudgeShownTurns.add(k);
3490
3714
  }, nudgeShownFor: (k) => nudgeShownTurns.has(k), clearNudgeTracking: () => {
3491
3715
  nudgeShownTurns.clear();
3492
- }, liveContextLimit, configFor, stateFor, save, acquireLock };
3716
+ }, liveContextLimit, configFor, reloadConfig, stateFor, save, acquireLock };
3493
3717
  }
3494
3718
 
3495
3719
  // node_modules/typebox/build/system/memory/memory.mjs
@@ -7904,6 +8128,9 @@ function estimateTokens(messages, coveredIds) {
7904
8128
  }
7905
8129
  return tokens;
7906
8130
  }
8131
+ function calibrateTokens(estimate, density) {
8132
+ return density === 1 ? estimate : Math.round(estimate * density);
8133
+ }
7907
8134
  function lastUserMessageId(entries) {
7908
8135
  for (let i = entries.length - 1; i >= 0; i--) {
7909
8136
  const e = entries[i];
@@ -7974,6 +8201,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7974
8201
  if (ranges.length === 0) return "No ranges provided.";
7975
8202
  const { state: initialState, coreMessages } = await runtime.stateFor(ctx);
7976
8203
  const config = runtime.configFor(ctx);
8204
+ const modelId = ctx.model?.id ?? "default";
7977
8205
  const systemPromptText = getSystemPromptText(ctx);
7978
8206
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
7979
8207
  const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState)) + systemPromptTokens;
@@ -7981,15 +8209,18 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7981
8209
  messages: coreMessages,
7982
8210
  state: initialState,
7983
8211
  config,
7984
- tokenCount: sentTokens
8212
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
7985
8213
  });
7986
8214
  const state = turn.state;
7987
8215
  const messages = turn.messages;
7988
- const beforeTokens = estimateTokens(messages, collectCoveredMessageIds(state));
8216
+ const density = runtime.density.densityFor(modelId);
8217
+ const beforeTokens = calibrateTokens(estimateTokens(messages, collectCoveredMessageIds(state)), density);
7989
8218
  const summaryMaxChars = args.summaryMaxChars;
7990
8219
  const topLevelTopic = args.topic;
7991
8220
  debug.event("compress-in", {
7992
8221
  sid: ctx.sessionManager.getSessionId(),
8222
+ modelId,
8223
+ density,
7993
8224
  ranges: ranges.length,
7994
8225
  spans: ranges.map((r) => ({ span: `${r.startId}..${r.endId}`, summaryLen: r.summary.length, summary: r.summary, topic: r.topic ?? topLevelTopic ?? null })),
7995
8226
  blocksBefore: state.blocks.length,
@@ -8005,7 +8236,14 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
8005
8236
  });
8006
8237
  await runtime.save(applied.state, ctx);
8007
8238
  const { blocksCreated, tokensCompressed, errors, warnings } = applied.result;
8008
- const afterTokens = Math.max(0, beforeTokens - tokensCompressed);
8239
+ const afterTurn = runtime.core.processTurn({
8240
+ messages: coreMessages,
8241
+ state: applied.state,
8242
+ config,
8243
+ tokenCount: calibrateTokens(sentTokens, density)
8244
+ });
8245
+ const afterTokens = calibrateTokens(estimateTokens(afterTurn.messages, collectCoveredMessageIds(applied.state)), density);
8246
+ const reclaimed = Math.max(0, beforeTokens - afterTokens);
8009
8247
  const newBlocks = applied.state.blocks.slice(-blocksCreated);
8010
8248
  debug.event("compress-out", {
8011
8249
  sid: ctx.sessionManager.getSessionId(),
@@ -8036,9 +8274,9 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
8036
8274
  logError("compress", { sid: ctx.sessionManager.getSessionId(), event: "errors", count: errors.length, errors: errors.slice(0, 5) });
8037
8275
  }
8038
8276
  if (warnings.length > 0) {
8039
- logError("compress", { sid: ctx.sessionManager.getSessionId(), event: "warnings", count: warnings.length, warnings: warnings.slice(0, 5) });
8277
+ logWarn("compress", { sid: ctx.sessionManager.getSessionId(), event: "warnings", count: warnings.length, warnings: warnings.slice(0, 5) });
8040
8278
  }
8041
- const lines = [`\u25A3 ACP | ${formatK2(beforeTokens)} \u2192 ${formatK2(afterTokens)} tokens (~${formatK2(tokensCompressed)} reclaimed, ${blocksCreated} block${blocksCreated > 1 ? "s" : ""})`];
8279
+ const lines = [`\u25A3 ACP | ${formatK2(beforeTokens)} \u2192 ${formatK2(afterTokens)} tokens (~${formatK2(reclaimed)} reclaimed, ${blocksCreated} block${blocksCreated > 1 ? "s" : ""})`];
8042
8280
  if (warnings.length > 0) lines.push("\u26A0\uFE0F " + warnings.join("; "));
8043
8281
  if (errors.length > 0) lines.push("Errors: " + errors.join("; "));
8044
8282
  return lines.join("\n");
@@ -8046,9 +8284,10 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
8046
8284
 
8047
8285
  // src/decompress-tool.ts
8048
8286
  import { writeFile, mkdir } from "fs/promises";
8049
- import { resolve, relative, isAbsolute, join as join3 } from "path";
8050
- import { tmpdir, homedir as homedir2 } from "os";
8051
- var AUTO_DIR = join3(homedir2() || tmpdir(), ".cache", "pi", "acp-decompress");
8287
+ import { existsSync as existsSync2, lstatSync, readlinkSync, realpathSync } from "fs";
8288
+ import { resolve, relative, isAbsolute, join as join5, basename as basename2, dirname as dirname3 } from "path";
8289
+ import { tmpdir, homedir as homedir3 } from "os";
8290
+ var AUTO_DIR = join5(homedir3() || tmpdir(), ".cache", "pi", "acp-decompress");
8052
8291
  var PREVIEW_CHARS = 600;
8053
8292
  var MESSAGE_INLINE_THRESHOLD = 2e3;
8054
8293
  var DecompressParams = typebox_exports.Object({
@@ -8084,23 +8323,48 @@ function makeDecompressTool(runtime) {
8084
8323
  }
8085
8324
  var ALLOWED_DIRS = [
8086
8325
  tmpdir(),
8087
- join3(homedir2(), ".cache", "opencode"),
8088
- join3(homedir2(), ".cache", "pi")
8326
+ join5(homedir3(), ".cache", "opencode"),
8327
+ join5(homedir3(), ".cache", "pi")
8089
8328
  ];
8090
8329
  function resolveToFilePath(targetPath) {
8091
- const expanded = targetPath.startsWith("~/") ? join3(homedir2(), targetPath.slice(2)) : targetPath;
8330
+ const expanded = targetPath.startsWith("~/") ? join5(homedir3(), targetPath.slice(2)) : targetPath;
8092
8331
  const resolved = resolve(expanded);
8093
- const isAllowed = ALLOWED_DIRS.some((dir) => {
8094
- const rel = relative(dir, resolved);
8332
+ let probe = resolved;
8333
+ const suffix = [];
8334
+ while (!existsSync2(probe) && probe !== dirname3(probe)) {
8335
+ suffix.unshift(basename2(probe));
8336
+ probe = dirname3(probe);
8337
+ }
8338
+ const real = existsSync2(probe) ? realpathSync(probe) : probe;
8339
+ let checked = real;
8340
+ for (const part of suffix) {
8341
+ checked = join5(checked, part);
8342
+ try {
8343
+ if (lstatSync(checked).isSymbolicLink()) {
8344
+ const target = readlinkSync(checked);
8345
+ checked = isAbsolute(target) ? resolve(target) : resolve(dirname3(checked), target);
8346
+ }
8347
+ } catch {
8348
+ }
8349
+ }
8350
+ const allowed = ALLOWED_DIRS.map((d) => {
8351
+ try {
8352
+ return realpathSync(d);
8353
+ } catch {
8354
+ return d;
8355
+ }
8356
+ });
8357
+ const isAllowed = allowed.some((dir) => {
8358
+ const rel = relative(dir, checked);
8095
8359
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
8096
8360
  });
8097
8361
  if (!isAllowed) {
8098
8362
  return { error: `Error: toFile path must be under ${tmpdir()}, ~/.cache/opencode, or ~/.cache/pi. Got: ${targetPath}` };
8099
8363
  }
8100
- return resolved;
8364
+ return checked;
8101
8365
  }
8102
8366
  function autoFilePath(blockId) {
8103
- return join3(AUTO_DIR, `${blockId}-${Date.now()}.txt`);
8367
+ return join5(AUTO_DIR, `${blockId}-${Date.now()}.txt`);
8104
8368
  }
8105
8369
  function headPreview(text) {
8106
8370
  if (text.length <= PREVIEW_CHARS) return text;
@@ -8163,7 +8427,7 @@ ${text}`;
8163
8427
  }
8164
8428
  async function handleDecompress(args, runtime, ctx) {
8165
8429
  const { state, coreMessages } = await runtime.stateFor(ctx);
8166
- const arg = args.blockId.trim();
8430
+ const arg = (args.blockId ?? "").trim();
8167
8431
  const owner = state.blocks.find((b) => b.effectiveMessageIds.includes(arg));
8168
8432
  if (owner) {
8169
8433
  return handleMessageRef(arg, owner.blockId, args, ctx);
@@ -8335,420 +8599,6 @@ function formatSize(tokens) {
8335
8599
  return `${(tokens / 1e6).toFixed(1)}M`;
8336
8600
  }
8337
8601
 
8338
- // node_modules/billion-context-kit/node_modules/acp-kernel/dist/index.js
8339
- import { createRequire as createRequire2 } from "module";
8340
- var BLOCKED_REF2 = "BLOCKED";
8341
- function refForRaw2(map, rawId) {
8342
- return map.byRaw[rawId] ?? null;
8343
- }
8344
- var require22 = createRequire2(import.meta.url);
8345
- function defaultCountTokens2(text) {
8346
- if (!text) return 0;
8347
- const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
8348
- const cjkCount = cjk?.length ?? 0;
8349
- return cjkCount + Math.ceil((text.length - cjkCount) / 4);
8350
- }
8351
- function formatTokens3(tokens) {
8352
- if (tokens < 1e3) return String(tokens);
8353
- if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
8354
- return Math.round(tokens / 1e3) + "K";
8355
- }
8356
- function classifyType2(message) {
8357
- if (message.contentType === "tool-call" || message.contentType === "tool-result") {
8358
- return message.toolName || "tool";
8359
- }
8360
- return message.contentType;
8361
- }
8362
- function escapeRegex2(s) {
8363
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8364
- }
8365
- var LT2 = "<";
8366
- var GT2 = ">";
8367
- var TAG_OPEN2 = LT2 + "acp ";
8368
- var TAG_CLOSE2 = LT2 + "/acp" + GT2;
8369
- function acpTag2(ref, tokens, type) {
8370
- return TAG_OPEN2 + 'tokens="' + formatTokens3(tokens) + '" type="' + type + '"' + GT2 + ref + TAG_CLOSE2;
8371
- }
8372
- function renderMessage2(message, map, countTokens, strategy) {
8373
- const ref = refForRaw2(map, message.id);
8374
- if (!ref || ref === BLOCKED_REF2) return message;
8375
- if (strategy === "none") return message;
8376
- if (strategy === "text-only" && message.contentType !== "text") {
8377
- return message;
8378
- }
8379
- const ownTagRe = new RegExp(
8380
- "^" + escapeRegex2(TAG_OPEN2) + "[^>]*" + GT2 + escapeRegex2(ref) + escapeRegex2(TAG_CLOSE2) + "\\n?"
8381
- );
8382
- const cleanText = (message.text || "").replace(ownTagRe, "");
8383
- const tokens = countTokens(cleanText);
8384
- const type = classifyType2(message);
8385
- const prefix = acpTag2(ref, tokens, type) + "\n";
8386
- if (!cleanText) return { ...message, text: prefix };
8387
- return { ...message, text: prefix + cleanText };
8388
- }
8389
- function renderVisibleRefs2(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
8390
- const map = state.messageRefs;
8391
- return messages.map(
8392
- (message) => renderMessage2(message, map, countTokens, strategy)
8393
- );
8394
- }
8395
- function createRenderRefsNode2(strategy) {
8396
- return {
8397
- name: "render-refs",
8398
- run(io, ctx) {
8399
- return {
8400
- ...io,
8401
- messages: renderVisibleRefs2(io.messages, io.state, ctx.countTokens, strategy)
8402
- };
8403
- }
8404
- };
8405
- }
8406
- var renderRefsNode2 = createRenderRefsNode2("all");
8407
- var COMPRESS_PHILOSOPHY2 = `Compression Philosophy:
8408
- - All compression serves the primary task, but be frugal.
8409
- - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
8410
- - Compress by need, not by percentage.
8411
- - Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
8412
- var HOW_TO_COMPRESS_RULES2 = `HOW TO COMPRESS
8413
-
8414
- When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
8415
-
8416
- KEEP VERBATIM \u2014 never paraphrase or abbreviate these:
8417
- - Full file paths with line numbers, directory prefix on every mention (\`lib/hooks.ts:347\`, \`src/index.ts:12-18\`, \`gatenet_v3/model.py:45\`). Never abbreviate to a bare filename (\`hooks.ts\`, \`model.py\`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.
8418
- - Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. \`kv_keys += define_gate * a_key[i](emb)\` is more useful than "see model_kvnet.py").
8419
- - Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).
8420
- - Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not "X is worse" alone (write "1.76\xD7 PPL gap because KV store is static", not "KVNet underperforms").
8421
- - Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing; without it the decision looks arbitrary).
8422
- - Constraints discovered ("must support Node 22", "no new dependencies", "AGENTS.md forbids \`as any\`").
8423
- - Exact values: versions, config keys, thresholds, magic numbers.
8424
- - User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., "User said: ..."), not as current directives. Losing these changes the task itself.
8425
- - The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., "initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause"). Losing the goal or its evolution makes all subsequent work appear unmotivated.
8426
- - Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.
8427
- - Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.
8428
- - Message refs of key anchors (\`m00420\`, \`m00510\u2013m00520\`) \u2014 they let you or a later reader jump back via decompress to the exact original.
8429
-
8430
- DROP \u2014 extract the signal, discard the vessel:
8431
- - Verbose logs (build/test/\`npm\` output) once you have captured the error line or the result.
8432
- - Duplicate file reads once the needed content is recorded.
8433
- - Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).
8434
- - Dead-end exploration \u2014 but PRESERVE the lesson in one line: "tried X, failed because Y".
8435
- - Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).
8436
- - Repeated status checks (\`git status\`, \`ls\`) once state is known.
8437
-
8438
- For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
8439
-
8440
- PRIORITY \u2014 when the summary must be compact, preserve in this order:
8441
- 1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
8442
- 2. Decisions and rationale.
8443
- 3. Exact technical artifacts: paths, signatures, errors, values.
8444
- 4. Conclusions and key findings.
8445
- 5. Lessons learned: what failed and why.
8446
-
8447
- Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
8448
- var TIER2_DISTILL_RULES2 = `TIER 2 COMPRESSION \u2014 DISTILLATION
8449
-
8450
- You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
8451
-
8452
- KEEP \u2014 these are the only things that survive distillation:
8453
- - Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
8454
- - Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
8455
- - Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
8456
- - Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
8457
- - Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
8458
- - Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
8459
- - Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
8460
- - Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
8461
-
8462
- DROP \u2014 these were useful during the work but are no longer needed:
8463
- - Exact line numbers, diffs, verbose function signatures, full code listings.
8464
- - Build/deploy process details, test execution steps.
8465
- - Review process details (who reviewed, what rounds, test counts).
8466
- - Verbose logs, command output, intermediate debugging steps.
8467
-
8468
- FORMAT:
8469
- - Start each distilled block with a source header line:
8470
- \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
8471
- Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
8472
- - 3-5 bullet points per source block, each a self-contained fact.
8473
- - Dense, scannable \u2014 no narrative prose.
8474
- - Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
8475
- - Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
8476
-
8477
- SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]."`;
8478
- var TIER3_CONDENSE_RULES2 = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
8479
-
8480
- You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
8481
-
8482
- PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
8483
- 1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
8484
- 2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
8485
- 3. Key decisions with architectural impact ("chose X over Y because Z").
8486
- 4. Critical constraints ("must support Node 22").
8487
- Drop everything else. Tier 3 is a lookup index, not a knowledge base.
8488
-
8489
- FORMAT:
8490
- - Start with a source header line:
8491
- \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
8492
- - Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
8493
- - No explanations, no rationale, no process \u2014 just the fact.
8494
- - Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
8495
- - Merge related facts from different source blocks if they concern the same topic.
8496
-
8497
- EXAMPLES:
8498
- - "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
8499
- - "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
8500
- - "Bug 1214 fixed \u2014 compress consumed all user messages"
8501
- - "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
8502
- - "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
8503
-
8504
- DROP:
8505
- - Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
8506
- - Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
8507
- - Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
8508
- - Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
8509
-
8510
- SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
8511
- var defaultPrompts2 = Object.freeze({
8512
- compressPhilosophy: COMPRESS_PHILOSOPHY2,
8513
- howToCompressRules: HOW_TO_COMPRESS_RULES2,
8514
- tier2DistillRules: TIER2_DISTILL_RULES2,
8515
- tier3CondenseRules: TIER3_CONDENSE_RULES2
8516
- });
8517
- function formatK3(n) {
8518
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
8519
- return `${n}`;
8520
- }
8521
- function formatRanges2(compressible, protectedRanges) {
8522
- if (compressible.length === 0 && protectedRanges.length === 0) {
8523
- return "[No specific ranges detected \u2014 compress any consumed content.]";
8524
- }
8525
- const refNum2 = (ref) => {
8526
- const m = ref.match(/\d+/);
8527
- return m ? parseInt(m[0], 10) : 0;
8528
- };
8529
- const entries = [];
8530
- for (const r of compressible) {
8531
- entries.push({
8532
- startRef: r.startRef,
8533
- endRef: r.endRef,
8534
- startNum: refNum2(r.startRef),
8535
- endNum: refNum2(r.endRef),
8536
- count: r.count,
8537
- tokens: r.tokens,
8538
- toolPct: r.toolPct,
8539
- textPct: r.textPct,
8540
- compressibleTokens: r.tokens,
8541
- compressibleCount: r.count,
8542
- protectedTokens: 0,
8543
- protectedCount: 0,
8544
- protectedTools: [],
8545
- dangerous: r.dangerous ?? false
8546
- });
8547
- }
8548
- for (const r of protectedRanges) {
8549
- entries.push({
8550
- startRef: r.startRef,
8551
- endRef: r.endRef,
8552
- startNum: refNum2(r.startRef),
8553
- endNum: refNum2(r.endRef),
8554
- count: r.count,
8555
- tokens: r.tokens,
8556
- toolPct: 0,
8557
- textPct: 0,
8558
- compressibleTokens: 0,
8559
- compressibleCount: 0,
8560
- protectedTokens: r.tokens,
8561
- protectedCount: r.count,
8562
- protectedTools: [...r.tools],
8563
- dangerous: false
8564
- });
8565
- }
8566
- entries.sort((a, b) => a.startNum - b.startNum);
8567
- const merged = [];
8568
- for (const e of entries) {
8569
- const last = merged[merged.length - 1];
8570
- if (last && e.startNum <= last.endNum + 1) {
8571
- last.endRef = e.endRef;
8572
- last.endNum = Math.max(last.endNum, e.endNum);
8573
- last.count += e.count;
8574
- last.tokens += e.tokens;
8575
- last.compressibleTokens += e.compressibleTokens;
8576
- last.compressibleCount += e.compressibleCount;
8577
- last.protectedTokens += e.protectedTokens;
8578
- last.protectedCount += e.protectedCount;
8579
- if (e.dangerous) last.dangerous = true;
8580
- for (const t of e.protectedTools) {
8581
- if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
8582
- }
8583
- } else {
8584
- merged.push({ ...e });
8585
- }
8586
- }
8587
- const lines = merged.map((e) => {
8588
- const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
8589
- if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
8590
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK3(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
8591
- }
8592
- if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
8593
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK3(e.tokens)} [${formatK3(e.compressibleTokens)} compressible | ${formatK3(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
8594
- }
8595
- return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK3(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
8596
- });
8597
- return `Compressible ranges (${merged.length}, oldest first):
8598
- ${lines.join("\n")}`;
8599
- }
8600
- var substringAlgorithm2 = {
8601
- name: "substring",
8602
- description: "Exact substring counting (original baseline). Predictable, no normalization.",
8603
- score(docs, query) {
8604
- const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 0);
8605
- if (terms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
8606
- return docs.map((d) => {
8607
- const haystack = d.text.toLowerCase();
8608
- let score = 0;
8609
- for (const term of terms) score += countOccurrences22(haystack, term);
8610
- return { ref: d.ref, score };
8611
- });
8612
- }
8613
- };
8614
- function countOccurrences22(haystack, needle) {
8615
- if (!needle) return 0;
8616
- return haystack.split(needle).length - 1;
8617
- }
8618
- function stem2(word) {
8619
- let w = word;
8620
- if (w.length <= 3) return w;
8621
- if (w.endsWith("ies")) w = w.slice(0, -3) + "y";
8622
- else if (w.endsWith("ses") || w.endsWith("xes") || w.endsWith("zes")) w = w.slice(0, -2);
8623
- else if (w.endsWith("ches") || w.endsWith("shes")) w = w.slice(0, -2);
8624
- else if (w.endsWith("s") && !w.endsWith("ss")) w = w.slice(0, -1);
8625
- if (w.endsWith("ing") && w.length > 5) w = w.slice(0, -3);
8626
- if (w.endsWith("ed") && w.length > 4) w = w.slice(0, -2);
8627
- if (w.endsWith("ation") && w.length > 6) w = w.slice(0, -3);
8628
- else if (w.endsWith("tion") && w.length > 5) w = w.slice(0, -4) + "t";
8629
- else if (w.endsWith("ion") && w.length > 4) w = w.slice(0, -3);
8630
- if (w.endsWith("ment") && w.length > 6) w = w.slice(0, -4);
8631
- if (w.endsWith("ness") && w.length > 6) w = w.slice(0, -4);
8632
- if (w.endsWith("ly") && w.length > 4) w = w.slice(0, -2);
8633
- return w;
8634
- }
8635
- var CJK2 = /[\u3400-\u9fff\uf900-\ufaff\u3040-\u30ff\uac00-\ud7af]/;
8636
- var CJK_RUN2 = new RegExp(`${CJK2.source}+`, "g");
8637
- var LATIN_WORD2 = /[a-z][a-z0-9_]*[a-z0-9]|[a-z0-9]/g;
8638
- function tokenize2(text, opts = {}) {
8639
- const lower = text.toLowerCase();
8640
- const tokens = [];
8641
- const latin = lower.match(LATIN_WORD2) ?? [];
8642
- for (let w of latin) {
8643
- if (w.length >= 2) {
8644
- if (opts.stem) w = stem2(w);
8645
- tokens.push(w);
8646
- }
8647
- }
8648
- const cjkRuns = lower.match(CJK_RUN2) ?? [];
8649
- for (const run of cjkRuns) {
8650
- if (run.length === 1) {
8651
- tokens.push(run);
8652
- } else {
8653
- for (let i = 0; i < run.length - 1; i++) tokens.push(run.slice(i, i + 2));
8654
- for (const ch of run) tokens.push(ch);
8655
- }
8656
- }
8657
- return tokens;
8658
- }
8659
- function charBigrams2(text) {
8660
- const grams = [];
8661
- for (let i = 0; i < text.length - 1; i++) {
8662
- const pair = text.slice(i, i + 2);
8663
- if (pair.trim().length === pair.length) grams.push(pair);
8664
- }
8665
- return grams;
8666
- }
8667
- function tfMap2(text, stem22) {
8668
- const m = /* @__PURE__ */ new Map();
8669
- for (const t of tokenize2(text, { stem: stem22 })) m.set(t, (m.get(t) ?? 0) + 1);
8670
- return m;
8671
- }
8672
- var bm25Algorithm2 = {
8673
- name: "bm25",
8674
- description: "BM25 with stemming + CJK bigram tokenization. IR-standard relevance ranking.",
8675
- score(docs, query) {
8676
- const N = docs.length;
8677
- const k1 = 1.2;
8678
- const b = 0.75;
8679
- const parsed = docs.map((d) => {
8680
- const text = d.text;
8681
- const tf = tfMap2(text, true);
8682
- let len = 0;
8683
- for (const v of tf.values()) len += v;
8684
- return { id: d.ref, tf, len };
8685
- });
8686
- const avgdl = parsed.reduce((s, d) => s + d.len, 0) / (N || 1);
8687
- const qTerms = tokenize2(query, { stem: true });
8688
- if (qTerms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
8689
- const idf = /* @__PURE__ */ new Map();
8690
- for (const t of new Set(qTerms)) {
8691
- let df = 0;
8692
- for (const d of parsed) if (d.tf.has(t)) df++;
8693
- idf.set(t, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
8694
- }
8695
- return parsed.map((d) => {
8696
- let score = 0;
8697
- for (const t of qTerms) {
8698
- const f = d.tf.get(t) ?? 0;
8699
- if (f === 0) continue;
8700
- const idfT = idf.get(t) ?? 0;
8701
- score += idfT * (f * (k1 + 1)) / (f + k1 * (1 - b + b * d.len / (avgdl || 1)));
8702
- }
8703
- return { ref: d.id, score };
8704
- });
8705
- }
8706
- };
8707
- var fuzzyAlgorithm2 = {
8708
- name: "fuzzy",
8709
- description: "Character bigram overlap. Typo-tolerant, script-agnostic, high recall.",
8710
- score(docs, query) {
8711
- const qTokens = query.toLowerCase().split(/[\s,]+/).filter((t) => t.length >= 4);
8712
- if (qTokens.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
8713
- const qGrams = /* @__PURE__ */ new Set();
8714
- for (const t of qTokens) for (const g of charBigrams2(t)) qGrams.add(g);
8715
- if (qGrams.size === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
8716
- return docs.map((d) => {
8717
- const haystack = d.text.toLowerCase();
8718
- const docGrams = new Set(charBigrams2(haystack));
8719
- let hits = 0;
8720
- for (const g of qGrams) if (docGrams.has(g)) hits++;
8721
- return { ref: d.ref, score: hits / qGrams.size };
8722
- });
8723
- }
8724
- };
8725
- var W_BM252 = 0.7;
8726
- var W_FUZZY2 = 0.3;
8727
- var hybridAlgorithm2 = {
8728
- name: "hybrid",
8729
- description: "Weighted BM25(stem) + fuzzy n-gram. Default \u2014 best precision + recall.",
8730
- score(docs, query) {
8731
- const bm = bm25Algorithm2.score(docs, query);
8732
- const fz = fuzzyAlgorithm2.score(docs, query);
8733
- const maxBm = Math.max(...bm.map((r) => r.score), 1e-9);
8734
- const maxFz = Math.max(...fz.map((r) => r.score), 1e-9);
8735
- const bmMap = new Map(bm.map((r) => [r.ref, r.score / maxBm]));
8736
- const fzMap = new Map(fz.map((r) => [r.ref, r.score / maxFz]));
8737
- return docs.map((d) => ({
8738
- ref: d.ref,
8739
- score: W_BM252 * (bmMap.get(d.ref) ?? 0) + W_FUZZY2 * (fzMap.get(d.ref) ?? 0)
8740
- }));
8741
- }
8742
- };
8743
- var registry22 = /* @__PURE__ */ new Map();
8744
- function registerSearchAlgorithm2(algo) {
8745
- registry22.set(algo.name, algo);
8746
- }
8747
- registerSearchAlgorithm2(substringAlgorithm2);
8748
- registerSearchAlgorithm2(bm25Algorithm2);
8749
- registerSearchAlgorithm2(fuzzyAlgorithm2);
8750
- registerSearchAlgorithm2(hybridAlgorithm2);
8751
-
8752
8602
  // node_modules/billion-context-kit/dist/index.js
8753
8603
  var VIABLE_RANGE_MIN_TOKENS = 200;
8754
8604
  function viableRanges(ranges) {
@@ -8831,14 +8681,14 @@ function buildStatusPanel(input) {
8831
8681
  const protectedRanges = nudge?.protectedRanges ?? [];
8832
8682
  if (ranges.length > 0 || protectedRanges.length > 0) {
8833
8683
  lines.push("");
8834
- lines.push(formatRanges2(ranges, protectedRanges));
8684
+ lines.push(formatRanges(ranges, protectedRanges));
8835
8685
  }
8836
8686
  if (activeBlocksList.length > 0) {
8837
8687
  lines.push("");
8838
8688
  lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8839
8689
  for (const b of activeBlocksList) {
8840
8690
  const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
8841
- const summaryTok = defaultCountTokens2(b.summary || "");
8691
+ const summaryTok = defaultCountTokens(b.summary || "");
8842
8692
  const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
8843
8693
  lines.push(` [${b.blockId}] T${b.tier} ${fmt2(origTok)}\u2192${fmt2(summaryTok)}${topic}`);
8844
8694
  }
@@ -8858,10 +8708,10 @@ function buildStatusPanel(input) {
8858
8708
  import {
8859
8709
  spawn
8860
8710
  } from "child_process";
8861
- import { createWriteStream, existsSync as existsSync2 } from "fs";
8711
+ import { createWriteStream, existsSync as existsSync3 } from "fs";
8862
8712
  import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
8863
8713
  import { tmpdir as tmpdir2 } from "os";
8864
- import { dirname as dirname3, join as join4, resolve as resolvePath } from "path";
8714
+ import { dirname as dirname4, join as join6, resolve as resolvePath } from "path";
8865
8715
 
8866
8716
  // src/footer-status.ts
8867
8717
  var FOOTER_STATUS_KEY = "billion-context-pi";
@@ -9265,7 +9115,7 @@ var IDLE_GRACE_MS = 5 * 6e4;
9265
9115
  var ASYNC_TIMEOUT_MS = 30 * 6e4;
9266
9116
  var KILL_GRACE_MS = 1e4;
9267
9117
  var RESULT_SUMMARY_CHARS = 500;
9268
- var OUT_DIR = join4(tmpdir2(), "acp-delegate");
9118
+ var OUT_DIR = join6(tmpdir2(), "acp-delegate");
9269
9119
  function delegateSpawnOptions(cwd, env) {
9270
9120
  return {
9271
9121
  cwd,
@@ -9275,13 +9125,13 @@ function delegateSpawnOptions(cwd, env) {
9275
9125
  };
9276
9126
  }
9277
9127
  var PI_CLI_ENTRY_RE = /[\\/]pi-coding-agent[\\/]dist[\\/]cli\.js$/;
9278
- var PI_PACKAGE_REL = join4("@earendil-works", "pi-coding-agent", "dist", "cli.js");
9128
+ var PI_PACKAGE_REL = join6("@earendil-works", "pi-coding-agent", "dist", "cli.js");
9279
9129
  function probeUpFromArgv(argv1) {
9280
- let dir = resolvePath(dirname3(argv1) || process.cwd());
9130
+ let dir = resolvePath(dirname4(argv1) || process.cwd());
9281
9131
  for (; ; ) {
9282
- const candidate = join4(dir, "node_modules", PI_PACKAGE_REL);
9283
- if (existsSync2(candidate)) return candidate;
9284
- const parent = dirname3(dir);
9132
+ const candidate = join6(dir, "node_modules", PI_PACKAGE_REL);
9133
+ if (existsSync3(candidate)) return candidate;
9134
+ const parent = dirname4(dir);
9285
9135
  if (parent === dir) return null;
9286
9136
  dir = parent;
9287
9137
  }
@@ -9289,12 +9139,12 @@ function probeUpFromArgv(argv1) {
9289
9139
  function piCliGlobalCandidates(env) {
9290
9140
  const candidates = [];
9291
9141
  if (process.platform === "win32") {
9292
- if (env.APPDATA) candidates.push(join4(env.APPDATA, "npm", "node_modules", PI_PACKAGE_REL));
9142
+ if (env.APPDATA) candidates.push(join6(env.APPDATA, "npm", "node_modules", PI_PACKAGE_REL));
9293
9143
  } else {
9294
9144
  const home = env.HOME ?? env.USERPROFILE;
9295
- if (home) candidates.push(join4(home, ".local", "lib", "node_modules", PI_PACKAGE_REL));
9296
- candidates.push(join4("/usr/local", "lib", "node_modules", PI_PACKAGE_REL));
9297
- candidates.push(join4("/usr", "lib", "node_modules", PI_PACKAGE_REL));
9145
+ if (home) candidates.push(join6(home, ".local", "lib", "node_modules", PI_PACKAGE_REL));
9146
+ candidates.push(join6("/usr/local", "lib", "node_modules", PI_PACKAGE_REL));
9147
+ candidates.push(join6("/usr", "lib", "node_modules", PI_PACKAGE_REL));
9298
9148
  }
9299
9149
  return candidates;
9300
9150
  }
@@ -9306,7 +9156,7 @@ function resolvePiCliEntry(argv1, env = process.env, piHost = true) {
9306
9156
  const probed = probeUpFromArgv(argv1);
9307
9157
  if (probed) return probed;
9308
9158
  for (const candidate of piCliGlobalCandidates(env)) {
9309
- if (existsSync2(candidate)) return candidate;
9159
+ if (existsSync3(candidate)) return candidate;
9310
9160
  }
9311
9161
  logWarn("delegate", { event: "cli-entry-unresolved", argv1, fallback: "argv[1]" });
9312
9162
  }
@@ -9446,6 +9296,11 @@ function makeEventApplier(opts, writers) {
9446
9296
  }
9447
9297
  var WAIT_TIMEOUT_MS_DEFAULT = 1e4;
9448
9298
  var WAIT_TIMEOUT_MS_MAX = 3e5;
9299
+ function resolveWaitTimeoutMs(raw) {
9300
+ if (raw === void 0) return WAIT_TIMEOUT_MS_DEFAULT;
9301
+ const ms = raw < 1e3 ? raw * 1e3 : raw;
9302
+ return Math.min(Math.max(ms, 1e3), WAIT_TIMEOUT_MS_MAX);
9303
+ }
9449
9304
  var DelegateParams = typebox_exports.Object({
9450
9305
  agent: typebox_exports.String({
9451
9306
  description: `Role of the delegate. One of: ${AGENT_NAMES.join(", ")}. See tool description for what each does.`
@@ -9477,7 +9332,7 @@ var WaitParams = typebox_exports.Object({
9477
9332
  runId: typebox_exports.String({ description: "The runId returned by acp_delegate to wait for." }),
9478
9333
  timeout: typebox_exports.Optional(
9479
9334
  typebox_exports.Integer({
9480
- description: `Maximum milliseconds to block waiting for the result. Default ${WAIT_TIMEOUT_MS_DEFAULT} (10s); max ${WAIT_TIMEOUT_MS_MAX} (300s). If the delegate does not finish in time, returns "failed (not ready)" \u2014 do NOT keep waiting or retry; go do other work, and a completion notification will still be injected when it completes.`
9335
+ description: `Maximum time to block waiting for the result, in milliseconds. Default ${WAIT_TIMEOUT_MS_DEFAULT} (10s); max ${WAIT_TIMEOUT_MS_MAX} (300s). Values below 1000 are treated as seconds (so 180 means 180s, not 180ms). If the delegate does not finish in time, returns "failed (not ready)" \u2014 do NOT keep waiting or retry; go do other work, and a completion notification will still be injected when it completes.`
9481
9336
  })
9482
9337
  )
9483
9338
  });
@@ -9636,10 +9491,7 @@ function makeDelegateWaitTool(_pi) {
9636
9491
  }
9637
9492
  return buildWaitResult(run, formatRunResult(run), displayMode);
9638
9493
  }
9639
- const timeoutMs = Math.min(
9640
- Math.max(args.timeout ?? WAIT_TIMEOUT_MS_DEFAULT, 1e3),
9641
- WAIT_TIMEOUT_MS_MAX
9642
- );
9494
+ const timeoutMs = resolveWaitTimeoutMs(args.timeout);
9643
9495
  if (run.waiter) {
9644
9496
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` already has a wait in progress; do not wait on it twice.` }] };
9645
9497
  }
@@ -9760,8 +9612,8 @@ async function runDelegate(pi, args, ctx, signal) {
9760
9612
  },
9761
9613
  { eofGraceMs: EOF_GRACE_MS, idleMs: IDLE_GRACE_MS, timeoutMs: ASYNC_TIMEOUT_MS, killGraceMs: KILL_GRACE_MS }
9762
9614
  );
9763
- const replyFile = join4(OUT_DIR, `${runId}.out`);
9764
- const activityFile = join4(OUT_DIR, `${runId}.activity`);
9615
+ const replyFile = join6(OUT_DIR, `${runId}.out`);
9616
+ const activityFile = join6(OUT_DIR, `${runId}.activity`);
9765
9617
  await mkdir2(OUT_DIR, { recursive: true });
9766
9618
  const replyStream = createWriteStream(replyFile, { flags: "a" });
9767
9619
  const activityStream = useJsonStream ? createWriteStream(activityFile, { flags: "a" }) : null;
@@ -9911,8 +9763,8 @@ async function runDelegate(pi, args, ctx, signal) {
9911
9763
  return formatSyncResult(args.agent, runId, args.task, result, file);
9912
9764
  }
9913
9765
  async function buildChildArgs(args, rolePrompt, ctx) {
9914
- const tmpDir = await mkdtemp(join4(tmpdir2(), "acp-delegate-"));
9915
- const promptFile = join4(tmpDir, "role.md");
9766
+ const tmpDir = await mkdtemp(join6(tmpdir2(), "acp-delegate-"));
9767
+ const promptFile = join6(tmpDir, "role.md");
9916
9768
  await writeFile2(promptFile, `${rolePrompt}
9917
9769
 
9918
9770
  ---
@@ -10049,7 +9901,7 @@ async function persistResult(runId, body) {
10049
9901
  await mkdir2(OUT_DIR, { recursive: true });
10050
9902
  } catch {
10051
9903
  }
10052
- const file = join4(OUT_DIR, `${runId}.out`);
9904
+ const file = join6(OUT_DIR, `${runId}.out`);
10053
9905
  try {
10054
9906
  await writeFile2(file, body, "utf8");
10055
9907
  return file;
@@ -10107,6 +9959,7 @@ async function handleStatus(args, runtime, ctx) {
10107
9959
  const { state, coreMessages } = await runtime.stateFor(ctx);
10108
9960
  const config = runtime.configFor(ctx);
10109
9961
  const coveredIds = collectCoveredMessageIds(state);
9962
+ const modelId = ctx.model?.id ?? "default";
10110
9963
  const systemPromptText = getSystemPromptText(ctx);
10111
9964
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10112
9965
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
@@ -10114,7 +9967,7 @@ async function handleStatus(args, runtime, ctx) {
10114
9967
  messages: coreMessages,
10115
9968
  state,
10116
9969
  config,
10117
- tokenCount: sentTokens
9970
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
10118
9971
  });
10119
9972
  const processed = turn.messages;
10120
9973
  const base = buildStatusReport(turn.state, processed, defaultCountTokens, {
@@ -10232,9 +10085,10 @@ async function statusReport(runtime, ctx) {
10232
10085
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10233
10086
  const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
10234
10087
  const coveredIds = collectCoveredMessageIds(state);
10088
+ const modelId = ctx.model?.id ?? "default";
10235
10089
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10236
- const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: sentTokens });
10237
- const versionStr = "0.1.38" ? `billion-context-pi@${"0.1.38"}` : void 0;
10090
+ const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId)) });
10091
+ const versionStr = "0.1.40" ? `billion-context-pi@${"0.1.40"}` : void 0;
10238
10092
  let text = buildStatusPanel({
10239
10093
  version: versionStr,
10240
10094
  tokenCount: sessionTokens,
@@ -10447,16 +10301,16 @@ function wireToolGuardrails(pi, runtime) {
10447
10301
 
10448
10302
  // src/update.ts
10449
10303
  import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
10450
- import { join as join5, dirname as dirname4 } from "path";
10304
+ import { join as join7, dirname as dirname5 } from "path";
10451
10305
  import { fileURLToPath } from "url";
10452
10306
  import { execFile } from "child_process";
10453
- import { homedir as homedir3 } from "os";
10454
- import { CONFIG_DIR_NAME as CONFIG_DIR_NAME2 } from "@earendil-works/pi-coding-agent";
10307
+ import { homedir as homedir4 } from "os";
10308
+ import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
10455
10309
  var PACKAGE_NAME = "billion-context-pi";
10456
10310
  var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
10457
10311
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/;
10458
10312
  var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
10459
- var THROTTLE_FILE = join5(homedir3(), CONFIG_DIR_NAME2, "agent", ".billion-context-pi-update-check");
10313
+ var THROTTLE_FILE = join7(homedir4(), CONFIG_DIR_NAME3, "agent", ".billion-context-pi-update-check");
10460
10314
  var updateInFlight = false;
10461
10315
  function parseVersion(v) {
10462
10316
  return v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
@@ -10480,7 +10334,7 @@ async function readLastCheck() {
10480
10334
  }
10481
10335
  async function writeLastCheck(timestamp) {
10482
10336
  try {
10483
- await mkdir3(dirname4(THROTTLE_FILE), { recursive: true });
10337
+ await mkdir3(dirname5(THROTTLE_FILE), { recursive: true });
10484
10338
  await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
10485
10339
  } catch {
10486
10340
  }
@@ -10494,20 +10348,20 @@ async function readPackageJson(path4) {
10494
10348
  }
10495
10349
  }
10496
10350
  function findNpmRoot(extDir) {
10497
- let dir = dirname4(extDir);
10351
+ let dir = dirname5(extDir);
10498
10352
  for (; ; ) {
10499
- if (dir.endsWith("node_modules")) return dirname4(dir);
10500
- const parent = dirname4(dir);
10353
+ if (dir.endsWith("node_modules")) return dirname5(dir);
10354
+ const parent = dirname5(dir);
10501
10355
  if (parent === dir) return void 0;
10502
10356
  dir = parent;
10503
10357
  }
10504
10358
  }
10505
10359
  async function findExtensionDir() {
10506
- let dir = dirname4(fileURLToPath(import.meta.url));
10360
+ let dir = dirname5(fileURLToPath(import.meta.url));
10507
10361
  for (; ; ) {
10508
- const pkg = await readPackageJson(join5(dir, "package.json"));
10362
+ const pkg = await readPackageJson(join7(dir, "package.json"));
10509
10363
  if (pkg?.name === PACKAGE_NAME) return dir;
10510
- const parent = dirname4(dir);
10364
+ const parent = dirname5(dir);
10511
10365
  if (parent === dir) return void 0;
10512
10366
  dir = parent;
10513
10367
  }
@@ -10556,7 +10410,7 @@ async function checkForUpdate(autoUpdate, notify) {
10556
10410
  const data = await res.json();
10557
10411
  const latest = data.version;
10558
10412
  if (!latest) return;
10559
- const current = runtimeVersion ?? "0.1.38";
10413
+ const current = runtimeVersion ?? "0.1.40";
10560
10414
  const hasUpdate = isNewer(latest, current);
10561
10415
  debug.event("update-check", {
10562
10416
  current,
@@ -10586,16 +10440,16 @@ async function checkForUpdate(autoUpdate, notify) {
10586
10440
  async function getRuntimeVersion() {
10587
10441
  const extDir = await findExtensionDir();
10588
10442
  if (!extDir) return void 0;
10589
- const pkg = await readPackageJson(join5(extDir, "package.json"));
10443
+ const pkg = await readPackageJson(join7(extDir, "package.json"));
10590
10444
  return pkg?.version;
10591
10445
  }
10592
10446
 
10593
10447
  // src/setup-subagent-tools.ts
10594
10448
  import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
10595
- import { existsSync as existsSync3 } from "fs";
10596
- import { homedir as homedir4 } from "os";
10597
- import { join as join6 } from "path";
10598
- import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
10449
+ import { existsSync as existsSync4 } from "fs";
10450
+ import { homedir as homedir5 } from "os";
10451
+ import { join as join8 } from "path";
10452
+ import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@earendil-works/pi-coding-agent";
10599
10453
  var ACP_TOOLS2 = ["compress", "decompress", "search_context", "acp_status"];
10600
10454
  var BUILTIN_DEFAULT_TOOLS = {
10601
10455
  advisor: ["read", "grep", "find", "ls", "bash", "intercom"],
@@ -10610,9 +10464,9 @@ var BUILTIN_DEFAULT_TOOLS = {
10610
10464
  };
10611
10465
  function resolveAgentDir() {
10612
10466
  const configured = process.env.PI_CODING_AGENT_DIR;
10613
- if (configured === "~") return homedir4();
10614
- if (configured?.startsWith("~/")) return join6(homedir4(), configured.slice(2));
10615
- return configured || join6(homedir4(), CONFIG_DIR_NAME3, "agent");
10467
+ if (configured === "~") return homedir5();
10468
+ if (configured?.startsWith("~/")) return join8(homedir5(), configured.slice(2));
10469
+ return configured || join8(homedir5(), CONFIG_DIR_NAME4, "agent");
10616
10470
  }
10617
10471
  function desiredTools(existing, name) {
10618
10472
  const base = Array.isArray(existing?.tools) && existing.tools.length > 0 ? [...existing.tools] : [...BUILTIN_DEFAULT_TOOLS[name] ?? []];
@@ -10622,7 +10476,7 @@ function desiredTools(existing, name) {
10622
10476
  return { tools: base, changed: true };
10623
10477
  }
10624
10478
  async function ensureSubagentAcpTools(settingsPath) {
10625
- const path4 = settingsPath ?? join6(resolveAgentDir(), "settings.json");
10479
+ const path4 = settingsPath ?? join8(resolveAgentDir(), "settings.json");
10626
10480
  let raw;
10627
10481
  let mtimeMs;
10628
10482
  try {
@@ -10657,7 +10511,7 @@ async function ensureSubagentAcpTools(settingsPath) {
10657
10511
  return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
10658
10512
  }
10659
10513
  const backupPath = `${path4}.acp-bak`;
10660
- if (!existsSync3(backupPath)) {
10514
+ if (!existsSync4(backupPath)) {
10661
10515
  try {
10662
10516
  await copyFile(path4, backupPath);
10663
10517
  } catch {
@@ -10720,64 +10574,6 @@ async function runSetupAndNotify(notify) {
10720
10574
  }
10721
10575
  }
10722
10576
 
10723
- // src/user-config.ts
10724
- import { promises as fs2 } from "fs";
10725
- import * as path3 from "path";
10726
- import { homedir as homedir5 } from "os";
10727
- import { CONFIG_DIR_NAME as CONFIG_DIR_NAME4 } from "@earendil-works/pi-coding-agent";
10728
- async function loadUserConfig(cwd) {
10729
- const home = homedir5();
10730
- const merged = {};
10731
- for (const base of [join8(home, CONFIG_DIR_NAME4), join8(cwd, CONFIG_DIR_NAME4)]) {
10732
- const file = join8(base, "acp.json");
10733
- try {
10734
- const raw = await fs2.readFile(file, "utf8");
10735
- const parsed = JSON.parse(raw);
10736
- if (parsed && typeof parsed === "object") {
10737
- Object.assign(merged, pickKnown(parsed));
10738
- debug.event("config-loaded", { file });
10739
- }
10740
- } catch (e) {
10741
- const code = e.code;
10742
- if (code !== "ENOENT") {
10743
- logWarn("config", { event: "load-failed", file, error: e instanceof Error ? e.message : String(e) });
10744
- }
10745
- }
10746
- }
10747
- return merged;
10748
- }
10749
- function join8(...parts) {
10750
- return path3.join(...parts);
10751
- }
10752
- var KNOWN = /* @__PURE__ */ new Set([
10753
- "debug",
10754
- "autoUpdate",
10755
- "modelContextLimit",
10756
- "toolBashDefaultTimeout",
10757
- "toolOutputMaxBytes",
10758
- "delegate",
10759
- "compress",
10760
- "displayUsage",
10761
- "prompts",
10762
- "acknowledgePromptsRisk"
10763
- ]);
10764
- function pickKnown(parsed) {
10765
- const out = {};
10766
- for (const [k, v] of Object.entries(parsed)) {
10767
- if (KNOWN.has(k)) out[k] = v;
10768
- }
10769
- return out;
10770
- }
10771
- function applyUserConfig(adapter, user) {
10772
- return {
10773
- ...adapter,
10774
- ...user,
10775
- coreOverrides: adapter.coreOverrides,
10776
- protectedTools: adapter.protectedTools,
10777
- preserveRecentMessages: adapter.preserveRecentMessages
10778
- };
10779
- }
10780
-
10781
10577
  // src/index.ts
10782
10578
  function createAcpExtension(adapter = {}) {
10783
10579
  return (pi) => {
@@ -10804,15 +10600,17 @@ function wireSessionLifecycle(pi, runtime) {
10804
10600
  pi.on("session_start", async (_event, ctx) => {
10805
10601
  runtime.store.invalidate();
10806
10602
  runtime.clearNudgeTracking();
10603
+ const modelId = ctx.model?.id ?? "default";
10604
+ runtime.density.resetModel(modelId);
10807
10605
  resetDelegateUsage();
10808
10606
  setDelegateDisplayUsage("separate");
10809
10607
  const sid = ctx.sessionManager.getSessionId();
10810
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.38" : null });
10608
+ runtime.clearSessionTracking(sid);
10609
+ const modelInfo = ctx.model;
10610
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.40" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
10811
10611
  try {
10812
- const user = await loadUserConfig(ctx.cwd);
10813
- runtime.setAdapter(applyUserConfig(runtime.adapter, user));
10612
+ await runtime.reloadConfig(ctx.cwd);
10814
10613
  setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
10815
- if (runtime.adapter.debug !== void 0) setDebugEnabled(runtime.adapter.debug);
10816
10614
  } catch (e) {
10817
10615
  logThrow("config", e, { sid, phase: "session_start" });
10818
10616
  }
@@ -10843,6 +10641,9 @@ function wireContextTransform(pi, runtime) {
10843
10641
  const sid = ctx.sessionManager.getSessionId();
10844
10642
  const release = await runtime.acquireLock(sid);
10845
10643
  try {
10644
+ await runtime.reloadConfig(ctx.cwd);
10645
+ const modelId = ctx.model?.id ?? "default";
10646
+ runtime.setCountModel(modelId);
10846
10647
  const { state, coreMessages, entries } = await runtime.stateFor(ctx, event.messages);
10847
10648
  const config = runtime.configFor(ctx);
10848
10649
  const coveredIds = collectCoveredMessageIds(state);
@@ -10850,9 +10651,15 @@ function wireContextTransform(pi, runtime) {
10850
10651
  const systemPromptText = getSystemPromptText(ctx);
10851
10652
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10852
10653
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10853
- const tokenCount = sentTokens;
10654
+ const tokenCount = calibrateTokens(sentTokens, runtime.density.densityFor(modelId));
10655
+ const postCompression = runtime.noteActiveBlocks(
10656
+ sid,
10657
+ state.blocks.filter((b) => b.active).map((b) => b.blockId)
10658
+ );
10854
10659
  debug.event("context-in", {
10855
10660
  sid,
10661
+ modelId,
10662
+ density: runtime.density.densityFor(modelId),
10856
10663
  eventMsgs: event.messages?.length ?? 0,
10857
10664
  entries: entries.length,
10858
10665
  coreMsgs: coreMessages.length,
@@ -10864,8 +10671,10 @@ function wireContextTransform(pi, runtime) {
10864
10671
  });
10865
10672
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
10866
10673
  await runtime.save(turn.state, ctx);
10674
+ runtime.density.update(modelId, realUsage?.tokens ?? null, sentTokens, postCompression);
10867
10675
  logInfo("turn", {
10868
10676
  sid,
10677
+ model: ctx.model?.id ?? null,
10869
10678
  inMsgs: coreMessages.length,
10870
10679
  outMsgs: turn.messages.length,
10871
10680
  tokens: tokenCount,
@@ -10877,6 +10686,8 @@ function wireContextTransform(pi, runtime) {
10877
10686
  activeBlocks: turn.state.blocks.filter((b) => b.active).length
10878
10687
  });
10879
10688
  debug.event("processTurn", {
10689
+ modelId,
10690
+ density: runtime.density.densityFor(modelId),
10880
10691
  outMsgs: turn.messages.length,
10881
10692
  summaryMsgs: turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
10882
10693
  prunedMsgs: coreMessages.length - turn.messages.length + turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,