billion-context-pi 0.1.35 → 0.1.37

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 = [];
@@ -263,7 +283,7 @@ function defaultConfig(modelContextLimit, overrides = {}) {
263
283
  const base = {
264
284
  tiers: { enabled: true, tier2Trigger: 5, tier3Trigger: 10 },
265
285
  nudge: {
266
- maxContextLimitPct: 0.55,
286
+ maxContextLimitPct: 0.75,
267
287
  minContextLimitPct: 0.45,
268
288
  frequency: 5,
269
289
  iterationThreshold: 15,
@@ -273,10 +293,10 @@ function defaultConfig(modelContextLimit, overrides = {}) {
273
293
  growthCap: 5e4,
274
294
  minGrowthFloor: 2e4,
275
295
  minGrowthRatio: 0.45,
276
- emergencyThresholdPct: 0.8
296
+ emergencyThresholdPct: 0.95
277
297
  },
278
298
  promotionThreshold: 5,
279
- truncate: { threshold: 1 },
299
+ truncate: { threshold: 0.95 },
280
300
  compress: {
281
301
  minCompressRange: 5e3,
282
302
  maxSummaryLength: 2e4,
@@ -306,6 +326,11 @@ function validateConfig(config) {
306
326
  "nudge.minContextLimitPct must not exceed nudge.maxContextLimitPct"
307
327
  );
308
328
  }
329
+ if (config.nudge.maxContextLimitPct > config.nudge.emergencyThresholdPct) {
330
+ errors.push(
331
+ "nudge.maxContextLimitPct must not exceed nudge.emergencyThresholdPct"
332
+ );
333
+ }
309
334
  if (config.promotionThreshold < 1) {
310
335
  errors.push("promotionThreshold must be >= 1");
311
336
  }
@@ -338,6 +363,18 @@ function parseBoundary(ref) {
338
363
  }
339
364
  return null;
340
365
  }
366
+ var BoundaryNotFoundError = class extends Error {
367
+ code = "BOUNDARY_NOT_FOUND";
368
+ kind;
369
+ endpoint;
370
+ constructor(kind, endpoint, message) {
371
+ super(message);
372
+ this.name = "BoundaryNotFoundError";
373
+ this.code = "BOUNDARY_NOT_FOUND";
374
+ this.kind = kind;
375
+ this.endpoint = endpoint;
376
+ }
377
+ };
341
378
  function resolveBoundaries(input) {
342
379
  const start = parseBoundary(input.startRef);
343
380
  const end = parseBoundary(input.endRef);
@@ -350,13 +387,8 @@ function resolveBoundaries(input) {
350
387
  input.messages.forEach(
351
388
  (message, index) => indexByRawId.set(message.id, index)
352
389
  );
353
- let startIndex = resolveAnchorIndex(start, input.state, indexByRawId);
354
- let endIndex = resolveAnchorIndex(end, input.state, indexByRawId);
355
- if (startIndex === null || endIndex === null) {
356
- throw new Error(
357
- `Boundary not found in visible context (likely consumed by an existing block). startId="${input.startRef}", endId="${input.endRef}".`
358
- );
359
- }
390
+ let startIndex = resolveAnchorIndex(start, input.state, indexByRawId, "start");
391
+ let endIndex = resolveAnchorIndex(end, input.state, indexByRawId, "end");
360
392
  if (startIndex > endIndex) {
361
393
  [startIndex, endIndex] = [endIndex, startIndex];
362
394
  }
@@ -387,16 +419,51 @@ function resolveBoundaries(input) {
387
419
  protectedGaps
388
420
  };
389
421
  }
390
- function resolveAnchorIndex(boundary, state, indexByRawId) {
422
+ function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
423
+ const label = endpoint === "start" ? "startId" : "endId";
391
424
  if (boundary.kind === "message") {
392
425
  const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
393
- if (!rawId) return null;
426
+ if (!rawId) {
427
+ throw new BoundaryNotFoundError(
428
+ "unknown",
429
+ endpoint,
430
+ `${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
431
+ );
432
+ }
394
433
  const index = indexByRawId.get(rawId);
395
- return index === void 0 ? null : index;
434
+ if (index === void 0) {
435
+ throw new BoundaryNotFoundError(
436
+ "consumed",
437
+ endpoint,
438
+ `${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
439
+ );
440
+ }
441
+ return index;
396
442
  }
397
443
  const block = blockById(state, `b${boundary.numericId}`);
398
- if (!block || !block.active) return null;
399
- return earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
444
+ if (!block) {
445
+ throw new BoundaryNotFoundError(
446
+ "unknown",
447
+ endpoint,
448
+ `${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
449
+ );
450
+ }
451
+ if (!block.active) {
452
+ throw new BoundaryNotFoundError(
453
+ "consumed",
454
+ endpoint,
455
+ `${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
456
+ );
457
+ }
458
+ const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
459
+ if (anchor === null) {
460
+ throw new BoundaryNotFoundError(
461
+ "consumed",
462
+ endpoint,
463
+ `${label}="b${boundary.numericId}" not found in visible context (block messages consumed by a higher-tier block).`
464
+ );
465
+ }
466
+ return anchor;
400
467
  }
401
468
  function formatPaddedRef(index) {
402
469
  return `m${String(index).padStart(5, "0")}`;
@@ -773,6 +840,38 @@ function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan =
773
840
  }
774
841
  return { startIndex: newStartIndex, endIndex: newEndIndex };
775
842
  }
843
+ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
844
+ if (startIndex > endIndex) {
845
+ return { startIndex, endIndex };
846
+ }
847
+ let newStartIndex = startIndex;
848
+ let newEndIndex = endIndex;
849
+ for (let i = startIndex; i <= endIndex && i < messages.length; i++) {
850
+ const msg = messages[i];
851
+ if (!msg) continue;
852
+ if (msg.contentType === "reasoning") {
853
+ let j = i;
854
+ while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
855
+ j++;
856
+ }
857
+ const companion = messages[j + 1];
858
+ if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call") && j + 1 > newEndIndex) {
859
+ newEndIndex = j + 1;
860
+ }
861
+ }
862
+ if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
863
+ let k = i - 1;
864
+ while (k >= 0 && messages[k].contentType === "reasoning") {
865
+ k--;
866
+ }
867
+ const runStart = k + 1;
868
+ if (runStart < i && runStart >= 0 && messages[runStart].contentType === "reasoning" && runStart < newStartIndex) {
869
+ newStartIndex = runStart;
870
+ }
871
+ }
872
+ }
873
+ return { startIndex: newStartIndex, endIndex: newEndIndex };
874
+ }
776
875
  function refNum(ref) {
777
876
  const n = parseInt(ref.slice(1), 10);
778
877
  return Number.isNaN(n) ? -1 : n;
@@ -927,6 +1026,9 @@ function runPipeline(nodes, initial, ctx) {
927
1026
  }
928
1027
  return io;
929
1028
  }
