billion-context-pi 0.1.46 → 0.1.47-pr.217.13

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
@@ -324,7 +324,7 @@ function viableRanges(ranges) {
324
324
  return ranges.filter((r) => r.tokens >= VIABLE_RANGE_MIN_TOKENS);
325
325
  }
326
326
 
327
- // node_modules/acp-kernel/dist/chunk-DPH62BGM.js
327
+ // node_modules/acp-kernel/dist/chunk-UX4LINT7.js
328
328
  function createInitialState() {
329
329
  return {
330
330
  blocks: [],
@@ -337,7 +337,8 @@ function createInitialState() {
337
337
  anchors: {},
338
338
  lastShownByTier: {}
339
339
  },
340
- stats: { tokensCompressed: 0, compressionCount: 0 },
340
+ stats: { tokensCompressed: 0, compressionCount: 0, absorbedTokens: 0 },
341
+ absorbed: [],
341
342
  nextBlockId: 1,
342
343
  nextRunId: 1
343
344
  };
@@ -601,6 +602,7 @@ function syncBlocks(messages, state) {
601
602
  tokenSnapshot: { ...state.tokenSnapshot ?? {} },
602
603
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
603
604
  stats: { ...state.stats },
605
+ absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
604
606
  nextBlockId: state.nextBlockId,
605
607
  nextRunId: state.nextRunId
606
608
  };
@@ -661,7 +663,14 @@ function defaultConfig(modelContextLimit, overrides = {}) {
661
663
  protectedTools: [],
662
664
  preserveRecentMessages: 5,
663
665
  preserveRecentTokens: 5e3,
664
- modelContextLimit
666
+ modelContextLimit,
667
+ absorb: {
668
+ enabled: false,
669
+ toolName: "absorb",
670
+ minToolTokens: 1e3,
671
+ contextThresholdPct: 0,
672
+ excludeTools: []
673
+ }
665
674
  };
666
675
  return {
667
676
  ...base,
@@ -669,7 +678,8 @@ function defaultConfig(modelContextLimit, overrides = {}) {
669
678
  tiers: { ...base.tiers, ...overrides.tiers },
670
679
  nudge: { ...base.nudge, ...overrides.nudge },
671
680
  truncate: { ...base.truncate, ...overrides.truncate },
672
- compress: { ...base.compress, ...overrides.compress }
681
+ compress: { ...base.compress, ...overrides.compress },
682
+ absorb: overrides.absorb ? { ...base.absorb, ...overrides.absorb } : base.absorb
673
683
  };
674
684
  }
675
685
  function validateConfig(config) {
@@ -699,6 +709,17 @@ function validateConfig(config) {
699
709
  if (config.tiers.tier3Trigger <= config.tiers.tier2Trigger) {
700
710
  errors.push("tiers.tier3Trigger must be greater than tiers.tier2Trigger");
701
711
  }
712
+ if (config.absorb) {
713
+ if (config.absorb.enabled && !config.absorb.toolName) {
714
+ errors.push("absorb.toolName must be a non-empty string when enabled");
715
+ }
716
+ if (!Number.isFinite(config.absorb.minToolTokens) || config.absorb.minToolTokens < 0) {
717
+ errors.push("absorb.minToolTokens must be >= 0");
718
+ }
719
+ if (config.absorb.contextThresholdPct < 0 || config.absorb.contextThresholdPct > 1) {
720
+ errors.push("absorb.contextThresholdPct must be in [0, 1]");
721
+ }
722
+ }
702
723
  return errors;
703
724
  }
704
725
  var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
@@ -1055,6 +1076,306 @@ function hideConsumedCompressCalls(state, messages) {
1055
1076
  }
1056
1077
  return { messages: result, hidden };
1057
1078
  }
1079
+ var COMPRESS_TOOL_NAME = "compress";
1080
+ var DECOMPRESS_TOOL_NAME = "decompress";
1081
+ var SEARCH_CONTEXT_TOOL_NAME = "search_context";
1082
+ var ACP_STATUS_TOOL_NAME = "acp_status";
1083
+ var ABSORB_TOOL_NAME = "absorb";
1084
+ var COMPRESS_TOOL = {
1085
+ name: COMPRESS_TOOL_NAME,
1086
+ description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}]. REQUIRED \u2014 compress without content is invalid.",
1087
+ input_schema: {
1088
+ type: "object",
1089
+ properties: {
1090
+ topic: {
1091
+ type: "string",
1092
+ description: "Optional short title for the compressed range"
1093
+ },
1094
+ content: {
1095
+ type: "array",
1096
+ description: "One or more ranges to compress into separate summary blocks",
1097
+ items: {
1098
+ type: "object",
1099
+ properties: {
1100
+ topic: { type: "string" },
1101
+ startId: {
1102
+ type: "string",
1103
+ description: "mNNNNN ref at the start of the range"
1104
+ },
1105
+ endId: {
1106
+ type: "string",
1107
+ description: "mNNNNN ref at the end of the range"
1108
+ },
1109
+ summary: {
1110
+ type: "string",
1111
+ description: "Self-contained summary replacing the range"
1112
+ }
1113
+ },
1114
+ required: ["startId", "endId", "summary"]
1115
+ }
1116
+ }
1117
+ },
1118
+ required: ["content"]
1119
+ }
1120
+ };
1121
+ var COMPRESS_TOOL_OPENAI = {
1122
+ type: "function",
1123
+ function: {
1124
+ name: COMPRESS_TOOL_NAME,
1125
+ description: COMPRESS_TOOL.description,
1126
+ parameters: {
1127
+ type: "object",
1128
+ properties: {
1129
+ topic: {
1130
+ type: "string",
1131
+ description: "Optional short title for the compressed range"
1132
+ },
1133
+ content: {
1134
+ type: "array",
1135
+ description: "One or more ranges to compress into separate summary blocks. REQUIRED \u2014 compress without content is invalid.",
1136
+ items: {
1137
+ type: "object",
1138
+ properties: {
1139
+ topic: { type: "string" },
1140
+ startId: {
1141
+ type: "string",
1142
+ description: "mNNNNN ref at the start of the range"
1143
+ },
1144
+ endId: {
1145
+ type: "string",
1146
+ description: "mNNNNN ref at the end of the range"
1147
+ },
1148
+ summary: {
1149
+ type: "string",
1150
+ description: "Self-contained summary replacing the range"
1151
+ }
1152
+ },
1153
+ required: ["startId", "endId", "summary"]
1154
+ }
1155
+ }
1156
+ },
1157
+ required: ["content"]
1158
+ }
1159
+ }
1160
+ };
1161
+ var DECOMPRESS_TOOL_OPENAI = {
1162
+ type: "function",
1163
+ function: {
1164
+ name: DECOMPRESS_TOOL_NAME,
1165
+ description: "Restores previously compressed content. Use when you need exact details lost in compression. By default restores one tier up. Use full:true for all the way to original messages. Use toFile to write to file instead of inflating context.",
1166
+ parameters: {
1167
+ type: "object",
1168
+ properties: {
1169
+ blockId: {
1170
+ type: "string",
1171
+ description: "Block ID to decompress (e.g. b5)"
1172
+ },
1173
+ toFile: {
1174
+ type: "string",
1175
+ description: "Optional: write content to file instead of context"
1176
+ },
1177
+ full: {
1178
+ type: "boolean",
1179
+ description: "Restore all the way to original messages"
1180
+ }
1181
+ },
1182
+ required: ["blockId"]
1183
+ }
1184
+ }
1185
+ };
1186
+ var SEARCH_CONTEXT_TOOL_OPENAI = {
1187
+ type: "function",
1188
+ function: {
1189
+ name: SEARCH_CONTEXT_TOOL_NAME,
1190
+ description: "Search through compressed block summaries by keyword. Use BEFORE decompressing to find the right block.",
1191
+ parameters: {
1192
+ type: "object",
1193
+ properties: {
1194
+ query: { type: "string", description: "Search query" },
1195
+ limit: { type: "number", description: "Max results (default 5)" }
1196
+ },
1197
+ required: ["query"]
1198
+ }
1199
+ }
1200
+ };
1201
+ var ACP_STATUS_TOOL_OPENAI = {
1202
+ type: "function",
1203
+ function: {
1204
+ name: ACP_STATUS_TOOL_NAME,
1205
+ description: "Show context usage and compressible ranges. No args = overview. Use to find what to compress next.",
1206
+ parameters: {
1207
+ type: "object",
1208
+ properties: {}
1209
+ }
1210
+ }
1211
+ };
1212
+ var DECOMPRESS_TOOL = {
1213
+ name: DECOMPRESS_TOOL_NAME,
1214
+ description: DECOMPRESS_TOOL_OPENAI.function.description,
1215
+ input_schema: DECOMPRESS_TOOL_OPENAI.function.parameters
1216
+ };
1217
+ var SEARCH_CONTEXT_TOOL = {
1218
+ name: SEARCH_CONTEXT_TOOL_NAME,
1219
+ description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
1220
+ input_schema: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
1221
+ };
1222
+ var ACP_STATUS_TOOL = {
1223
+ name: ACP_STATUS_TOOL_NAME,
1224
+ description: ACP_STATUS_TOOL_OPENAI.function.description,
1225
+ input_schema: ACP_STATUS_TOOL_OPENAI.function.parameters
1226
+ };
1227
+ var COMPRESS_TOOL_RESPONSES = {
1228
+ type: "function",
1229
+ name: COMPRESS_TOOL_NAME,
1230
+ description: COMPRESS_TOOL.description,
1231
+ parameters: COMPRESS_TOOL_OPENAI.function.parameters
1232
+ };
1233
+ var DECOMPRESS_TOOL_RESPONSES = {
1234
+ type: "function",
1235
+ name: DECOMPRESS_TOOL_OPENAI.function.name,
1236
+ description: DECOMPRESS_TOOL_OPENAI.function.description,
1237
+ parameters: DECOMPRESS_TOOL_OPENAI.function.parameters
1238
+ };
1239
+ var SEARCH_CONTEXT_TOOL_RESPONSES = {
1240
+ type: "function",
1241
+ name: SEARCH_CONTEXT_TOOL_OPENAI.function.name,
1242
+ description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
1243
+ parameters: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
1244
+ };
1245
+ var ACP_STATUS_TOOL_RESPONSES = {
1246
+ type: "function",
1247
+ name: ACP_STATUS_TOOL_OPENAI.function.name,
1248
+ description: ACP_STATUS_TOOL_OPENAI.function.description,
1249
+ parameters: ACP_STATUS_TOOL_OPENAI.function.parameters
1250
+ };
1251
+ var ACP_TOOL_NAMES = /* @__PURE__ */ new Set([
1252
+ COMPRESS_TOOL_NAME,
1253
+ DECOMPRESS_TOOL_NAME,
1254
+ SEARCH_CONTEXT_TOOL_NAME,
1255
+ ACP_STATUS_TOOL_NAME
1256
+ ]);
1257
+ var ALWAYS_PROTECTED_TOOLS = ["compress"];
1258
+ var NEVER_PRESERVE_RECENT_TOOLS = [
1259
+ "decompress",
1260
+ "search_context",
1261
+ "read",
1262
+ "bash"
1263
+ ];
1264
+ function isNeverPreserveRecent(msg) {
1265
+ if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
1266
+ return false;
1267
+ }
1268
+ if (!msg.toolName) return false;
1269
+ return NEVER_PRESERVE_RECENT_TOOLS.includes(msg.toolName);
1270
+ }
1271
+ function matchToolPattern(toolName, pattern) {
1272
+ if (pattern.endsWith("*")) {
1273
+ return toolName.startsWith(pattern.slice(0, -1));
1274
+ }
1275
+ return toolName === pattern;
1276
+ }
1277
+ function isMessageProtected(msg, config) {
1278
+ if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
1279
+ return false;
1280
+ }
1281
+ if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
1282
+ return true;
1283
+ }
1284
+ for (const pattern of config.protectedTools) {
1285
+ if (matchToolPattern(msg.toolName, pattern)) return true;
1286
+ }
1287
+ if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
1288
+ return false;
1289
+ }
1290
+ function collectProtectedToolCallIds(messages, config) {
1291
+ const ids = /* @__PURE__ */ new Set();
1292
+ for (const m of messages) {
1293
+ if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
1294
+ ids.add(m.toolCallId);
1295
+ }
1296
+ }
1297
+ return ids;
1298
+ }
1299
+ function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
1300
+ if (isMessageProtected(msg, config)) return true;
1301
+ if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
1302
+ return true;
1303
+ }
1304
+ return false;
1305
+ }
1306
+ var ABSORB_PROMPT_MARKER = "[ACP absorb]";
1307
+ var DEFAULT_ABSORB_CONFIG = {
1308
+ enabled: false,
1309
+ toolName: ABSORB_TOOL_NAME,
1310
+ minToolTokens: 1e3,
1311
+ contextThresholdPct: 0,
1312
+ excludeTools: []
1313
+ };
1314
+ function resolveAbsorbConfig(config) {
1315
+ return { ...DEFAULT_ABSORB_CONFIG, ...config.absorb ?? {} };
1316
+ }
1317
+ function formatTokenCount(tokens) {
1318
+ if (tokens < 1e3) return String(tokens);
1319
+ if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
1320
+ return Math.round(tokens / 1e3) + "K";
1321
+ }
1322
+ function buildAbsorbPrompt(ref, tokens, toolName = ABSORB_TOOL_NAME) {
1323
+ return `${ABSORB_PROMPT_MARKER} This tool result (~${formatTokenCount(tokens)} tokens) will be REMOVED from context. Your IMMEDIATE next action: call ${toolName}({ ref: "${ref}", summary: "..." }) \u2014 summary = distilled essentials only (outcome, key values, exact paths:lines, error text verbatim, decisions). Afterwards work from your summary; do NOT re-run this tool. If the result contains nothing you need, call ${toolName} with summary "(nothing needed)".`;
1324
+ }
1325
+ function isAcpOrConfiguredTool(toolName, cfg) {
1326
+ if (!toolName) return false;
1327
+ if (toolName === cfg.toolName) return true;
1328
+ return ACP_TOOL_NAMES.has(toolName);
1329
+ }
1330
+ function isAbsorbCandidate(msg, config) {
1331
+ if (msg.contentType !== "tool-result" || !msg.toolCallId) return false;
1332
+ const cfg = resolveAbsorbConfig(config);
1333
+ if (isAcpOrConfiguredTool(msg.toolName, cfg)) return false;
1334
+ if (isMessageProtected(msg, config)) return false;
1335
+ for (const pattern of cfg.excludeTools) {
1336
+ if (msg.toolName && matchToolPattern(msg.toolName, pattern)) return false;
1337
+ }
1338
+ return true;
1339
+ }
1340
+ function hideAbsorbedMessages(messages, state) {
1341
+ const records = state.absorbed ?? [];
1342
+ if (records.length === 0) return messages;
1343
+ const hidden = /* @__PURE__ */ new Set();
1344
+ for (const record of records) {
1345
+ if (record.callMessageId) hidden.add(record.callMessageId);
1346
+ if (record.resultMessageId) hidden.add(record.resultMessageId);
1347
+ }
1348
+ return messages.filter((msg) => !hidden.has(msg.id));
1349
+ }
1350
+ function appendAbsorbPrompts(messages, state, config, tokenCount, countTokens) {
1351
+ const cfg = resolveAbsorbConfig(config);
1352
+ if (!cfg.enabled) return { messages, promptedCount: 0 };
1353
+ const limit = config.modelContextLimit;
1354
+ if (cfg.contextThresholdPct > 0 && limit > 0 && tokenCount < cfg.contextThresholdPct * limit) {
1355
+ return { messages, promptedCount: 0 };
1356
+ }
1357
+ const absorbedIds = /* @__PURE__ */ new Set();
1358
+ for (const record of state.absorbed ?? []) {
1359
+ if (record.resultMessageId) absorbedIds.add(record.resultMessageId);
1360
+ }
1361
+ let promptedCount = 0;
1362
+ const out = messages.map((msg) => {
1363
+ if (!isAbsorbCandidate(msg, config)) return msg;
1364
+ if (absorbedIds.has(msg.id)) return msg;
1365
+ const text = msg.text ?? "";
1366
+ if (text.includes(ABSORB_PROMPT_MARKER)) return msg;
1367
+ const tokens = countTokens(text);
1368
+ if (tokens < cfg.minToolTokens) return msg;
1369
+ const ref = refForRaw(state.messageRefs, msg.id);
1370
+ if (!ref || ref === BLOCKED_REF) return msg;
1371
+ promptedCount++;
1372
+ return {
1373
+ ...msg,
1374
+ text: text + "\n\n" + buildAbsorbPrompt(ref, tokens, cfg.toolName)
1375
+ };
1376
+ });
1377
+ return { messages: out, promptedCount };
1378
+ }
1058
1379
  var registry = /* @__PURE__ */ new Map();
