billion-context-pi 0.1.38 → 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) {
@@ -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,
@@ -3428,8 +3556,14 @@ function pruneOrphanRefs(state, messages) {
3428
3556
  }
3429
3557
  }
3430
3558
  function createRuntime(adapter) {
3431
- 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
+ });
3432
3565
  const store = new SessionStateStore();
3566
+ const lastActiveBlockIds = /* @__PURE__ */ new Map();
3433
3567
  const locks = /* @__PURE__ */ new Map();
3434
3568
  let adapterRef = adapter;
3435
3569
  let promptsRef = defaultPrompts;
@@ -3454,7 +3588,8 @@ function createRuntime(adapter) {
3454
3588
  return m?.contextWindow ?? 0;
3455
3589
  }
3456
3590
  function configFor(ctx) {
3457
- return resolveConfig(adapterRef, liveContextLimit(ctx));
3591
+ const m = ctx.model;
3592
+ return resolveConfig(adapterRef, liveContextLimit(ctx), m?.provider, m?.id);
3458
3593
  }
3459
3594
  async function stateFor(ctx, liveMessages) {
3460
3595
  const sm = ctx.sessionManager;
@@ -3477,7 +3612,19 @@ function createRuntime(adapter) {
3477
3612
  const sm = ctx.sessionManager;
3478
3613
  await store.save(state, sm.getSessionFile() ?? void 0, sm.getSessionId());
3479
3614
  }
3480
- 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() {
3481
3628
  return adapterRef;
3482
3629
  }, setAdapter: (a) => {
3483
3630
  adapterRef = a;
@@ -7904,6 +8051,9 @@ function estimateTokens(messages, coveredIds) {
7904
8051
  }
7905
8052
  return tokens;
7906
8053
  }
8054
+ function calibrateTokens(estimate, density) {
8055
+ return density === 1 ? estimate : Math.round(estimate * density);
8056
+ }
7907
8057
  function lastUserMessageId(entries) {
7908
8058
  for (let i = entries.length - 1; i >= 0; i--) {
7909
8059
  const e = entries[i];
@@ -7974,6 +8124,7 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7974
8124
  if (ranges.length === 0) return "No ranges provided.";
7975
8125
  const { state: initialState, coreMessages } = await runtime.stateFor(ctx);
7976
8126
  const config = runtime.configFor(ctx);
8127
+ const modelId = ctx.model?.id ?? "default";
7977
8128
  const systemPromptText = getSystemPromptText(ctx);
7978
8129
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
7979
8130
  const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState)) + systemPromptTokens;
@@ -7981,15 +8132,18 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7981
8132
  messages: coreMessages,
7982
8133
  state: initialState,
7983
8134
  config,
7984
- tokenCount: sentTokens
8135
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
7985
8136
  });
7986
8137
  const state = turn.state;
7987
8138
  const messages = turn.messages;
7988
- const beforeTokens = estimateTokens(messages, collectCoveredMessageIds(state));
8139
+ const density = runtime.density.densityFor(modelId);
8140
+ const beforeTokens = calibrateTokens(estimateTokens(messages, collectCoveredMessageIds(state)), density);
7989
8141
  const summaryMaxChars = args.summaryMaxChars;
7990
8142
  const topLevelTopic = args.topic;
