billion-context-pi 0.1.36 → 0.1.38

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
@@ -132,9 +132,11 @@ function prune(messages, state, options = {}) {
132
132
  const indexById = /* @__PURE__ */ new Map();
133
133
  messages.forEach((message, index) => indexById.set(message.id, index));
134
134
  const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
135
- return stripOrphanedToolResults(
136
- stripOrphanedToolCalls(
137
- rebuildMessages(messages, covered, firstUserIndex, anchors)
135
+ return stripOrphanedReasoning(
136
+ stripOrphanedToolResults(
137
+ stripOrphanedToolCalls(
138
+ rebuildMessages(messages, covered, firstUserIndex, anchors)
139
+ )
138
140
  )
139
141
  );
140
142
  }
@@ -211,6 +213,24 @@ function stripOrphanedToolCalls(messages) {
211
213
  (m) => m.contentType !== "tool-call" || !m.toolCallId || m.toolName === "compress" || knownResultIds.has(m.toolCallId)
212
214
  );
213
215
  }
216
+ function stripOrphanedReasoning(messages) {
217
+ const drop = /* @__PURE__ */ new Set();
218
+ for (let i = 0; i < messages.length; i++) {
219
+ if (drop.has(i)) continue;
220
+ if (messages[i].contentType !== "reasoning") continue;
221
+ let j = i;
222
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
223
+ j++;
224
+ }
225
+ const companion = messages[j + 1];
226
+ const hasCompanion = companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call");
227
+ if (!hasCompanion) {
228
+ for (let k = i; k <= j; k++) drop.add(k);
229
+ }
230
+ }
231
+ if (drop.size === 0) return messages;
232
+ return messages.filter((_, i) => !drop.has(i));
233
+ }
214
234
  function syncBlocks(messages, state) {
215
235
  const presentIds = new Set(messages.map((message) => message.id));
216
236
  const deactivated = [];
@@ -273,7 +293,8 @@ function defaultConfig(modelContextLimit, overrides = {}) {
273
293
  growthCap: 5e4,
274
294
  minGrowthFloor: 2e4,
275
295
  minGrowthRatio: 0.45,
276
- emergencyThresholdPct: 0.95
296
+ emergencyThresholdPct: 0.95,
297
+ tier2GrowthMultiplier: 1.5
277
298
  },
278
299
  promotionThreshold: 5,
279
300
  truncate: { threshold: 0.95 },
@@ -343,6 +364,18 @@ function parseBoundary(ref) {
343
364
  }
344
365
  return null;
345
366
  }
367
+ var BoundaryNotFoundError = class extends Error {
368
+ code = "BOUNDARY_NOT_FOUND";
369
+ kind;
370
+ endpoint;
371
+ constructor(kind, endpoint, message) {
372
+ super(message);
373
+ this.name = "BoundaryNotFoundError";
374
+ this.code = "BOUNDARY_NOT_FOUND";
375
+ this.kind = kind;
376
+ this.endpoint = endpoint;
377
+ }
378
+ };
346
379
  function resolveBoundaries(input) {
347
380
  const start = parseBoundary(input.startRef);
348
381
  const end = parseBoundary(input.endRef);
@@ -355,13 +388,8 @@ function resolveBoundaries(input) {
355
388
  input.messages.forEach(
356
389
  (message, index) => indexByRawId.set(message.id, index)
357
390
  );
358
- let startIndex = resolveAnchorIndex(start, input.state, indexByRawId);
359
- let endIndex = resolveAnchorIndex(end, input.state, indexByRawId);
360
- if (startIndex === null || endIndex === null) {
361
- throw new Error(
362
- `Boundary not found in visible context (likely consumed by an existing block). startId="${input.startRef}", endId="${input.endRef}".`
363
- );
364
- }
391
+ let startIndex = resolveAnchorIndex(start, input.state, indexByRawId, "start");
392
+ let endIndex = resolveAnchorIndex(end, input.state, indexByRawId, "end");
365
393
  if (startIndex > endIndex) {
366
394
  [startIndex, endIndex] = [endIndex, startIndex];
367
395
  }
@@ -392,16 +420,51 @@ function resolveBoundaries(input) {
392
420
  protectedGaps
393
421
  };
394
422
  }
395
- function resolveAnchorIndex(boundary, state, indexByRawId) {
423
+ function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
424
+ const label = endpoint === "start" ? "startId" : "endId";
396
425
  if (boundary.kind === "message") {
397
426
  const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
398
- if (!rawId) return null;
427
+ if (!rawId) {
428
+ throw new BoundaryNotFoundError(
429
+ "unknown",
430
+ endpoint,
431
+ `${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
432
+ );
433
+ }
399
434
  const index = indexByRawId.get(rawId);
400
- return index === void 0 ? null : index;
435
+ if (index === void 0) {
436
+ throw new BoundaryNotFoundError(
437
+ "consumed",
438
+ endpoint,
439
+ `${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
440
+ );
441
+ }
442
+ return index;
401
443
  }
402
444
  const block = blockById(state, `b${boundary.numericId}`);
403
- if (!block || !block.active) return null;
404
- return earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
445
+ if (!block) {
446
+ throw new BoundaryNotFoundError(
447
+ "unknown",
448
+ endpoint,
449
+ `${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
450
+ );
451
+ }
452
+ if (!block.active) {
453
+ throw new BoundaryNotFoundError(
454
+ "consumed",
455
+ endpoint,
456
+ `${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
457
+ );
458
+ }
459
+ const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
460
+ if (anchor === null) {
461
+ throw new BoundaryNotFoundError(
462
+ "consumed",
463
+ endpoint,
464
+ `${label}="b${boundary.numericId}" not found in visible context (block messages consumed by a higher-tier block).`
465
+ );
466
+ }
467
+ return anchor;
405
468
  }
406
469
  function formatPaddedRef(index) {
407
470
  return `m${String(index).padStart(5, "0")}`;
@@ -778,6 +841,38 @@ function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan =
778
841
  }
779
842
  return { startIndex: newStartIndex, endIndex: newEndIndex };
780
843
  }
844
+ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
845
+ if (startIndex > endIndex) {
846
+ return { startIndex, endIndex };
847
+ }
848
+ let newStartIndex = startIndex;
849
+ let newEndIndex = endIndex;
850
+ for (let i = startIndex; i <= endIndex && i < messages.length; i++) {
851
+ const msg = messages[i];
852
+ if (!msg) continue;
853
+ if (msg.contentType === "reasoning") {
854
+ let j = i;
855
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
856
+ j++;
857
+ }
858
+ const companion = messages[j + 1];
859
+ if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call") && j + 1 > newEndIndex) {
860
+ newEndIndex = j + 1;
861
+ }
862
+ }
863
+ if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
864
+ let k = i - 1;
865
+ while (k >= 0 && messages[k].contentType === "reasoning") {
866
+ k--;
867
+ }
868
+ const runStart = k + 1;
869
+ if (runStart < i && runStart >= 0 && messages[runStart].contentType === "reasoning" && runStart < newStartIndex) {
870
+ newStartIndex = runStart;
871
+ }
872
+ }
873
+ }
874
+ return { startIndex: newStartIndex, endIndex: newEndIndex };
875
+ }
781
876
  function refNum(ref) {
782
877
  const n = parseInt(ref.slice(1), 10);
783
878
  return Number.isNaN(n) ? -1 : n;
@@ -924,6 +1019,44 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
924
1019
  protected: protectedRanges
925
1020
  };
926
1021
  }
