billion-context-pi 0.1.71 → 0.1.72

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
@@ -14,7 +14,7 @@ import { readFileSync as readFileSync6 } from "fs";
14
14
  import { homedir as homedir10 } from "os";
15
15
  import { join as join16 } from "path";
16
16
 
17
- // node_modules/acp-kernel/dist/chunk-37CVFHFQ.js
17
+ // node_modules/acp-kernel/dist/chunk-QPVKTDYF.js
18
18
  import { createRequire } from "module";
19
19
  var require2 = createRequire(import.meta.url);
20
20
  function defaultCountTokens(text) {
@@ -216,7 +216,7 @@ function formatRanges(compressible, protectedRanges) {
216
216
  if (compressible.length === 0 && protectedRanges.length === 0) {
217
217
  return "[No specific ranges detected \u2014 compress any consumed content.]";
218
218
  }
219
- const refNum2 = (ref) => {
219
+ const refNum3 = (ref) => {
220
220
  const m2 = ref.match(/\d+/);
221
221
  return m2 ? parseInt(m2[0], 10) : 0;
222
222
  };
@@ -225,8 +225,8 @@ function formatRanges(compressible, protectedRanges) {
225
225
  entries.push({
226
226
  startRef: r.startRef,
227
227
  endRef: r.endRef,
228
- startNum: refNum2(r.startRef),
229
- endNum: refNum2(r.endRef),
228
+ startNum: refNum3(r.startRef),
229
+ endNum: refNum3(r.endRef),
230
230
  count: r.count,
231
231
  tokens: r.tokens,
232
232
  userMsgs: r.userMsgs ?? 0,
@@ -244,8 +244,8 @@ function formatRanges(compressible, protectedRanges) {
244
244
  entries.push({
245
245
  startRef: r.startRef,
246
246
  endRef: r.endRef,
247
- startNum: refNum2(r.startRef),
248
- endNum: refNum2(r.endRef),
247
+ startNum: refNum3(r.startRef),
248
+ endNum: refNum3(r.endRef),
249
249
  count: r.count,
250
250
  tokens: r.tokens,
251
251
  userMsgs: 0,
@@ -379,6 +379,24 @@ var VIABLE_RANGE_MIN_TOKENS = 200;
379
379
  function viableRanges(ranges) {
380
380
  return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
381
381
  }
382
+ function isHighSurrogate(c) {
383
+ return c >= 55296 && c <= 56319;
384
+ }
385
+ function isLowSurrogate(c) {
386
+ return c >= 56320 && c <= 57343;
387
+ }
388
+ function clampPrefix(text, maxUnits) {
389
+ const cut = Math.min(maxUnits, text.length);
390
+ if (cut > 0 && isHighSurrogate(text.charCodeAt(cut - 1))) return text.slice(0, cut - 1);
391
+ return text.slice(0, cut);
392
+ }
393
+ function clampWindow(text, start, end) {
394
+ let s = Math.max(0, Math.min(start, text.length));
395
+ let e = Math.min(text.length, Math.max(s, end));
396
+ if (s > 0 && isLowSurrogate(text.charCodeAt(s))) s += 1;
397
+ if (e > s && isHighSurrogate(text.charCodeAt(e - 1))) e -= 1;
398
+ return text.slice(s, Math.max(s, e));
399
+ }
382
400
 
383
401
  // node_modules/acp-kernel/dist/chunk-UX4LINT7.js
384
402
  function createInitialState() {
@@ -566,9 +584,43 @@ function collectSummaryAnchors(state, indexById, summaryIndexById) {
566
584
  anchors.sort((left, right) => left.insertAt - right.insertAt);
567
585
  return anchors;
568
586
  }
587
+ function pairSafeAnchorIndex(messages, index) {
588
+ let safe = index;
589
+ while (safe > 0 && safe < messages.length && messages[safe - 1].role === "assistant" && messages[safe].role === "assistant") {
590
+ safe--;
591
+ }
592
+ const resultIndexByCallId2 = /* @__PURE__ */ new Map();
593
+ messages.forEach((message, at) => {
594
+ if (message.contentType !== "tool-result") return;
595
+ if (typeof message.toolCallId !== "string") return;
596
+ if (!resultIndexByCallId2.has(message.toolCallId)) {
597
+ resultIndexByCallId2.set(message.toolCallId, at);
598
+ }
599
+ });
600
+ for (let guard = 0; guard < messages.length; guard++) {
601
+ let moved = safe;
602
+ for (let at = 0; at < safe && at < messages.length; at++) {
603
+ const message = messages[at];
604
+ if (message.role !== "assistant") continue;
605
+ if (message.contentType !== "tool-call") continue;
606
+ if (typeof message.toolCallId !== "string") continue;
607
+ const resultIndex = resultIndexByCallId2.get(message.toolCallId);
608
+ if (resultIndex !== void 0 && resultIndex >= safe && resultIndex + 1 > moved) {
609
+ moved = resultIndex + 1;
610
+ }
611
+ }
612
+ if (moved === safe) break;
613
+ safe = moved;
614
+ }
615
+ return safe;
616
+ }
569
617
  function rebuildMessages(messages, covered, firstUserIndex, anchors) {
618
+ const safeAnchors = anchors.map((anchor) => ({
619
+ ...anchor,
620
+ insertAt: pairSafeAnchorIndex(messages, anchor.insertAt)
621
+ })).sort((left, right) => left.insertAt - right.insertAt);
570
622
  const result = [];
571
- const pending = [...anchors];
623
+ const pending = [...safeAnchors];
572
624
  const anchoredSummaryIds = new Set(
573
625
  anchors.map((anchor) => summaryMessageId(anchor.blockId))
574
626
  );
@@ -661,6 +713,7 @@ function syncBlocks(messages, state) {
661
713
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
662
714
  stats: { ...state.stats },
663
715
  absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
716
+ terminalStreak: state.terminalStreak,
664
717
  nextBlockId: state.nextBlockId,
665
718
  nextRunId: state.nextRunId
666
719
  };
@@ -716,7 +769,7 @@ function defaultConfig(modelContextLimit, overrides = {}) {
716
769
  tier2GrowthMultiplier: 1.5
717
770
  },
718
771
  promotionThreshold: 5,
719
- truncate: { threshold: 0.95 },
772
+ truncate: { threshold: 0.95, terminalEscapeAfter: 3 },
720
773
  compress: {
721
774
  minCompressRange: 5e3,
722
775
  maxSummaryLength: 2e4,
@@ -768,6 +821,9 @@ function validateConfig(config) {
768
821
  if (config.truncate.threshold <= 0 || config.truncate.threshold > 1) {
769
822
  errors.push("truncate.threshold must be in (0, 1]");
770
823
  }
824
+ if (config.truncate.terminalEscapeAfter !== void 0 && (!Number.isInteger(config.truncate.terminalEscapeAfter) || config.truncate.terminalEscapeAfter < 0)) {
825
+ errors.push("truncate.terminalEscapeAfter must be an integer >= 0");
826
+ }
771
827
  for (const tier of [config.tiers.tier2Trigger, config.tiers.tier3Trigger]) {
772
828
  if (tier < 1) errors.push("tier triggers must be >= 1");
773
829
  }
@@ -1013,47 +1069,69 @@ var DEFAULTS = {
1013
1069
  };
1014
1070
  function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
1015
1071
  const opts = { ...DEFAULTS, ...options };
1016
- if (config.modelContextLimit <= 0) return { messages, truncatedCount: 0, savedTokens: 0 };
1017
- const threshold = config.truncate.threshold * config.modelContextLimit;
1018
- if (tokenCount < threshold) return { messages, truncatedCount: 0, savedTokens: 0 };
1019
- const protectedIndex = messages.length - opts.protectRecentMessages;
1020
- const candidates = [];
1021
- for (let index = 0; index < messages.length; index++) {
1022
- if (index >= protectedIndex) break;
1023
- const message = messages[index];
1024
- if (message.contentType !== "tool-result") continue;
1025
- const text = message.text ?? "";
1026
- if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
1027
- const tokens = countTokens(text);
1028
- if (tokens < opts.minOutputTokens) continue;
1029
- candidates.push({ index, tokens });
1072
+ const limit = config.modelContextLimit;
1073
+ if (limit <= 0 || tokenCount < config.truncate.threshold * limit) {
1074
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound: 0 };
1030
1075
  }
1031
- if (candidates.length === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
1032
- candidates.sort((left, right) => right.tokens - left.tokens);
1033
- const targetTokens = threshold * 0.9;
1034
- let savedTokens = 0;
1035
- const edits = /* @__PURE__ */ new Map();
1076
+ const protectedIndex = messages.length - opts.protectRecentMessages;
1077
+ const findCandidates = (predicate) => {
1078
+ const found = [];
1079
+ for (let i = 0; i < protectedIndex; i++) {
1080
+ const message = messages[i];
1081
+ if (!predicate(message)) continue;
1082
+ const text = message.text ?? "";
1083
+ if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
1084
+ if (countTokens(text) < opts.minOutputTokens) continue;
1085
+ found.push(message);
1086
+ }
1087
+ return found.sort(
1088
+ (a, b2) => countTokens(b2.text ?? "") - countTokens(a.text ?? "")
1089
+ );
1090
+ };
1091
+ const toolResults = findCandidates((m2) => m2.contentType === "tool-result");
1092
+ const textMessages = options.includeTextMessages ? findCandidates(
1093
+ (m2) => m2.contentType === "text" && (m2.role === "user" || m2.role === "assistant") && !isRenderedSummaryMessage(m2) && !m2.text?.startsWith(SUMMARY_HEADER)
1094
+ ) : [];
1095
+ const candidatesFound = toolResults.length + textMessages.length;
1036
1096
  let truncatedCount = 0;
1037
- for (const candidate of candidates) {
1038
- if (tokenCount - savedTokens <= targetTokens) break;
1039
- const original = messages[candidate.index].text ?? "";
1040
- if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) continue;
1041
- const prefix = original.slice(0, opts.keepPrefixChars);
1042
- const suffix = original.slice(-opts.keepSuffixChars);
1043
- const replacement = prefix + `
1044
-
1045
- ...${TRUNCATION_MARKER} \u2014 original ~${candidate.tokens} tokens]...
1046
-
1047
- ` + suffix;
1048
- edits.set(candidate.index, replacement);
1049
- savedTokens += candidate.tokens - countTokens(replacement);
1050
- truncatedCount++;
1051
- }
1052
- if (truncatedCount === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
1053
- const updated = messages.map(
1054
- (message, index) => edits.has(index) ? { ...message, text: edits.get(index) } : message
1055
- );
1056
- return { messages: updated, truncatedCount, savedTokens };
1097
+ let savedTokens = 0;
1098
+ let remaining = tokenCount;
1099
+ const targetTokens = config.truncate.threshold * limit * 0.9;
1100
+ const replacements = /* @__PURE__ */ new Map();
1101
+ const applyTo = (candidates) => {
1102
+ for (const candidate of candidates) {
1103
+ if (remaining <= targetTokens) break;
1104
+ const original = candidate.text ?? "";
1105
+ const tokens = countTokens(original);
1106
+ if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) {
1107
+ continue;
1108
+ }
1109
+ const prefix = clampPrefix(original, opts.keepPrefixChars);
1110
+ const suffix = clampWindow(original, original.length - opts.keepSuffixChars, original.length);
1111
+ const replacement = `${prefix}
1112
+
1113
+ ...${TRUNCATION_MARKER} \u2014 original ~${tokens} tokens]...
1114
+
1115
+ ${suffix}`;
1116
+ replacements.set(candidate.id, replacement);
1117
+ truncatedCount++;
1118
+ remaining -= tokens - countTokens(replacement);
1119
+ savedTokens += tokens - countTokens(replacement);
1120
+ }
1121
+ };
1122
+ applyTo(toolResults);
1123
+ applyTo(textMessages);
1124
+ if (replacements.size === 0) {
1125
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound };
1126
+ }
1127
+ return {
1128
+ messages: messages.map(
1129
+ (m2) => replacements.has(m2.id) ? { ...m2, text: replacements.get(m2.id) } : m2
1130
+ ),
1131
+ truncatedCount,
1132
+ savedTokens,
1133
+ candidatesFound
1134
+ };
1057
1135
  }
1058
1136
  var KEEP_LAST_ORPHANED = 2;
1059
1137
  function rangeKey(startRef, endRef) {
@@ -1106,7 +1184,7 @@ function compactEntry(entry) {
1106
1184
  if (!entry || typeof entry !== "object") return entry;
1107
1185
  const e = entry;
1108
1186
  if (typeof e.summary !== "string" || e.summary.length <= SUMMARY_STUB_CHARS) return entry;
1109
- return { ...e, summary: `${e.summary.slice(0, SUMMARY_STUB_CHARS - 1)}\u2026` };
1187
+ return { ...e, summary: `${clampPrefix(e.summary, SUMMARY_STUB_CHARS - 1)}\u2026` };
1110
1188
  }
1111
1189
  function serializeCompacted(obj, content, contentWasString) {
1112
1190
  let changed = false;
@@ -1205,6 +1283,7 @@ var COMPRESS_TOOL_NAME = "compress";
1205
1283
  var DECOMPRESS_TOOL_NAME = "decompress";
1206
1284
  var SEARCH_CONTEXT_TOOL_NAME = "search_context";
1207
1285
  var ACP_STATUS_TOOL_NAME = "acp_status";
1286
+ var ACP_CACHE_TOOL_NAME = "acp_cache";
1208
1287
  var ABSORB_TOOL_NAME = "absorb";
1209
1288
  var ACP_TEXT_OPEN = "<acp_compress>";
1210
1289
  var ACP_TEXT_CLOSE = "</acp_compress>";
@@ -1308,10 +1387,15 @@ var COMPRESS_TOOL_OPENAI = {
1308
1387
  }
1309
1388
  };
1310
1389
  var TEXT_PROMPT_SECTIONS = [
1311
- ["acpTags", `ACP TAGS
1390
+ [
1391
+ "acpTags",
1392
+ `ACP TAGS
1312
1393
 
1313
- Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata. NEVER echo these history tags. Use only the ref ID (e.g. m00005), never the XML wrapper.`],
1314
- ["textProtocol", `COMPRESSION PROTOCOL (TEXT)
1394
+ Each message in the conversation is annotated with a <acp tokens="2.1K" type="tool:bash">m00175</acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata. NEVER echo these history tags. Use only the ref ID (e.g. m00005), never the XML wrapper.`
1395
+ ],
1396
+ [
1397
+ "textProtocol",
1398
+ `COMPRESSION PROTOCOL (TEXT)
1315
1399
 
1316
1400
  You manage context by emitting a special trigger in your text output. When you decide a range of conversation is genuinely consumed and should be compressed into a summary, output EXACTLY this marker (the proxy intercepts and executes it; the marker is stripped from what the user sees):
1317
1401
 
@@ -1322,8 +1406,11 @@ Rules for the trigger:
1322
1406
  - JSON shape matches the compress tool: {"content":[{startId,endId,summary,topic?}]}. Batch multiple ranges in one trigger.
1323
1407
  - After emitting the marker, STOP your turn. Do not continue with other text \u2014 the proxy will execute the compression and return the result, then you continue fresh.
1324
1408
  - Do NOT wrap the marker in code fences, quotes, or commentary.
1325
- - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.`],
1326
- ["textTools", `ACP TOOLS (TEXT TRIGGERS)
1409
+ - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.`
1410
+ ],
1411
+ [
1412
+ "textTools",
1413
+ `ACP TOOLS (TEXT TRIGGERS)
1327
1414
 
1328
1415
  Since host tools cannot coexist with a declared tools field, ALL ACP tools use text triggers. Emit the marker; the proxy intercepts and executes it; the marker is stripped from what the user sees.
1329
1416
 
@@ -1343,13 +1430,19 @@ Since host tools cannot coexist with a declared tools field, ALL ACP tools use t
1343
1430
  Rules for ALL triggers:
1344
1431
  - Output on its own, NO surrounding prose. Just the raw marker.
1345
1432
  - After emitting, STOP your turn. The proxy executes and returns the result.
1346
- - Do NOT wrap in code fences, quotes, or commentary.`]
1433
+ - Do NOT wrap in code fences, quotes, or commentary.`
1434
+ ]
1347
1435
  ];
1348
1436
  var HYBRID_PROMPT_SECTIONS = [
1349
- ["acpTags", `ACP TAGS
1437
+ [
1438
+ "acpTags",
1439
+ `ACP TAGS
1350
1440
 
1351
- Each message in the conversation is annotated with a <acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata. NEVER echo these history tags. Use only the ref ID (e.g. m00005), never the XML wrapper.`],
1352
- ["textProtocol", `COMPRESSION PROTOCOL (TEXT)
1441
+ Each message in the conversation is annotated with a <acp> tag showing its reference ID, approximate token size, and content type. These tags are system metadata. NEVER echo these history tags. Use only the ref ID (e.g. m00005), never the XML wrapper.`
1442
+ ],
1443
+ [
1444
+ "textProtocol",
1445
+ `COMPRESSION PROTOCOL (TEXT)
1353
1446
 
1354
1447
  You manage context by emitting a special trigger in your text output. When you decide a range of conversation is genuinely consumed and should be compressed into a summary, output EXACTLY this marker (the proxy intercepts and executes it; the marker is stripped from what the user sees):
1355
1448
 
@@ -1360,8 +1453,11 @@ Rules for the trigger:
1360
1453
  - JSON shape: {"content":[{startId,endId,summary,topic?}]}. Batch multiple ranges in one trigger.
1361
1454
  - After emitting the marker, STOP your turn. Do not continue with other text \u2014 the proxy will execute the compression and return the result, then you continue fresh.
1362
1455
  - Do NOT wrap the marker in code fences, quotes, or commentary.
1363
- - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.`],
1364
- ["functionTools", `ACP TOOLS (FUNCTION CALLS)
1456
+ - NEVER compress on short conversations or when context is small (well below the window limit). Only compress when context is genuinely large.`
1457
+ ],
1458
+ [
1459
+ "functionTools",
1460
+ `ACP TOOLS (FUNCTION CALLS)
1365
1461
 
1366
1462
  The proxy also provides these as real function tools you can call directly (they appear in your tool list). Call them like any other function; the proxy executes them and returns the result, then you continue.
1367
1463
 
@@ -1369,7 +1465,8 @@ The proxy also provides these as real function tools you can call directly (they
1369
1465
  - search_context \u2014 search compressed block summaries by keyword. Arguments: {"query":"...","limit":5}.
1370
1466
  - decompress \u2014 restore compressed content for exact details. Arguments: {"blockId":"b5"} (optional "toFile":"/tmp/x.txt", "full":true).
1371
1467
 
1372
- Note: compress is ONLY available via the text marker above (it needs batch ranges + an immediate stop), NOT as a function tool.`]
1468
+ Note: compress is ONLY available via the text marker above (it needs batch ranges + an immediate stop), NOT as a function tool.`
1469
+ ]
1373
1470
  ];
1374
1471
  var DECOMPRESS_TOOL_OPENAI = {
1375
1472
  type: "function",
@@ -1422,6 +1519,18 @@ var ACP_STATUS_TOOL_OPENAI = {
1422
1519
  }
1423
1520
  }
1424
1521
  };
1522
+ var ACP_CACHE_TOOL_DESCRIPTION = "Prompt-cache reconciliation: grand ledger (total input/cached/output, overall hit rate) with every request's miss split into new content / compression re-pay / TTL expiry, plus per-fold economics (breakeven turns vs measured cadence). Read-only. Call when asked about cache hits, cache invalidation, or what compression costs.";
1523
+ var ACP_CACHE_TOOL_OPENAI = {
1524
+ type: "function",
1525
+ function: {
1526
+ name: ACP_CACHE_TOOL_NAME,
1527
+ description: ACP_CACHE_TOOL_DESCRIPTION,
1528
+ parameters: {
1529
+ type: "object",
1530
+ properties: {}
1531
+ }
1532
+ }
1533
+ };
1425
1534
  var DECOMPRESS_TOOL = {
1426
1535
  name: DECOMPRESS_TOOL_NAME,
1427
1536
  description: DECOMPRESS_TOOL_OPENAI.function.description,
@@ -1437,6 +1546,11 @@ var ACP_STATUS_TOOL = {
1437
1546
  description: ACP_STATUS_TOOL_OPENAI.function.description,
1438
1547
  input_schema: ACP_STATUS_TOOL_OPENAI.function.parameters
1439
1548
  };
1549
+ var ACP_CACHE_TOOL = {
1550
+ name: ACP_CACHE_TOOL_NAME,
1551
+ description: ACP_CACHE_TOOL_DESCRIPTION,
1552
+ input_schema: ACP_CACHE_TOOL_OPENAI.function.parameters
1553
+ };
1440
1554
  var COMPRESS_TOOL_RESPONSES = {
1441
1555
  type: "function",
1442
1556
  name: COMPRESS_TOOL_NAME,
@@ -1461,11 +1575,18 @@ var ACP_STATUS_TOOL_RESPONSES = {
1461
1575
  description: ACP_STATUS_TOOL_OPENAI.function.description,
1462
1576
  parameters: ACP_STATUS_TOOL_OPENAI.function.parameters
1463
1577
  };
1578
+ var ACP_CACHE_TOOL_RESPONSES = {
1579
+ type: "function",
1580
+ name: ACP_CACHE_TOOL_NAME,
1581
+ description: ACP_CACHE_TOOL_DESCRIPTION,
1582
+ parameters: ACP_CACHE_TOOL_OPENAI.function.parameters
1583
+ };
1464
1584
  var ACP_TOOL_NAMES = /* @__PURE__ */ new Set([
1465
1585
  COMPRESS_TOOL_NAME,
1466
1586
  DECOMPRESS_TOOL_NAME,
1467
1587
  SEARCH_CONTEXT_TOOL_NAME,
1468
- ACP_STATUS_TOOL_NAME
1588
+ ACP_STATUS_TOOL_NAME,
1589
+ ACP_CACHE_TOOL_NAME
1469
1590
  ]);
1470
1591
  var ALWAYS_PROTECTED_TOOLS = ["compress"];
1471
1592
  var NEVER_PRESERVE_RECENT_TOOLS = [
@@ -2084,8 +2205,9 @@ function mergeRangesToThreshold(ranges, minChars) {
2084
2205
  batchChars = 0;
2085
2206
  }
2086
2207
  }
2087
- if (batch.length > 0) {
2088
- result.push(mergeBatch(batch));
2208
+ if (batch.length > 0 && result.length > 0) {
2209
+ const prev = result[result.length - 1];
2210
+ result[result.length - 1] = mergeBatch([prev, ...batch]);
2089
2211
  }
2090
2212
  return result;
2091
2213
  }
@@ -2288,6 +2410,7 @@ function createCore(ports = {}) {
2288
2410
  state.nudge.lastPerMessageNudgeTokens = 0;
2289
2411
  state.nudge.lastNudgeShownTokens = 0;
2290
2412
  state.nudge.lastShownByTier = {};
2413
+ state.terminalStreak = 0;
2291
2414
  }
2292
2415
  return {
2293
2416
  state,
@@ -2317,7 +2440,9 @@ function createCore(ports = {}) {
2317
2440
  return {
2318
2441
  messages: result.messages,
2319
2442
  state: result.state,
2320
- nudge: result.effects.nudge
2443
+ nudge: result.effects.nudge,
2444
+ terminalEscape: result.effects.terminalEscape,
2445
+ truncationSkipped: result.effects.truncationSkipped
2321
2446
  };
2322
2447
  }
2323
2448
  function decompress(blockId, state) {
@@ -2510,18 +2635,49 @@ var emergencyTruncateNode = {
2510
2635
  name: "emergency-truncate",
2511
2636
  run(io, ctx) {
2512
2637
  const usage = ctx.config.modelContextLimit > 0 ? ctx.tokenCount / ctx.config.modelContextLimit : 0;
2513
- if (usage < ctx.config.truncate.threshold) return io;
2638
+ const prevStreak = io.state.terminalStreak ?? 0;
2639
+ if (usage < ctx.config.truncate.threshold) {
2640
+ return prevStreak > 0 ? { ...io, state: { ...io.state, terminalStreak: 0 } } : io;
2641
+ }
2514
2642
  const trunc = truncateLargeToolOutputs(
2515
2643
  io.messages,
2516
2644
  ctx.tokenCount,
2517
2645
  ctx.config,
2518
2646
  ctx.countTokens,
2519
- { protectRecentMessages: ctx.config.preserveRecentMessages }
2647
+ {
2648
+ protectRecentMessages: ctx.config.preserveRecentMessages,
2649
+ includeTextMessages: true
2650
+ }
2520
2651
  );
2652
+ const nudge = io.effects.nudge;
2653
+ const minBenefit = nudge?.breakdown.minPressureBenefit ?? 0;
2654
+ const maxPending = nudge?.breakdown.maxPending ?? 0;
2655
+ const noViableCompression = nudge !== void 0 && (minBenefit > 0 ? maxPending < minBenefit : maxPending <= 0);
2656
+ const stuck = noViableCompression && trunc.savedTokens <= 0;
2657
+ const streak = stuck ? prevStreak + 1 : 0;
2658
+ const escapeAfter = ctx.config.truncate.terminalEscapeAfter ?? 3;
2659
+ const triggered = stuck && escapeAfter > 0 && streak >= escapeAfter;
2660
+ const effects = {
2661
+ ...io.effects,
2662
+ truncatedCount: trunc.truncatedCount
2663
+ };
2664
+ if (trunc.savedTokens <= 0) {
2665
+ effects.truncationSkipped = trunc.candidatesFound === 0 ? `emergency-truncate ran at ${Math.round(usage * 100)}% usage but found no truncatable content (no oversized tool-result or text message outside the last ${ctx.config.preserveRecentMessages} messages)` : `emergency-truncate found ${trunc.candidatesFound} candidate(s) at ${Math.round(usage * 100)}% usage but none were large enough to save tokens`;
2666
+ }
2667
+ if (triggered) {
2668
+ effects.terminalEscape = {
2669
+ message: `Usage at ${Math.round(usage * 100)}% (${ctx.tokenCount}/${ctx.config.modelContextLimit} tokens) persists with nothing compressible above the benefit floor and no truncatable content: compression cannot reduce this context below the limit. Start a new session or use native compaction.`,
2670
+ usage,
2671
+ tokenCount: ctx.tokenCount,
2672
+ modelContextLimit: ctx.config.modelContextLimit,
2673
+ stuckEvents: streak
2674
+ };
2675
+ }
2521
2676
  return {
2522
2677
  ...io,
2523
2678
  messages: trunc.messages,
2524
- effects: { ...io.effects, truncatedCount: trunc.truncatedCount }
2679
+ state: { ...io.state, terminalStreak: streak },
2680
+ effects
2525
2681
  };
2526
2682
  }
2527
2683
  };
@@ -2614,7 +2770,8 @@ function applySingleRange(input) {
2614
2770
  for (const m2 of input.messages) {
2615
2771
  if (!m2.id) continue;
2616
2772
  if (m2.contentType === "reasoning") reasoningIds.add(m2.id);
2617
- if (m2.role === "assistant" && m2.contentType === "tool-call") callIds.add(m2.id);
2773
+ if (m2.role === "assistant" && m2.contentType === "tool-call")
2774
+ callIds.add(m2.id);
2618
2775
  }
2619
2776
  const withdrawIds = /* @__PURE__ */ new Set();
2620
2777
  let splitTurnCount = 0;
@@ -2630,17 +2787,42 @@ function applySingleRange(input) {
2630
2787
  splitTurnCount++;
2631
2788
  for (const id of group) withdrawIds.add(id);
2632
2789
  }
2790
+ const resultIdByCallId = /* @__PURE__ */ new Map();
2791
+ for (const m2 of input.messages) {
2792
+ if (!m2.id || m2.contentType !== "tool-result") continue;
2793
+ if (typeof m2.toolCallId !== "string") continue;
2794
+ if (!resultIdByCallId.has(m2.toolCallId)) {
2795
+ resultIdByCallId.set(m2.toolCallId, m2.id);
2796
+ }
2797
+ }
2798
+ let splitPairCount = 0;
2799
+ for (const m2 of input.messages) {
2800
+ if (!m2.id || m2.contentType !== "tool-call") continue;
2801
+ if (typeof m2.toolCallId !== "string") continue;
2802
+ const resultId = resultIdByCallId.get(m2.toolCallId);
2803
+ if (resultId === void 0) continue;
2804
+ if (effectiveMessageIds.has(m2.id) === effectiveMessageIds.has(resultId)) {
2805
+ continue;
2806
+ }
2807
+ withdrawIds.add(m2.id);
2808
+ withdrawIds.add(resultId);
2809
+ splitPairCount++;
2810
+ }
2633
2811
  if (withdrawIds.size > 0) {
2634
2812
  for (const id of withdrawIds) effectiveMessageIds.delete(id);
2635
2813
  const beforeWithdraw = filteredIds.length;
2636
2814
  filteredIds = filteredIds.filter((id) => !withdrawIds.has(id));
2815
+ const splitDesc = [
2816
+ splitTurnCount > 0 ? `${splitTurnCount} turn(s)` : null,
2817
+ splitPairCount > 0 ? `${splitPairCount} tool call/result pair(s)` : null
2818
+ ].filter((part) => part !== null).join(" and ");
2637
2819
  if (filteredIds.length === 0 && consumedBlockIds.length === 0) {
2638
2820
  throw new Error(
2639
- `Range would split ${splitTurnCount} turn(s) at the protected-zone boundary: a visible tool-call must keep its reasoning run (strict-echo providers reject a rebuilt request that lost it). Shrink the range to end before the turn starts, or wait until the whole turn ages out of the protected zone.`
2821
+ `Range would split ${splitDesc} at the protected-zone boundary: a visible tool-call must keep its reasoning run and its results (strict providers reject a rebuilt request that lost either). Shrink the range to end before the turn starts, or wait until the whole turn ages out of the protected zone.`
2640
2822
  );
2641
2823
  }
2642
2824
  warnings.push(
2643
- `Withdrawn ${beforeWithdraw - filteredIds.length} message(s) from compression range to keep ${splitTurnCount} turn(s) intact (visible tool-call would lose its reasoning run).`
2825
+ `Withdrawn ${beforeWithdraw - filteredIds.length} message(s) from compression range to keep ${splitDesc} intact (visible tool-call would lose its reasoning run or its results).`
2644
2826
  );
2645
2827
  }
2646
2828
  }
@@ -2860,6 +3042,7 @@ function decideNudge(input) {
2860
3042
  const t1Eff = tiers[1]?.pending ?? 0;
2861
3043
  const t2Pen = tiers[2]?.pending ?? 0;
2862
3044
  const t3Pen = tiers[3]?.pending ?? 0;
3045
+ const maxPending = Math.max(0, t1Eff, t2Pen, t3Pen);
2863
3046
  const firstSightMassReady = state.nudge.lastNudgeShownTokens === 0 && baseline === 0 && usage >= config.nudge.minContextLimitPct && Math.max(t1Eff, t2Pen, t3Pen) >= nudgeGrowthTokens;
2864
3047
  const growthReady = firstSightMassReady || growthSinceReference >= growthFloor;
2865
3048
  const t2Count = tiers[2]?.targetBlocks.length ?? 0;
@@ -2932,10 +3115,6 @@ function decideNudge(input) {
2932
3115
  (t) => ((tiers[t]?.pending ?? 0) >= nudgeGrowthTokens || countReady(t)) && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor
2933
3116
  ).map((t) => `T${t} (cadence)`);
2934
3117
  const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
2935
- const maxPending = Math.max(
2936
- 0,
2937
- ...Object.values(tiers).map((t) => t.pending)
2938
- );
2939
3118
  const pendingShort = maxPending < nudgeGrowthTokens;
2940
3119
  const growthShort = growthSinceReference < growthFloor;
2941
3120
  const parts = [];
@@ -2979,7 +3158,8 @@ function decideNudge(input) {
2979
3158
  minPressureBenefit,
2980
3159
  pendingT1: tiers[1].pending,
2981
3160
  pendingT2: tiers[2].pending,
2982
- pendingT3: tiers[3].pending
3161
+ pendingT3: tiers[3].pending,
3162
+ maxPending
2983
3163
  },
2984
3164
  contextBreakdown: ctxBreakdown
2985
3165
  };
@@ -3019,6 +3199,7 @@ function cloneState(state) {
3019
3199
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
3020
3200
  stats: { ...state.stats },
3021
3201
  absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
3202
+ terminalStreak: state.terminalStreak,
3022
3203
  nextBlockId: state.nextBlockId,
3023
3204
  nextRunId: state.nextRunId
3024
3205
  };
@@ -3488,7 +3669,7 @@ function renderUncompressedRanges(visible) {
3488
3669
  lines.push(" (no uncompressed messages)");
3489
3670
  return lines.join("\n");
3490
3671
  }
3491
- const refNum2 = (ref) => {
3672
+ const refNum22 = (ref) => {
3492
3673
  const m2 = ref.match(/\d+/);
3493
3674
  return m2 ? parseInt(m2[0], 10) : 0;
3494
3675
  };
@@ -3505,16 +3686,16 @@ function renderUncompressedRanges(visible) {
3505
3686
  };
3506
3687
  const merged = [];
3507
3688
  for (const m2 of visible) {
3508
- const num2 = refNum2(m2.ref);
3689
+ const num3 = refNum22(m2.ref);
3509
3690
  const last = merged[merged.length - 1];
3510
- if (last && num2 === last.startNum + last.count) {
3691
+ if (last && num3 === last.startNum + last.count) {
3511
3692
  last.endRef = m2.ref;
3512
3693
  last.count += 1;
3513
3694
  last.tokens += m2.tokens;
3514
3695
  last.toolTokens.set(m2.tool, (last.toolTokens.get(m2.tool) ?? 0) + m2.tokens);
3515
3696
  } else {
3516
3697
  const toolTokens = /* @__PURE__ */ new Map([[m2.tool, m2.tokens]]);
3517
- merged.push({ startRef: m2.ref, endRef: m2.ref, startNum: num2, count: 1, tokens: m2.tokens, toolTokens });
3698
+ merged.push({ startRef: m2.ref, endRef: m2.ref, startNum: num3, count: 1, tokens: m2.tokens, toolTokens });
3518
3699
  }
3519
3700
  }
3520
3701
  for (const r of merged.slice(0, 30)) {
@@ -3587,6 +3768,276 @@ function renderCompressedDrilldown(blocks, state, sort, limit, countTokens, meta
3587
3768
  }
3588
3769
  return lines.join("\n");
3589
3770
  }
3771
+ function decomposeSample(prev, cur, pendingFolds) {
3772
+ const missed = Math.max(0, cur.input - cur.cached);
3773
+ const growth = prev ? Math.max(0, cur.input - prev.input) : 0;
3774
+ const newContent = Math.min(missed, growth);
3775
+ const remainder = missed - newContent;
3776
+ let compRepay = 0;
3777
+ let foldIndex = null;
3778
+ if (remainder > 0 && pendingFolds.length > 0) {
3779
+ let bestX = Infinity;
3780
+ let owner = -1;
3781
+ for (let i = 0; i < pendingFolds.length; i++) {
3782
+ const f = pendingFolds[i];
3783
+ const x2 = f.firstFoldStartTokens ?? f.viewAfter ?? 0;
3784
+ if (x2 < bestX) {
3785
+ bestX = x2;
3786
+ owner = i;
3787
+ }
3788
+ }
3789
+ const structuralExcess = Math.max(0, cur.input - bestX - newContent);
3790
+ compRepay = Math.min(remainder, structuralExcess);
3791
+ if (compRepay > 0) foldIndex = owner;
3792
+ }
3793
+ return {
3794
+ missed,
3795
+ newContent,
3796
+ compRepay,
3797
+ ttlRepay: remainder - compRepay,
3798
+ foldIndex
3799
+ };
3800
+ }
3801
+ function hitPct(input, cached) {
3802
+ return input > 0 ? cached / input * 100 : 0;
3803
+ }
3804
+ function computeFoldEconomics(f, price) {
3805
+ const w2 = price?.w ?? 1;
3806
+ const r = price?.r ?? 0.1;
3807
+ const q2 = price?.q ?? 4;
3808
+ const netTokenDelta = f.T + f.sigma - f.S;
3809
+ const savingPerTurn = f.S - f.sigma;
3810
+ const oneTimeCostUnits = (w2 - r) * f.T + q2 * f.sigma - r * f.S;
3811
+ const perTurnSavingUnits = savingPerTurn * r;
3812
+ const breakevenTurns = perTurnSavingUnits > 0 ? Math.max(0, oneTimeCostUnits) / perTurnSavingUnits : null;
3813
+ const paidBack = f.turnsToNextFold !== null && breakevenTurns !== null ? f.turnsToNextFold >= breakevenTurns : null;
3814
+ return {
3815
+ seq: f.seq,
3816
+ at: f.at,
3817
+ S: f.S,
3818
+ sigma: f.sigma,
3819
+ Vprime: f.Vprime ?? null,
3820
+ hPct: f.hPct,
3821
+ T: f.T,
3822
+ requestsAfter: f.requestsAfter,
3823
+ savedSoFar: Math.max(0, savingPerTurn) * f.requestsAfter,
3824
+ turnsToNextFold: f.turnsToNextFold,
3825
+ netTokenDelta,
3826
+ oneTimeCostUnits: round1(oneTimeCostUnits),
3827
+ perTurnSavingUnits: round1(perTurnSavingUnits),
3828
+ breakevenTurns,
3829
+ paidBack
3830
+ };
3831
+ }
3832
+ function summarizeFoldEconomics(foldEcon) {
3833
+ const econ = {
3834
+ folds: foldEcon.length,
3835
+ grossSaved: foldEcon.reduce((n, e) => n + e.savedSoFar, 0),
3836
+ repayCost: foldEcon.reduce((n, e) => n + e.T, 0),
3837
+ summaryCost: foldEcon.reduce((n, e) => n + e.sigma, 0),
3838
+ netTokens: 0,
3839
+ paidBackCount: 0,
3840
+ notPaidBackCount: 0,
3841
+ unobservedCount: 0
3842
+ };
3843
+ econ.netTokens = econ.grossSaved - econ.repayCost - econ.summaryCost;
3844
+ for (const e of foldEcon) {
3845
+ if (e.paidBack === true) econ.paidBackCount++;
3846
+ else if (e.paidBack === false) econ.notPaidBackCount++;
3847
+ else econ.unobservedCount++;
3848
+ }
3849
+ return econ;
3850
+ }
3851
+ function buildCacheReport(samplesIn, foldsIn, opts = {}) {
3852
+ const samples = [...samplesIn].sort((a, b2) => a.at - b2.at);
3853
+ const folds = [...foldsIn].sort(
3854
+ (a, b2) => a.at - b2.at || a.tokensCompressed - b2.tokensCompressed
3855
+ );
3856
+ const maxLines = opts.maxLines && opts.maxLines > 0 ? opts.maxLines : 512;
3857
+ const price = opts.priceProfile;
3858
+ const w2 = price?.w ?? 1;
3859
+ const r = price?.r ?? 0.1;
3860
+ const q2 = price?.q ?? 4;
3861
+ const decs = [];
3862
+ for (let i = 0; i < samples.length; i++) {
3863
+ const s = samples[i];
3864
+ const prev = i > 0 ? samples[i - 1] : null;
3865
+ const prevAt = prev ? prev.at : Number.NEGATIVE_INFINITY;
3866
+ const pending = [];
3867
+ let pendingBase = 0;
3868
+ for (let j2 = 0; j2 < folds.length; j2++) {
3869
+ const fj = folds[j2];
3870
+ if (fj.at <= prevAt) continue;
3871
+ if (fj.at > s.at) break;
3872
+ if (pending.length === 0) pendingBase = j2;
3873
+ pending.push(fj);
3874
+ }
3875
+ const d2 = decomposeSample(prev, s, pending);
3876
+ decs.push({
3877
+ ...d2,
3878
+ foldSeq: d2.foldIndex !== null ? pendingBase + d2.foldIndex + 1 : null
3879
+ });
3880
+ }
3881
+ const totals = {
3882
+ requests: samples.length,
3883
+ input: 0,
3884
+ cached: 0,
3885
+ output: 0,
3886
+ hitPct: 0,
3887
+ newContent: 0,
3888
+ compRepay: 0,
3889
+ ttlRepay: 0,
3890
+ residual: 0,
3891
+ balanced: true
3892
+ };
3893
+ for (let i = 0; i < samples.length; i++) {
3894
+ const s = samples[i];
3895
+ const d2 = decs[i];
3896
+ totals.input += s.input;
3897
+ totals.cached += s.cached;
3898
+ totals.output += s.output ?? 0;
3899
+ totals.newContent += d2.newContent;
3900
+ totals.compRepay += d2.compRepay;
3901
+ totals.ttlRepay += d2.ttlRepay;
3902
+ }
3903
+ totals.hitPct = round1(hitPct(totals.input, totals.cached));
3904
+ totals.residual = totals.input - totals.cached - (totals.newContent + totals.compRepay + totals.ttlRepay);
3905
+ totals.balanced = totals.residual === 0;
3906
+ const lines = samples.map((s, i) => {
3907
+ const { foldIndex, ...rest } = decs[i];
3908
+ return {
3909
+ seq: i + 1,
3910
+ at: s.at,
3911
+ input: s.input,
3912
+ cached: s.cached,
3913
+ output: s.output ?? 0,
3914
+ hitPct: round1(hitPct(s.input, s.cached)),
3915
+ ...rest
3916
+ };
3917
+ });
3918
+ const linesOmitted = Math.max(0, lines.length - maxLines);
3919
+ const visibleLines = lines.slice(linesOmitted);
3920
+ const foldEcon = folds.map((f, j2) => {
3921
+ const seq = j2 + 1;
3922
+ const S = f.tokensCompressed;
3923
+ const sigma = f.summaryTokens ?? 0;
3924
+ const T2 = decs.reduce(
3925
+ (n, d2) => d2.foldSeq === seq ? n + d2.compRepay : n,
3926
+ 0
3927
+ );
3928
+ let hPct = null;
3929
+ let requestsAfter = 0;
3930
+ for (const s of samples) {
3931
+ if (s.at <= f.at) continue;
3932
+ requestsAfter++;
3933
+ if (hPct === null) hPct = round1(hitPct(s.input, s.cached));
3934
+ }
3935
+ const nextAt = j2 + 1 < folds.length ? folds[j2 + 1]?.at : void 0;
3936
+ const turnsToNextFold = nextAt !== void 0 ? samples.filter((s) => s.at > f.at && s.at <= nextAt).length : null;
3937
+ return computeFoldEconomics(
3938
+ {
3939
+ seq,
3940
+ at: f.at,
3941
+ S,
3942
+ sigma,
3943
+ Vprime: f.viewAfter,
3944
+ hPct,
3945
+ T: T2,
3946
+ requestsAfter,
3947
+ turnsToNextFold
3948
+ },
3949
+ { w: w2, r, q: q2 }
3950
+ );
3951
+ });
3952
+ const econ = summarizeFoldEconomics(foldEcon);
3953
+ return {
3954
+ generatedAt: Date.now(),
3955
+ profile: { w: w2, r, q: q2 },
3956
+ totals,
3957
+ economics: econ,
3958
+ folds: foldEcon,
3959
+ lines: visibleLines,
3960
+ linesOmitted
3961
+ };
3962
+ }
3963
+ function round1(n) {
3964
+ return Math.round(n * 10) / 10;
3965
+ }
3966
+ function fmtTok(n) {
3967
+ const v2 = Math.round(n);
3968
+ return v2 >= 1e6 ? `${(v2 / 1e6).toFixed(2)}M` : v2 >= 1e4 ? `${(v2 / 1e3).toFixed(1)}K` : String(v2);
3969
+ }
3970
+ function fmtTime(at) {
3971
+ const d2 = new Date(at);
3972
+ const p = (x2) => String(x2).padStart(2, "0");
3973
+ return `${p(d2.getHours())}:${p(d2.getMinutes())}:${p(d2.getSeconds())}`;
3974
+ }
3975
+ function formatCacheReport(report, sessionLabel) {
3976
+ const t = report.totals;
3977
+ const out = [];
3978
+ out.push(
3979
+ `ACP CACHE REPORT${sessionLabel ? ` (${sessionLabel})` : ""} \u2014 ${t.requests} requests`
3980
+ );
3981
+ out.push("");
3982
+ out.push("GRAND LEDGER");
3983
+ out.push(` total input ${fmtTok(t.input)} tok`);
3984
+ out.push(
3985
+ ` total cached ${fmtTok(t.cached)} tok (hit ${t.hitPct.toFixed(1)}%)`
3986
+ );
3987
+ out.push(` total output ${fmtTok(t.output)} tok`);
3988
+ out.push(
3989
+ ` miss breakdown (input \u2212 cached = ${fmtTok(Math.max(0, t.input - t.cached))} tok):`
3990
+ );
3991
+ out.push(
3992
+ ` new content ${fmtTok(t.newContent)} tok (fresh append \u2014 not an invalidation)`
3993
+ );
3994
+ out.push(` compress re-pay ${fmtTok(t.compRepay)} tok (caused by folds)`);
3995
+ out.push(
3996
+ ` ttl/other ${fmtTok(t.ttlRepay)} tok (stable-prefix misses: TTL expiry / eviction)`
3997
+ );
3998
+ out.push(
3999
+ ` identity check ${t.balanced ? "OK" : "BROKEN"} \u2014 ${fmtTok(t.input)} = ${fmtTok(t.cached)} + ${fmtTok(t.newContent)} + ${fmtTok(t.compRepay)} + ${fmtTok(t.ttlRepay)} (residual ${t.residual})`
4000
+ );
4001
+ const e = report.economics;
4002
+ if (e.folds > 0) {
4003
+ const p = report.profile;
4004
+ out.push("");
4005
+ out.push(`FOLD ECONOMICS (${e.folds} folds @ w=${p.w} r=${p.r} q=${p.q})`);
4006
+ out.push(
4007
+ ` gross saved ${fmtTok(e.grossSaved)} tok \xB7 repay cost ${fmtTok(e.repayCost)} tok \xB7 summary cost ${fmtTok(e.summaryCost)} tok \u2192 net ${e.netTokens >= 0 ? "+" : ""}${fmtTok(e.netTokens)} tok`
4008
+ );
4009
+ out.push(
4010
+ ` verdict: ${e.paidBackCount} paid back, ${e.notPaidBackCount} not paid back, ${e.unobservedCount} unobserved`
4011
+ );
4012
+ for (const f of report.folds) {
4013
+ const nstar = f.breakevenTurns === null ? "n/a" : f.breakevenTurns.toFixed(1);
4014
+ const k = f.turnsToNextFold === null ? "\u2014" : String(f.turnsToNextFold);
4015
+ const verdict = f.paidBack === null ? "?" : f.paidBack ? "PAID BACK" : "NOT PAID BACK";
4016
+ const h = f.hPct === null ? "n/a" : `${f.hPct.toFixed(1)}%`;
4017
+ out.push(
4018
+ ` #${f.seq} ${fmtTime(f.at)} S=${fmtTok(f.S)} \u03C3=${fmtTok(f.sigma)} h=${h} T=${fmtTok(f.T)} \u0394C\u2081=${fmtTok(f.oneTimeCostUnits)} \u0394s=${fmtTok(f.perTurnSavingUnits)}/turn n*=${nstar} k=${k} \u2192 ${verdict}`
4019
+ );
4020
+ }
4021
+ }
4022
+ if (report.lines.length > 0) {
4023
+ out.push("");
4024
+ if (report.linesOmitted > 0)
4025
+ out.push(
4026
+ `LINE ITEMS (last ${report.lines.length} of ${report.lines.length + report.linesOmitted}):`
4027
+ );
4028
+ else out.push("LINE ITEMS:");
4029
+ out.push(
4030
+ " # time input cached hit% new comp ttl fold"
4031
+ );
4032
+ for (const l3 of report.lines) {
4033
+ const fold = l3.foldSeq !== null ? `#${l3.foldSeq}` : "";
4034
+ out.push(
4035
+ ` ${String(l3.seq).padStart(4)} ${fmtTime(l3.at)} ${fmtTok(l3.input).padStart(7)} ${fmtTok(l3.cached).padStart(7)} ${l3.hitPct.toFixed(1).padStart(5)}% ${fmtTok(l3.newContent).padStart(6)} ${fmtTok(l3.compRepay).padStart(6)} ${fmtTok(l3.ttlRepay).padStart(6)} ${fold}`
4036
+ );
4037
+ }
4038
+ }
4039
+ return out.join("\n");
4040
+ }
3590
4041
  function renderMessage2(m2) {
3591
4042
  const parts = [];
3592
4043
  switch (m2.contentType) {
@@ -3672,7 +4123,7 @@ function parseCompressArgs(input, opts) {
3672
4123
  return parseObjectValue(input, callId, diag);
3673
4124
  }
3674
4125
  function parseStringInput(raw, callId, diag) {
3675
- diag.rawPrefix = raw.slice(0, 800);
4126
+ diag.rawPrefix = clampPrefix(raw, 800);
3676
4127
  diag.length = raw.length;
3677
4128
  const cleaned = stripFence(raw.trim());
3678
4129
  const first = parseStringCore(cleaned, callId, diag);
@@ -3856,7 +4307,7 @@ function deriveTopicFromSummary(summary) {
3856
4307
  const source = heading !== null ? heading[1] : summary.split("\n").find((l3) => l3.trim().length > 0) ?? "";
3857
4308
  const text = source.trim().replace(/^[\u2022\-*]\s+/, "");
3858
4309
  if (text.length === 0) return void 0;
3859
- return text.length > 60 ? text.slice(0, 60).trimEnd() : text;
4310
+ return text.length > 60 ? clampPrefix(text, 60).trimEnd() : text;
3860
4311
  }
3861
4312
  function splitLineEntries(content) {
3862
4313
  const parts = content.split(/\n(?=\s*(?:["']\s*,\s*)?["']?\s*[mb]\d{1,7}\s*(?:[-\u2013\u2014\u2026~]|\.\.\.|to)\s*[mb]\d{1,7}\b)/i).map((p) => p.trim().replace(/^(?:["']\s*,?\s*)+/, "").replace(/["']\s*$/, "")).filter((p) => p.length > 0);
@@ -4369,7 +4820,7 @@ function messageDocs(msgs) {
4369
4820
  kind: "message",
4370
4821
  ref: m2.ref,
4371
4822
  text: m2.text,
4372
- title: `${m2.role}: ${m2.text.slice(0, 60)}`,
4823
+ title: `${m2.role}: ${clampPrefix(m2.text, 60)}`,
4373
4824
  role: m2.role,
4374
4825
  blockId: m2.blockId,
4375
4826
  tier: m2.tier,
@@ -4431,7 +4882,7 @@ function searchBlocks(docs, query, options = {}) {
4431
4882
  function makePreview(text, query, len) {
4432
4883
  if (!text) return "";
4433
4884
  const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 1);
4434
- if (terms.length === 0) return text.slice(0, len);
4885
+ if (terms.length === 0) return clampPrefix(text, len);
4435
4886
  const lower = text.toLowerCase();
4436
4887
  let hitIdx = -1;
4437
4888
  for (const term of terms) {
@@ -4441,13 +4892,13 @@ function makePreview(text, query, len) {
4441
4892
  break;
4442
4893
  }
4443
4894
  }
4444
- if (hitIdx < 0) return text.slice(0, len);
4895
+ if (hitIdx < 0) return clampPrefix(text, len);
4445
4896
  const half = Math.max(0, Math.floor(len / 2) - 10);
4446
4897
  const start = Math.max(0, hitIdx - half);
4447
4898
  const end = Math.min(text.length, start + len);
4448
4899
  const prefix = start > 0 ? "\u2026" : "";
4449
4900
  const suffix = end < text.length ? "\u2026" : "";
4450
- return prefix + text.slice(start, end).trim() + suffix;
4901
+ return prefix + clampWindow(text, start, end).trim() + suffix;
4451
4902
  }
4452
4903
 
4453
4904
  // src/log.ts
@@ -5695,7 +6146,7 @@ When a background delegate finishes, an automated completion notification is inj
5695
6146
  `;
5696
6147
 
5697
6148
  // src/surface.ts
5698
- var TOOL_NAMES = /* @__PURE__ */ new Set(["compress", "decompress", "search_context", "acp_status"]);
6149
+ var TOOL_NAMES = /* @__PURE__ */ new Set(["compress", "decompress", "search_context", "acp_status", "acp_cache"]);
5699
6150
  var NUDGE_KEYS = /* @__PURE__ */ new Set(["efficiencyNote", "emergencyHeader", "t2Guidance", "t3Guidance"]);
5700
6151
  function normalizeGuidelines(g2) {
5701
6152
  return Array.isArray(g2) ? g2 : [g2];
@@ -11672,7 +12123,7 @@ function resolveActivePack(adapter, cwd, provider, modelId, resolver) {
11672
12123
  }
11673
12124
  var HOST_VERSION = (() => {
11674
12125
  try {
11675
- return "0.1.71";
12126
+ return "0.1.72";
11676
12127
  } catch {
11677
12128
  return "dev";
11678
12129
  }
@@ -13720,6 +14171,71 @@ async function handleStatus(args, runtime, ctx) {
13720
14171
  ${extra.join("\n")}` : base;
13721
14172
  }
13722
14173
 
14174
+ // src/cache-tool.ts
14175
+ var CacheParams = typebox_exports.Object({});
14176
+ var num = (v2) => typeof v2 === "number" && Number.isFinite(v2) && v2 > 0 ? v2 : 0;
14177
+ var refNum2 = (ref) => Number(ref.replace(/\D/g, "")) || 0;
14178
+ function cacheSamples(entries) {
14179
+ const out = [];
14180
+ for (const e of entries) {
14181
+ if (e.type !== "message") continue;
14182
+ const m2 = e.message;
14183
+ if (m2?.role !== "assistant" || !m2.usage) continue;
14184
+ const read = num(m2.usage.cacheRead);
14185
+ const write = num(m2.usage.cacheWrite);
14186
+ const fresh = num(m2.usage.input);
14187
+ if (fresh + read + write <= 0) continue;
14188
+ out.push({ at: Date.parse(e.timestamp) || 0, input: fresh + read + write, cached: read, output: num(m2.usage.output) });
14189
+ }
14190
+ return out;
14191
+ }
14192
+ function foldEvents(state) {
14193
+ const byRef = state.messageRefs?.byRef ?? {};
14194
+ const snap = state.tokenSnapshot ?? {};
14195
+ const prefixBefore = (ref) => {
14196
+ if (!ref || refNum2(ref) === 0) return void 0;
14197
+ let n = 0;
14198
+ for (const key of Object.keys(byRef)) {
14199
+ if (refNum2(key) < refNum2(ref)) n += snap[key] ?? 0;
14200
+ }
14201
+ return n;
14202
+ };
14203
+ return [...state.blocks].sort((a, b2) => a.createdAt - b2.createdAt).map((b2) => ({
14204
+ at: b2.createdAt,
14205
+ tokensCompressed: b2.compressedTokens,
14206
+ summaryTokens: defaultCountTokens(b2.summary || ""),
14207
+ firstFoldStartTokens: prefixBefore(b2.startRef)
14208
+ }));
14209
+ }
14210
+ async function cacheReportText(runtime, ctx) {
14211
+ const { state, entries } = await runtime.stateFor(ctx);
14212
+ const report = buildCacheReport(cacheSamples(entries), foldEvents(state));
14213
+ return formatCacheReport(report, ctx.sessionManager.getSessionId());
14214
+ }
14215
+ function makeCacheTool(runtime, overrides) {
14216
+ return applyToolPromptOverrides({
14217
+ name: "acp_cache",
14218
+ label: "ACP Cache Report",
14219
+ description: "Prompt-cache reconciliation: grand ledger (total input/cached/output, session hit rate) with every request's miss split into new content / compression re-pay / TTL expiry, plus per-fold economics (one-time cost, breakeven turns vs measured cadence). Read-only.",
14220
+ promptSnippet: "acp_cache({})",
14221
+ promptGuidelines: [
14222
+ "Call when asked about cache hits, cache invalidation, or what compression costs.",
14223
+ "Read-only: reports numbers, never mutates context."
14224
+ ],
14225
+ parameters: CacheParams,
14226
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
14227
+ if (runtime.refused) return { details: void 0, content: [{ type: "text", text: runtime.refusalMessage ?? UNSUPPORTED_HOST_MESSAGE }] };
14228
+ try {
14229
+ const text = await cacheReportText(runtime, ctx);
14230
+ return { details: void 0, content: [{ type: "text", text }] };
14231
+ } catch (e) {
14232
+ logThrow("cache", e, { sid: ctx.sessionManager.getSessionId() });
14233
+ throw e;
14234
+ }
14235
+ }
14236
+ }, overrides);
14237
+ }
14238
+
13723
14239
  // src/commands.ts
13724
14240
  import * as path12 from "path";
13725
14241
 
@@ -13847,7 +14363,7 @@ function parseExportArgs(args) {
13847
14363
  function topicFallback(summary) {
13848
14364
  const first = summary.split(/[.\n]/)[0] ?? "";
13849
14365
  const t = first.trim().replace(/^["'`]+/, "").trim();
13850
- return t.length <= 30 ? t : `${t.slice(0, 30).trimEnd()}\u2026`;
14366
+ return t.length <= 30 ? t : `${clampPrefix(t, 30).trimEnd()}\u2026`;
13851
14367
  }
13852
14368
  function formatCompactTokens2(count) {
13853
14369
  if (count < 1e3) return count.toString();
@@ -13856,7 +14372,7 @@ function formatCompactTokens2(count) {
13856
14372
  if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
13857
14373
  return `${Math.round(count / 1e6)}M`;
13858
14374
  }
13859
- function num(v2) {
14375
+ function num2(v2) {
13860
14376
  return typeof v2 === "number" && Number.isFinite(v2) && v2 > 0 ? v2 : 0;
13861
14377
  }
13862
14378
  function cacheHitStats(usages) {
@@ -13865,9 +14381,9 @@ function cacheHitStats(usages) {
13865
14381
  let requests = 0;
13866
14382
  let last;
13867
14383
  for (const u of usages) {
13868
- const read = num(u?.cacheRead);
13869
- const write = num(u?.cacheWrite);
13870
- const input = num(u?.input);
14384
+ const read = num2(u?.cacheRead);
14385
+ const write = num2(u?.cacheWrite);
14386
+ const input = num2(u?.input);
13871
14387
  if (read + write <= 0) continue;
13872
14388
  const total = read + write + input;
13873
14389
  if (total <= 0) continue;
@@ -18195,6 +18711,26 @@ function makeCommands(runtime, pi) {
18195
18711
  handler: statusHandler
18196
18712
  }
18197
18713
  },
18714
+ {
18715
+ name: "acp-cache",
18716
+ options: {
18717
+ description: "Prompt-cache reconciliation: grand ledger (input/cached/hit rate) with every request's miss split into new content / compression re-pay / TTL expiry, plus per-fold economics.",
18718
+ handler: async (_args, ctx) => {
18719
+ let text;
18720
+ try {
18721
+ text = await cacheReportText(runtime, ctx);
18722
+ } catch (e) {
18723
+ ctx.ui.notify(e instanceof Error ? e.message : String(e), "error");
18724
+ return;
18725
+ }
18726
+ if (typeof pi?.sendMessage === "function") {
18727
+ pi.sendMessage({ customType: ACP_STATUS_CUSTOM_TYPE, content: text, display: true });
18728
+ return;
18729
+ }
18730
+ ctx.ui.notify(text);
18731
+ }
18732
+ }
18733
+ },
18198
18734
  {
18199
18735
  name: "acp-export",
18200
18736
  options: {
@@ -18332,7 +18868,7 @@ async function statusReport(runtime, ctx) {
18332
18868
  const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
18333
18869
  const viewSentTokens = adjustedTokenCount(runtime.core, coreMessages, state, config, sentTokens, imageTokens, systemPromptTokens);
18334
18870
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: anchorStale ? viewSentTokens : Math.max(viewSentTokens, realUsage?.tokens ?? 0) });
18335
- const versionStr = "0.1.71" ? `billion-context-pi@${"0.1.71"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
18871
+ const versionStr = "0.1.72" ? `billion-context-pi@${"0.1.72"} \xB7 pack: ${resolveSurfaceMeta(runtime.adapter, ctx?.cwd ?? process.cwd(), ctx?.model?.provider, ctx?.model?.id).pack}` : void 0;
18336
18872
  let text = buildStatusPanel({
18337
18873
  version: versionStr,
18338
18874
  tokenCount: sessionTokens,
@@ -18944,7 +19480,7 @@ async function autoInstallLatest(latest, extDirOverride) {
18944
19480
  "--no-fund",
18945
19481
  "--no-save"
18946
19482
  ];
18947
- const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.71";
19483
+ const prevVersion = (await readPackageJson(join15(extDir, "package.json")))?.version ?? "0.1.72";
18948
19484
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
18949
19485
  if (code !== 0) {
18950
19486
  logWarn("update", {
@@ -18962,7 +19498,7 @@ async function autoInstallLatest(latest, extDirOverride) {
18962
19498
  }
18963
19499
  const verify = await verifyInstall(npmDir, latest);
18964
19500
  if (!verify.ok) {
18965
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.71";
19501
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.72";
18966
19502
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
18967
19503
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
18968
19504
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -19037,7 +19573,7 @@ async function checkForUpdate(autoUpdate, notify) {
19037
19573
  }
19038
19574
  const latest = await fetchLatestVersion(tag);
19039
19575
  if (!latest) return;
19040
- const current = runtimeVersion ?? "0.1.71";
19576
+ const current = runtimeVersion ?? "0.1.72";
19041
19577
  const hasUpdate = isVersionNewer(latest, current);
19042
19578
  debug.event("update-check", {
19043
19579
  current,
@@ -19115,6 +19651,13 @@ var PROXY_STAND_DOWN_MESSAGE = [
19115
19651
  "If your traffic reaches the proxy via HTTPS_PROXY instead (no /bili/ prefix in the URL), export BILLION_CONTEXT_PROXY=1 before starting pi.",
19116
19652
  "Docs: https://github.com/ranxianglei/billion-context"
19117
19653
  ].join("\n");
19654
+ function nativeStandDownMessage(host) {
19655
+ return [
19656
+ `[billion-context-pi] billion-context native host detected (BILLION_CONTEXT_NATIVE=${host}) \u2014 the in-process native plugin proxies model API traffic and owns compression.`,
19657
+ "ACP client-side compression has been disabled for this session to avoid double compression; nothing else to configure.",
19658
+ "Docs: https://github.com/ranxianglei/billion-context"
19659
+ ].join("\n");
19660
+ }
19118
19661
 
19119
19662
  // src/index.ts
19120
19663
  function createAcpExtension(adapter = {}) {
@@ -19128,16 +19671,18 @@ function createAcpExtension(adapter = {}) {
19128
19671
  return;
19129
19672
  }
19130
19673
  const runtime = createRuntime(adapter);
19131
- let proxyWarned = false;
19674
+ let standDownWarned = false;
19132
19675
  const standDownIfProxied = (ctx) => {
19133
- if (!isBiliProxyBaseUrl(ctx.model?.baseUrl)) return false;
19676
+ const nativeHost = process.env.BILLION_CONTEXT_NATIVE || void 0;
19677
+ if (nativeHost === void 0 && !process.env.BILLION_CONTEXT_PROXY && !isBiliProxyBaseUrl(ctx.model?.baseUrl)) return false;
19678
+ const message = nativeHost !== void 0 ? nativeStandDownMessage(nativeHost) : PROXY_STAND_DOWN_MESSAGE;
19134
19679
  runtime.refused = true;
19135
- runtime.refusalMessage = PROXY_STAND_DOWN_MESSAGE;
19136
- if (!proxyWarned) {
19137
- proxyWarned = true;
19138
- logWarn("host", { event: "proxy-baseurl-detected", sid: ctx.sessionManager.getSessionId(), action: "refused" });
19139
- if (ctx.hasUI) ctx.ui.notify(PROXY_STAND_DOWN_MESSAGE, "warning");
19140
- else console.error(PROXY_STAND_DOWN_MESSAGE);
19680
+ runtime.refusalMessage = message;
19681
+ if (!standDownWarned) {
19682
+ standDownWarned = true;
19683
+ logWarn("host", { event: nativeHost !== void 0 ? "native-host-detected" : "proxy-detected", sid: ctx.sessionManager.getSessionId(), action: "refused", native: nativeHost ?? null });
19684
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
19685
+ else console.error(message);
19141
19686
  }
19142
19687
  return true;
19143
19688
  };
@@ -19154,6 +19699,7 @@ function createAcpExtension(adapter = {}) {
19154
19699
  pi.registerTool(makeDecompressTool(runtime, toolSurface.decompress));
19155
19700
  pi.registerTool(makeSearchTool(runtime, toolSurface.search_context));
19156
19701
  pi.registerTool(makeStatusTool(runtime, toolSurface.acp_status));
19702
+ pi.registerTool(makeCacheTool(runtime, toolSurface.acp_cache));
19157
19703
  for (const { name, options } of makeCommands(runtime, pi)) {
19158
19704
  pi.registerCommand(name, options);
19159
19705
  }
@@ -19229,7 +19775,7 @@ function wireSessionLifecycle(pi, runtime, standDownIfProxied) {
19229
19775
  setDelegatePolicy(DEFAULT_DELEGATE_POLICY);
19230
19776
  const sid = ctx.sessionManager.getSessionId();
19231
19777
  const modelInfo = ctx.model;
19232
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.71" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
19778
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.72" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
19233
19779
  let delegateStoodDown = false;
19234
19780
  try {
19235
19781
  await runtime.reloadConfig(ctx.cwd);