1029
+ function rangeError(spec, message) {
1030
+ return `range ${spec.startRef}..${spec.endRef}: ${message}`;
1031
+ }
930
1032
  function createCore(ports = {}) {
931
1033
  const countTokens = ports.countTokens ?? defaultCountTokens;
932
1034
  function applyCompression(input) {
@@ -938,20 +1040,44 @@ function createCore(ports = {}) {
938
1040
  const warnings = [];
939
1041
  const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config, countTokens);
940
1042
  const preExistingCoverage = collectCoverage(state);
941
- const rangeIndexSets = [];
1043
+ const classifications = /* @__PURE__ */ new Map();
1044
+ const classificationErrors = [];
1045
+ const consumedRanges = [];
942
1046
  for (const spec of input.ranges) {
943
- let resolved;
944
1047
  try {
945
- resolved = resolveBoundaries({
1048
+ const resolved = resolveBoundaries({
946
1049
  startRef: spec.startRef,
947
1050
  endRef: spec.endRef,
948
1051
  messages: input.messages,
949
1052
  state
950
1053
  });
951
- } catch {
952
- continue;
1054
+ classifications.set(spec, { status: "ok", resolved });
1055
+ } catch (error) {
1056
+ if (error instanceof BoundaryNotFoundError) {
1057
+ classifications.set(
1058
+ spec,
1059
+ error.kind === "unknown" ? { status: "unknown", error } : { status: "consumed", error }
1060
+ );
1061
+ if (error.kind === "consumed") {
1062
+ consumedRanges.push(spec);
1063
+ } else {
1064
+ classificationErrors.push(rangeError(spec, error.message));
1065
+ }
1066
+ } else {
1067
+ classifications.set(spec, {
1068
+ status: "invalid",
1069
+ error: error instanceof Error ? error : new Error(String(error))
1070
+ });
1071
+ classificationErrors.push(
1072
+ rangeError(spec, error instanceof Error ? error.message : String(error))
1073
+ );
1074
+ }
953
1075
  }
954
- const indices = resolved.messageIds.map(
1076
+ }
1077
+ const rangeIndexSets = [];
1078
+ for (const [spec, resolution] of classifications) {
1079
+ if (resolution.status !== "ok") continue;
1080
+ const indices = resolution.resolved.messageIds.map(
955
1081
  (id) => input.messages.findIndex((m) => m.id === id)
956
1082
  ).filter((i) => i >= 0);
957
1083
  rangeIndexSets.push({ spec, indices });
@@ -978,37 +1104,27 @@ function createCore(ports = {}) {
978
1104
  if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
979
1105
  let totalRangeChars = 0;
980
1106
  let hasBlockBoundaryRange = false;
981
- for (const spec of input.ranges) {
982
- if (skipSpecs.has(spec)) continue;
983
- let resolved;
984
- try {
985
- resolved = resolveBoundaries({
986
- startRef: spec.startRef,
987
- endRef: spec.endRef,
988
- messages: input.messages,
989
- state
990
- });
991
- } catch {
992
- continue;
993
- }
994
- if (resolved.boundaryKind === "block") {
1107
+ let countedRanges = 0;
1108
+ for (const [spec, resolution] of classifications) {
1109
+ if (resolution.status !== "ok" || skipSpecs.has(spec)) continue;
1110
+ if (resolution.resolved.boundaryKind === "block") {
995
1111
  hasBlockBoundaryRange = true;
996
1112
  continue;
997
1113
  }
998
- for (const id of resolved.messageIds) {
1114
+ countedRanges++;
1115
+ for (const id of resolution.resolved.messageIds) {
999
1116
  const msg = input.messages.find((m) => m.id === id);
1000
1117
  totalRangeChars += msg?.text?.length ?? 0;
1001
1118
  }
1002
1119
  }
1003
1120
  if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
1121
+ 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.`;
1004
1122
  return {
1005
1123
  state: input.state,
1006
1124
  result: {
1007
1125
  blocksCreated: 0,
1008
1126
  tokensCompressed: 0,
1009
- errors: [
1010
- `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.`
1011
- ],
1127
+ errors: [gateMessage, ...classificationErrors],
1012
1128
  warnings: []
1013
1129
  }
1014
1130
  };
@@ -1016,6 +1132,18 @@ function createCore(ports = {}) {
1016
1132
  }
1017
1133
  for (const spec of input.ranges) {
1018
1134
  if (skipSpecs.has(spec)) continue;
1135
+ const resolution = classifications.get(spec);
1136
+ if (resolution === void 0) continue;
1137
+ if (resolution.status === "consumed") {
1138
+ warnings.push(
1139
+ `Skipped range (${spec.startRef}..${spec.endRef}) \u2014 already compressed (messages consumed by existing block(s)); nothing to compress.`
1140
+ );
1141
+ continue;
1142
+ }
1143
+ if (resolution.status === "unknown" || resolution.status === "invalid") {
1144
+ errors.push(rangeError(spec, resolution.error.message));
1145
+ continue;
1146
+ }
1019
1147
  try {
1020
1148
  const outcome = applySingleRange({
1021
1149
  spec,
@@ -1031,7 +1159,7 @@ function createCore(ports = {}) {
1031
1159
  tokensCompressed += outcome.tokens;
1032
1160
  warnings.push(...outcome.warnings);
1033
1161
  } catch (error) {
1034
- errors.push(error instanceof Error ? error.message : String(error));
1162
+ errors.push(rangeError(spec, error instanceof Error ? error.message : String(error)));
1035
1163
  }
1036
1164
  }
1037
1165
  state.stats.compressionCount += blocksCreated;
@@ -1239,7 +1367,7 @@ function applySingleRange(input) {
1239
1367
  messages: input.messages,
1240
1368
  state: input.state
1241
1369
  });
1242
- const rangeMessageIds = applyToolPairAdjustment(
1370
+ const rangeMessageIds = applyPairBoundaryAdjustments(
1243
1371
  resolved,
1244
1372
  input.messages
1245
1373
  );
@@ -1353,20 +1481,33 @@ function applySingleRange(input) {
1353
1481
  }
1354
1482
  return { tokens: compressedTokens, warnings };
1355
1483
  }
1356
- function applyToolPairAdjustment(resolved, messages) {
1484
+ function applyPairBoundaryAdjustments(resolved, messages) {
1357
1485
  if (resolved.boundaryKind === "block") {
1358
1486
  return resolved.messageIds;
1359
1487
  }
1360
- const adjusted = adjustBoundariesForToolPairs(
1361
- resolved.startIndex,
1362
- resolved.endIndex,
1363
- messages
1364
- );
1365
- if (adjusted.startIndex === resolved.startIndex && adjusted.endIndex === resolved.endIndex) {
1488
+ let startIndex = resolved.startIndex;
1489
+ let endIndex = resolved.endIndex;
1490
+ for (let pass = 0; pass < 2; pass++) {
1491
+ const reasoningAdjusted = adjustBoundariesForReasoningPairs(
1492
+ startIndex,
1493
+ endIndex,
1494
+ messages
1495
+ );
1496
+ const toolAdjusted = adjustBoundariesForToolPairs(
1497
+ reasoningAdjusted.startIndex,
1498
+ reasoningAdjusted.endIndex,
1499
+ messages
1500
+ );
1501
+ const changed = toolAdjusted.startIndex !== startIndex || toolAdjusted.endIndex !== endIndex;
1502
+ startIndex = toolAdjusted.startIndex;
1503
+ endIndex = toolAdjusted.endIndex;
1504
+ if (!changed) break;
1505
+ }
1506
+ if (startIndex === resolved.startIndex && endIndex === resolved.endIndex) {
1366
1507
  return resolved.messageIds;
1367
1508
  }
1368
1509
  const ids = [];
1369
- for (let i = adjusted.startIndex; i <= adjusted.endIndex; i++) {
1510
+ for (let i = startIndex; i <= endIndex; i++) {
1370
1511
  const msg = messages[i];
1371
1512
  if (msg) ids.push(msg.id);
1372
1513
  }
@@ -1466,6 +1607,7 @@ function decideNudge(input) {
1466
1607
  const limit = config.modelContextLimit;
1467
1608
  const usage = limit > 0 ? tokenCount / limit : 0;
1468
1609
  const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
1610
+ const overLimit = usage >= config.nudge.maxContextLimitPct;
1469
1611
  const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
1470
1612
  const baseline = state.nudge.lastPerMessageNudgeTokens;
1471
1613
  const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
@@ -1482,7 +1624,7 @@ function decideNudge(input) {
1482
1624
  let injectedTier = null;
1483
1625
  let injectedReason = "";
1484
1626
  const growthReady = growthSinceReference >= growthFloor;
1485
- if (!emergencyOverride && growthReady) {
1627
+ if (!overLimit && growthReady) {
1486
1628
  for (const tier of [1, 2, 3]) {
1487
1629
  if (!config.tiers.enabled && tier > 1) break;
1488
1630
  const info = tiers[tier];
@@ -1494,11 +1636,26 @@ function decideNudge(input) {
1494
1636
  injectedReason = tier === 1 ? `T1 compressible ${info.pending} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%` : `T${tier} distill ready: ${info.targetBlocks.length} tier-${tier - 1} blocks (${info.pending} tokens) >= ${nudgeGrowthTokens}, usage ${Math.round(usage * 100)}%`;
1495
1637
  break;
1496
1638
  }
1639
+ } else if (overLimit) {
1640
+ for (const tier of [1, 2, 3]) {
1641
+ if (!config.tiers.enabled && tier > 1) break;
1642
+ const info = tiers[tier];
1643
+ if (!info || info.pending < config.compress.minCompressRange) continue;
1644
+ injectedTier = tier;
1645
+ injectedReason = emergencyOverride ? `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%, T${tier} pending ${info.pending}` : `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}%, T${tier} pending ${info.pending}`;
1646
+ break;
1647
+ }
1497
1648
  }
1498
- const shouldInject = emergencyOverride || injectedTier !== null;
1649
+ const shouldInject = injectedTier !== null || overLimit && (rec?.recommendedRanges?.length ?? 0) > 0;
1499
1650
  let reason;
1500
- if (emergencyOverride) {
1501
- reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%`;
1651
+ if (emergencyOverride && injectedTier !== null) {
1652
+ reason = injectedReason;
1653
+ } else if (emergencyOverride) {
1654
+ reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}% (no compressible content)`;
1655
+ } else if (overLimit && injectedTier !== null) {
1656
+ reason = injectedReason;
1657
+ } else if (overLimit) {
1658
+ reason = `OVER-LIMIT: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.maxContextLimitPct * 100)}% (no compressible content)`;
1502
1659
  } else if (injectedTier !== null) {
1503
1660
  reason = injectedReason;
1504
1661
  } else {
@@ -1534,6 +1691,7 @@ function decideNudge(input) {
1534
1691
  nudgeGrowthTokens,
1535
1692
  growthFloor,
1536
1693
  hasPendingNudge: hasPendingNudge ? 1 : 0,
1694
+ overLimit: overLimit ? 1 : 0,
1537
1695
  emergencyOverride: emergencyOverride ? 1 : 0,
1538
1696
  pendingT1: tiers[1].pending,
1539
1697
  pendingT2: tiers[2].pending,
@@ -1705,12 +1863,39 @@ DROP:
1705
1863
  - Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
1706
1864
 
1707
1865
  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.`;
1708
- 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.
1866
+ var defaultPrompts = Object.freeze({
1867
+ compressPhilosophy: COMPRESS_PHILOSOPHY,
1868
+ howToCompressRules: HOW_TO_COMPRESS_RULES,
1869
+ tier2DistillRules: TIER2_DISTILL_RULES,
1870
+ tier3CondenseRules: TIER3_CONDENSE_RULES
1871
+ });
1872
+ function resolvePrompts(overrides, options = {}) {
1873
+ const clean = {};
1874
+ if (overrides) {
1875
+ for (const [key, value] of Object.entries(overrides)) {
1876
+ if (typeof value === "string") {
1877
+ clean[key] = value;
1878
+ }
1879
+ }
1880
+ }
1881
+ const keys = Object.keys(clean);
1882
+ if (keys.length > 0 && !options.acknowledgeRisk) {
1883
+ throw new Error(
1884
+ `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).`
1885
+ );
1886
+ }
1887
+ return { ...defaultPrompts, ...clean };
1888
+ }
1889
+ function efficiencyNote(prompts) {
1890
+ 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.
1709
1891
 
1710
- ${COMPRESS_PHILOSOPHY}`;
1711
- var EMERGENCY_HEADER = `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
1892
+ ${prompts.compressPhilosophy}`;
1893
+ }
1894
+ function emergencyHeader(prompts) {
1895
+ return `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
1712
1896
 
1713
- ${COMPRESS_PHILOSOPHY}`;
1897
+ ${prompts.compressPhilosophy}`;
1898
+ }
1714
1899
  function formatK(n) {
1715
1900
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1716
1901
  return `${n}`;
@@ -1818,7 +2003,7 @@ function formatRanges(compressible, protectedRanges) {
1818
2003
  return `Compressible ranges (${merged.length}, oldest first):
1819
2004
  ${lines.join("\n")}`;
1820
2005
  }
1821
- function renderNudgeText(decision) {
2006
+ function renderNudgeText(decision, prompts = defaultPrompts) {
1822
2007
  const breakdownStr = formatBreakdown(decision.contextBreakdown);
1823
2008
  const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
1824
2009
  if (decision.tier !== null && decision.tier >= 2) {
@@ -1830,7 +2015,7 @@ function renderNudgeText(decision) {
1830
2015
  return {
1831
2016
  voice: "gentle",
1832
2017
  text: [
1833
- EFFICIENCY_NOTE,
2018
+ efficiencyNote(prompts),
1834
2019
  "",
1835
2020
  breakdownStr,
1836
2021
  "",
@@ -1839,22 +2024,22 @@ function renderNudgeText(decision) {
1839
2024
  blockList,
1840
2025
  `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
1841
2026
  "",
1842
- HOW_TO_COMPRESS_RULES,
2027
+ prompts.howToCompressRules,
1843
2028
  "",
1844
- isT2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES
2029
+ isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
1845
2030
  ].join("\n")
1846
2031
  };
1847
2032
  }
1848
- const isEmergency = !!decision.breakdown?.emergencyOverride;
2033
+ const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
1849
2034
  if (isEmergency) {
1850
2035
  return {
1851
2036
  voice: "emergency",
1852
2037
  text: [
1853
- EMERGENCY_HEADER,
2038
+ emergencyHeader(prompts),
1854
2039
  "",
1855
2040
  breakdownStr,
1856
2041
  "",
1857
- HOW_TO_COMPRESS_RULES,
2042
+ prompts.howToCompressRules,
1858
2043
  "",
1859
2044
  `{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
1860
2045
  "Only use IDs from visible messages above. Compress older work first.",
@@ -1866,11 +2051,11 @@ function renderNudgeText(decision) {
1866
2051
  return {
1867
2052
  voice: "gentle",
1868
2053
  text: [
1869
- EFFICIENCY_NOTE,
2054
+ efficiencyNote(prompts),
1870
2055
  "",
1871
2056
  breakdownStr,
1872
2057
  "",
1873
- HOW_TO_COMPRESS_RULES,
2058
+ prompts.howToCompressRules,
1874
2059
  "",
1875
2060
  rangesStr,
1876
2061
  "",
@@ -2403,16 +2588,47 @@ function makePreview(text, query, len) {
2403
2588
  // src/config.ts
2404
2589
  var DEFAULT_TOOL_BASH_TIMEOUT = 60;
2405
2590
  var DEFAULT_TOOL_OUTPUT_MAX_BYTES = 2e5;
2591
+ function resolveDelegate(adapter) {
2592
+ const d = adapter.delegate;
2593
+ if (typeof d === "object" && d !== null) {
2594
+ return {
2595
+ enabled: d.enabled !== false,
2596
+ displayUsage: d.displayUsage ?? adapter.displayUsage ?? "separate"
2597
+ };
2598
+ }
2599
+ return {
2600
+ enabled: d !== false,
2601
+ displayUsage: adapter.displayUsage ?? "separate"
2602
+ };
2603
+ }
2406
2604
  function resolveConfig(adapter, liveContextLimit) {
2407
2605
  const envLimit = process.env.ACP_MODEL_CONTEXT_LIMIT;
2408
2606
  const envLimitNum = envLimit ? Number(envLimit) : NaN;
2409
2607
  const FALLBACK_LIMIT = 15e4;
2410
2608
  const limit = !Number.isNaN(envLimitNum) && envLimitNum > 0 ? envLimitNum : adapter.modelContextLimit && adapter.modelContextLimit > 0 ? adapter.modelContextLimit : liveContextLimit > 0 ? liveContextLimit : FALLBACK_LIMIT;
2411
- return defaultConfig(limit, {
2609
+ const config = defaultConfig(limit, {
2412
2610
  protectedTools: adapter.protectedTools ?? [],
2413
2611
  preserveRecentMessages: adapter.preserveRecentMessages ?? 5,
2414
2612
  ...adapter.coreOverrides
2415
2613
  });
2614
+ const c = adapter.compress;
2615
+ if (c?.maxContextLimit !== void 0) config.nudge.maxContextLimitPct = parsePercent(c.maxContextLimit);
2616
+ if (c?.emergencyThresholdPercent !== void 0) {
2617
+ const pct2 = parsePercent(c.emergencyThresholdPercent);
2618
+ config.nudge.emergencyThresholdPct = pct2;
2619
+ config.truncate.threshold = pct2;
2620
+ }
2621
+ if (c?.nudgeGrowthTokens !== void 0) {
2622
+ config.nudge.growthFloor = c.nudgeGrowthTokens;
2623
+ config.nudge.growthCap = c.nudgeGrowthTokens;
2624
+ }
2625
+ return config;
2626
+ }
2627
+ function parsePercent(v) {
2628
+ if (typeof v === "number") return v;
2629
+ const s = v.trim();
2630
+ if (s.endsWith("%")) return Number(s.slice(0, -1)) / 100;
2631
+ return Number(s);
2416
2632
  }
2417
2633
 
2418
2634
  // src/messages.ts
@@ -2927,6 +3143,99 @@ function mergeInitialState(parsed) {
2927
3143
  };
2928
3144
  }
2929
3145
 
3146
+ // src/sequence-match.ts
3147
+ function findUniqueLongestRun(candidates, live) {
3148
+ if (candidates.length === 0 || live.length === 0) return void 0;
3149
+ const ids = /* @__PURE__ */ new Map();
3150
+ const intern = (key) => {
3151
+ const existing = ids.get(key);
3152
+ if (existing !== void 0) return existing;
3153
+ const id = ids.size + 1;
3154
+ ids.set(key, id);
3155
+ return id;
3156
+ };
3157
+ const candidateIds = candidates.map(intern);
3158
+ const liveIds = live.map(intern);
3159
+ const separator = ids.size + 1;
3160
+ const sequence = [...candidateIds, separator, ...liveIds];
3161
+ const suffixArray = buildSuffixArray(sequence);
3162
+ const lcp = buildLcp(sequence, suffixArray);
3163
+ const candidateLength = candidates.length;
3164
+ const liveOffset = candidateLength + 1;
3165
+ const sourceOf = (suffix) => {
3166
+ if (suffix < candidateLength) return 0;
3167
+ if (suffix >= liveOffset) return 1;
3168
+ return void 0;
3169
+ };
3170
+ let bestLength = 0;
3171
+ for (let index = 1; index < suffixArray.length; index++) {
3172
+ const leftSource = sourceOf(suffixArray[index - 1]);
3173
+ const rightSource = sourceOf(suffixArray[index]);
3174
+ if (leftSource === void 0 || rightSource === void 0 || leftSource === rightSource) continue;
3175
+ bestLength = Math.max(bestLength, lcp[index]);
3176
+ }
3177
+ if (bestLength === 0) return void 0;
3178
+ let pairCount = 0;
3179
+ let uniqueCandidateStart = -1;
3180
+ let uniqueLiveStart = -1;
3181
+ for (let start = 0; start < suffixArray.length; ) {
3182
+ let end = start;
3183
+ while (end + 1 < suffixArray.length && lcp[end + 1] >= bestLength) end++;
3184
+ if (end > start) {
3185
+ const candidateStarts = [];
3186
+ const liveStarts = [];
3187
+ for (let index = start; index <= end; index++) {
3188
+ const suffix = suffixArray[index];
3189
+ const source = sourceOf(suffix);
3190
+ if (source === 0) candidateStarts.push(suffix);
3191
+ else if (source === 1) liveStarts.push(suffix - liveOffset);
3192
+ }
3193
+ const groupPairs = candidateStarts.length * liveStarts.length;
3194
+ pairCount += groupPairs;
3195
+ if (groupPairs === 1) {
3196
+ uniqueCandidateStart = candidateStarts[0];
3197
+ uniqueLiveStart = liveStarts[0];
3198
+ }
3199
+ if (pairCount > 1) return void 0;
3200
+ }
3201
+ start = end + 1;
3202
+ }
3203
+ return pairCount === 1 ? { candidateStart: uniqueCandidateStart, liveStart: uniqueLiveStart, length: bestLength } : void 0;
3204
+ }
3205
+ function buildSuffixArray(sequence) {
3206
+ const suffixArray = sequence.map((_, index) => index);
3207
+ let ranks = [...sequence];
3208
+ for (let width = 1; width < sequence.length; width *= 2) {
3209
+ suffixArray.sort((left, right) => ranks[left] - ranks[right] || (ranks[left + width] ?? -1) - (ranks[right + width] ?? -1));
3210
+ const nextRanks = Array(sequence.length);
3211
+ nextRanks[suffixArray[0]] = 0;
3212
+ for (let index = 1; index < suffixArray.length; index++) {
3213
+ const previous = suffixArray[index - 1];
3214
+ const current = suffixArray[index];
3215
+ const differs = ranks[previous] !== ranks[current] || (ranks[previous + width] ?? -1) !== (ranks[current + width] ?? -1);
3216
+ nextRanks[current] = nextRanks[previous] + (differs ? 1 : 0);
3217
+ }
3218
+ ranks = nextRanks;
3219
+ if (ranks[suffixArray.at(-1)] === sequence.length - 1) break;
3220
+ }
3221
+ return suffixArray;
3222
+ }
3223
+ function buildLcp(sequence, suffixArray) {
3224
+ const positions = Array(sequence.length);
3225
+ for (let index = 0; index < suffixArray.length; index++) positions[suffixArray[index]] = index;
3226
+ const lcp = Array(sequence.length).fill(0);
3227
+ let length = 0;
3228
+ for (let start = 0; start < sequence.length; start++) {
3229
+ const position = positions[start];
3230
+ if (position === 0) continue;
3231
+ const previous = suffixArray[position - 1];
3232
+ while (start + length < sequence.length && previous + length < sequence.length && sequence[start + length] === sequence[previous + length]) length++;
3233
+ lcp[position] = length;
3234
+ if (length > 0) length--;
3235
+ }
3236
+ return lcp;
3237
+ }
3238
+
2930
3239
  // src/runtime.ts
2931
3240
  function readContextEntries(sm) {
2932
3241
  const source = sm;
@@ -2940,15 +3249,17 @@ function isPiHost(sm) {
2940
3249
  }
2941
3250
  function mergeLiveEntries(entries, live, state, origins) {
2942
3251
  const persisted = entries.filter((e) => e.type === "message");
2943
- const matched = matchPersistedSuffix(persisted, live);
2944
- const prior = matchOrigins(origins, live);
3252
+ const liveIdentities = live.map(messageIdentity);
3253
+ const persistedIdentities = persisted.map((entry) => messageIdentity(entry.message));
3254
+ const persistedRange = findUniqueLongestRun(persistedIdentities, normalizePersistedMatchKeys(persisted, persistedIdentities, live, liveIdentities));
3255
+ const originRange = findUniqueLongestRun(origins.map((origin) => origin.identity), liveIdentities);
2945
3256
  const out = [];
2946
3257
  const nextOrigins = [];
2947
3258
  const usedIds = /* @__PURE__ */ new Set();
2948
3259
  for (let i = 0; i < live.length; i++) {
2949
3260
  const msg = live[i];
2950
- const entry = matched[i];
2951
- const origin = prior[i];
3261
+ const entry = valueInRange(persisted, persistedRange, i);
3262
+ const origin = valueInRange(origins, originRange, i);
2952
3263
  if (entry) {
2953
3264
  if (origin) migrateLiveRefs(state, origin.rawId, entry.id);
2954
3265
  else migrateTaggedRef(state, msg, entry.id);
@@ -2958,23 +3269,13 @@ function mergeLiveEntries(entries, live, state, origins) {
2958
3269
  const id = origin?.rawId ?? nextLiveId(state, usedIds, i);
2959
3270
  usedIds.add(id);
2960
3271
  out.push({ type: "message", id, parentId: null, timestamp: String(msg.timestamp ?? Date.now()), message: msg });
2961
- nextOrigins.push({ rawId: id, identity: messageIdentity(msg) });
3272
+ nextOrigins.push({ rawId: id, identity: liveIdentities[i] });
2962
3273
  }
2963
3274
  origins.splice(0, origins.length, ...nextOrigins);
2964
- const unmatched = live.length - matched.length;
3275
+ const unmatched = live.length - (persistedRange?.length ?? 0);
2965
3276
  if (unmatched > 0) logInfo("runtime", { event: "merge-live-entries", live: live.length, unmatched });
2966
3277
  return out;
2967
3278
  }
2968
- function matchOrigins(origins, live) {
2969
- for (let start = 0; start < origins.length; start++) {
2970
- const count = origins.length - start;
2971
- if (count > live.length) continue;
2972
- if (origins.slice(start).every((origin, index) => origin.identity === messageIdentity(live[index]))) {
2973
- return [...origins.slice(start), ...Array(live.length - count)];
2974
- }
2975
- }
2976
- return Array(live.length);
2977
- }
2978
3279
  function nextLiveId(state, used, index) {
2979
3280
  let id = `live-${index}`;
2980
3281
  let suffix = index;
@@ -3001,31 +3302,39 @@ function migrateLiveRefs(state, liveId, stableId) {
3001
3302
  delete state.messageRefs.byRaw[rawId];
3002
3303
  }
3003
3304
  }
3004
- function matchPersistedSuffix(persisted, live) {
3005
- for (let start = 0; start < persisted.length; start++) {
3006
- const count = persisted.length - start;
3007
- if (count > live.length) continue;
3008
- const suffix = persisted.slice(start);
3009
- if (suffix.every((entry, index) => sameMessage(entry.message, live[index]))) return suffix;
3010
- }
3011
- return [];
3305
+ var NO_PERSISTED_MATCH = /* @__PURE__ */ Symbol("no-persisted-match");
3306
+ function normalizePersistedMatchKeys(persisted, persistedIdentities, live, liveIdentities) {
3307
+ const persistedByStructure = /* @__PURE__ */ new Map();
3308
+ for (let index = 0; index < persisted.length; index++) {
3309
+ const key = toolResultStructureKey(persisted[index].message);
3310
+ if (key === void 0) continue;
3311
+ persistedByStructure.set(key, persistedByStructure.has(key) ? -1 : index);
3312
+ }
3313
+ return live.map((message, liveIndex) => {
3314
+ const key = toolResultStructureKey(message);
3315
+ const candidateIndex = key === void 0 ? void 0 : persistedByStructure.get(key);
3316
+ if (candidateIndex === void 0) return liveIdentities[liveIndex];
3317
+ if (candidateIndex < 0) return NO_PERSISTED_MATCH;
3318
+ return sameToolResult(persisted[candidateIndex].message, message) ? persistedIdentities[candidateIndex] : liveIdentities[liveIndex];
3319
+ });
3012
3320
  }
3013
- function sameMessage(a, b) {
3014
- try {
3015
- if (messageIdentity(a) === messageIdentity(b)) return true;
3016
- const ra = a.role;
3017
- const rb = b.role;
3018
- if (ra !== rb || ra !== "toolResult") return false;
3019
- const ca = a.content;
3020
- const cb = b.content;
3021
- return sameNonTextBlocks(ca, cb) && matchesStoredText(extractText(ca), extractText(cb));
3022
- } catch (e) {
3023
- logWarn("runtime", { event: "message-compare-failed", error: e instanceof Error ? e.message : String(e) });
3024
- return a === b;
3025
- }
3321
+ function toolResultStructureKey(message) {
3322
+ if (message.role !== "toolResult") return void 0;
3323
+ return `${message.toolName}\0${message.toolCallId}`;
3324
+ }
3325
+ function valueInRange(values, range, liveIndex) {
3326
+ if (!range || liveIndex < range.liveStart || liveIndex >= range.liveStart + range.length) return void 0;
3327
+ return values[range.candidateStart + liveIndex - range.liveStart];
3328
+ }
3329
+ function sameToolResult(stored, visible) {
3330
+ if (stored.role !== "toolResult" || visible.role !== "toolResult") return false;
3331
+ return sameNonTextBlocks(stored.content, visible.content) && matchesStoredText(extractText(stored.content), extractText(visible.content));
3026
3332
  }
3027
3333
  function sameNonTextBlocks(a, b) {
3028
- const nonText = (blocks) => blocks.filter((block) => block.type !== "text");
3334
+ const nonText = (blocks) => blocks.filter((block) => {
3335
+ if (!block || typeof block !== "object" || !("type" in block)) return true;
3336
+ return block.type !== "text";
3337
+ });
3029
3338
  try {
3030
3339
  const na = Array.isArray(a) ? nonText(a) : [];
3031
3340
  const nb = Array.isArray(b) ? nonText(b) : [];
@@ -3053,6 +3362,7 @@ function createRuntime(adapter) {
3053
3362
  const store = new SessionStateStore();
3054
3363
  const locks = /* @__PURE__ */ new Map();
3055
3364
  let adapterRef = adapter;
3365
+ let promptsRef = defaultPrompts;
3056
3366
  const nudgeShownTurns = /* @__PURE__ */ new Set();
3057
3367
  async function acquireLock(sid) {
3058
3368
  const prev = locks.get(sid) ?? Promise.resolve();
@@ -3101,6 +3411,10 @@ function createRuntime(adapter) {
3101
3411
  return adapterRef;
3102
3412
  }, setAdapter: (a) => {
3103
3413
  adapterRef = a;
3414
+ }, get prompts() {
3415
+ return promptsRef;
3416
+ }, setPrompts: (p) => {
3417
+ promptsRef = p;
3104
3418
  }, markNudgeShown: (k) => {
3105
3419
  nudgeShownTurns.add(k);
3106
3420
  }, nudgeShownFor: (k) => nudgeShownTurns.has(k), clearNudgeTracking: () => {
@@ -7715,6 +8029,18 @@ function findMessageContent(ref, ctx) {
7715
8029
  }
7716
8030
  return null;
7717
8031
  }
8032
+ function resolveBlockMessages(block, coreMessages, ctx) {
8033
+ const neededBaseIds = new Set(block.effectiveMessageIds.map((id) => id.split("#")[0]));
8034
+ const presentBaseIds = new Set(coreMessages.map((m) => m.id.split("#")[0]));
8035
+ const missingBaseIds = [...neededBaseIds].filter((id) => !presentBaseIds.has(id));
8036
+ if (missingBaseIds.length === 0) return coreMessages;
8037
+ const extra = [];
8038
+ for (const baseId of missingBaseIds) {
8039
+ const entry = ctx.sessionManager.getEntry(baseId);
8040
+ if (entry) extra.push(...entriesToCoreMessages([entry]));
8041
+ }
8042
+ return [...coreMessages, ...extra];
8043
+ }
7718
8044
  async function handleMessageRef(ref, ownerBlockId, args, ctx) {
7719
8045
  const found = findMessageContent(ref, ctx);
7720
8046
  if (!found || !found.text) {
@@ -7762,7 +8088,8 @@ async function handleDecompress(args, runtime, ctx) {
7762
8088
  return `Block ${blockId} not found. Active blocks: ${active || "(none)"}.`;
7763
8089
  }
7764
8090
  const full = args.full ?? false;
7765
- const { text, count } = collectBlockContent(state, block, coreMessages, { full });
8091
+ const resolved = resolveBlockMessages(block, coreMessages, ctx);
8092
+ const { text, count } = collectBlockContent(state, block, resolved, { full });
7766
8093
  if (count === 0) return `Block ${blockId} has no restorable message content.`;
7767
8094
  if (args.inline === true && !args.toFile) {
7768
8095
  debug.event("decompress", { blockId, full, count, mode: "inline" });
@@ -7924,10 +8251,10 @@ function formatSize(tokens) {
7924
8251
  import {
7925
8252
  spawn
7926
8253
  } from "child_process";
7927
- import { createWriteStream } from "fs";
8254
+ import { createWriteStream, existsSync as existsSync2 } from "fs";
7928
8255
  import { mkdir as mkdir2, mkdtemp, writeFile as writeFile2, rm, appendFile } from "fs/promises";
7929
8256
  import { tmpdir as tmpdir2 } from "os";
7930
- import { join as join4 } from "path";
8257
+ import { dirname as dirname3, join as join4, resolve as resolvePath } from "path";
7931
8258
 
7932
8259
  // src/footer-status.ts
7933
8260
  var FOOTER_STATUS_KEY = "billion-context-pi";
@@ -8340,6 +8667,44 @@ function delegateSpawnOptions(cwd, env) {
8340
8667
  shell: false
8341
8668
  };
8342
8669
  }
8670
+ var PI_CLI_ENTRY_RE = /[\\/]pi-coding-agent[\\/]dist[\\/]cli\.js$/;
8671
+ var PI_PACKAGE_REL = join4("@earendil-works", "pi-coding-agent", "dist", "cli.js");
8672
+ function probeUpFromArgv(argv1) {
8673
+ let dir = resolvePath(dirname3(argv1) || process.cwd());
8674
+ for (; ; ) {
8675
+ const candidate = join4(dir, "node_modules", PI_PACKAGE_REL);
8676
+ if (existsSync2(candidate)) return candidate;
8677
+ const parent = dirname3(dir);
8678
+ if (parent === dir) return null;
8679
+ dir = parent;
8680
+ }
8681
+ }
8682
+ function piCliGlobalCandidates(env) {
8683
+ const candidates = [];
8684
+ if (process.platform === "win32") {
8685
+ if (env.APPDATA) candidates.push(join4(env.APPDATA, "npm", "node_modules", PI_PACKAGE_REL));
8686
+ } else {
8687
+ const home = env.HOME ?? env.USERPROFILE;
8688
+ if (home) candidates.push(join4(home, ".local", "lib", "node_modules", PI_PACKAGE_REL));
8689
+ candidates.push(join4("/usr/local", "lib", "node_modules", PI_PACKAGE_REL));
8690
+ candidates.push(join4("/usr", "lib", "node_modules", PI_PACKAGE_REL));
8691
+ }
8692
+ return candidates;
8693
+ }
8694
+ function resolvePiCliEntry(argv1, env = process.env, piHost = true) {
8695
+ const explicit = env.PI_CLI_PATH;
8696
+ if (explicit) return explicit;
8697
+ if (argv1 && PI_CLI_ENTRY_RE.test(argv1)) return argv1;
8698
+ if (piHost) {
8699
+ const probed = probeUpFromArgv(argv1);
8700
+ if (probed) return probed;
8701
+ for (const candidate of piCliGlobalCandidates(env)) {
8702
+ if (existsSync2(candidate)) return candidate;
8703
+ }
8704
+ logWarn("delegate", { event: "cli-entry-unresolved", argv1, fallback: "argv[1]" });
8705
+ }
8706
+ return argv1;
8707
+ }
8343
8708
  var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
8344
8709
  var RESTRICTED_TOOLS = "read,bash,grep,find,ls";
8345
8710
  var AGENTS = {
@@ -8761,7 +9126,7 @@ async function runDelegate(pi, args, ctx, signal) {
8761
9126
  logInfo("delegate", { event: "spawn", agent: args.agent, runId, cwd, async: isAsync, useJsonStream, mode: ctx.mode, parentDepth });
8762
9127
  const child = spawn(
8763
9128
  process.execPath,
8764
- [process.argv[1], ...cliArgs],
9129
+ [resolvePiCliEntry(process.argv[1] ?? "", process.env, isPiHost(ctx.sessionManager)), ...cliArgs],
8765
9130
  delegateSpawnOptions(cwd, childEnv)
8766
9131
  );
8767
9132
  child.stdin?.once("error", (e) => {
@@ -9172,7 +9537,7 @@ async function handleStatus(args, runtime, ctx) {
9172
9537
  const costStr = cost > 0 ? ` ($${cost.toFixed(4)})` : "";
9173
9538
  extra.push("\u2500\u2500 Session delegate usage (excluded from main totals) \u2500\u2500");
9174
9539
  extra.push(`Tokens: ${delegateUsage.input.toLocaleString()} in, ${delegateUsage.output.toLocaleString()} out (${delegateUsage.totalTokens.toLocaleString()} total)${costStr}`);
9175
- } else if (runtime.adapter.displayUsage === "merged") {
9540
+ } else if (resolveDelegate(runtime.adapter).displayUsage === "merged") {
9176
9541
  extra.push("");
9177
9542
  extra.push("merged mode: delegate usage is included in main session totals.");
9178
9543
  } else {
@@ -9293,7 +9658,7 @@ async function statusReport(runtime, ctx) {
9293
9658
  const activeBlocksList = state.blocks.filter((b) => b.active);
9294
9659
  const totalBlocksList = state.blocks;
9295
9660
  const lines = [];
9296
- const versionStr = "0.1.35" ? `billion-context-pi@${"0.1.35"}` : "";
9661
+ const versionStr = "0.1.37" ? `billion-context-pi@${"0.1.37"}` : "";
9297
9662
  lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
9298
9663
  lines.push("\u2502 ACP Context Analysis \u2502");
9299
9664
  lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
@@ -9370,7 +9735,8 @@ async function statusReport(runtime, ctx) {
9370
9735
  }
9371
9736
 
9372
9737
  // src/system-prompt.ts
9373
- var ACP_SYSTEM_PROMPT = `
9738
+ function buildAcpSystemPrompt(prompts) {
9739
+ return `
9374
9740
  ACP context management
9375
9741
 
9376
9742
  ACP TAGS
@@ -9394,7 +9760,7 @@ You have four context-management tools:
9394
9760
  - 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" }).
9395
9761
  - 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.
9396
9762
 
9397
- ${COMPRESS_PHILOSOPHY}
9763
+ ${prompts.compressPhilosophy}
9398
9764
 
9399
9765
  WHEN TO COMPRESS
9400
9766
 
@@ -9412,7 +9778,7 @@ WHEN NOT TO COMPRESS
9412
9778
  - 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.
9413
9779
  - Protected tool outputs \u2014 hard-excluded from compression ranges, survive intact in visible context.
9414
9780
 
9415
- ${HOW_TO_COMPRESS_RULES}
9781
+ ${prompts.howToCompressRules}
9416
9782
 
9417
9783
  MULTI-TIER COMPRESSION
9418
9784
 
@@ -9420,9 +9786,9 @@ Summaries accumulate as the session grows. When tier-1 summaries pile up, the sy
9420
9786
 
9421
9787
  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.
9422
9788
 
9423
- ${TIER2_DISTILL_RULES}
9789
+ ${prompts.tier2DistillRules}
9424
9790
 
9425
- ${TIER3_CONDENSE_RULES}
9791
+ ${prompts.tier3CondenseRules}
9426
9792
 
9427
9793
  THE PHILOSOPHY OF DECOMPRESS
9428
9794
 
@@ -9432,6 +9798,7 @@ CONTEXT BREAKDOWN
9432
9798
 
9433
9799
  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.
9434
9800
  `;
9801
+ }
9435
9802
  var ACP_DELEGATE_PROMPT = `
9436
9803
  ACP_DELEGATE NOTIFICATIONS
9437
9804
 
@@ -9560,7 +9927,7 @@ function wireToolGuardrails(pi, runtime) {
9560
9927
 
9561
9928
  // src/update.ts
9562
9929
  import { readFile, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
9563
- import { join as join5, dirname as dirname3 } from "path";
9930
+ import { join as join5, dirname as dirname4 } from "path";
9564
9931
  import { fileURLToPath } from "url";
9565
9932
  import { execFile } from "child_process";
9566
9933
  import { homedir as homedir3 } from "os";
@@ -9593,7 +9960,7 @@ async function readLastCheck() {
9593
9960
  }
9594
9961
  async function writeLastCheck(timestamp) {
9595
9962
  try {
9596
- await mkdir3(dirname3(THROTTLE_FILE), { recursive: true });
9963
+ await mkdir3(dirname4(THROTTLE_FILE), { recursive: true });
9597
9964
  await writeFile3(THROTTLE_FILE, String(timestamp), "utf-8");
9598
9965
  } catch {
9599
9966
  }
@@ -9607,20 +9974,20 @@ async function readPackageJson(path4) {
9607
9974
  }
9608
9975
  }
9609
9976
  function findNpmRoot(extDir) {
9610
- let dir = dirname3(extDir);
9977
+ let dir = dirname4(extDir);
9611
9978
  for (; ; ) {
9612
- if (dir.endsWith("node_modules")) return dirname3(dir);
9613
- const parent = dirname3(dir);
9979
+ if (dir.endsWith("node_modules")) return dirname4(dir);
9980
+ const parent = dirname4(dir);
9614
9981
  if (parent === dir) return void 0;
9615
9982
  dir = parent;
9616
9983
  }
9617
9984
  }
9618
9985
  async function findExtensionDir() {
9619
- let dir = dirname3(fileURLToPath(import.meta.url));
9986
+ let dir = dirname4(fileURLToPath(import.meta.url));
9620
9987
  for (; ; ) {
9621
9988
  const pkg = await readPackageJson(join5(dir, "package.json"));
9622
9989
  if (pkg?.name === PACKAGE_NAME) return dir;
9623
- const parent = dirname3(dir);
9990
+ const parent = dirname4(dir);
9624
9991
  if (parent === dir) return void 0;
9625
9992
  dir = parent;
9626
9993
  }
@@ -9669,7 +10036,7 @@ async function checkForUpdate(autoUpdate, notify) {
9669
10036
  const data = await res.json();
9670
10037
  const latest = data.version;
9671
10038
  if (!latest) return;
9672
- const current = runtimeVersion ?? "0.1.35";
10039
+ const current = runtimeVersion ?? "0.1.37";
9673
10040
  const hasUpdate = isNewer(latest, current);
9674
10041
  debug.event("update-check", {
9675
10042
  current,
@@ -9705,7 +10072,7 @@ async function getRuntimeVersion() {
9705
10072
 
9706
10073
  // src/setup-subagent-tools.ts
9707
10074
  import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename } from "fs/promises";
9708
- import { existsSync as existsSync2 } from "fs";
10075
+ import { existsSync as existsSync3 } from "fs";
9709
10076
  import { homedir as homedir4 } from "os";
9710
10077
  import { join as join6 } from "path";
9711
10078
  import { CONFIG_DIR_NAME as CONFIG_DIR_NAME3 } from "@earendil-works/pi-coding-agent";
@@ -9770,7 +10137,7 @@ async function ensureSubagentAcpTools(settingsPath) {
9770
10137
  return { path: path4, action: "skipped", reason: "all builtin agents already have ACP tools" };
9771
10138
  }
9772
10139
  const backupPath = `${path4}.acp-bak`;
9773
- if (!existsSync2(backupPath)) {
10140
+ if (!existsSync3(backupPath)) {
9774
10141
  try {
9775
10142
  await copyFile(path4, backupPath);
9776
10143
  } catch {
@@ -9862,7 +10229,18 @@ async function loadUserConfig(cwd) {
9862
10229
  function join8(...parts) {
9863
10230
  return path3.join(...parts);
9864
10231
  }
9865
- var KNOWN = /* @__PURE__ */ new Set(["debug", "autoUpdate", "modelContextLimit", "delegate", "displayUsage", "toolBashDefaultTimeout", "toolOutputMaxBytes"]);
10232
+ var KNOWN = /* @__PURE__ */ new Set([
10233
+ "debug",
10234
+ "autoUpdate",
10235
+ "modelContextLimit",
10236
+ "toolBashDefaultTimeout",
10237
+ "toolOutputMaxBytes",
10238
+ "delegate",
10239
+ "compress",
10240
+ "displayUsage",
10241
+ "prompts",
10242
+ "acknowledgePromptsRisk"
10243
+ ]);
9866
10244
  function pickKnown(parsed) {
9867
10245
  const out = {};
9868
10246
  for (const [k, v] of Object.entries(parsed)) {
@@ -9874,8 +10252,6 @@ function applyUserConfig(adapter, user) {
9874
10252
  return {
9875
10253
  ...adapter,
9876
10254
  ...user,
9877
- // coreOverrides / protectedTools / preserveRecentMessages are not overridable
9878
- // from acp.json (keep them from the factory config).
9879
10255
  coreOverrides: adapter.coreOverrides,
9880
10256
  protectedTools: adapter.protectedTools,
9881
10257
  preserveRecentMessages: adapter.preserveRecentMessages
@@ -9911,16 +10287,22 @@ function wireSessionLifecycle(pi, runtime) {
9911
10287
  resetDelegateUsage();
9912
10288
  setDelegateDisplayUsage("separate");
9913
10289
  const sid = ctx.sessionManager.getSessionId();
9914
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.35" : null });
10290
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.37" : null });
9915
10291
  try {
9916
10292
  const user = await loadUserConfig(ctx.cwd);
9917
10293
  runtime.setAdapter(applyUserConfig(runtime.adapter, user));
9918
- setDelegateDisplayUsage(runtime.adapter.displayUsage ?? "separate");
10294
+ setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
9919
10295
  if (runtime.adapter.debug !== void 0) setDebugEnabled(runtime.adapter.debug);
9920
10296
  } catch (e) {
9921
10297
  logThrow("config", e, { sid, phase: "session_start" });
9922
10298
  }
9923
- if (runtime.adapter.delegate !== false) {
10299
+ try {
10300
+ runtime.setPrompts(resolvePrompts(runtime.adapter.prompts, { acknowledgeRisk: runtime.adapter.acknowledgePromptsRisk === true }));
10301
+ } catch (e) {
10302
+ logWarn("config", { event: "prompts-resolve-failed", error: e instanceof Error ? e.message : String(e) });
10303
+ runtime.setPrompts(defaultPrompts);
10304
+ }
10305
+ if (resolveDelegate(runtime.adapter).enabled) {
9924
10306
  pi.registerTool(makeDelegateTool(pi));
9925
10307
  pi.registerTool(makeDelegateWaitTool(pi));
9926
10308
  pi.registerTool(makeDelegateCancelTool(pi));
@@ -9980,7 +10362,7 @@ function wireContextTransform(pi, runtime) {
9980
10362
  prunedMsgs: coreMessages.length - turn.messages.length + turn.messages.filter((m) => m.id.startsWith("acp_summary")).length,
9981
10363
  nudgeShouldInject: turn.nudge?.shouldInject ?? false,
9982
10364
  nudgeReason: turn.nudge?.reason ?? null,
9983
- nudgeVoice: turn.nudge ? renderNudgeText(turn.nudge).voice : null,
10365
+ nudgeVoice: turn.nudge ? renderNudgeText(turn.nudge, runtime.prompts).voice : null,
9984
10366
  nudgePct: turn.nudge ? Math.round(turn.nudge.contextUsage * 100) : null,
9985
10367
  nudgeTier: turn.nudge?.tier ?? null,
9986
10368
  nudgeCompressibleCount: turn.nudge?.compressibleRanges.length ?? 0,
@@ -9997,8 +10379,8 @@ function wireContextTransform(pi, runtime) {
9997
10379
  const turnKey = lastUserMessageId(entries) ?? sid;
9998
10380
  const alreadyShown = !emergency && runtime.nudgeShownFor(turnKey);
9999
10381
  if (!alreadyShown) {
10000
- rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active)));
10001
- const rendered = renderNudgeText(turn.nudge);
10382
+ rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts));
10383
+ const rendered = renderNudgeText(turn.nudge, runtime.prompts);
10002
10384
  const top = [...turn.nudge.compressibleRanges].sort((a, b) => b.tokens - a.tokens)[0];
10003
10385
  const example = top ? `
10004
10386
 
@@ -10032,8 +10414,9 @@ ${rendered.text}${example}`);
10032
10414
  function wireSystemPrompt(pi, runtime) {
10033
10415
  pi.on("before_agent_start", (event) => {
10034
10416
  const delegate = runtime.adapter.delegate !== false;
10035
- const prompt = delegate ? `${ACP_SYSTEM_PROMPT}
10036
- ${ACP_DELEGATE_PROMPT}` : ACP_SYSTEM_PROMPT;
10417
+ const acp = buildAcpSystemPrompt(runtime.prompts);
10418
+ const prompt = delegate ? `${acp}
10419
+ ${ACP_DELEGATE_PROMPT}` : acp;
10037
10420
  return { systemPrompt: formatSystemPromptForEvent(event.systemPrompt, prompt) };
10038
10421
  });
10039
10422
  }
@@ -10049,8 +10432,8 @@ function collectOriginals(entries) {
10049
10432
  }
10050
10433
  return map;
10051
10434
  }
10052
- function nudgeMessage(nudge, blocks) {
10053
- const rendered = renderNudgeText(nudge);
10435
+ function nudgeMessage(nudge, blocks, prompts) {
10436
+ const rendered = renderNudgeText(nudge, prompts);
10054
10437
  const lines = [rendered.text];
10055
10438
  if (blocks.length > 0) {
10056
10439
  const totalSummary = blocks.reduce((s, b) => s + Math.ceil((b.summary || "").length / 4), 0);