1022
+ function mergeBatch(batch) {
1023
+ const first = batch[0];
1024
+ const last = batch[batch.length - 1];
1025
+ const count = batch.reduce((s, r) => s + r.count, 0);
1026
+ const tokens = batch.reduce((s, r) => s + r.tokens, 0);
1027
+ const toolPct = Math.round(
1028
+ batch.reduce((s, r) => s + r.toolPct * r.count, 0) / count
1029
+ );
1030
+ const merged = {
1031
+ startRef: first.startRef,
1032
+ endRef: last.endRef,
1033
+ count,
1034
+ tokens,
1035
+ toolPct,
1036
+ textPct: 100 - toolPct
1037
+ };
1038
+ if (batch.some((r) => r.dangerous === true)) {
1039
+ merged.dangerous = true;
1040
+ }
1041
+ return merged;
1042
+ }
1043
+ function mergeRangesToThreshold(ranges, minChars) {
1044
+ if (minChars <= 0 || ranges.length === 0) return ranges;
1045
+ const result = [];
1046
+ let batch = [];
1047
+ for (const r of ranges) {
1048
+ batch.push(r);
1049
+ const batchTokens = batch.reduce((s, x) => s + x.tokens, 0);
1050
+ if (batchTokens * 4 >= minChars) {
1051
+ result.push(mergeBatch(batch));
1052
+ batch = [];
1053
+ }
1054
+ }
1055
+ if (batch.length > 0) {
1056
+ result.push(mergeBatch(batch));
1057
+ }
1058
+ return result;
1059
+ }
927
1060
  function runPipeline(nodes, initial, ctx) {
928
1061
  let io = initial;
929
1062
  for (const node of nodes) {
@@ -932,6 +1065,9 @@ function runPipeline(nodes, initial, ctx) {
932
1065
  }
933
1066
  return io;
934
1067
  }
1068
+ function rangeError(spec, message) {
1069
+ return `range ${spec.startRef}..${spec.endRef}: ${message}`;
1070
+ }
935
1071
  function createCore(ports = {}) {
936
1072
  const countTokens = ports.countTokens ?? defaultCountTokens;
937
1073
  function applyCompression(input) {
@@ -943,20 +1079,44 @@ function createCore(ports = {}) {
943
1079
  const warnings = [];
944
1080
  const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config, countTokens);
945
1081
  const preExistingCoverage = collectCoverage(state);
946
- const rangeIndexSets = [];
1082
+ const classifications = /* @__PURE__ */ new Map();
1083
+ const classificationErrors = [];
1084
+ const consumedRanges = [];
947
1085
  for (const spec of input.ranges) {
948
- let resolved;
949
1086
  try {
950
- resolved = resolveBoundaries({
1087
+ const resolved = resolveBoundaries({
951
1088
  startRef: spec.startRef,
952
1089
  endRef: spec.endRef,
953
1090
  messages: input.messages,
954
1091
  state
955
1092
  });
956
- } catch {
957
- continue;
1093
+ classifications.set(spec, { status: "ok", resolved });
1094
+ } catch (error) {
1095
+ if (error instanceof BoundaryNotFoundError) {
1096
+ classifications.set(
1097
+ spec,
1098
+ error.kind === "unknown" ? { status: "unknown", error } : { status: "consumed", error }
1099
+ );
1100
+ if (error.kind === "consumed") {
1101
+ consumedRanges.push(spec);
1102
+ } else {
1103
+ classificationErrors.push(rangeError(spec, error.message));
1104
+ }
1105
+ } else {
1106
+ classifications.set(spec, {
1107
+ status: "invalid",
1108
+ error: error instanceof Error ? error : new Error(String(error))
1109
+ });
1110
+ classificationErrors.push(
1111
+ rangeError(spec, error instanceof Error ? error.message : String(error))
1112
+ );
1113
+ }
958
1114
  }
959
- const indices = resolved.messageIds.map(
1115
+ }
1116
+ const rangeIndexSets = [];
1117
+ for (const [spec, resolution] of classifications) {
1118
+ if (resolution.status !== "ok") continue;
1119
+ const indices = resolution.resolved.messageIds.map(
960
1120
  (id) => input.messages.findIndex((m) => m.id === id)
961
1121
  ).filter((i) => i >= 0);
962
1122
  rangeIndexSets.push({ spec, indices });
@@ -983,37 +1143,27 @@ function createCore(ports = {}) {
983
1143
  if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
984
1144
  let totalRangeChars = 0;
985
1145
  let hasBlockBoundaryRange = false;
986
- for (const spec of input.ranges) {
987
- if (skipSpecs.has(spec)) continue;
988
- let resolved;
989
- try {
990
- resolved = resolveBoundaries({
991
- startRef: spec.startRef,
992
- endRef: spec.endRef,
993
- messages: input.messages,
994
- state
995
- });
996
- } catch {
997
- continue;
998
- }
999
- if (resolved.boundaryKind === "block") {
1146
+ let countedRanges = 0;
1147
+ for (const [spec, resolution] of classifications) {
1148
+ if (resolution.status !== "ok" || skipSpecs.has(spec)) continue;
1149
+ if (resolution.resolved.boundaryKind === "block") {
1000
1150
  hasBlockBoundaryRange = true;
1001
1151
  continue;
1002
1152
  }
1003
- for (const id of resolved.messageIds) {
1153
+ countedRanges++;
1154
+ for (const id of resolution.resolved.messageIds) {
1004
1155
  const msg = input.messages.find((m) => m.id === id);
1005
1156
  totalRangeChars += msg?.text?.length ?? 0;
1006
1157
  }
1007
1158
  }
1008
1159
  if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
1160
+ const gateMessage = consumedRanges.length > 0 ? `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}); remaining compressible content ${totalRangeChars} chars < min ${input.config.compress.minCompressRange}. Nothing to do \u2014 run acp_status to see current compressible ranges.` : `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`;
1009
1161
  return {
1010
1162
  state: input.state,
1011
1163
  result: {
1012
1164
  blocksCreated: 0,
1013
1165
  tokensCompressed: 0,
1014
- errors: [
1015
- `Total compressible content too small (${totalRangeChars} chars across ${input.ranges.length} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`
1016
- ],
1166
+ errors: [gateMessage, ...classificationErrors],
1017
1167
  warnings: []
1018
1168
  }
1019
1169
  };
@@ -1021,6 +1171,18 @@ function createCore(ports = {}) {
1021
1171
  }
1022
1172
  for (const spec of input.ranges) {
1023
1173
  if (skipSpecs.has(spec)) continue;
1174
+ const resolution = classifications.get(spec);
1175
+ if (resolution === void 0) continue;
1176
+ if (resolution.status === "consumed") {
1177
+ warnings.push(
1178
+ `Skipped range (${spec.startRef}..${spec.endRef}) \u2014 already compressed (messages consumed by existing block(s)); nothing to compress.`
1179
+ );
1180
+ continue;
1181
+ }
1182
+ if (resolution.status === "unknown" || resolution.status === "invalid") {
1183
+ errors.push(rangeError(spec, resolution.error.message));
1184
+ continue;
1185
+ }
1024
1186
  try {
1025
1187
  const outcome = applySingleRange({
1026
1188
  spec,
@@ -1036,7 +1198,7 @@ function createCore(ports = {}) {
1036
1198
  tokensCompressed += outcome.tokens;
1037
1199
  warnings.push(...outcome.warnings);
1038
1200
  } catch (error) {
1039
- errors.push(error instanceof Error ? error.message : String(error));
1201
+ errors.push(rangeError(spec, error instanceof Error ? error.message : String(error)));
1040
1202
  }
1041
1203
  }
1042
1204
  state.stats.compressionCount += blocksCreated;
@@ -1174,7 +1336,10 @@ var recommendNode = {
1174
1336
  const nothingToCompress = contextRanges.compressible.length === 0;
1175
1337
  const recommendation = {
1176
1338
  contextRanges,
1177
- recommendedRanges: contextRanges.compressible,
1339
+ recommendedRanges: mergeRangesToThreshold(
1340
+ contextRanges.compressible,
1341
+ ctx.config.compress.minCompressRange
1342
+ ),
1178
1343
  nothingToCompress
1179
1344
  };
1180
1345
  return { ...io, effects: { ...io.effects, recommendation } };
@@ -1200,6 +1365,7 @@ var nudgeNode = {
1200
1365
  if (baseline > 0 && ctx.tokenCount < baseline - nudgeGrowthTokens) {
1201
1366
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
1202
1367
  stamped.lastNudgeShownTokens = 0;
1368
+ stamped.lastShownByTier = {};
1203
1369
  }
1204
1370
  if (stamped.lastPerMessageNudgeTokens === 0) {
1205
1371
  stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
@@ -1244,7 +1410,7 @@ function applySingleRange(input) {
1244
1410
  messages: input.messages,
1245
1411
  state: input.state
1246
1412
  });
1247
- const rangeMessageIds = applyToolPairAdjustment(
1413
+ const rangeMessageIds = applyPairBoundaryAdjustments(
1248
1414
  resolved,
1249
1415
  input.messages
1250
1416
  );
@@ -1358,20 +1524,33 @@ function applySingleRange(input) {
1358
1524
  }
1359
1525
  return { tokens: compressedTokens, warnings };
1360
1526
  }
1361
- function applyToolPairAdjustment(resolved, messages) {
1527
+ function applyPairBoundaryAdjustments(resolved, messages) {
1362
1528
  if (resolved.boundaryKind === "block") {
1363
1529
  return resolved.messageIds;
1364
1530
  }
1365
- const adjusted = adjustBoundariesForToolPairs(
1366
- resolved.startIndex,
1367
- resolved.endIndex,
1368
- messages
1369
- );
1370
- if (adjusted.startIndex === resolved.startIndex && adjusted.endIndex === resolved.endIndex) {
1531
+ let startIndex = resolved.startIndex;
1532
+ let endIndex = resolved.endIndex;
1533
+ for (let pass = 0; pass < 2; pass++) {
1534
+ const reasoningAdjusted = adjustBoundariesForReasoningPairs(
1535
+ startIndex,
1536
+ endIndex,
1537
+ messages
1538
+ );
1539
+ const toolAdjusted = adjustBoundariesForToolPairs(
1540
+ reasoningAdjusted.startIndex,
1541
+ reasoningAdjusted.endIndex,
1542
+ messages
1543
+ );
1544
+ const changed = toolAdjusted.startIndex !== startIndex || toolAdjusted.endIndex !== endIndex;
1545
+ startIndex = toolAdjusted.startIndex;
1546
+ endIndex = toolAdjusted.endIndex;
1547
+ if (!changed) break;
1548
+ }
1549
+ if (startIndex === resolved.startIndex && endIndex === resolved.endIndex) {
1371
1550
  return resolved.messageIds;
1372
1551
  }
1373
1552
  const ids = [];
1374
- for (let i = adjusted.startIndex; i <= adjusted.endIndex; i++) {
1553
+ for (let i = startIndex; i <= endIndex; i++) {
1375
1554
  const msg = messages[i];
1376
1555
  if (msg) ids.push(msg.id);
1377
1556
  }
@@ -1455,10 +1634,11 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
1455
1634
  )
1456
1635
  );
1457
1636
  }
1458
- function pendingByTier(state, recommendation, countTokens) {
1637
+ function pendingByTier(state, recommendation, countTokens, minCompressRange) {
1459
1638
  const out = {};
1460
- const compressible = recommendation?.contextRanges.compressible ?? [];
1461
- out[1] = { pending: compressible.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1639
+ const merged = recommendation?.recommendedRanges ?? [];
1640
+ const effective = minCompressRange > 0 ? merged.filter((r) => r.tokens * 4 >= minCompressRange) : merged;
1641
+ out[1] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
1462
1642
  const active = activeBlocks(state);
1463
1643
  const t1 = active.filter((b) => b.tier === 1);
1464
1644
  const t2 = active.filter((b) => b.tier === 2);
@@ -1473,6 +1653,7 @@ function decideNudge(input) {
1473
1653
  const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
1474
1654
  const overLimit = usage >= config.nudge.maxContextLimitPct;
1475
1655
  const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
1656
+ const pressure = overLimit || emergencyOverride;
1476
1657
  const baseline = state.nudge.lastPerMessageNudgeTokens;
1477
1658
  const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
1478
1659
  const hasPendingNudge = hadPendingNudge;
@@ -1484,44 +1665,67 @@ function decideNudge(input) {
1484
1665
  );
1485
1666
  const growthSinceReference = tokenCount - growthReference;
1486
1667
  const rec = recommendation;
1487
- const tiers = pendingByTier(state, rec, countTokens);
1668
+ const tiers = pendingByTier(
1669
+ state,
1670
+ rec,
1671
+ countTokens,
1672
+ config.compress.minCompressRange
1673
+ );
1674
+ const tier2Threshold = Math.round(
1675
+ nudgeGrowthTokens * (config.nudge.tier2GrowthMultiplier ?? 1.5)
1676
+ );
1488
1677
  let injectedTier = null;
1489
1678
  let injectedReason = "";
1490
1679
  const growthReady = growthSinceReference >= growthFloor;
1491
- if (!overLimit && growthReady) {
1492
- for (const tier of [1, 2, 3]) {
1493
- if (!config.tiers.enabled && tier > 1) break;
1494
- const info = tiers[tier];
1495
- if (!info || info.pending < nudgeGrowthTokens) continue;
1496
- const lastShown = state.nudge.lastShownByTier[tier] ?? 0;
1497
- const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1498
- if (!cadenceMet) continue;
1499
- injectedTier = tier;
1500
- injectedReason = tier === 1 ? `T1 compressible ${info.pending} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%` : `T${tier} distill ready: ${info.targetBlocks.length} tier-${tier - 1} blocks (${info.pending} tokens) >= ${nudgeGrowthTokens}, usage ${Math.round(usage * 100)}%`;
1501
- break;
1680
+ const t1Eff = tiers[1]?.pending ?? 0;
1681
+ const t2Pen = tiers[2]?.pending ?? 0;
1682
+ const t3Pen = tiers[3]?.pending ?? 0;
1683
+ if (pressure) {
1684
+ const candidates = [1];
1685
+ if (config.tiers.enabled) {
1686
+ candidates.push(2, 3);
1687
+ }
1688
+ let best = null;
1689
+ let bestPending = 0;
1690
+ for (const t of candidates) {
1691
+ const p = tiers[t]?.pending ?? 0;
1692
+ if (p > bestPending) {
1693
+ bestPending = p;
1694
+ best = t;
1695
+ }
1502
1696
  }
1503
- } else if (overLimit) {
1504
- for (const tier of [1, 2, 3]) {
1505
- if (!config.tiers.enabled && tier > 1) break;
1506
- const info = tiers[tier];
1507
- if (!info || info.pending < config.compress.minCompressRange) continue;
1508
- injectedTier = tier;
1509
- injectedReason = emergencyOverride ? `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%, T${tier} pending ${info.pending}` : `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}%, T${tier} pending ${info.pending}`;
1510
- break;
1697
+ if (best !== null && bestPending > 0) {
1698
+ injectedTier = best;
1699
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1700
+ injectedReason = best === 1 ? `${label} T1: max effective pending ${bestPending}, usage ${Math.round(usage * 100)}%` : `${label} T${best} distill: max pending ${bestPending} (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}), usage ${Math.round(usage * 100)}%`;
1701
+ }
1702
+ } else if (growthReady) {
1703
+ if (t1Eff >= nudgeGrowthTokens) {
1704
+ injectedTier = 1;
1705
+ injectedReason = `T1 effective ${t1Eff} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%`;
1706
+ } else if (config.tiers.enabled && t2Pen >= tier2Threshold && t2Pen > t1Eff) {
1707
+ const lastShown = state.nudge.lastShownByTier[2] ?? 0;
1708
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1709
+ if (cadenceMet) {
1710
+ injectedTier = 2;
1711
+ injectedReason = `T2 distill ready: ${tiers[2].targetBlocks.length} tier-1 blocks (${t2Pen} tokens) >= ${tier2Threshold} (1.5x) and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
1712
+ }
1713
+ } else if (config.tiers.enabled && t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff) {
1714
+ const lastShown = state.nudge.lastShownByTier[3] ?? 0;
1715
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
1716
+ if (cadenceMet) {
1717
+ injectedTier = 3;
1718
+ injectedReason = `T3 condense ready: ${tiers[3].targetBlocks.length} tier-2 blocks (${t3Pen} tokens) >= ${tier2Threshold} (1.5x) and > T2 ${t2Pen} and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
1719
+ }
1511
1720
  }
1512
1721
  }
1513
- const shouldInject = injectedTier !== null || overLimit && (rec?.recommendedRanges?.length ?? 0) > 0;
1722
+ const shouldInject = injectedTier !== null;
1514
1723
  let reason;
1515
- if (emergencyOverride && injectedTier !== null) {
1516
- reason = injectedReason;
1517
- } else if (emergencyOverride) {
1518
- reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}% (no compressible content)`;
1519
- } else if (overLimit && injectedTier !== null) {
1520
- reason = injectedReason;
1521
- } else if (overLimit) {
1522
- reason = `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}% (no compressible content)`;
1523
- } else if (injectedTier !== null) {
1724
+ if (injectedTier !== null) {
1524
1725
  reason = injectedReason;
1726
+ } else if (pressure) {
1727
+ const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
1728
+ reason = `${label}: usage ${Math.round(usage * 100)}% but no tier has effective compressible content (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 nudge suppressed to avoid offering ranges below minCompressRange`;
1525
1729
  } else {
1526
1730
  const tiersList = [1, 2, 3];
1527
1731
  const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
@@ -1727,12 +1931,39 @@ DROP:
1727
1931
  - Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
1728
1932
 
1729
1933
  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.`;
1730
- var EFFICIENCY_NOTE = `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
1934
+ var defaultPrompts = Object.freeze({
1935
+ compressPhilosophy: COMPRESS_PHILOSOPHY,
1936
+ howToCompressRules: HOW_TO_COMPRESS_RULES,
1937
+ tier2DistillRules: TIER2_DISTILL_RULES,
1938
+ tier3CondenseRules: TIER3_CONDENSE_RULES
1939
+ });
1940
+ function resolvePrompts(overrides, options = {}) {
1941
+ const clean = {};
1942
+ if (overrides) {
1943
+ for (const [key, value] of Object.entries(overrides)) {
1944
+ if (typeof value === "string") {
1945
+ clean[key] = value;
1946
+ }
1947
+ }
1948
+ }
1949
+ const keys = Object.keys(clean);
1950
+ if (keys.length > 0 && !options.acknowledgeRisk) {
1951
+ throw new Error(
1952
+ `resolvePrompts: overriding compression rules requires { acknowledgeRisk: true }. Overridden keys: ${keys.join(", ")}. These rules are quality-critical (tuned over months of production use); changing them can degrade summary quality and break retrieval (summaries may lose paths, signatures, decisions).`
1953
+ );
1954
+ }
1955
+ return { ...defaultPrompts, ...clean };
1956
+ }
1957
+ function efficiencyNote(prompts) {
1958
+ return `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
1731
1959
 
1732
- ${COMPRESS_PHILOSOPHY}`;
1733
- var EMERGENCY_HEADER = `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
1960
+ ${prompts.compressPhilosophy}`;
1961
+ }
1962
+ function emergencyHeader(prompts) {
1963
+ return `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
1734
1964
 
1735
- ${COMPRESS_PHILOSOPHY}`;
1965
+ ${prompts.compressPhilosophy}`;
1966
+ }
1736
1967
  function formatK(n) {
1737
1968
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1738
1969
  return `${n}`;
@@ -1840,43 +2071,45 @@ function formatRanges(compressible, protectedRanges) {
1840
2071
  return `Compressible ranges (${merged.length}, oldest first):
1841
2072
  ${lines.join("\n")}`;
1842
2073
  }
1843
- function renderNudgeText(decision) {
2074
+ function renderNudgeText(decision, prompts = defaultPrompts) {
1844
2075
  const breakdownStr = formatBreakdown(decision.contextBreakdown);
1845
2076
  const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
2077
+ const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
1846
2078
  if (decision.tier !== null && decision.tier >= 2) {
1847
2079
  const isT2 = decision.tier === 2;
1848
2080
  const targets = decision.tierTargetBlocks ?? [];
1849
2081
  const blockList = formatTierTargetBlocks(targets);
1850
2082
  const startId = targets[0]?.blockId ?? "b1";
1851
2083
  const endId = targets[targets.length - 1]?.blockId ?? "b5";
2084
+ const voice = isEmergency ? "emergency" : "gentle";
2085
+ const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
1852
2086
  return {
1853
- voice: "gentle",
2087
+ voice,
1854
2088
  text: [
1855
- EFFICIENCY_NOTE,
2089
+ efficiencyNote(prompts),
1856
2090
  "",
1857
2091
  breakdownStr,
1858
2092
  "",
1859
- `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
2093
+ triggerLine,
1860
2094
  isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
1861
2095
  blockList,
1862
2096
  `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
1863
2097
  "",
1864
- HOW_TO_COMPRESS_RULES,
2098
+ prompts.howToCompressRules,
1865
2099
  "",
1866
- isT2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES
2100
+ isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
1867
2101
  ].join("\n")
1868
2102
  };
1869
2103
  }
1870
- const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
1871
2104
  if (isEmergency) {
1872
2105
  return {
1873
2106
  voice: "emergency",
1874
2107
  text: [
1875
- EMERGENCY_HEADER,
2108
+ emergencyHeader(prompts),
1876
2109
  "",
1877
2110
  breakdownStr,
1878
2111
  "",
1879
- HOW_TO_COMPRESS_RULES,
2112
+ prompts.howToCompressRules,
1880
2113
  "",
1881
2114
  `{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
1882
2115
  "Only use IDs from visible messages above. Compress older work first.",
@@ -1888,11 +2121,11 @@ function renderNudgeText(decision) {
1888
2121
  return {
1889
2122
  voice: "gentle",
1890
2123
  text: [
1891
- EFFICIENCY_NOTE,
2124
+ efficiencyNote(prompts),
1892
2125
  "",
1893
2126
  breakdownStr,
1894
2127
  "",
1895
- HOW_TO_COMPRESS_RULES,
2128
+ prompts.howToCompressRules,
1896
2129
  "",
1897
2130
  rangesStr,
1898
2131
  "",
@@ -2231,9 +2464,9 @@ function charBigrams(text) {
2231
2464
  }
2232
2465
  return grams;
2233
2466
  }
2234
- function tfMap(text, stem2) {
2467
+ function tfMap(text, stem22) {
2235
2468
  const m = /* @__PURE__ */ new Map();
2236
- for (const t of tokenize(text, { stem: stem2 })) m.set(t, (m.get(t) ?? 0) + 1);
2469
+ for (const t of tokenize(text, { stem: stem22 })) m.set(t, (m.get(t) ?? 0) + 1);
2237
2470
  return m;
2238
2471
  }
2239
2472
  var bm25Algorithm = {
@@ -2980,6 +3213,99 @@ function mergeInitialState(parsed) {
2980
3213
  };
2981
3214
  }
2982
3215
 
3216
+ // src/sequence-match.ts
3217
+ function findUniqueLongestRun(candidates, live) {
3218
+ if (candidates.length === 0 || live.length === 0) return void 0;
3219
+ const ids = /* @__PURE__ */ new Map();
3220
+ const intern = (key) => {
3221
+ const existing = ids.get(key);
3222
+ if (existing !== void 0) return existing;
3223
+ const id = ids.size + 1;
3224
+ ids.set(key, id);
3225
+ return id;
3226
+ };
3227
+ const candidateIds = candidates.map(intern);
3228
+ const liveIds = live.map(intern);
3229
+ const separator = ids.size + 1;
3230
+ const sequence = [...candidateIds, separator, ...liveIds];
3231
+ const suffixArray = buildSuffixArray(sequence);
3232
+ const lcp = buildLcp(sequence, suffixArray);
3233
+ const candidateLength = candidates.length;
3234
+ const liveOffset = candidateLength + 1;
3235
+ const sourceOf = (suffix) => {
3236
+ if (suffix < candidateLength) return 0;
3237
+ if (suffix >= liveOffset) return 1;
3238
+ return void 0;
3239
+ };
3240
+ let bestLength = 0;
3241
+ for (let index = 1; index < suffixArray.length; index++) {
3242
+ const leftSource = sourceOf(suffixArray[index - 1]);
3243
+ const rightSource = sourceOf(suffixArray[index]);
3244
+ if (leftSource === void 0 || rightSource === void 0 || leftSource === rightSource) continue;
3245
+ bestLength = Math.max(bestLength, lcp[index]);
3246
+ }
3247
+ if (bestLength === 0) return void 0;
3248
+ let pairCount = 0;
3249
+ let uniqueCandidateStart = -1;
3250
+ let uniqueLiveStart = -1;
3251
+ for (let start = 0; start < suffixArray.length; ) {
3252
+ let end = start;
3253
+ while (end + 1 < suffixArray.length && lcp[end + 1] >= bestLength) end++;
3254
+ if (end > start) {
3255
+ const candidateStarts = [];
3256
+ const liveStarts = [];
3257
+ for (let index = start; index <= end; index++) {
3258
+ const suffix = suffixArray[index];
3259
+ const source = sourceOf(suffix);
3260
+ if (source === 0) candidateStarts.push(suffix);
3261
+ else if (source === 1) liveStarts.push(suffix - liveOffset);
3262
+ }
3263
+ const groupPairs = candidateStarts.length * liveStarts.length;
3264
+ pairCount += groupPairs;
3265
+ if (groupPairs === 1) {
3266
+ uniqueCandidateStart = candidateStarts[0];
3267
+ uniqueLiveStart = liveStarts[0];
3268
+ }
3269
+ if (pairCount > 1) return void 0;
3270
+ }
3271
+ start = end + 1;
3272
+ }
3273
+ return pairCount === 1 ? { candidateStart: uniqueCandidateStart, liveStart: uniqueLiveStart, length: bestLength } : void 0;
3274
+ }
3275
+ function buildSuffixArray(sequence) {
3276
+ const suffixArray = sequence.map((_, index) => index);
3277
+ let ranks = [...sequence];
3278
+ for (let width = 1; width < sequence.length; width *= 2) {
3279
+ suffixArray.sort((left, right) => ranks[left] - ranks[right] || (ranks[left + width] ?? -1) - (ranks[right + width] ?? -1));
3280
+ const nextRanks = Array(sequence.length);
3281
+ nextRanks[suffixArray[0]] = 0;
3282
+ for (let index = 1; index < suffixArray.length; index++) {
3283
+ const previous = suffixArray[index - 1];
3284
+ const current = suffixArray[index];
3285
+ const differs = ranks[previous] !== ranks[current] || (ranks[previous + width] ?? -1) !== (ranks[current + width] ?? -1);
3286
+ nextRanks[current] = nextRanks[previous] + (differs ? 1 : 0);
3287
+ }
3288
+ ranks = nextRanks;
3289
+ if (ranks[suffixArray.at(-1)] === sequence.length - 1) break;
3290
+ }
3291
+ return suffixArray;
3292
+ }
3293
+ function buildLcp(sequence, suffixArray) {
3294
+ const positions = Array(sequence.length);
3295
+ for (let index = 0; index < suffixArray.length; index++) positions[suffixArray[index]] = index;
3296
+ const lcp = Array(sequence.length).fill(0);
3297
+ let length = 0;
3298
+ for (let start = 0; start < sequence.length; start++) {
3299
+ const position = positions[start];
3300
+ if (position === 0) continue;
3301
+ const previous = suffixArray[position - 1];
3302
+ while (start + length < sequence.length && previous + length < sequence.length && sequence[start + length] === sequence[previous + length]) length++;
3303
+ lcp[position] = length;
3304
+ if (length > 0) length--;
3305
+ }
3306
+ return lcp;
3307
+ }
3308
+
2983
3309
  // src/runtime.ts
2984
3310
  function readContextEntries(sm) {
2985
3311
  const source = sm;
@@ -2993,15 +3319,17 @@ function isPiHost(sm) {
2993
3319
  }
2994
3320
  function mergeLiveEntries(entries, live, state, origins) {
2995
3321
  const persisted = entries.filter((e) => e.type === "message");
2996
- const matched = matchPersistedSuffix(persisted, live);
2997
- const prior = matchOrigins(origins, live);
3322
+ const liveIdentities = live.map(messageIdentity);
3323
+ const persistedIdentities = persisted.map((entry) => messageIdentity(entry.message));
3324
+ const persistedRange = findUniqueLongestRun(persistedIdentities, normalizePersistedMatchKeys(persisted, persistedIdentities, live, liveIdentities));
3325
+ const originRange = findUniqueLongestRun(origins.map((origin) => origin.identity), liveIdentities);
2998
3326
  const out = [];
2999
3327
  const nextOrigins = [];
3000
3328
  const usedIds = /* @__PURE__ */ new Set();
3001
3329
  for (let i = 0; i < live.length; i++) {
3002
3330
  const msg = live[i];
3003
- const entry = matched[i];
3004
- const origin = prior[i];
3331
+ const entry = valueInRange(persisted, persistedRange, i);
3332
+ const origin = valueInRange(origins, originRange, i);
3005
3333
  if (entry) {
3006
3334
  if (origin) migrateLiveRefs(state, origin.rawId, entry.id);
3007
3335
  else migrateTaggedRef(state, msg, entry.id);
@@ -3011,23 +3339,13 @@ function mergeLiveEntries(entries, live, state, origins) {
3011
3339
  const id = origin?.rawId ?? nextLiveId(state, usedIds, i);
3012
3340
  usedIds.add(id);
3013
3341
  out.push({ type: "message", id, parentId: null, timestamp: String(msg.timestamp ?? Date.now()), message: msg });
3014
- nextOrigins.push({ rawId: id, identity: messageIdentity(msg) });
3342
+ nextOrigins.push({ rawId: id, identity: liveIdentities[i] });
3015
3343
  }
3016
3344
  origins.splice(0, origins.length, ...nextOrigins);
3017
- const unmatched = live.length - matched.length;
3345
+ const unmatched = live.length - (persistedRange?.length ?? 0);
3018
3346
  if (unmatched > 0) logInfo("runtime", { event: "merge-live-entries", live: live.length, unmatched });
3019
3347
  return out;
3020
3348
  }
3021
- function matchOrigins(origins, live) {
3022
- for (let start = 0; start < origins.length; start++) {
3023
- const count = origins.length - start;
3024
- if (count > live.length) continue;
3025
- if (origins.slice(start).every((origin, index) => origin.identity === messageIdentity(live[index]))) {
3026
- return [...origins.slice(start), ...Array(live.length - count)];
3027
- }
3028
- }
3029
- return Array(live.length);
3030
- }
3031
3349
  function nextLiveId(state, used, index) {
3032
3350
  let id = `live-${index}`;
3033
3351
  let suffix = index;
@@ -3054,31 +3372,39 @@ function migrateLiveRefs(state, liveId, stableId) {
3054
3372
  delete state.messageRefs.byRaw[rawId];
3055
3373
  }
3056
3374
  }
3057
- function matchPersistedSuffix(persisted, live) {
3058
- for (let start = 0; start < persisted.length; start++) {
3059
- const count = persisted.length - start;
3060
- if (count > live.length) continue;
3061
- const suffix = persisted.slice(start);
3062
- if (suffix.every((entry, index) => sameMessage(entry.message, live[index]))) return suffix;
3063
- }
3064
- return [];
3375
+ var NO_PERSISTED_MATCH = /* @__PURE__ */ Symbol("no-persisted-match");
3376
+ function normalizePersistedMatchKeys(persisted, persistedIdentities, live, liveIdentities) {
3377
+ const persistedByStructure = /* @__PURE__ */ new Map();
3378
+ for (let index = 0; index < persisted.length; index++) {
3379
+ const key = toolResultStructureKey(persisted[index].message);
3380
+ if (key === void 0) continue;
3381
+ persistedByStructure.set(key, persistedByStructure.has(key) ? -1 : index);
3382
+ }
3383
+ return live.map((message, liveIndex) => {
3384
+ const key = toolResultStructureKey(message);
3385
+ const candidateIndex = key === void 0 ? void 0 : persistedByStructure.get(key);
3386
+ if (candidateIndex === void 0) return liveIdentities[liveIndex];
3387
+ if (candidateIndex < 0) return NO_PERSISTED_MATCH;
3388
+ return sameToolResult(persisted[candidateIndex].message, message) ? persistedIdentities[candidateIndex] : liveIdentities[liveIndex];
3389
+ });
3065
3390
  }
3066
- function sameMessage(a, b) {
3067
- try {
3068
- if (messageIdentity(a) === messageIdentity(b)) return true;
3069
- const ra = a.role;
3070
- const rb = b.role;
3071
- if (ra !== rb || ra !== "toolResult") return false;
3072
- const ca = a.content;
3073
- const cb = b.content;
3074
- return sameNonTextBlocks(ca, cb) && matchesStoredText(extractText(ca), extractText(cb));
3075
- } catch (e) {
3076
- logWarn("runtime", { event: "message-compare-failed", error: e instanceof Error ? e.message : String(e) });
3077
- return a === b;
3078
- }
3391
+ function toolResultStructureKey(message) {
3392
+ if (message.role !== "toolResult") return void 0;
3393
+ return `${message.toolName}\0${message.toolCallId}`;
3394
+ }
3395
+ function valueInRange(values, range, liveIndex) {
3396
+ if (!range || liveIndex < range.liveStart || liveIndex >= range.liveStart + range.length) return void 0;
3397
+ return values[range.candidateStart + liveIndex - range.liveStart];
3398
+ }
3399
+ function sameToolResult(stored, visible) {
3400
+ if (stored.role !== "toolResult" || visible.role !== "toolResult") return false;
3401
+ return sameNonTextBlocks(stored.content, visible.content) && matchesStoredText(extractText(stored.content), extractText(visible.content));
3079
3402
  }
3080
3403
  function sameNonTextBlocks(a, b) {
3081
- const nonText = (blocks) => blocks.filter((block) => block.type !== "text");
3404
+ const nonText = (blocks) => blocks.filter((block) => {
3405
+ if (!block || typeof block !== "object" || !("type" in block)) return true;
3406
+ return block.type !== "text";
3407
+ });
3082
3408
  try {
3083
3409
  const na = Array.isArray(a) ? nonText(a) : [];
3084
3410
  const nb = Array.isArray(b) ? nonText(b) : [];
@@ -3106,6 +3432,7 @@ function createRuntime(adapter) {
3106
3432
  const store = new SessionStateStore();
3107
3433
  const locks = /* @__PURE__ */ new Map();
3108
3434
  let adapterRef = adapter;
3435
+ let promptsRef = defaultPrompts;
3109
3436
  const nudgeShownTurns = /* @__PURE__ */ new Set();
3110
3437
  async function acquireLock(sid) {
3111
3438
  const prev = locks.get(sid) ?? Promise.resolve();
@@ -3154,6 +3481,10 @@ function createRuntime(adapter) {
3154
3481
  return adapterRef;
3155
3482
  }, setAdapter: (a) => {
3156
3483
  adapterRef = a;
3484
+ }, get prompts() {
3485
+ return promptsRef;
3486
+ }, setPrompts: (p) => {
3487
+ promptsRef = p;
3157
3488
  }, markNudgeShown: (k) => {
3158
3489
  nudgeShownTurns.add(k);
3159
3490
  }, nudgeShownFor: (k) => nudgeShownTurns.has(k), clearNudgeTracking: () => {
@@ -7581,6 +7912,23 @@ function lastUserMessageId(entries) {
7581
7912
  return void 0;
7582
7913
  }
7583
7914
 
7915
+ // src/compat.ts
7916
+ function normalizeSystemPrompt(input) {
7917
+ if (input === void 0) return "";
7918
+ if (Array.isArray(input)) return input.join("\n");
7919
+ return input;
7920
+ }
7921
+ function formatSystemPromptForEvent(base, append) {
7922
+ const normalized = normalizeSystemPrompt(base);
7923
+ return `${normalized}
7924
+
7925
+ ${append}`;
7926
+ }
7927
+ function getSystemPromptText(ctx) {
7928
+ const result = ctx.getSystemPrompt?.();
7929
+ return normalizeSystemPrompt(result);
7930
+ }
7931
+
7584
7932
  // src/compress-tool.ts
7585
7933
  function formatK2(n) {
7586
7934
  return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
@@ -7626,13 +7974,14 @@ async function handleCompress(args, runtime, ctx, toolCallId) {
7626
7974
  if (ranges.length === 0) return "No ranges provided.";
7627
7975
  const { state: initialState, coreMessages } = await runtime.stateFor(ctx);
7628
7976
  const config = runtime.configFor(ctx);
7629
- const estimatedTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState));
7630
- const realUsage = ctx.getContextUsage?.();
7977
+ const systemPromptText = getSystemPromptText(ctx);
7978
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
7979
+ const sentTokens = estimateTokens(coreMessages, collectCoveredMessageIds(initialState)) + systemPromptTokens;
7631
7980
  const turn = runtime.core.processTurn({
7632
7981
  messages: coreMessages,
7633
7982
  state: initialState,
7634
7983
  config,
7635
- tokenCount: realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : estimatedTokens
7984
+ tokenCount: sentTokens
7636
7985
  });
7637
7986
  const state = turn.state;
7638
7987
  const messages = turn.messages;
@@ -7768,6 +8117,18 @@ function findMessageContent(ref, ctx) {
7768
8117
  }
7769
8118
  return null;
7770
8119
  }
8120
+ function resolveBlockMessages(block, coreMessages, ctx) {
8121
+ const neededBaseIds = new Set(block.effectiveMessageIds.map((id) => id.split("#")[0]));
8122
+ const presentBaseIds = new Set(coreMessages.map((m) => m.id.split("#")[0]));
8123
+ const missingBaseIds = [...neededBaseIds].filter((id) => !presentBaseIds.has(id));
8124
+ if (missingBaseIds.length === 0) return coreMessages;
8125
+ const extra = [];
8126
+ for (const baseId of missingBaseIds) {
8127
+ const entry = ctx.sessionManager.getEntry(baseId);
8128
+ if (entry) extra.push(...entriesToCoreMessages([entry]));
8129
+ }
8130
+ return [...coreMessages, ...extra];
8131
+ }
7771
8132
  async function handleMessageRef(ref, ownerBlockId, args, ctx) {
7772
8133
  const found = findMessageContent(ref, ctx);
7773
8134
  if (!found || !found.text) {
@@ -7815,7 +8176,8 @@ async function handleDecompress(args, runtime, ctx) {
7815
8176
  return `Block ${blockId} not found. Active blocks: ${active || "(none)"}.`;
7816
8177
  }
7817
8178
  const full = args.full ?? false;
7818
- const { text, count } = collectBlockContent(state, block, coreMessages, { full });
8179
+ const resolved = resolveBlockMessages(block, coreMessages, ctx);
8180
+ const { text, count } = collectBlockContent(state, block, resolved, { full });
7819
8181
  if (count === 0) return `Block ${blockId} has no restorable message content.`;
7820
8182
  if (args.inline === true && !args.toFile) {
7821
8183
  debug.event("decompress", { blockId, full, count, mode: "inline" });
@@ -7973,105 +8335,624 @@ function formatSize(tokens) {
7973
8335
  return `${(tokens / 1e6).toFixed(1)}M`;
7974
8336
  }
7975
8337
 
7976
- // src/delegate-tool.ts
7977
- import {
7978
- spawn
7979
- } from "child_process";
7980
- import { createWriteStream } from "fs";
7981
- import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
7982
- import { tmpdir as tmpdir2 } from "os";
7983
- import { join as join4 } from "path";
7984
-
7985
- // src/footer-status.ts
7986
- var FOOTER_STATUS_KEY = "billion-context-pi";
7987
- var ui;
7988
- var lastFooterText = "";
7989
- function formatCompactTokens(count) {
7990
- if (count < 1e3) return count.toString();
7991
- if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
7992
- if (count < 1e6) return `${Math.round(count / 1e3)}k`;
7993
- if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
7994
- return `${Math.round(count / 1e6)}M`;
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;
7995
8343
  }
7996
- function initFooterStatus(ctx) {
7997
- ui = ctx.ui;
7998
- lastFooterText = void 0;
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);
7999
8350
  }
8000
- function updateFooterStatus() {
8001
- if (!ui) return;
8002
- const usage = getDelegateUsage();
8003
- let text;
8004
- if (usage && usage.totalTokens > 0) {
8005
- const costStr = usage.cost.total > 0 ? ` ($${usage.cost.total.toFixed(4)})` : "";
8006
- text = `sub-agents \u2191${formatCompactTokens(usage.input)} \u2193${formatCompactTokens(usage.output)}${costStr}`;
8007
- }
8008
- if ((text ?? "") === lastFooterText) return;
8009
- lastFooterText = text ?? "";
8010
- try {
8011
- ui.setStatus(FOOTER_STATUS_KEY, text);
8012
- } catch {
8013
- }
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";
8014
8355
  }
8015
- function disposeFooterStatus() {
8016
- if (ui) {
8017
- try {
8018
- ui.setStatus(FOOTER_STATUS_KEY, void 0);
8019
- } catch {
8020
- }
8356
+ function classifyType2(message) {
8357
+ if (message.contentType === "tool-call" || message.contentType === "tool-result") {
8358
+ return message.toolName || "tool";
8021
8359
  }
8022
- ui = void 0;
8023
- lastFooterText = "";
8024
- }
8025
-
8026
- // src/fleet-widget.ts
8027
- var DELEGATE_WIDGET_KEY = "billion-context-pi-delegates";
8028
- var REFRESH_MS = 500;
8029
- var MAX_TASK_LEN = 48;
8030
- var ui2;
8031
- var timer;
8032
- var lastRenderKey = "";
8033
- var runsSnapshot;
8034
- function truncateTask(task) {
8035
- const oneLine = task.replace(/\n/g, " ").trim();
8036
- if (oneLine.length <= MAX_TASK_LEN) return oneLine;
8037
- return `${oneLine.slice(0, MAX_TASK_LEN - 1)}\u2026`;
8360
+ return message.contentType;
8038
8361
  }
8039
- function renderLines(runs2) {
8040
- if (runs2.length === 0) return void 0;
8041
- const now = Date.now();
8042
- const header = runs2.length === 1 ? `acp_delegate \xB7 1 running` : `acp_delegate \xB7 ${runs2.length} running`;
8043
- const rows = runs2.map((r) => {
8044
- const elapsed = Math.max(0, Math.round((now - r.startedAt) / 1e3));
8045
- return ` \u25CF ${r.agent} (${elapsed}s) \u2014 ${truncateTask(r.task)}`;
8046
- });
8047
- return [header, ...rows];
8362
+ function escapeRegex2(s) {
8363
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8048
8364
  }
8049
- function renderKeyFor(runs2) {
8050
- return runs2.map((r) => `${r.agent}:${Math.round((Date.now() - r.startedAt) / 1e3)}:${truncateTask(r.task)}`).join("|");
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;
8051
8371
  }
8052
- function stopTimer() {
8053
- if (timer) {
8054
- clearInterval(timer);
8055
- timer = void 0;
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;
8056
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 };
8057
8388
  }
8058
- function clearWidget() {
8059
- if (!ui2) return;
8060
- try {
8061
- ui2.setWidget(DELEGATE_WIDGET_KEY, void 0);
8062
- } catch {
8063
- }
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
+ );
8064
8394
  }
8065
- function refresh() {
8066
- if (!ui2) return;
8067
- const runs2 = runsSnapshot ? runsSnapshot() : [];
8068
- if (runs2.length === 0) {
8069
- if (lastRenderKey !== "") {
8070
- lastRenderKey = "";
8071
- clearWidget();
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
+ };
8072
8403
  }
8073
- updateFooterStatus();
8074
- stopTimer();
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
+ // node_modules/billion-context-kit/dist/index.js
8753
+ var VIABLE_RANGE_MIN_TOKENS = 200;
8754
+ function viableRanges(ranges) {
8755
+ return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
8756
+ }
8757
+ function topicFallback(summary) {
8758
+ const first = summary.split(/[.\n]/)[0] ?? "";
8759
+ const t = first.trim().replace(/^["'`]+/, "").trim();
8760
+ return t.length <= 30 ? t : `${t.slice(0, 30).trimEnd()}\u2026`;
8761
+ }
8762
+ function formatCompactTokens(count) {
8763
+ if (count < 1e3) return count.toString();
8764
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
8765
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
8766
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
8767
+ return `${Math.round(count / 1e6)}M`;
8768
+ }
8769
+ function bar(value, total, width = 20) {
8770
+ if (total === 0) return "";
8771
+ const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
8772
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
8773
+ }
8774
+ function buildStatusPanel(input) {
8775
+ const { tokenCount, state, nudge, modelContextLimit } = input;
8776
+ const fmt2 = input.fmtTokens ?? formatCompactTokens;
8777
+ const bd = nudge?.contextBreakdown;
8778
+ const limit = modelContextLimit;
8779
+ const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
8780
+ const systemPromptTokens = input.systemPromptTokens;
8781
+ const sentTotal = classified + systemPromptTokens;
8782
+ const sessionOnly = input.unprunedTokens !== void 0 ? Math.max(0, input.unprunedTokens - sentTotal) : 0;
8783
+ const displayTotal = tokenCount;
8784
+ const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
8785
+ const sentPct = limit > 0 ? Math.round(sentTotal / limit * 100) : 0;
8786
+ const activeBlocksList = state.blocks.filter((b) => b.active);
8787
+ const totalBlocksList = state.blocks;
8788
+ const lines = [];
8789
+ lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
8790
+ lines.push("\u2502 ACP Context Analysis \u2502");
8791
+ lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
8792
+ if (input.version) lines.push(input.version);
8793
+ lines.push("");
8794
+ lines.push(`Context (session accounting, host footer scale): ${displayPct}% (${fmt2(displayTotal)} / ${fmt2(limit)}) \u2014 never shrinks; includes compressed originals`);
8795
+ if (nudge && bd) {
8796
+ const growth = bd.growth;
8797
+ if (growth > 0 && displayTotal > 0) {
8798
+ lines.push(`Growth: +${fmt2(growth)} since last nudge`);
8799
+ }
8800
+ lines.push("");
8801
+ lines.push(`Sent to LLM (after compression, est.): ${fmt2(sentTotal)}${limit > 0 ? ` (${sentPct}% of limit)` : ""}`);
8802
+ if (input.unprunedTokens !== void 0 && sessionOnly > 0) {
8803
+ lines.push(`Session-only (compressed originals, est.): ${fmt2(sessionOnly)} \u2014 pruned from every request; the footer/nudge still count them`);
8804
+ }
8805
+ lines.push("");
8806
+ lines.push("Token Breakdown (sent view):");
8807
+ const categories = [
8808
+ { label: "Tool", value: bd.tool },
8809
+ { label: "SysPrompt", value: systemPromptTokens },
8810
+ { label: "Text", value: bd.text },
8811
+ { label: "Code", value: bd.code },
8812
+ { label: "Summaries", value: bd.summaries }
8813
+ ];
8814
+ for (const cat of categories) {
8815
+ if (cat.value <= 0) continue;
8816
+ const pct2 = sentTotal > 0 ? Math.round(cat.value / sentTotal * 100) : 0;
8817
+ const b = bar(cat.value, sentTotal);
8818
+ lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmt2(cat.value)}`);
8819
+ }
8820
+ }
8821
+ lines.push("");
8822
+ if (nudge) {
8823
+ if (nudge.shouldInject) {
8824
+ const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
8825
+ lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
8826
+ } else {
8827
+ lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
8828
+ }
8829
+ }
8830
+ const ranges = viableRanges(nudge?.compressibleRanges ?? []);
8831
+ const protectedRanges = nudge?.protectedRanges ?? [];
8832
+ if (ranges.length > 0 || protectedRanges.length > 0) {
8833
+ lines.push("");
8834
+ lines.push(formatRanges2(ranges, protectedRanges));
8835
+ }
8836
+ if (activeBlocksList.length > 0) {
8837
+ lines.push("");
8838
+ lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8839
+ for (const b of activeBlocksList) {
8840
+ const topic = b.topic ? `: ${b.topic}` : `: ${topicFallback(b.summary || "")}`;
8841
+ const summaryTok = defaultCountTokens2(b.summary || "");
8842
+ const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
8843
+ lines.push(` [${b.blockId}] T${b.tier} ${fmt2(origTok)}\u2192${fmt2(summaryTok)}${topic}`);
8844
+ }
8845
+ } else if (totalBlocksList.length > 0) {
8846
+ lines.push("");
8847
+ lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmt2(state.stats.tokensCompressed)} tokens compressed)`);
8848
+ } else {
8849
+ lines.push("");
8850
+ lines.push("Blocks: none (nothing compressed yet)");
8851
+ }
8852
+ lines.push("");
8853
+ lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
8854
+ return lines.join("\n");
8855
+ }
8856
+
8857
+ // src/delegate-tool.ts
8858
+ import {
8859
+ spawn
8860
+ } from "child_process";
8861
+ import { createWriteStream, existsSync as existsSync2 } from "fs";
8862
+ import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
8863
+ import { tmpdir as tmpdir2 } from "os";
8864
+ import { dirname as dirname3, join as join4, resolve as resolvePath } from "path";
8865
+
8866
+ // src/footer-status.ts
8867
+ var FOOTER_STATUS_KEY = "billion-context-pi";
8868
+ var ui;
8869
+ var lastFooterText = "";
8870
+ function formatCompactTokens2(count) {
8871
+ if (count < 1e3) return count.toString();
8872
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
8873
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
8874
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
8875
+ return `${Math.round(count / 1e6)}M`;
8876
+ }
8877
+ function initFooterStatus(ctx) {
8878
+ ui = ctx.ui;
8879
+ lastFooterText = void 0;
8880
+ }
8881
+ function updateFooterStatus() {
8882
+ if (!ui) return;
8883
+ const usage = getDelegateUsage();
8884
+ let text;
8885
+ if (usage && usage.totalTokens > 0) {
8886
+ const costStr = usage.cost.total > 0 ? ` ($${usage.cost.total.toFixed(4)})` : "";
8887
+ text = `sub-agents \u2191${formatCompactTokens2(usage.input)} \u2193${formatCompactTokens2(usage.output)}${costStr}`;
8888
+ }
8889
+ if ((text ?? "") === lastFooterText) return;
8890
+ lastFooterText = text ?? "";
8891
+ try {
8892
+ ui.setStatus(FOOTER_STATUS_KEY, text);
8893
+ } catch {
8894
+ }
8895
+ }
8896
+ function disposeFooterStatus() {
8897
+ if (ui) {
8898
+ try {
8899
+ ui.setStatus(FOOTER_STATUS_KEY, void 0);
8900
+ } catch {
8901
+ }
8902
+ }
8903
+ ui = void 0;
8904
+ lastFooterText = "";
8905
+ }
8906
+
8907
+ // src/fleet-widget.ts
8908
+ var DELEGATE_WIDGET_KEY = "billion-context-pi-delegates";
8909
+ var REFRESH_MS = 500;
8910
+ var MAX_TASK_LEN = 48;
8911
+ var ui2;
8912
+ var timer;
8913
+ var lastRenderKey = "";
8914
+ var runsSnapshot;
8915
+ function truncateTask(task) {
8916
+ const oneLine = task.replace(/\n/g, " ").trim();
8917
+ if (oneLine.length <= MAX_TASK_LEN) return oneLine;
8918
+ return `${oneLine.slice(0, MAX_TASK_LEN - 1)}\u2026`;
8919
+ }
8920
+ function renderLines(runs2) {
8921
+ if (runs2.length === 0) return void 0;
8922
+ const now = Date.now();
8923
+ const header = runs2.length === 1 ? `acp_delegate \xB7 1 running` : `acp_delegate \xB7 ${runs2.length} running`;
8924
+ const rows = runs2.map((r) => {
8925
+ const elapsed = Math.max(0, Math.round((now - r.startedAt) / 1e3));
8926
+ return ` \u25CF ${r.agent} (${elapsed}s) \u2014 ${truncateTask(r.task)}`;
8927
+ });
8928
+ return [header, ...rows];
8929
+ }
8930
+ function renderKeyFor(runs2) {
8931
+ return runs2.map((r) => `${r.agent}:${Math.round((Date.now() - r.startedAt) / 1e3)}:${truncateTask(r.task)}`).join("|");
8932
+ }
8933
+ function stopTimer() {
8934
+ if (timer) {
8935
+ clearInterval(timer);
8936
+ timer = void 0;
8937
+ }
8938
+ }
8939
+ function clearWidget() {
8940
+ if (!ui2) return;
8941
+ try {
8942
+ ui2.setWidget(DELEGATE_WIDGET_KEY, void 0);
8943
+ } catch {
8944
+ }
8945
+ }
8946
+ function refresh() {
8947
+ if (!ui2) return;
8948
+ const runs2 = runsSnapshot ? runsSnapshot() : [];
8949
+ if (runs2.length === 0) {
8950
+ if (lastRenderKey !== "") {
8951
+ lastRenderKey = "";
8952
+ clearWidget();
8953
+ }
8954
+ updateFooterStatus();
8955
+ stopTimer();
8075
8956
  return;
8076
8957
  }
8077
8958
  const sorted = [...runs2].sort((a, b) => a.startedAt - b.startedAt);
@@ -8393,6 +9274,44 @@ function delegateSpawnOptions(cwd, env) {
8393
9274
  shell: false
8394
9275
  };
8395
9276
  }
9277
+ 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");
9279
+ function probeUpFromArgv(argv1) {
9280
+ let dir = resolvePath(dirname3(argv1) || process.cwd());
9281
+ for (; ; ) {
9282
+ const candidate = join4(dir, "node_modules", PI_PACKAGE_REL);
9283
+ if (existsSync2(candidate)) return candidate;
9284
+ const parent = dirname3(dir);
9285
+ if (parent === dir) return null;
9286
+ dir = parent;
9287
+ }
9288
+ }
9289
+ function piCliGlobalCandidates(env) {
9290
+ const candidates = [];
9291
+ if (process.platform === "win32") {
9292
+ if (env.APPDATA) candidates.push(join4(env.APPDATA, "npm", "node_modules", PI_PACKAGE_REL));
9293
+ } else {
9294
+ 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));
9298
+ }
9299
+ return candidates;
9300
+ }
9301
+ function resolvePiCliEntry(argv1, env = process.env, piHost = true) {
9302
+ const explicit = env.PI_CLI_PATH;
9303
+ if (explicit) return explicit;
9304
+ if (argv1 && PI_CLI_ENTRY_RE.test(argv1)) return argv1;
9305
+ if (piHost) {
9306
+ const probed = probeUpFromArgv(argv1);
9307
+ if (probed) return probed;
9308
+ for (const candidate of piCliGlobalCandidates(env)) {
9309
+ if (existsSync2(candidate)) return candidate;
9310
+ }
9311
+ logWarn("delegate", { event: "cli-entry-unresolved", argv1, fallback: "argv[1]" });
9312
+ }
9313
+ return argv1;
9314
+ }
8396
9315
  var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
8397
9316
  var RESTRICTED_TOOLS = "read,bash,grep,find,ls";
8398
9317
  var AGENTS = {
@@ -8814,7 +9733,7 @@ async function runDelegate(pi, args, ctx, signal) {
8814
9733
  logInfo("delegate", { event: "spawn", agent: args.agent, runId, cwd, async: isAsync, useJsonStream, mode: ctx.mode, parentDepth });
8815
9734
  const child = spawn(
8816
9735
  process.execPath,
8817
- [process.argv[1], ...cliArgs],
9736
+ [resolvePiCliEntry(process.argv[1] ?? "", process.env, isPiHost(ctx.sessionManager)), ...cliArgs],
8818
9737
  delegateSpawnOptions(cwd, childEnv)
8819
9738
  );
8820
9739
  child.stdin?.once("error", (e) => {
@@ -9187,13 +10106,15 @@ function makeStatusTool(runtime) {
9187
10106
  async function handleStatus(args, runtime, ctx) {
9188
10107
  const { state, coreMessages } = await runtime.stateFor(ctx);
9189
10108
  const config = runtime.configFor(ctx);
9190
- const tokenCount = estimateTokens(coreMessages, collectCoveredMessageIds(state));
9191
- const realUsage = ctx.getContextUsage?.();
10109
+ const coveredIds = collectCoveredMessageIds(state);
10110
+ const systemPromptText = getSystemPromptText(ctx);
10111
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10112
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
9192
10113
  const turn = runtime.core.processTurn({
9193
10114
  messages: coreMessages,
9194
10115
  state,
9195
10116
  config,
9196
- tokenCount: realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : tokenCount
10117
+ tokenCount: sentTokens
9197
10118
  });
9198
10119
  const processed = turn.messages;
9199
10120
  const base = buildStatusReport(turn.state, processed, defaultCountTokens, {
@@ -9205,7 +10126,7 @@ async function handleStatus(args, runtime, ctx) {
9205
10126
  });
9206
10127
  if (args.scope) return base;
9207
10128
  const nudge = turn.nudge;
9208
- const ranges = nudge?.compressibleRanges ?? [];
10129
+ const ranges = viableRanges(nudge?.compressibleRanges ?? []);
9209
10130
  const protectedRanges = nudge?.protectedRanges ?? [];
9210
10131
  const extra = [];
9211
10132
  if (nudge) {
@@ -9236,23 +10157,6 @@ async function handleStatus(args, runtime, ctx) {
9236
10157
  ${extra.join("\n")}` : base;
9237
10158
  }
9238
10159
 
9239
- // src/compat.ts
9240
- function normalizeSystemPrompt(input) {
9241
- if (input === void 0) return "";
9242
- if (Array.isArray(input)) return input.join("\n");
9243
- return input;
9244
- }
9245
- function formatSystemPromptForEvent(base, append) {
9246
- const normalized = normalizeSystemPrompt(base);
9247
- return `${normalized}
9248
-
9249
- ${append}`;
9250
- }
9251
- function getSystemPromptText(ctx) {
9252
- const result = ctx.getSystemPrompt?.();
9253
- return normalizeSystemPrompt(result);
9254
- }
9255
-
9256
10160
  // src/commands.ts
9257
10161
  function makeCommands(runtime) {
9258
10162
  return [
@@ -9320,110 +10224,39 @@ ${text}`);
9320
10224
  }
9321
10225
  ];
9322
10226
  }
9323
- function fmtTokens(n) {
9324
- return formatCompactTokens(n);
9325
- }
9326
- function bar(value, total, width = 20) {
9327
- if (total === 0) return "";
9328
- const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
9329
- return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
9330
- }
9331
10227
  async function statusReport(runtime, ctx) {
9332
10228
  const { state, coreMessages } = await runtime.stateFor(ctx);
9333
10229
  const config = runtime.configFor(ctx);
9334
10230
  const realUsage = ctx.getContextUsage?.();
9335
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
9336
- const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount });
9337
- const nudge = turn.nudge;
9338
- const bd = nudge?.contextBreakdown;
9339
- const limit = config.modelContextLimit;
9340
- const classified = bd ? bd.system + bd.tool + bd.summaries + bd.code + bd.text : 0;
9341
10231
  const systemPromptText = getSystemPromptText(ctx);
9342
10232
  const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
9343
- const framework = bd ? Math.max(0, tokenCount - classified - systemPromptTokens) : 0;
9344
- const displayTotal = tokenCount;
9345
- const displayPct = limit > 0 ? Math.round(displayTotal / limit * 100) : 0;
9346
- const activeBlocksList = state.blocks.filter((b) => b.active);
9347
- const totalBlocksList = state.blocks;
9348
- const lines = [];
9349
- const versionStr = "0.1.36" ? `billion-context-pi@${"0.1.36"}` : "";
9350
- lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
9351
- lines.push("\u2502 ACP Context Analysis \u2502");
9352
- lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
9353
- if (versionStr) lines.push(versionStr);
9354
- lines.push("");
9355
- lines.push(`Context: ${displayPct}% (${fmtTokens(displayTotal)} / ${fmtTokens(limit)})`);
9356
- if (nudge && bd) {
9357
- const growth = bd.growth;
9358
- if (growth > 0 && displayTotal > 0) {
9359
- lines.push(`Growth: +${fmtTokens(growth)} since last nudge`);
9360
- }
9361
- if (displayTotal > 0) {
9362
- lines.push("");
9363
- lines.push("Token Breakdown:");
9364
- const categories = [
9365
- { label: "Tool", value: bd.tool },
9366
- { label: "SysPrompt", value: systemPromptTokens },
9367
- { label: "Framework", value: framework },
9368
- { label: "Text", value: bd.text },
9369
- { label: "Code", value: bd.code },
9370
- { label: "Summaries", value: bd.summaries }
9371
- ];
9372
- for (const cat of categories) {
9373
- if (cat.value <= 0) continue;
9374
- const pct2 = displayTotal > 0 ? Math.round(cat.value / displayTotal * 100) : 0;
9375
- const b = bar(cat.value, displayTotal);
9376
- lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmtTokens(cat.value)}`);
9377
- }
9378
- }
9379
- }
9380
- lines.push("");
9381
- if (nudge) {
9382
- if (nudge.shouldInject) {
9383
- const tierInfo = nudge.tier ? ` [T${nudge.tier} distillation]` : "";
9384
- lines.push(`Nudge: ACTIVE${tierInfo} \u2014 ${nudge.reason}`);
9385
- } else {
9386
- lines.push(`Nudge: idle \u2014 ${nudge.reason}`);
9387
- }
9388
- }
9389
- const ranges = nudge?.compressibleRanges ?? [];
9390
- const protectedRanges = nudge?.protectedRanges ?? [];
9391
- if (ranges.length > 0 || protectedRanges.length > 0) {
9392
- lines.push("");
9393
- lines.push(formatRanges(ranges, protectedRanges));
9394
- }
9395
- if (activeBlocksList.length > 0) {
9396
- lines.push("");
9397
- lines.push(`Blocks: ${activeBlocksList.length} active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
9398
- for (const b of activeBlocksList) {
9399
- const topic = b.topic ? `: ${b.topic}` : "";
9400
- const summaryTok = defaultCountTokens(b.summary || "");
9401
- const origTok = b.compressedTokens > 0 ? b.compressedTokens : summaryTok;
9402
- lines.push(` [${b.blockId}] T${b.tier} ${fmtTokens(origTok)}\u2192${fmtTokens(summaryTok)}${topic}`);
9403
- }
9404
- } else if (totalBlocksList.length > 0) {
9405
- lines.push("");
9406
- lines.push(`Blocks: 0 active / ${totalBlocksList.length} total (${fmtTokens(state.stats.tokensCompressed)} tokens compressed)`);
9407
- } else {
9408
- lines.push("");
9409
- lines.push("Blocks: none (nothing compressed yet)");
9410
- }
9411
- lines.push("");
10233
+ const sessionTokens = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : defaultCountTokens(coreMessages.map((m) => m.text ?? "").join("\n"));
10234
+ const coveredIds = collectCoveredMessageIds(state);
10235
+ 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;
10238
+ let text = buildStatusPanel({
10239
+ version: versionStr,
10240
+ tokenCount: sessionTokens,
10241
+ systemPromptTokens,
10242
+ state: turn.state,
10243
+ nudge: turn.nudge,
10244
+ modelContextLimit: config.modelContextLimit,
10245
+ unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0)
10246
+ });
9412
10247
  const delegateUsage = getDelegateUsage();
9413
10248
  if (delegateUsage && delegateUsage.totalTokens > 0) {
9414
- lines.push("");
9415
10249
  const cost = delegateUsage.cost.total;
9416
10250
  const costStr = cost > 0 ? ` ($${cost.toFixed(4)})` : "";
9417
- lines.push("\u2500\u2500 Session delegate usage (excluded from main totals) \u2500\u2500");
9418
- lines.push(`Tokens: ${delegateUsage.input.toLocaleString()} in, ${delegateUsage.output.toLocaleString()} out (${delegateUsage.totalTokens.toLocaleString()} total)${costStr}`);
10251
+ text += "\n\n\u2500\u2500 Session delegate usage (excluded from main totals) \u2500\u2500\n";
10252
+ text += `Tokens: ${delegateUsage.input.toLocaleString()} in, ${delegateUsage.output.toLocaleString()} out (${delegateUsage.totalTokens.toLocaleString()} total)${costStr}`;
9419
10253
  }
9420
- lines.push("");
9421
- lines.push("Tag visibility: tags injected to LLM only (deep copy), not persisted in session, not shown in terminal.");
9422
- return lines.join("\n");
10254
+ return text;
9423
10255
  }
9424
10256
 
9425
10257
  // src/system-prompt.ts
9426
- var ACP_SYSTEM_PROMPT = `
10258
+ function buildAcpSystemPrompt(prompts) {
10259
+ return `
9427
10260
  ACP context management
9428
10261
 
9429
10262
  ACP TAGS
@@ -9447,7 +10280,7 @@ You have four context-management tools:
9447
10280
  - search_context \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: search_context({ query: "auth token refresh" }).
9448
10281
  - acp_status \u2014 Context status with compressible ranges. No args = overview + totals. scope:"uncompressed" for range view; add view:"messages" for per-message listing. scope:"compressed" for block details.
9449
10282
 
9450
- ${COMPRESS_PHILOSOPHY}
10283
+ ${prompts.compressPhilosophy}
9451
10284
 
9452
10285
  WHEN TO COMPRESS
9453
10286
 
@@ -9465,7 +10298,7 @@ WHEN NOT TO COMPRESS
9465
10298
  - Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria. If a message in the range must stay verbatim, exclude it from the compress range instead of compressing it.
9466
10299
  - Protected tool outputs \u2014 hard-excluded from compression ranges, survive intact in visible context.
9467
10300
 
9468
- ${HOW_TO_COMPRESS_RULES}
10301
+ ${prompts.howToCompressRules}
9469
10302
 
9470
10303
  MULTI-TIER COMPRESSION
9471
10304
 
@@ -9473,9 +10306,9 @@ Summaries accumulate as the session grows. When tier-1 summaries pile up, the sy
9473
10306
 
9474
10307
  To compress blocks: use block IDs as boundaries: compress({ content: [{ startId: "b3", endId: "b15", summary: "..." }] }). This deactivates the consumed blocks and creates a new higher-tier block.
9475
10308
 
9476
- ${TIER2_DISTILL_RULES}
10309
+ ${prompts.tier2DistillRules}
9477
10310
 
9478
- ${TIER3_CONDENSE_RULES}
10311
+ ${prompts.tier3CondenseRules}
9479
10312
 
9480
10313
  THE PHILOSOPHY OF DECOMPRESS
9481
10314
 
@@ -9485,6 +10318,7 @@ CONTEXT BREAKDOWN
9485
10318
 
9486
10319
  When context usage passes a threshold, the system appends a breakdown showing where tokens are spent. Compress the largest ranges first when the current step no longer needs them.
9487
10320
  `;
10321
+ }
9488
10322
  var ACP_DELEGATE_PROMPT = `
9489
10323
  ACP_DELEGATE NOTIFICATIONS
9490
10324
 
@@ -9613,7 +10447,7 @@ function wireToolGuardrails(pi, runtime) {
9613
10447
 
9614
10448
  // src/update.ts
9615
10449
  import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
9616
- import { join as join5, dirname as dirname3 } from "path";
10450
+ import { join as join5, dirname as dirname4 } from "path";
9617
10451
  import { fileURLToPath } from "url";
9618
10452
  import { execFile } from "child_process";
9619
10453
  import { homedir as homedir3 } from "os";
@@ -9646,7 +10480,7 @@ async function readLastCheck() {
9646
10480
  }
9647
10481
  async function writeLastCheck(timestamp) {
9648
10482
  try {
9649
- await mkdir3(dirname3(THROTTLE_FILE), { recursive: true });
10483
+ await mkdir3(dirname4(THROTTLE_FILE), { recursive: true });
9650
10484
  await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
9651
10485
  } catch {
9652
10486
  }
@@ -9660,20 +10494,20 @@ async function readPackageJson(path4) {
9660
10494
  }
9661
10495
  }
9662
10496
  function findNpmRoot(extDir) {
9663
- let dir = dirname3(extDir);
10497
+ let dir = dirname4(extDir);
9664
10498
  for (; ; ) {
9665
- if (dir.endsWith("node_modules")) return dirname3(dir);
9666
- const parent = dirname3(dir);
10499
+ if (dir.endsWith("node_modules")) return dirname4(dir);
10500
+ const parent = dirname4(dir);
9667
10501
  if (parent === dir) return void 0;
9668
10502
  dir = parent;
9669
10503
  }
9670
10504
  }
9671
10505
  async function findExtensionDir() {
9672
- let dir = dirname3(fileURLToPath(import.meta.url));
10506
+ let dir = dirname4(fileURLToPath(import.meta.url));
9673
10507
  for (; ; ) {
9674
10508
  const pkg = await readPackageJson(join5(dir, "package.json"));
9675
10509
  if (pkg?.name === PACKAGE_NAME) return dir;
9676
- const parent = dirname3(dir);
10510
+ const parent = dirname4(dir);
9677
10511
  if (parent === dir) return void 0;
9678
10512
  dir = parent;
9679
10513
  }
@@ -9722,7 +10556,7 @@ async function checkForUpdate(autoUpdate, notify) {
9722
10556
  const data = await res.json();
9723
10557
  const latest = data.version;
9724
10558
  if (!latest) return;
9725
- const current = runtimeVersion ?? "0.1.36";
10559
+ const current = runtimeVersion ?? "0.1.38";
9726
10560
  const hasUpdate = isNewer(latest, current);
9727
10561
  debug.event("update-check", {
9728
10562
  current,
@@ -9758,7 +10592,7 @@ async function getRuntimeVersion() {
9758
10592
 
9759
10593
  // src/setup-subagent-tools.ts
9760
10594
  import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
9761
- import { existsSync as existsSync2 } from "fs";
10595
+ import { existsSync as existsSync3 } from "fs";
9762
10596
  import { homedir as homedir4 } from "os";
9763
10597
  import { join as join6 } from "path";
9764
10598
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
@@ -9823,7 +10657,7 @@ async function ensureSubagentAcpTools(settingsPath) {
9823
10657
  return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
9824
10658
  }
9825
10659
  const backupPath = `${path4}.acp-bak`;
9826
- if (!existsSync2(backupPath)) {
10660
+ if (!existsSync3(backupPath)) {
9827
10661
  try {
9828
10662
  await copyFile(path4, backupPath);
9829
10663
  } catch {
@@ -9923,7 +10757,9 @@ var KNOWN = /* @__PURE__ */ new Set([
9923
10757
  "toolOutputMaxBytes",
9924
10758
  "delegate",
9925
10759
  "compress",
9926
- "displayUsage"
10760
+ "displayUsage",
10761
+ "prompts",
10762
+ "acknowledgePromptsRisk"
9927
10763
  ]);
9928
10764
  function pickKnown(parsed) {
9929
10765
  const out = {};
@@ -9971,7 +10807,7 @@ function wireSessionLifecycle(pi, runtime) {
9971
10807
  resetDelegateUsage();
9972
10808
  setDelegateDisplayUsage("separate");
9973
10809
  const sid = ctx.sessionManager.getSessionId();
9974
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.36" : null });
10810
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.38" : null });
9975
10811
  try {
9976
10812
  const user = await loadUserConfig(ctx.cwd);
9977
10813
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
@@ -9980,6 +10816,12 @@ function wireSessionLifecycle(pi, runtime) {
9980
10816
  } catch (e) {
9981
10817
  logThrow("config", e, { sid, phase: "session_start" });
9982
10818
  }
10819
+ try {
10820
+ runtime.setPrompts(resolvePrompts(runtime.adapter.prompts, { acknowledgeRisk: runtime.adapter.acknowledgePromptsRisk === true }));
10821
+ } catch (e) {
10822
+ logWarn("config", { event: "prompts-resolve-failed", error: e instanceof Error ? e.message : String(e) });
10823
+ runtime.setPrompts(defaultPrompts);
10824
+ }
9983
10825
  if (resolveDelegate(runtime.adapter).enabled) {
9984
10826
  pi.registerTool(makeDelegateTool(pi));
9985
10827
  pi.registerTool(makeDelegateWaitTool(pi));
@@ -10005,17 +10847,17 @@ function wireContextTransform(pi, runtime) {
10005
10847
  const config = runtime.configFor(ctx);
10006
10848
  const coveredIds = collectCoveredMessageIds(state);
10007
10849
  const realUsage = ctx.getContextUsage?.();
10008
- const estimated = estimateTokens(coreMessages, coveredIds);
10009
- const tokenCount = realUsage?.tokens && realUsage.tokens > 0 ? realUsage.tokens : estimated;
10850
+ const systemPromptText = getSystemPromptText(ctx);
10851
+ const systemPromptTokens = systemPromptText ? defaultCountTokens(systemPromptText) : 0;
10852
+ const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10853
+ const tokenCount = sentTokens;
10010
10854
  debug.event("context-in", {
10011
10855
  sid,
10012
10856
  eventMsgs: event.messages?.length ?? 0,
10013
10857
  entries: entries.length,
10014
10858
  coreMsgs: coreMessages.length,
10015
10859
  tokenCount,
10016
- estimatedTokens: estimated,
10017
- realTokens: realUsage?.tokens ?? null,
10018
- realPercent: realUsage?.percent ?? null,
10860
+ sessionTokens: realUsage?.tokens ?? null,
10019
10861
  limit: config.modelContextLimit,
10020
10862
  blocksBefore: state.blocks.length,
10021
10863
  activeBefore: state.blocks.filter((b) => b.active).length
@@ -10040,7 +10882,7 @@ function wireContextTransform(pi, runtime) {
10040
10882
  prunedMsgs: coreMessages.length - turn.messages.length + turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
10041
10883
  nudgeShouldInject: turn.nudge?.shouldInject ?? false,
10042
10884
  nudgeReason: turn.nudge?.reason ?? null,
10043
- nudgeVoice: turn.nudge ? renderNudgeText(turn.nudge).voice : null,
10885
+ nudgeVoice: turn.nudge ? renderNudgeText(turn.nudge, runtime.prompts).voice : null,
10044
10886
  nudgePct: turn.nudge ? Math.round(turn.nudge.contextUsage * 100) : null,
10045
10887
  nudgeTier: turn.nudge?.tier ?? null,
10046
10888
  nudgeCompressibleCount: turn.nudge?.compressibleRanges.length ?? 0,
@@ -10054,11 +10896,12 @@ function wireContextTransform(pi, runtime) {
10054
10896
  const debugOn2 = debug.enabled;
10055
10897
  if (turn.nudge?.shouldInject) {
10056
10898
  const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
10899
+ turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
10057
10900
  const turnKey = lastUserMessageId(entries) ?? sid;
10058
10901
  const alreadyShown = !emergency && runtime.nudgeShownFor(turnKey);
10059
10902
  if (!alreadyShown) {
10060
- rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active)));
10061
- const rendered = renderNudgeText(turn.nudge);
10903
+ rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts));
10904
+ const rendered = renderNudgeText(turn.nudge, runtime.prompts);
10062
10905
  const top = [...turn.nudge.compressibleRanges].sort((a, b) => b.tokens - a.tokens)[0];
10063
10906
  const example = top ? `
10064
10907
 
@@ -10092,8 +10935,9 @@ ${rendered.text}${example}`);
10092
10935
  function wireSystemPrompt(pi, runtime) {
10093
10936
  pi.on("before_agent_start", (event) => {
10094
10937
  const delegate = runtime.adapter.delegate !== false;
10095
- const prompt = delegate ? `${ACP_SYSTEM_PROMPT}
10096
- ${ACP_DELEGATE_PROMPT}` : ACP_SYSTEM_PROMPT;
10938
+ const acp = buildAcpSystemPrompt(runtime.prompts);
10939
+ const prompt = delegate ? `${acp}
10940
+ ${ACP_DELEGATE_PROMPT}` : acp;
10097
10941
  return { systemPrompt: formatSystemPromptForEvent(event.systemPrompt, prompt) };
10098
10942
  });
10099
10943
  }
@@ -10109,8 +10953,8 @@ function collectOriginals(entries) {
10109
10953
  }
10110
10954
  return map;
10111
10955
  }
10112
- function nudgeMessage(nudge, blocks) {
10113
- const rendered = renderNudgeText(nudge);
10956
+ function nudgeMessage(nudge, blocks, prompts) {
10957
+ const rendered = renderNudgeText(nudge, prompts);
10114
10958
  const lines = [rendered.text];
10115
10959
  if (blocks.length > 0) {
10116
10960
  const totalSummary = blocks.reduce((s, b) => s + Math.ceil((b.summary || "").length / 4), 0);