7991
8143
  debug.event("compress-in", {
7992
8144
  sid: ctx.sessionManager.getSessionId(),
8145
+ modelId,
8146
+ density,
7993
8147
  ranges: ranges.length,
7994
8148
  spans: ranges.map((r) => ({ span: `${r.startId}..${r.endId}`, summaryLen: r.summary.length, summary: r.summary, topic: r.topic ?? topLevelTopic ?? null })),
7995
8149
  blocksBefore: state.blocks.length,
@@ -8046,7 +8200,8 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
8046
8200
 
8047
8201
  // src/decompress-tool.ts
8048
8202
  import { writeFile, mkdir } from "fs/promises";
8049
- 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";
8050
8205
  import { tmpdir, homedir as homedir2 } from "os";
8051
8206
  var AUTO_DIR = join3(homedir2() || tmpdir(), ".cache", "pi", "acp-decompress");
8052
8207
  var PREVIEW_CHARS = 600;
@@ -8090,14 +8245,39 @@ var ALLOWED_DIRS = [
8090
8245
  function resolveToFilePath(targetPath) {
8091
8246
  const expanded = targetPath.startsWith("~/") ? join3(homedir2(), targetPath.slice(2)) : targetPath;
8092
8247
  const resolved = resolve(expanded);
8093
- const isAllowed = ALLOWED_DIRS.some((dir) => {
8094
- 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);
8095
8275
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
8096
8276
  });
8097
8277
  if (!isAllowed) {
8098
8278
  return { error: `Error: toFile path must be under ${tmpdir()}, ~/.cache/opencode, or ~/.cache/pi. Got: ${targetPath}` };
8099
8279
  }
8100
- return resolved;
8280
+ return checked;
8101
8281
  }
8102
8282
  function autoFilePath(blockId) {
8103
8283
  return join3(AUTO_DIR, `${blockId}-${Date.now()}.txt`);
@@ -8163,7 +8343,7 @@ ${text}`;
8163
8343
  }
8164
8344
  async function handleDecompress(args, runtime, ctx) {
8165
8345
  const { state, coreMessages } = await runtime.stateFor(ctx);
8166
- const arg = args.blockId.trim();
8346
+ const arg = (args.blockId ?? "").trim();
8167
8347
  const owner = state.blocks.find((b) => b.effectiveMessageIds.includes(arg));
8168
8348
  if (owner) {
8169
8349
  return handleMessageRef(arg, owner.blockId, args, ctx);
@@ -8335,420 +8515,6 @@ function formatSize(tokens) {
8335
8515
  return `${(tokens / 1e6).toFixed(1)}M`;
8336
8516
  }
8337
8517
 
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
8518
  // node_modules/billion-context-kit/dist/index.js
8753
8519
  var VIABLE_RANGE_MIN_TOKENS = 200;
8754
8520
  function viableRanges(ranges) {
@@ -8831,14 +8597,14 @@ function buildStatusPanel(input) {
8831
8597
  const protectedRanges = nudge?.protectedRanges ?? [];
8832
8598
  if (ranges.length > 0 || protectedRanges.length > 0) {
8833
8599
  lines.push("");
8834
- lines.push(formatRanges2(ranges, protectedRanges));
8600
+ lines.push(formatRanges(ranges, protectedRanges));
8835
8601
  }
8836
8602
  if (activeBlocksList.length > 0) {
8837
8603
  lines.push("");
8838
8604
  lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8839
8605
  for (const b of activeBlocksList) {
8840
8606
  const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
8841
- const summaryTok = defaultCountTokens2(b.summary || "");
8607
+ const summaryTok = defaultCountTokens(b.summary || "");
8842
8608
  const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
8843
8609
  lines.push(` [${b.blockId}] T${b.tier} ${fmt2(origTok)}\u2192${fmt2(summaryTok)}${topic}`);
8844
8610
  }
@@ -8858,10 +8624,10 @@ function buildStatusPanel(input) {
8858
8624
  import {
8859
8625
  spawn
8860
8626
  } from "child_process";
8861
- import { createWriteStream, existsSync as existsSync2 } from "fs";
8627
+ import { createWriteStream, existsSync as existsSync3 } from "fs";
8862
8628
  import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
8863
8629
  import { tmpdir as tmpdir2 } from "os";
8864
- 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";
8865
8631
 
8866
8632
  // src/footer-status.ts
8867
8633
  var FOOTER_STATUS_KEY = "billion-context-pi";
@@ -9277,11 +9043,11 @@ function delegateSpawnOptions(cwd, env) {
9277
9043
  var PI_CLI_ENTRY_RE = /[\\/]pi-coding-agent[\\/]dist[\\/]cli\.js$/;
9278
9044
  var PI_PACKAGE_REL = join4("@earendil-works", "pi-coding-agent", "dist", "cli.js");
9279
9045
  function probeUpFromArgv(argv1) {
9280
- let dir = resolvePath(dirname3(argv1) || process.cwd());
9046
+ let dir = resolvePath(dirname4(argv1) || process.cwd());
9281
9047
  for (; ; ) {
9282
9048
  const candidate = join4(dir, "node_modules", PI_PACKAGE_REL);
9283
- if (existsSync2(candidate)) return candidate;
9284
- const parent = dirname3(dir);
9049
+ if (existsSync3(candidate)) return candidate;
9050
+ const parent = dirname4(dir);
9285
9051
  if (parent === dir) return null;
9286
9052
  dir = parent;
9287
9053
  }
@@ -9306,7 +9072,7 @@ function resolvePiCliEntry(argv1, env = process.env, piHost = true) {
9306
9072
  const probed = probeUpFromArgv(argv1);
9307
9073
  if (probed) return probed;
9308
9074
  for (const candidate of piCliGlobalCandidates(env)) {
9309
- if (existsSync2(candidate)) return candidate;
9075
+ if (existsSync3(candidate)) return candidate;
9310
9076
  }
9311
9077
  logWarn("delegate", { event: "cli-entry-unresolved", argv1, fallback: "argv[1]" });
9312
9078
  }
@@ -10107,6 +9873,7 @@ async function handleStatus(args, runtime, ctx) {
10107
9873
  const { state, coreMessages } = await runtime.stateFor(ctx);
10108
9874
  const config = runtime.configFor(ctx);
10109
9875
  const coveredIds = collectCoveredMessageIds(state);
9876
+ const modelId = ctx.model?.id ?? "default";
10110
9877
  const systemPromptText = getSystemPromptText(ctx);
10111
9878
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10112
9879
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
@@ -10114,7 +9881,7 @@ async function handleStatus(args, runtime, ctx) {
10114
9881
  messages: coreMessages,
10115
9882
  state,
10116
9883
  config,
10117
- tokenCount: sentTokens
9884
+ tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId))
10118
9885
  });
10119
9886
  const processed = turn.messages;
10120
9887
  const base = buildStatusReport(turn.state, processed, defaultCountTokens, {
@@ -10232,9 +9999,10 @@ async function statusReport(runtime, ctx) {
10232
9999
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10233
10000
  const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
10234
10001
  const coveredIds = collectCoveredMessageIds(state);
10002
+ const modelId = ctx.model?.id ?? "default";
10235
10003
  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;
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;
10238
10006
  let text = buildStatusPanel({
10239
10007
  version: versionStr,
10240
10008
  tokenCount: sessionTokens,
@@ -10447,7 +10215,7 @@ function wireToolGuardrails(pi, runtime) {
10447
10215
 
10448
10216
  // src/update.ts
10449
10217
  import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
10450
- import { join as join5, dirname as dirname4 } from "path";
10218
+ import { join as join5, dirname as dirname5 } from "path";
10451
10219
  import { fileURLToPath } from "url";
10452
10220
  import { execFile } from "child_process";
10453
10221
  import { homedir as homedir3 } from "os";
@@ -10480,7 +10248,7 @@ async function readLastCheck() {
10480
10248
  }
10481
10249
  async function writeLastCheck(timestamp) {
10482
10250
  try {
10483
- await mkdir3(dirname4(THROTTLE_FILE), { recursive: true });
10251
+ await mkdir3(dirname5(THROTTLE_FILE), { recursive: true });
10484
10252
  await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
10485
10253
  } catch {
10486
10254
  }
@@ -10494,20 +10262,20 @@ async function readPackageJson(path4) {
10494
10262
  }
10495
10263
  }
10496
10264
  function findNpmRoot(extDir) {
10497
- let dir = dirname4(extDir);
10265
+ let dir = dirname5(extDir);
10498
10266
  for (; ; ) {
10499
- if (dir.endsWith("node_modules")) return dirname4(dir);
10500
- const parent = dirname4(dir);
10267
+ if (dir.endsWith("node_modules")) return dirname5(dir);
10268
+ const parent = dirname5(dir);
10501
10269
  if (parent === dir) return void 0;
10502
10270
  dir = parent;
10503
10271
  }
10504
10272
  }
10505
10273
  async function findExtensionDir() {
10506
- let dir = dirname4(fileURLToPath(import.meta.url));
10274
+ let dir = dirname5(fileURLToPath(import.meta.url));
10507
10275
  for (; ; ) {
10508
10276
  const pkg = await readPackageJson(join5(dir, "package.json"));
10509
10277
  if (pkg?.name === PACKAGE_NAME) return dir;
10510
- const parent = dirname4(dir);
10278
+ const parent = dirname5(dir);
10511
10279
  if (parent === dir) return void 0;
10512
10280
  dir = parent;
10513
10281
  }
@@ -10556,7 +10324,7 @@ async function checkForUpdate(autoUpdate, notify) {
10556
10324
  const data = await res.json();
10557
10325
  const latest = data.version;
10558
10326
  if (!latest) return;
10559
- const current = runtimeVersion ?? "0.1.38";
10327
+ const current = runtimeVersion ?? "0.1.39";
10560
10328
  const hasUpdate = isNewer(latest, current);
10561
10329
  debug.event("update-check", {
10562
10330
  current,
@@ -10592,7 +10360,7 @@ async function getRuntimeVersion() {
10592
10360
 
10593
10361
  // src/setup-subagent-tools.ts
10594
10362
  import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
10595
- import { existsSync as existsSync3 } from "fs";
10363
+ import { existsSync as existsSync4 } from "fs";
10596
10364
  import { homedir as homedir4 } from "os";
10597
10365
  import { join as join6 } from "path";
10598
10366
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
@@ -10657,7 +10425,7 @@ async function ensureSubagentAcpTools(settingsPath) {
10657
10425
  return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
10658
10426
  }
10659
10427
  const backupPath = `${path4}.acp-bak`;
10660
- if (!existsSync3(backupPath)) {
10428
+ if (!existsSync4(backupPath)) {
10661
10429
  try {
10662
10430
  await copyFile(path4, backupPath);
10663
10431
  } catch {
@@ -10804,10 +10572,14 @@ function wireSessionLifecycle(pi, runtime) {
10804
10572
  pi.on("session_start", async (_event, ctx) => {
10805
10573
  runtime.store.invalidate();
10806
10574
  runtime.clearNudgeTracking();
10575
+ const modelId = ctx.model?.id ?? "default";
10576
+ runtime.density.resetModel(modelId);
10807
10577
  resetDelegateUsage();
10808
10578
  setDelegateDisplayUsage("separate");
10809
10579
  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 });
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 });
10811
10583
  try {
10812
10584
  const user = await loadUserConfig(ctx.cwd);
10813
10585
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -10843,6 +10615,8 @@ function wireContextTransform(pi, runtime) {
10843
10615
  const sid = ctx.sessionManager.getSessionId();
10844
10616
  const release = await runtime.acquireLock(sid);
10845
10617
  try {
10618
+ const modelId = ctx.model?.id ?? "default";
10619
+ runtime.setCountModel(modelId);
10846
10620
  const { state, coreMessages, entries } = await runtime.stateFor(ctx, event.messages);
10847
10621
  const config = runtime.configFor(ctx);
10848
10622
  const coveredIds = collectCoveredMessageIds(state);
@@ -10850,9 +10624,15 @@ function wireContextTransform(pi, runtime) {
10850
10624
  const systemPromptText = getSystemPromptText(ctx);
10851
10625
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10852
10626
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10853
- const tokenCount = sentTokens;
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
+ );
10854
10632
  debug.event("context-in", {
10855
10633
  sid,
10634
+ modelId,
10635
+ density: runtime.density.densityFor(modelId),
10856
10636
  eventMsgs: event.messages?.length ?? 0,
10857
10637
  entries: entries.length,
10858
10638
  coreMsgs: coreMessages.length,
@@ -10864,8 +10644,10 @@ function wireContextTransform(pi, runtime) {
10864
10644
  });
10865
10645
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
10866
10646
  await runtime.save(turn.state, ctx);
10647
+ runtime.density.update(modelId, realUsage?.tokens ?? null, sentTokens, postCompression);
10867
10648
  logInfo("turn", {
10868
10649
  sid,
10650
+ model: ctx.model?.id ?? null,
10869
10651
  inMsgs: coreMessages.length,
10870
10652
  outMsgs: turn.messages.length,
10871
10653
  tokens: tokenCount,
@@ -10877,6 +10659,8 @@ function wireContextTransform(pi, runtime) {
10877
10659
  activeBlocks: turn.state.blocks.filter((b) => b.active).length
10878
10660
  });
10879
10661
  debug.event("processTurn", {
10662
+ modelId,
10663
+ density: runtime.density.densityFor(modelId),
10880
10664
  outMsgs: turn.messages.length,
10881
10665
  summaryMsgs: turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
10882
10666
  prunedMsgs: coreMessages.length - turn.messages.length + turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,