1059
1380
  function listMessageFilters() {
1060
1381
  return [...registry.values()];
@@ -1205,55 +1526,6 @@ function createRenderRefsNode(strategy) {
1205
1526
  };
1206
1527
  }
1207
1528
  var renderRefsNode = createRenderRefsNode("all");
1208
- var ALWAYS_PROTECTED_TOOLS = ["compress"];
1209
- var NEVER_PRESERVE_RECENT_TOOLS = [
1210
- "decompress",
1211
- "search_context",
1212
- "read",
1213
- "bash"
1214
- ];
1215
- function isNeverPreserveRecent(msg) {
1216
- if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
1217
- return false;
1218
- }
1219
- if (!msg.toolName) return false;
1220
- return NEVER_PRESERVE_RECENT_TOOLS.includes(msg.toolName);
1221
- }
1222
- function matchToolPattern(toolName, pattern) {
1223
- if (pattern.endsWith("*")) {
1224
- return toolName.startsWith(pattern.slice(0, -1));
1225
- }
1226
- return toolName === pattern;
1227
- }
1228
- function isMessageProtected(msg, config) {
1229
- if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
1230
- return false;
1231
- }
1232
- if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
1233
- return true;
1234
- }
1235
- for (const pattern of config.protectedTools) {
1236
- if (matchToolPattern(msg.toolName, pattern)) return true;
1237
- }
1238
- if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
1239
- return false;
1240
- }
1241
- function collectProtectedToolCallIds(messages, config) {
1242
- const ids = /* @__PURE__ */ new Set();
1243
- for (const m of messages) {
1244
- if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
1245
- ids.add(m.toolCallId);
1246
- }
1247
- }
1248
- return ids;
1249
- }
1250
- function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
1251
- if (isMessageProtected(msg, config)) return true;
1252
- if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
1253
- return true;
1254
- }
1255
- return false;
1256
- }
1257
1529
  function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
1258
1530
  const callIdsInRange = /* @__PURE__ */ new Set();
1259
1531
  for (let i = startIndex; i <= endIndex; i++) {
@@ -1628,7 +1900,7 @@ function createCore(ports = {}) {
1628
1900
  if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
1629
1901
  const live = activeBlocks(state).map((b) => b.blockId).sort((x, y) => numericBlockId(x) - numericBlockId(y));
1630
1902
  const liveHint = live.length > 0 ? ` Current active blocks span ${live[0]}..${live[live.length - 1]} \u2014 retry with startId/endId set to active block IDs in that span.` : "";
1631
- const gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref failed with "does not exist in this session". Refs recorded before an earlier compress are stale: each successful compress renumbers the remaining refs. Run acp_status, then re-issue the compress in the same turn using only the refs it reports.` : 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.${liveHint}` : `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.`;
1903
+ const gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref failed with "does not exist in this session". Refs recorded before an earlier compress are stale: each successful compress renumbers the remaining refs. Run acp_status, then call the compress tool again using only the refs it reports.` : consumedRanges.length > 0 ? `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}) \u2014 your refs are stale: a prior compress renumbered the remaining refs, so this range now falls inside an active block. Run acp_status, then call the compress tool again using only the CURRENT compressible ranges it reports.${liveHint}` : `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.`;
1632
1904
  return {
1633
1905
  state: input.state,
1634
1906
  result: {
@@ -1746,6 +2018,8 @@ function createCore(ports = {}) {
1746
2018
  assignRefsNode,
1747
2019
  syncBlocksNode,
1748
2020
  pruneNode,
2021
+ absorbHideNode,
2022
+ absorbPromptNode,
1749
2023
  filterNode,
1750
2024
  hideCompressCallsNode,
1751
2025
  recommendNode,
@@ -1791,6 +2065,31 @@ var pruneNode = {
1791
2065
  return { ...io, messages: prune(io.messages, io.state) };
1792
2066
  }
1793
2067
  };
2068
+ var absorbHideNode = {
2069
+ name: "absorb-hide",
2070
+ enabled: (io) => (io.state.absorbed?.length ?? 0) > 0,
2071
+ run(io) {
2072
+ return { ...io, messages: hideAbsorbedMessages(io.messages, io.state) };
2073
+ }
2074
+ };
2075
+ var absorbPromptNode = {
2076
+ name: "absorb-prompt",
2077
+ enabled: (_io, ctx) => ctx.config.absorb?.enabled === true,
2078
+ run(io, ctx) {
2079
+ const applied = appendAbsorbPrompts(
2080
+ io.messages,
2081
+ io.state,
2082
+ ctx.config,
2083
+ ctx.tokenCount,
2084
+ ctx.countTokens
2085
+ );
2086
+ return {
2087
+ ...io,
2088
+ messages: applied.messages,
2089
+ effects: { ...io.effects, absorbPromptedCount: applied.promptedCount }
2090
+ };
2091
+ }
2092
+ };
1794
2093
  var filterNode = {
1795
2094
  name: "filter",
1796
2095
  enabled: (_io, ctx) => !!ctx.config.messageFilters?.enabled && listMessageFilters().length > 0,
@@ -2328,6 +2627,7 @@ function cloneState(state) {
2328
2627
  tokenSnapshot: { ...state.tokenSnapshot ?? {} },
2329
2628
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
2330
2629
  stats: { ...state.stats },
2630
+ absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
2331
2631
  nextBlockId: state.nextBlockId,
2332
2632
  nextRunId: state.nextRunId
2333
2633
  };
@@ -2544,14 +2844,14 @@ function renderUncompressedRanges(visible) {
2544
2844
  };
2545
2845
  const merged = [];
2546
2846
  for (const m of visible) {
2547
- const num = refNum2(m.ref);
2847
+ const num2 = refNum2(m.ref);
2548
2848
  const last = merged[merged.length - 1];
2549
- if (last && num === last.startNum + last.count) {
2849
+ if (last && num2 === last.startNum + last.count) {
2550
2850
  last.endRef = m.ref;
2551
2851
  last.count += 1;
2552
2852
  last.tokens += m.tokens;
2553
2853
  } else {
2554
- merged.push({ startRef: m.ref, endRef: m.ref, startNum: num, count: 1, tokens: m.tokens, tool: m.tool });
2854
+ merged.push({ startRef: m.ref, endRef: m.ref, startNum: num2, count: 1, tokens: m.tokens, tool: m.tool });
2555
2855
  }
2556
2856
  }
2557
2857
  for (const r of merged.slice(0, 30)) {
@@ -2618,6 +2918,296 @@ function renderCompressedDrilldown(blocks, state, sort, limit, countTokens) {
2618
2918
  }
2619
2919
  return lines.join("\n");
2620
2920
  }
2921
+ function parseCompressArgs(input, opts) {
2922
+ const callId = opts?.callId;
2923
+ const diag = { ok: false, kind: "ok", invalidItems: 0 };
2924
+ if (input === null || input === void 0) {
2925
+ diag.kind = "empty-input";
2926
+ return finish([], diag);
2927
+ }
2928
+ if (typeof input === "string") {
2929
+ return parseStringInput(input, callId, diag);
2930
+ }
2931
+ if (typeof input !== "object" || Array.isArray(input)) {
2932
+ diag.kind = "not-object";
2933
+ return finish([], diag);
2934
+ }
2935
+ return parseObjectValue(input, callId, diag);
2936
+ }
2937
+ function parseStringInput(raw, callId, diag) {
2938
+ diag.rawPrefix = raw.slice(0, 800);
2939
+ diag.length = raw.length;
2940
+ const cleaned = stripFence(raw.trim());
2941
+ if (cleaned === "") {
2942
+ diag.kind = "empty-input";
2943
+ return finish([], diag);
2944
+ }
2945
+ let value = tryParseLenient(cleaned);
2946
+ if (typeof value === "string") {
2947
+ const inner = tryParseLenient(stripFence(value));
2948
+ if (inner !== void 0) value = inner;
2949
+ }
2950
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2951
+ return parseObjectValue(value, callId, diag);
2952
+ }
2953
+ if (value !== void 0) {
2954
+ diag.kind = "not-object";
2955
+ return finish([], diag);
2956
+ }
2957
+ const entries = salvageContentEntries(cleaned);
2958
+ return finishSalvage(entries, callId, diag, looksTruncated(cleaned));
2959
+ }
2960
+ function parseObjectValue(value, callId, diag) {
2961
+ diag.keys = Object.keys(value);
2962
+ const content = value["content"];
2963
+ if (content === void 0) {
2964
+ const single = validateEntry(value, callId);
2965
+ if (single) {
2966
+ diag.kind = "ok";
2967
+ return finish([single], diag);
2968
+ }
2969
+ diag.kind = "missing-content";
2970
+ return finish([], diag);
2971
+ }
2972
+ let entries;
2973
+ let salvaged = false;
2974
+ if (Array.isArray(content)) {
2975
+ entries = content;
2976
+ } else if (typeof content === "string") {
2977
+ const parsed = parseContentArray(content);
2978
+ if (parsed === null) {
2979
+ diag.kind = "content-not-array";
2980
+ return finish([], diag);
2981
+ }
2982
+ entries = parsed.entries;
2983
+ salvaged = parsed.salvaged;
2984
+ } else {
2985
+ diag.kind = "content-not-array";
2986
+ return finish([], diag);
2987
+ }
2988
+ const { ranges, invalid } = validateEntries(entries, callId);
2989
+ const topTopic = stringOr(value["topic"]);
2990
+ const topMaxChars = value["summaryMaxChars"];
2991
+ const hasTopMaxChars = typeof topMaxChars === "number" && Number.isFinite(topMaxChars);
2992
+ if (topTopic !== void 0 || hasTopMaxChars) {
2993
+ for (const r of ranges) {
2994
+ if (r.topic === void 0 && topTopic !== void 0) r.topic = topTopic;
2995
+ if (r.summaryMaxChars === void 0 && hasTopMaxChars) r.summaryMaxChars = topMaxChars;
2996
+ }
2997
+ }
2998
+ diag.invalidItems = invalid;
2999
+ diag.kind = salvaged ? "truncated" : ranges.length > 0 ? "ok" : "no-valid-ranges";
3000
+ return finish(ranges, diag);
3001
+ }
3002
+ function parseContentArray(s) {
3003
+ const cleaned = stripFence(s.trim());
3004
+ let value = cleaned === "" ? void 0 : tryParseLenient(cleaned);
3005
+ if (typeof value === "string") {
3006
+ value = tryParseLenient(stripFence(value));
3007
+ }
3008
+ if (Array.isArray(value)) {
3009
+ return { entries: value, salvaged: false };
3010
+ }
3011
+ if (value === void 0) {
3012
+ const entries = salvageContentEntries('{"content": ' + cleaned);
3013
+ return { entries, salvaged: entries.length > 0 };
3014
+ }
3015
+ return null;
3016
+ }
3017
+ function finish(ranges, diag) {
3018
+ diag.ok = ranges.length > 0;
3019
+ return { ranges, diagnostics: diag };
3020
+ }
3021
+ function finishSalvage(entries, callId, diag, truncatedShape) {
3022
+ const { ranges, invalid } = validateEntries(entries, callId);
3023
+ diag.invalidItems = invalid;
3024
+ diag.kind = entries.length > 0 || truncatedShape ? "truncated" : "malformed-json";
3025
+ return finish(ranges, diag);
3026
+ }
3027
+ function validateEntries(entries, callId) {
3028
+ const ranges = [];
3029
+ let invalid = 0;
3030
+ for (const entry of entries) {
3031
+ const range = validateEntry(entry, callId);
3032
+ if (range !== null) ranges.push(range);
3033
+ else invalid++;
3034
+ }
3035
+ return { ranges, invalid };
3036
+ }
3037
+ function validateEntry(entry, callId) {
3038
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
3039
+ const e = entry;
3040
+ const start = stringOr(e["startRef"]) ?? stringOr(e["startId"]) ?? stringOr(e["messageId"]);
3041
+ const end = stringOr(e["endRef"]) ?? stringOr(e["endId"]) ?? stringOr(e["messageId"]);
3042
+ if (start === void 0 || end === void 0) return null;
3043
+ const summary = stringOr(e["summary"]);
3044
+ if (summary === void 0) return null;
3045
+ const range = { startRef: start, endRef: end, summary };
3046
+ const topic = stringOr(e["topic"]);
3047
+ if (topic !== void 0) range.topic = topic;
3048
+ const maxChars = e["summaryMaxChars"];
3049
+ if (typeof maxChars === "number" && Number.isFinite(maxChars)) range.summaryMaxChars = maxChars;
3050
+ if (callId !== void 0) range.compressCallId = callId;
3051
+ return range;
3052
+ }
3053
+ function stringOr(value) {
3054
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3055
+ }
3056
+ function tryParseLenient(s) {
3057
+ if (s === "") return void 0;
3058
+ try {
3059
+ return JSON.parse(s);
3060
+ } catch {
3061
+ }
3062
+ const noTrailingCommas = stripTrailingCommas(s);
3063
+ if (noTrailingCommas !== s) {
3064
+ try {
3065
+ return JSON.parse(noTrailingCommas);
3066
+ } catch {
3067
+ }
3068
+ }
3069
+ const fixed = escapeRawNewlinesInStrings(noTrailingCommas);
3070
+ if (fixed !== noTrailingCommas) {
3071
+ try {
3072
+ return JSON.parse(fixed);
3073
+ } catch {
3074
+ }
3075
+ }
3076
+ return void 0;
3077
+ }
3078
+ function stripFence(s) {
3079
+ if (!s.startsWith("```")) return s;
3080
+ const firstNewline = s.indexOf("\n");
3081
+ if (firstNewline === -1) return s;
3082
+ const bodyStart = firstNewline + 1;
3083
+ const end = s.lastIndexOf("```");
3084
+ return end > bodyStart ? s.slice(bodyStart, end).trim() : s.slice(bodyStart).trim();
3085
+ }
3086
+ function stripTrailingCommas(s) {
3087
+ let out = "";
3088
+ let inString = false;
3089
+ let escaped = false;
3090
+ for (let i = 0; i < s.length; i++) {
3091
+ const ch = s.charAt(i);
3092
+ if (inString) {
3093
+ out += ch;
3094
+ if (escaped) escaped = false;
3095
+ else if (ch === "\\") escaped = true;
3096
+ else if (ch === '"') inString = false;
3097
+ continue;
3098
+ }
3099
+ if (ch === '"') {
3100
+ inString = true;
3101
+ out += ch;
3102
+ continue;
3103
+ }
3104
+ if (ch === ",") {
3105
+ let j = i + 1;
3106
+ while (j < s.length && (s.charAt(j) === " " || s.charAt(j) === " " || s.charAt(j) === "\n" || s.charAt(j) === "\r")) j++;
3107
+ if (j < s.length && (s.charAt(j) === "}" || s.charAt(j) === "]")) continue;
3108
+ }
3109
+ out += ch;
3110
+ }
3111
+ return out;
3112
+ }
3113
+ function escapeRawNewlinesInStrings(s) {
3114
+ let out = "";
3115
+ let inString = false;
3116
+ let escaped = false;
3117
+ for (let i = 0; i < s.length; i++) {
3118
+ const ch = s.charAt(i);
3119
+ if (!inString) {
3120
+ if (ch === '"') inString = true;
3121
+ out += ch;
3122
+ continue;
3123
+ }
3124
+ if (escaped) {
3125
+ out += ch;
3126
+ escaped = false;
3127
+ continue;
3128
+ }
3129
+ if (ch === "\\") {
3130
+ out += ch;
3131
+ escaped = true;
3132
+ continue;
3133
+ }
3134
+ if (ch === "\n") {
3135
+ out += "\\n";
3136
+ continue;
3137
+ }
3138
+ if (ch === "\r") {
3139
+ out += "\\r";
3140
+ continue;
3141
+ }
3142
+ if (ch === " ") {
3143
+ out += "\\t";
3144
+ continue;
3145
+ }
3146
+ if (ch === '"') inString = false;
3147
+ out += ch;
3148
+ }
3149
+ return out;
3150
+ }
3151
+ function looksTruncated(s) {
3152
+ let depth = 0;
3153
+ let inString = false;
3154
+ let escaped = false;
3155
+ for (let i = 0; i < s.length; i++) {
3156
+ const ch = s.charAt(i);
3157
+ if (inString) {
3158
+ if (escaped) escaped = false;
3159
+ else if (ch === "\\") escaped = true;
3160
+ else if (ch === '"') inString = false;
3161
+ continue;
3162
+ }
3163
+ if (ch === '"') {
3164
+ inString = true;
3165
+ continue;
3166
+ }
3167
+ if (ch === "{" || ch === "[") depth++;
3168
+ else if (ch === "}" || ch === "]") depth--;
3169
+ }
3170
+ return depth > 0 || inString;
3171
+ }
3172
+ function salvageContentEntries(raw) {
3173
+ const match = /"content"\s*:\s*\[/.exec(raw);
3174
+ if (match === null) return [];
3175
+ const arrayStart = match.index + match[0].length - 1;
3176
+ const entries = [];
3177
+ let depth = 0;
3178
+ let inString = false;
3179
+ let escaped = false;
3180
+ let entryStart = -1;
3181
+ for (let i = arrayStart + 1; i < raw.length; i++) {
3182
+ const ch = raw.charAt(i);
3183
+ if (inString) {
3184
+ if (escaped) escaped = false;
3185
+ else if (ch === "\\") escaped = true;
3186
+ else if (ch === '"') inString = false;
3187
+ continue;
3188
+ }
3189
+ if (ch === '"') {
3190
+ inString = true;
3191
+ continue;
3192
+ }
3193
+ if (ch === "{" || ch === "[") {
3194
+ if (depth === 0 && ch === "{" && entryStart === -1) entryStart = i;
3195
+ depth++;
3196
+ continue;
3197
+ }
3198
+ if (ch === "}" || ch === "]") {
3199
+ depth--;
3200
+ if (depth < 0) break;
3201
+ if (depth === 0 && entryStart !== -1) {
3202
+ const entrySlice = raw.slice(entryStart, i + 1);
3203
+ entryStart = -1;
3204
+ const parsed = tryParseLenient(entrySlice);
3205
+ if (parsed !== void 0) entries.push(parsed);
3206
+ }
3207
+ }
3208
+ }
3209
+ return entries;
3210
+ }
2621
3211
  function stem(word) {
2622
3212
  let w = word;
2623
3213
  if (w.length <= 3) return w;
@@ -4017,6 +4607,7 @@ function pruneOrphanRefs(state, messages) {
4017
4607
  }
4018
4608
  }
4019
4609
  var MAX_COMPRESS_ATTEMPTS = 3;
4610
+ var MAX_RETRY_PROMPT_FIRES = 5;
4020
4611
  function createRuntime(adapter) {
4021
4612
  const density = new DensityEstimator();
4022
4613
  let countModelId = "default";
@@ -4061,33 +4652,52 @@ function createRuntime(adapter) {
4061
4652
  const compressOutcomeSeen = /* @__PURE__ */ new Set();
4062
4653
  let compressFailTurnKey = null;
4063
4654
  let compressFailCount = 0;
4655
+ let retryPromptFires = 0;
4656
+ let retryExhaustedNotified = false;
4064
4657
  function noteCompressOutcomes(turnKey, outcomes) {
4065
4658
  if (compressFailTurnKey !== turnKey) {
4066
4659
  compressFailTurnKey = turnKey;
4067
4660
  compressFailCount = 0;
4661
+ retryPromptFires = 0;
4662
+ retryExhaustedNotified = false;
4068
4663
  }
4069
4664
  const prevCount = compressFailCount;
4665
+ let sawNewOutcome = false;
4070
4666
  for (const o of outcomes) {
4071
4667
  if (compressOutcomeSeen.has(o.toolCallId)) continue;
4072
4668
  compressOutcomeSeen.add(o.toolCallId);
4669
+ sawNewOutcome = true;
4073
4670
  if (o.isError || o.noop === true) {
4074
4671
  compressFailCount += 1;
4075
4672
  } else if (o.success) {
4076
4673
  compressFailCount = 0;
4077
4674
  }
4078
4675
  }
4676
+ if (sawNewOutcome) {
4677
+ retryPromptFires = 0;
4678
+ retryExhaustedNotified = false;
4679
+ }
4079
4680
  const latest = outcomes.length > 0 ? outcomes[outcomes.length - 1] : void 0;
4080
- const retryFor = latest && (latest.isError || latest.noop === true) && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS ? latest.toolCallId : null;
4681
+ const latestFailed = latest !== void 0 && (latest.isError || latest.noop === true);
4682
+ const retryFor = latestFailed && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS && retryPromptFires < MAX_RETRY_PROMPT_FIRES ? latest.toolCallId : null;
4683
+ if (retryFor !== null) retryPromptFires += 1;
4081
4684
  const cappedNow = compressFailCount >= MAX_COMPRESS_ATTEMPTS && prevCount < MAX_COMPRESS_ATTEMPTS;
4082
- return { count: compressFailCount, retryFor, cappedNow };
4685
+ const exhaustedNow = retryFor === null && latestFailed && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS && retryPromptFires >= MAX_RETRY_PROMPT_FIRES && !retryExhaustedNotified;
4686
+ if (exhaustedNow) retryExhaustedNotified = true;
4687
+ return { count: compressFailCount, retryFor, cappedNow, exhaustedNow, fires: retryPromptFires };
4083
4688
  }
4084
4689
  function compressRetryCappedFor(turnKey) {
4085
4690
  return compressFailTurnKey === turnKey && compressFailCount >= MAX_COMPRESS_ATTEMPTS;
4086
4691
  }
4692
+ function compressRetryExhaustedFor(turnKey) {
4693
+ return compressFailTurnKey === turnKey && compressFailCount >= 1 && compressFailCount < MAX_COMPRESS_ATTEMPTS && retryPromptFires >= MAX_RETRY_PROMPT_FIRES;
4694
+ }
4087
4695
  function clearCompressRetryTracking() {
4088
4696
  compressOutcomeSeen.clear();
4089
4697
  compressFailTurnKey = null;
4090
4698
  compressFailCount = 0;
4699
+ retryPromptFires = 0;
4700
+ retryExhaustedNotified = false;
4091
4701
  }
4092
4702
  async function acquireLock(sid) {
4093
4703
  const prev = locks.get(sid) ?? Promise.resolve();
@@ -4176,7 +4786,7 @@ function createRuntime(adapter) {
4176
4786
  nudgeShownTurns.add(k);
4177
4787
  }, nudgeShownFor: (k) => nudgeShownTurns.has(k), clearNudgeTracking: () => {
4178
4788
  nudgeShownTurns.clear();
4179
- }, noteCompressOutcomes, compressRetryCappedFor, clearCompressRetryTracking, liveContextLimit, configFor, reloadConfig, stateFor, save, acquireLock, overflowFor, overflowDrop, throttleFor, throttleDrop };
4789
+ }, noteCompressOutcomes, compressRetryCappedFor, compressRetryExhaustedFor, clearCompressRetryTracking, liveContextLimit, configFor, reloadConfig, stateFor, save, acquireLock, overflowFor, overflowDrop, throttleFor, throttleDrop };
4180
4790
  }
4181
4791
 
4182
4792
  // node_modules/typebox/build/system/memory/memory.mjs
@@ -8668,25 +9278,24 @@ function makeCompressTool(runtime) {
8668
9278
  }
8669
9279
  };
8670
9280
  }
8671
- function normalizeRanges(content) {
8672
- let ranges = content ?? [];
8673
- if (typeof ranges === "string") {
8674
- try {
8675
- ranges = JSON.parse(ranges);
8676
- } catch (e) {
8677
- return `Invalid content: not valid JSON (${e instanceof Error ? e.message : String(e)}). content must be an ARRAY of {startId, endId, summary} objects \u2014 pass the array directly, not a string.`;
8678
- }
9281
+ function normalizeRanges(args) {
9282
+ const { ranges, diagnostics } = parseCompressArgs(args);
9283
+ if (ranges.length === 0) {
9284
+ if (Array.isArray(args.content) && args.content.length === 0) return [];
9285
+ return describeDiagnostics(diagnostics, args.content);
8679
9286
  }
8680
- if (!Array.isArray(ranges)) {
8681
- return `Invalid content: expected an array of ranges, got ${ranges === null ? "null" : typeof ranges}.`;
9287
+ return ranges.map((r) => ({ startId: r.startRef, endId: r.endRef, summary: r.summary, topic: r.topic }));
9288
+ }
9289
+ function describeDiagnostics(diagnostics, content) {
9290
+ const shape = typeof content === "string" ? "a JSON-encoded string (non-strict-tool providers stringify array arguments)" : content === null ? "null" : `a ${typeof content}`;
9291
+ const base = `Invalid compress content (${diagnostics.kind}): got ${shape}`;
9292
+ if (diagnostics.kind === "truncated") {
9293
+ return `${base}; the input was truncated and no complete ranges could be recovered. Shorten the summary or split into smaller ranges.`;
8682
9294
  }
8683
- for (const [i, r] of ranges.entries()) {
8684
- const o = r;
8685
- if (!o || typeof o !== "object" || typeof o.startId !== "string" || typeof o.endId !== "string" || typeof o.summary !== "string") {
8686
- return `Invalid content[${i}]: each range must be an object with string fields startId, endId, summary.`;
8687
- }
9295
+ if (diagnostics.invalidItems > 0) {
9296
+ return `${base}; ${diagnostics.invalidItems} entr${diagnostics.invalidItems === 1 ? "y was" : "ies were"} dropped as invalid. Each range must be an object with string fields startId, endId, summary.`;
8688
9297
  }
8689
- return ranges;
9298
+ return `${base}. content must be an ARRAY of {startId, endId, summary} objects.`;
8690
9299
  }
8691
9300
  function compressPanelBlocks(text) {
8692
9301
  if (!text.trimStart().startsWith("\u25A3 ACP |")) return -1;
@@ -8713,7 +9322,7 @@ function tier3OnlyRewrite(newBlocks, allBlocks) {
8713
9322
  return spans;
8714
9323
  }
8715
9324
  async function handleCompress(args, runtime, ctx, toolCallId) {
8716
- const maybeRanges = normalizeRanges(args.content);
9325
+ const maybeRanges = normalizeRanges(args);
8717
9326
  if (typeof maybeRanges === "string") throw new Error(maybeRanges);
8718
9327
  const ranges = maybeRanges;
8719
9328
  if (ranges.length === 0) return "No ranges provided.";
@@ -9923,7 +10532,7 @@ function makeDelegateWaitTool(_pi) {
9923
10532
  return new Promise((resolve3) => {
9924
10533
  let settled = false;
9925
10534
  let timer2;
9926
- const finish = (result) => {
10535
+ const finish2 = (result) => {
9927
10536
  if (settled) return;
9928
10537
  settled = true;
9929
10538
  run.waiter = void 0;
@@ -9932,19 +10541,19 @@ function makeDelegateWaitTool(_pi) {
9932
10541
  resolve3(result);
9933
10542
  };
9934
10543
  const onAbort = () => {
9935
- finish({ details: void 0, content: [{ type: "text", text: `Aborted; delegate \`${args.runId}\` is still running in the background. A notification will be injected when it finishes.` }] });
10544
+ finish2({ details: void 0, content: [{ type: "text", text: `Aborted; delegate \`${args.runId}\` is still running in the background. A notification will be injected when it finishes.` }] });
9936
10545
  };
9937
10546
  run.waiter = () => {
9938
10547
  run.consumed = true;
9939
10548
  if (run.status === "cancelled") {
9940
- finish(buildWaitResult(run, `Delegate \`${run.runId}\` was cancelled (no result).${remainingLineForWait(run.runId)}`, displayMode));
10549
+ finish2(buildWaitResult(run, `Delegate \`${run.runId}\` was cancelled (no result).${remainingLineForWait(run.runId)}`, displayMode));
9941
10550
  return;
9942
10551
  }
9943
- finish(buildWaitResult(run, formatRunResult(run), displayMode));
10552
+ finish2(buildWaitResult(run, formatRunResult(run), displayMode));
9944
10553
  };
9945
10554
  signal?.addEventListener("abort", onAbort);
9946
10555
  timer2 = setTimeout(
9947
- () => finish({ details: void 0, content: [{ type: "text", text: `Failed: delegate \`${args.runId}\` result not ready after ${Math.round(timeoutMs / 1e3)}s. Do NOT keep waiting or retry \u2014 go do other work now. The run continues in the background and a completion notification (with the result file path) will be injected into the chat when it finishes.` }] }),
10556
+ () => finish2({ details: void 0, content: [{ type: "text", text: `Failed: delegate \`${args.runId}\` result not ready after ${Math.round(timeoutMs / 1e3)}s. Do NOT keep waiting or retry \u2014 go do other work now. The run continues in the background and a completion notification (with the result file path) will be injected into the chat when it finishes.` }] }),
9948
10557
  timeoutMs
9949
10558
  );
9950
10559
  });
@@ -10222,20 +10831,20 @@ function waitForChild(child, signal) {
10222
10831
  });
10223
10832
  const timer2 = setTimeout(() => {
10224
10833
  child.kill("SIGTERM");
10225
- finish({ code: null, stdout: "", stderr: stderrText, timedOut: true });
10834
+ finish2({ code: null, stdout: "", stderr: stderrText, timedOut: true });
10226
10835
  }, SYNC_TIMEOUT_MS);
10227
10836
  const onAbort = () => {
10228
10837
  clearTimeout(timer2);
10229
10838
  child.kill("SIGTERM");
10230
10839
  };
10231
10840
  signal?.addEventListener("abort", onAbort, { once: true });
10232
- function finish(r) {
10841
+ function finish2(r) {
10233
10842
  clearTimeout(timer2);
10234
10843
  signal?.removeEventListener("abort", onAbort);
10235
10844
  resolve3(r);
10236
10845
  }
10237
10846
  child.on("close", (code) => {
10238
- finish({
10847
+ finish2({
10239
10848
  code,
10240
10849
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
10241
10850
  stderr: stderrText,
@@ -10243,7 +10852,7 @@ function waitForChild(child, signal) {
10243
10852
  });
10244
10853
  });
10245
10854
  child.on("error", (err) => {
10246
- finish({ code: null, stdout: "", stderr: err.message, timedOut: false });
10855
+ finish2({ code: null, stdout: "", stderr: err.message, timedOut: false });
10247
10856
  });
10248
10857
  });
10249
10858
  }
@@ -10448,6 +11057,37 @@ function formatCompactTokens2(count) {
10448
11057
  if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
10449
11058
  return `${Math.round(count / 1e6)}M`;
10450
11059
  }
11060
+ function num(v) {
11061
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
11062
+ }
11063
+ function cacheHitStats(usages) {
11064
+ let cacheRead = 0;
11065
+ let billedPrompt = 0;
11066
+ let requests = 0;
11067
+ let last;
11068
+ for (const u of usages) {
11069
+ const read = num(u?.cacheRead);
11070
+ const write = num(u?.cacheWrite);
11071
+ const input = num(u?.input);
11072
+ if (read + write <= 0) continue;
11073
+ const total = read + write + input;
11074
+ if (total <= 0) continue;
11075
+ cacheRead += read;
11076
+ billedPrompt += total;
11077
+ requests += 1;
11078
+ last = read / total;
11079
+ }
11080
+ return {
11081
+ session: requests > 0 ? cacheRead / billedPrompt : void 0,
11082
+ last,
11083
+ requests,
11084
+ cacheRead,
11085
+ billedPrompt
11086
+ };
11087
+ }
11088
+ function formatHitRate(rate) {
11089
+ return `${(Math.max(0, Math.min(1, rate)) * 100).toFixed(1)}%`;
11090
+ }
10451
11091
  function bar(value, total, width = 20) {
10452
11092
  if (total === 0) return "";
10453
11093
  const filled = Math.max(0, Math.min(width, Math.round(value / total * width)));
@@ -10500,6 +11140,15 @@ function buildStatusPanel(input) {
10500
11140
  lines.push(` ${cat.label.padEnd(10)} ${b} ${String(pct2).padStart(3)}% ${fmt2(cat.value)}`);
10501
11141
  }
10502
11142
  }
11143
+ if (input.cacheUsages) {
11144
+ const cache2 = cacheHitStats(input.cacheUsages);
11145
+ if (cache2.requests > 0 && cache2.session !== void 0 && cache2.last !== void 0) {
11146
+ lines.push("");
11147
+ lines.push(
11148
+ `Prompt cache (provider-reported): ${formatHitRate(cache2.last)} last \xB7 ${formatHitRate(cache2.session)} session avg \u2014 ${fmt2(cache2.cacheRead)} of ${fmt2(cache2.billedPrompt)} billed prompt tokens served from cache (${cache2.requests} req)`
11149
+ );
11150
+ }
11151
+ }
10503
11152
  lines.push("");
10504
11153
  if (nudge) {
10505
11154
  if (nudge.shouldInject) {
@@ -10730,6 +11379,16 @@ function ensureSubagentAcpTools(settingsPath, options) {
10730
11379
  }
10731
11380
 
10732
11381
  // src/commands.ts
11382
+ function cacheUsageSamples(entries) {
11383
+ const out = [];
11384
+ for (const e of entries) {
11385
+ if (e.type !== "message") continue;
11386
+ const m = e.message;
11387
+ if (m?.role !== "assistant" || !m.usage) continue;
11388
+ out.push({ input: m.usage.input ?? 0, cacheRead: m.usage.cacheRead ?? 0, cacheWrite: m.usage.cacheWrite ?? 0 });
11389
+ }
11390
+ return out;
11391
+ }
10733
11392
  function makeCommands(runtime) {
10734
11393
  return [
10735
11394
  {
@@ -10816,7 +11475,7 @@ ${text}`);
10816
11475
  ];
10817
11476
  }
10818
11477
  async function statusReport(runtime, ctx) {
10819
- const { state, coreMessages } = await runtime.stateFor(ctx);
11478
+ const { state, coreMessages, entries } = await runtime.stateFor(ctx);
10820
11479
  const config = runtime.configFor(ctx);
10821
11480
  const realUsage = ctx.getContextUsage?.();
10822
11481
  const systemPromptText = getSystemPromptText(ctx);
@@ -10826,7 +11485,7 @@ async function statusReport(runtime, ctx) {
10826
11485
  const modelId = ctx.model?.id ?? "default";
10827
11486
  const sentTokens = estimateTokens(coreMessages, coveredIds) + systemPromptTokens;
10828
11487
  const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: calibrateTokens(sentTokens, runtime.density.densityFor(modelId)) });
10829
- const versionStr = "0.1.46" ? `billion-context-pi@${"0.1.46"}` : void 0;
11488
+ const versionStr = "0.1.47" ? `billion-context-pi@${"0.1.47"}` : void 0;
10830
11489
  let text = buildStatusPanel({
10831
11490
  version: versionStr,
10832
11491
  tokenCount: sessionTokens,
@@ -10834,7 +11493,8 @@ async function statusReport(runtime, ctx) {
10834
11493
  state: turn.state,
10835
11494
  nudge: turn.nudge,
10836
11495
  modelContextLimit: config.modelContextLimit,
10837
- unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0)
11496
+ unprunedTokens: coreMessages.reduce((sum, m) => sum + defaultCountTokens(m.text ?? ""), 0),
11497
+ cacheUsages: cacheUsageSamples(entries ?? [])
10838
11498
  });
10839
11499
  const delegateUsage = getDelegateUsage();
10840
11500
  if (delegateUsage && delegateUsage.totalTokens > 0) {
@@ -11025,14 +11685,12 @@ function wireToolGuardrails(pi, runtime) {
11025
11685
  const fullPath = isBash ? event.details?.fullOutputPath : void 0;
11026
11686
  const timeoutSecs = isBash && event.isError ? detectBashTimeout(event.content) : void 0;
11027
11687
  let modified;
11028
- const max = runtime.adapter.toolOutputMaxBytes;
11029
- if (max !== void 0 && max > 0) {
11030
- const next = capToolOutput(event.content, max, fullPath);
11031
- if (next) {
11032
- modified = next;
11033
- debug.event("guardrail-output-cap", { max, hadPath: !!fullPath });
11034
- logWarn("guardrail", { event: "output-cap", max, hadPath: !!fullPath });
11035
- }
11688
+ const max = runtime.adapter.toolOutputMaxBytes ?? DEFAULT_TOOL_OUTPUT_MAX_BYTES;
11689
+ const next = capToolOutput(event.content, max, fullPath);
11690
+ if (next) {
11691
+ modified = next;
11692
+ debug.event("guardrail-output-cap", { max, hadPath: !!fullPath });
11693
+ logWarn("guardrail", { event: "output-cap", max, hadPath: !!fullPath });
11036
11694
  }
11037
11695
  if (timeoutSecs !== void 0) {
11038
11696
  modified = appendTimeoutNotice(modified ?? event.content, timeoutSecs);
@@ -11188,7 +11846,7 @@ async function autoInstallLatest(latest, extDirOverride) {
11188
11846
  "--no-fund",
11189
11847
  "--no-save"
11190
11848
  ];
11191
- const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.46";
11849
+ const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.47";
11192
11850
  const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
11193
11851
  if (code !== 0) {
11194
11852
  logWarn("update", {
@@ -11201,7 +11859,7 @@ async function autoInstallLatest(latest, extDirOverride) {
11201
11859
  }
11202
11860
  const verify = await verifyInstall(npmDir, latest);
11203
11861
  if (!verify.ok) {
11204
- const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.46";
11862
+ const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.47";
11205
11863
  logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
11206
11864
  const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
11207
11865
  logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
@@ -11262,7 +11920,7 @@ async function checkForUpdate(autoUpdate, notify) {
11262
11920
  const runtimeVersion = await getRuntimeVersion();
11263
11921
  const latest = await fetchLatestVersion();
11264
11922
  if (!latest) return;
11265
- const current = runtimeVersion ?? "0.1.46";
11923
+ const current = runtimeVersion ?? "0.1.47";
11266
11924
  const hasUpdate = isNewer(latest, current);
11267
11925
  debug.event("update-check", {
11268
11926
  current,
@@ -11303,6 +11961,10 @@ async function getRuntimeVersion() {
11303
11961
  // src/index.ts
11304
11962
  function createAcpExtension(adapter = {}) {
11305
11963
  return (pi) => {
11964
+ if (process.env.BILLION_CONTEXT_PROXY) {
11965
+ console.log("[bcp] disabled: BILLION_CONTEXT_PROXY detected \u2014 proxy handles compression");
11966
+ return;
11967
+ }
11306
11968
  const runtime = createRuntime(adapter);
11307
11969
  wireCompactionDisable(pi);
11308
11970
  wireSessionLifecycle(pi, runtime);
@@ -11337,7 +11999,7 @@ function wireSessionLifecycle(pi, runtime) {
11337
11999
  const sid = ctx.sessionManager.getSessionId();
11338
12000
  runtime.clearSessionTracking(sid);
11339
12001
  const modelInfo = ctx.model;
11340
- logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.46" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
12002
+ logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.47" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
11341
12003
  try {
11342
12004
  await runtime.reloadConfig(ctx.cwd);
11343
12005
  setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
@@ -11466,7 +12128,8 @@ function wireContextTransform(pi, runtime) {
11466
12128
  const emergency = turn.nudge.breakdown?.emergencyOverride === 1;
11467
12129
  turn.nudge.compressibleRanges = viableRanges(turn.nudge.compressibleRanges);
11468
12130
  const retryCapped = runtime.compressRetryCappedFor(turnKey);
11469
- const alreadyShown = retryCapped || !emergency && runtime.nudgeShownFor(turnKey);
12131
+ const retryQuiet = retryCapped || runtime.compressRetryExhaustedFor(turnKey);
12132
+ const alreadyShown = retryQuiet || !emergency && runtime.nudgeShownFor(turnKey);
11470
12133
  if (!alreadyShown) {
11471
12134
  rebuilt.push(nudgeMessage(turn.nudge, turn.state.blocks.filter((b) => b.active), runtime.prompts));
11472
12135
  const rendered = renderNudgeText(turn.nudge, runtime.prompts);
@@ -11491,7 +12154,7 @@ ${rendered.text}${example}`);
11491
12154
  const failed = outcome.retryFor !== null ? compressOutcomes.find((o) => o.toolCallId === outcome.retryFor) : void 0;
11492
12155
  if (failed) {
11493
12156
  rebuilt.push(compressRetryMessage(failed.text, outcome.count, MAX_COMPRESS_ATTEMPTS));
11494
- logWarn("nudge", { sid, event: "compress-retry-inject", attempt: outcome.count, max: MAX_COMPRESS_ATTEMPTS, toolCallId: failed.toolCallId });
12157
+ logWarn("nudge", { sid, event: "compress-retry-inject", attempt: outcome.count, max: MAX_COMPRESS_ATTEMPTS, fires: outcome.fires, fireMax: MAX_RETRY_PROMPT_FIRES, toolCallId: failed.toolCallId });
11495
12158
  debug.event("compress-retry-injected", { sid, turnKey, attempt: outcome.count, toolCallId: failed.toolCallId, text: failed.text.slice(0, 200) });
11496
12159
  } else if (outcome.cappedNow) {
11497
12160
  logWarn("nudge", { sid, event: "compress-retry-capped", failures: outcome.count });
@@ -11499,6 +12162,12 @@ ${rendered.text}${example}`);
11499
12162
  if (ctx.hasUI) {
11500
12163
  ctx.ui.notify(`[ACP] compress failed ${outcome.count}\xD7 this turn \u2014 retry prompts disabled until the next user message.`);
11501
12164
  }
12165
+ } else if (outcome.exhaustedNow) {
12166
+ logWarn("nudge", { sid, event: "compress-retry-exhausted", failures: outcome.count, fires: outcome.fires, fireMax: MAX_RETRY_PROMPT_FIRES });
12167
+ debug.event("compress-retry-exhausted", { sid, turnKey, failures: outcome.count });
12168
+ if (ctx.hasUI) {
12169
+ ctx.ui.notify(`[ACP] compress retry prompt ignored ${MAX_RETRY_PROMPT_FIRES}\xD7 this turn \u2014 retry prompts paused until a new compress attempt or the next user message.`);
12170
+ }
11502
12171
  }
11503
12172
  }
11504
12173
  debug.event("context-out", { outMsgs: rebuilt.length, injected: turn.nudge?.shouldInject ?? false, emergency: turn.nudge?.breakdown?.emergencyOverride === 1 });