claude-code-infinite 1.0.29 → 1.0.30

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.
Files changed (76) hide show
  1. package/README.md +57 -7
  2. package/dist/cli-lifecycle.d.ts +45 -1
  3. package/dist/cli-lifecycle.d.ts.map +1 -1
  4. package/dist/cli-lifecycle.js +59 -0
  5. package/dist/cli-lifecycle.js.map +1 -1
  6. package/dist/cli-main.js +49 -15
  7. package/dist/cli-main.js.map +1 -1
  8. package/dist/count-tokens.d.ts +15 -0
  9. package/dist/count-tokens.d.ts.map +1 -1
  10. package/dist/count-tokens.js +23 -1
  11. package/dist/count-tokens.js.map +1 -1
  12. package/dist/diagnostics-schema.d.ts.map +1 -1
  13. package/dist/diagnostics-schema.js +5 -0
  14. package/dist/diagnostics-schema.js.map +1 -1
  15. package/dist/diagnostics.d.ts.map +1 -1
  16. package/dist/diagnostics.js +18 -1
  17. package/dist/diagnostics.js.map +1 -1
  18. package/dist/final-index.d.ts +7 -1
  19. package/dist/final-index.d.ts.map +1 -1
  20. package/dist/final-index.js +10 -3
  21. package/dist/final-index.js.map +1 -1
  22. package/dist/image-refs.d.ts +51 -0
  23. package/dist/image-refs.d.ts.map +1 -0
  24. package/dist/image-refs.js +169 -0
  25. package/dist/image-refs.js.map +1 -0
  26. package/dist/image-tokens.d.ts +37 -0
  27. package/dist/image-tokens.d.ts.map +1 -0
  28. package/dist/image-tokens.js +158 -0
  29. package/dist/image-tokens.js.map +1 -0
  30. package/dist/memtree-body.d.ts +52 -0
  31. package/dist/memtree-body.d.ts.map +1 -0
  32. package/dist/memtree-body.js +99 -0
  33. package/dist/memtree-body.js.map +1 -0
  34. package/dist/memtree.d.ts +77 -3
  35. package/dist/memtree.d.ts.map +1 -1
  36. package/dist/memtree.js +267 -53
  37. package/dist/memtree.js.map +1 -1
  38. package/dist/paused-session.d.ts.map +1 -1
  39. package/dist/paused-session.js +11 -8
  40. package/dist/paused-session.js.map +1 -1
  41. package/dist/prefix-store.d.ts +2 -0
  42. package/dist/prefix-store.d.ts.map +1 -1
  43. package/dist/prefix-store.js +4 -0
  44. package/dist/prefix-store.js.map +1 -1
  45. package/dist/prepared-compression.d.ts +78 -0
  46. package/dist/prepared-compression.d.ts.map +1 -0
  47. package/dist/prepared-compression.js +127 -0
  48. package/dist/prepared-compression.js.map +1 -0
  49. package/dist/proxy.d.ts +363 -14
  50. package/dist/proxy.d.ts.map +1 -1
  51. package/dist/proxy.js +708 -90
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/reqlog.d.ts +55 -0
  54. package/dist/reqlog.d.ts.map +1 -1
  55. package/dist/reqlog.js.map +1 -1
  56. package/dist/request-sizing.d.ts +21 -2
  57. package/dist/request-sizing.d.ts.map +1 -1
  58. package/dist/request-sizing.js +57 -11
  59. package/dist/request-sizing.js.map +1 -1
  60. package/dist/route-identity.d.ts +27 -0
  61. package/dist/route-identity.d.ts.map +1 -1
  62. package/dist/route-identity.js +88 -8
  63. package/dist/route-identity.js.map +1 -1
  64. package/dist/token-estimate.d.ts +3 -20
  65. package/dist/token-estimate.d.ts.map +1 -1
  66. package/dist/token-estimate.js +9 -50
  67. package/dist/token-estimate.js.map +1 -1
  68. package/dist/tool-output-store.d.ts +12 -0
  69. package/dist/tool-output-store.d.ts.map +1 -0
  70. package/dist/tool-output-store.js +146 -0
  71. package/dist/tool-output-store.js.map +1 -0
  72. package/dist/tool-result-truncation.d.ts +32 -0
  73. package/dist/tool-result-truncation.d.ts.map +1 -0
  74. package/dist/tool-result-truncation.js +135 -0
  75. package/dist/tool-result-truncation.js.map +1 -0
  76. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -43,7 +43,9 @@ import { PromptAccounting } from "./prompt-accounting.js";
43
43
  import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures, pausedRefusal } from "./route-fallback.js";
44
44
  import { createCountTokensSession, CountTokensCooldowns, plausibleSample, } from "./count-tokens.js";
45
45
  import { RequestSizing, estimateRequestTokens, fitsMeasured, fitsNative, outputReservation } from "./request-sizing.js";
46
- import { routeMessageHash, stablePrefixMessageHash } from "./route-identity.js";
46
+ import { extendsHistory, mediaFreeMessageHash, prefixIdentity, prefixMessageMatches, } from "./route-identity.js";
47
+ import { COMPRESSION_RESERVE_TOKENS, compressionTrigger, PreparedCompressions, prepareStartTokens } from "./prepared-compression.js";
48
+ import { truncateToolResults } from "./tool-result-truncation.js";
47
49
  import http from "node:http";
48
50
  import https from "node:https";
49
51
  import { createHash, randomBytes } from "node:crypto";
@@ -62,6 +64,7 @@ import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
62
64
  import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
63
65
  import { isResumableSessionId } from "./resume-hint.js";
64
66
  import { conservativeBodyTokens } from "./token-estimate.js";
67
+ import { bodyImages } from "./image-tokens.js";
65
68
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
66
69
  import { rateLimitFromResponse } from "./rate-limit.js";
67
70
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
@@ -78,6 +81,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
78
81
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
79
82
  /** Safety cap on a long-budget compress, should a transport ignore its abort. */
80
83
  const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
84
+ /** The most a blocking compress with a fallback may wait (its budget grows with its upload). */
85
+ const MAX_COMPRESS_WAIT_MS = 60_000;
81
86
  const SKIP_REQUEST_HEADERS = new Set([
82
87
  "host",
83
88
  "connection",
@@ -96,20 +101,18 @@ const SKIP_RESPONSE_HEADERS = new Set([
96
101
  ]);
97
102
  /**
98
103
  * Budget fallback until the server reports `model_budget_tokens`, mirroring the
99
- * server's ccc policy (polychat index_budget.py): every Claude model compresses
100
- * near its window, NEAR_WINDOW_FALLBACK_RATIO of it but no later than Claude
101
- * Code's own auto-compact point (window - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS)
102
- * and never below FALLBACK_BUDGET_WINDOW_RATIO: 900k of 1M, 167k of 200k; at
103
- * most a Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
104
- * FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts a 1M
105
- * window at 967k, so MemTree compresses no earlier than ~70k before it would;
106
- * ccc lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
107
- * jumps past the budget reaches the proxy and is compressed here.
104
+ * server's ccc policy (polychat index_budget.py near_window_budget_tokens):
105
+ * every Claude model compresses near its window, at the window less the
106
+ * reply's room (COMPRESSION_RESERVE_TOKENS, 40k) and never below
107
+ * FALLBACK_BUDGET_WINDOW_RATIO of it: 960k of 1M, 160k of 200k; at most a
108
+ * Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
109
+ * FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts at
110
+ * window - 33k (967k of 1M), so MemTree compresses just before it would; ccc
111
+ * lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
112
+ * jumps past the budget reaches the proxy and is compressed here, on a
113
+ * prepared prefix when there is one (prepared-compression.ts).
108
114
  */
109
115
  export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
110
- export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
111
- /** Claude Code 2.1.302 auto-compacts at window - min(max_tokens, 20k) - 13k. */
112
- const CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS = 33_000;
113
116
  /** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
114
117
  const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
115
118
  /**
@@ -125,10 +128,14 @@ export function fallbackBudgetTokens(model, modelContextLimit) {
125
128
  if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
126
129
  return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
127
130
  }
128
- const nearWindow = Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), Math.min(Math.floor(modelContextLimit * NEAR_WINDOW_FALLBACK_RATIO), modelContextLimit - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS));
131
+ const nearWindow = nearWindowBudgetTokens(modelContextLimit);
129
132
  const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
130
133
  return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
131
134
  }
135
+ /** A Claude model's near-window budget before any ceiling (fallbackBudgetTokens). */
136
+ function nearWindowBudgetTokens(modelContextLimit) {
137
+ return Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), modelContextLimit - COMPRESSION_RESERVE_TOKENS);
138
+ }
132
139
  /**
133
140
  * An explicit target is capped at this share of the budget: a compaction to
134
141
  * the budget itself would be over budget again on the next turn and
@@ -320,6 +327,7 @@ export function startProxy(opts) {
320
327
  const activeRequests = new Set();
321
328
  const acceptedRequests = new Set();
322
329
  const shutdownAbort = new AbortController();
330
+ const backgroundAbort = new AbortController();
323
331
  const state = {
324
332
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
325
333
  keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
@@ -347,6 +355,7 @@ export function startProxy(opts) {
347
355
  toolRecoveryAttemptedLanes: new Map(),
348
356
  toolRecoveryCooldownUntil: 0,
349
357
  shutdownSignal: shutdownAbort.signal,
358
+ backgroundSignal: backgroundAbort.signal,
350
359
  routeInstallFault: opts.routeInstallFault,
351
360
  memtreePages: new Map(),
352
361
  memorySourcePages: new Map(),
@@ -362,6 +371,9 @@ export function startProxy(opts) {
362
371
  defaultCompactOff: opts.defaultCompactTarget === null,
363
372
  compactNow: new Set(),
364
373
  stablePrefixes: new Map(),
374
+ prepared: new PreparedCompressions(),
375
+ preparing: new Map(),
376
+ backgroundCompress: new Map(),
365
377
  serverBudgets: new Map(),
366
378
  serverReportsBudget: false,
367
379
  countTokensCooldowns: new CountTokensCooldowns(),
@@ -412,6 +424,7 @@ export function startProxy(opts) {
412
424
  const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
413
425
  ? Math.floor(timeoutMs)
414
426
  : 5_000;
427
+ backgroundAbort.abort();
415
428
  const quiesced = (async () => {
416
429
  // Once close completes, every accepted request has entered the tracked
417
430
  // set.
@@ -1089,7 +1102,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1089
1102
  if (!rides) {
1090
1103
  const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
1091
1104
  if (measured.source === "exact") {
1092
- sizeSlot.set({ tokens: measured.tokens, forwardedBytes: forwardCandidate.body.length });
1105
+ sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
1093
1106
  }
1094
1107
  }
1095
1108
  rec.routeLane = requestRouteLane;
@@ -1150,12 +1163,18 @@ async function handleMessages(req, res, opts, upstream, state) {
1150
1163
  },
1151
1164
  // The ride's bytes are only what failed; recovery always compresses the
1152
1165
  // ORIGINAL conversation (the one MemTree indexed), never the ride itself.
1153
- recoverLate: async (_ride, rideTokens) => {
1154
- if (rec.compress || compactionMode(opts, state, requestSessionKey).mode === "off" ||
1166
+ recoverLate: async (_ride, rideTokens, target) => {
1167
+ // A compression already made may be redone only to a smaller target.
1168
+ if ((rec.compress && target === undefined) ||
1169
+ compactionMode(opts, state, requestSessionKey).mode === "off" ||
1170
+ !hasEarlierNonToolUserMessage(messages) ||
1155
1171
  (rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
1156
1172
  return undefined;
1157
1173
  return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
1158
- modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate });
1174
+ modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
1175
+ route: { key: requestRouteKey, messages, epoch: routeEpoch,
1176
+ lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
1177
+ ...(target !== undefined ? { target } : {}) });
1159
1178
  },
1160
1179
  delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
1161
1180
  });
@@ -1302,6 +1321,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1302
1321
  ride: toolRide,
1303
1322
  canCompress,
1304
1323
  });
1324
+ if (isMainRequest && toolSessionId !== undefined && canCompress &&
1325
+ (plan.kind === "ride" || plan.kind === "pass")) {
1326
+ maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
1327
+ sessionId: toolSessionId, modelContextLimit, rec, clientMeta });
1328
+ }
1305
1329
  if (plan.kind === "ride")
1306
1330
  toolRide = plan.ride;
1307
1331
  if (plan.kind === "pass" && plan.original) {
@@ -1341,9 +1365,13 @@ async function handleMessages(req, res, opts, upstream, state) {
1341
1365
  // for growth instead of paying a blocking call on every tool turn.
1342
1366
  rec.routeRecovery = { outcome: "backoff" };
1343
1367
  }
1344
- else if (Date.now() < state.toolRecoveryCooldownUntil) {
1368
+ else if (Date.now() < state.toolRecoveryCooldownUntil &&
1369
+ !(plan.overWindow && !plan.fallback && (!plan.uncountedOverWindowOnly ||
1370
+ await wholeRequestRefused(requestSizing, originalCandidate, sizingLimits())))) {
1345
1371
  // A recent attempt burned the full compress budget and still
1346
- // failed. Every tool turn appends a tool_result and rehashes, so
1372
+ // failed. (Not when this turn is over the window with no prefix to
1373
+ // ride and delivery would refuse it: skipping could only end in a
1374
+ // refusal, so it compresses anyway, on the long budget.) Every tool turn appends a tool_result and rehashes, so
1347
1375
  // compress() dedup can never absorb the repeat: without this
1348
1376
  // cooldown a MemTree outage would add the whole blocking budget to
1349
1377
  // every tool turn over the budget. A cooldown skip does not start
@@ -1424,9 +1452,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1424
1452
  return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1425
1453
  toolForwarding.settle(delivered);
1426
1454
  if (toolRide)
1427
- noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
1455
+ noteRideSize(toolRide.sizeHolder, rec, sendBody);
1428
1456
  else {
1429
- noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
1457
+ noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
1430
1458
  }
1431
1459
  }));
1432
1460
  }
@@ -1469,19 +1497,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1469
1497
  const sessionId = requestSessionId(req);
1470
1498
  // Stable-prefix (edge) compaction: the main thread's human turns only. The
1471
1499
  // recap rides the main route above; subagents keep per-turn compression.
1472
- const edge = isMainRequest && sessionId !== undefined
1473
- ? await planEdgeCompaction({
1474
- sizing: requestSizing,
1475
- opts,
1476
- state,
1477
- body,
1478
- messages,
1479
- sessionId,
1480
- modelContextLimit,
1481
- forwardBody,
1482
- rec,
1483
- })
1484
- : undefined;
1500
+ const planEdge = () => planEdgeCompaction({ sizing: requestSizing, opts, state, body, messages,
1501
+ sessionId: sessionId, modelContextLimit, forwardBody, rec });
1502
+ let edge = isMainRequest && sessionId !== undefined ? await planEdge() : undefined;
1503
+ if (edge?.kind === "compress" && (state.preparing.has(sessionId) ||
1504
+ rec.compaction?.preparedWaitMs !== undefined || rec.compaction?.prepared !== undefined)) {
1505
+ // Never beside the session's preparation (both would be billed): wait for
1506
+ // it, then plan again with the settings now current (a /memtree-compact
1507
+ // that arrived while it waited or counted) and the preparation it left.
1508
+ const waited = rec.compaction?.preparedWaitMs ?? 0;
1509
+ const prepared = rec.compaction?.prepared;
1510
+ await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
1511
+ const total = waited + (rec.compaction?.preparedWaitMs ?? 0);
1512
+ edge = await planEdge();
1513
+ if (rec.compaction) {
1514
+ rec.compaction.preparedWaitMs = total;
1515
+ rec.compaction.prepared ??= prepared;
1516
+ }
1517
+ }
1518
+ // Not beside a blocking compress call: the two would overlap on the server.
1519
+ if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
1520
+ maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
1521
+ modelContextLimit, rec, clientMeta });
1522
+ }
1485
1523
  // An active subagent can repeat/embed the human prompt in its own request;
1486
1524
  // producer suppression must also preserve the arm for the later main call.
1487
1525
  if (displayForThisTurn)
@@ -1496,6 +1534,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1496
1534
  // A lagging index left history verbatim: a refusal waits for indexing.
1497
1535
  if (result)
1498
1536
  noteIndexLag(opts, rec, result);
1537
+ // Too large for the window: delivery recompresses it once, smaller.
1538
+ if (result && !rec.indexBehind) {
1539
+ noteOversizedCompression(rec, requestSizing, body, compressedRaw, rec.compaction?.targetTokens);
1540
+ }
1499
1541
  releaseUncommittedRouteDecision();
1500
1542
  recordTurn(rec, "followup-degraded", rawBody);
1501
1543
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
@@ -1596,7 +1638,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1596
1638
  : stable.installed
1597
1639
  : undefined;
1598
1640
  if (sizePrefix)
1599
- notePrefixSize(sizePrefix, rec, compressedRaw.length);
1641
+ notePrefixSize(sizePrefix, rec, compressedRaw);
1600
1642
  // A route already installed at protocol-complete deliberately survives
1601
1643
  // delivered=false: that settle may be the fast-tool abort (client
1602
1644
  // consumed message_stop, closed the SSE response, and its immediate
@@ -1633,7 +1675,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1633
1675
  }
1634
1676
  }));
1635
1677
  };
1636
- if (existingRide && (!edge || edge.kind === "off")) {
1678
+ if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
1637
1679
  const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
1638
1680
  const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
1639
1681
  candidateSizeOwners.set(candidate, owner);
@@ -1665,8 +1707,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1665
1707
  const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
1666
1708
  const noteWholeSize = () => {
1667
1709
  if (isMainRequest)
1668
- notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
1710
+ notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
1669
1711
  };
1712
+ if (edge?.kind === "pass") {
1713
+ // Clearly under the budget by its own count: the compress call could only
1714
+ // pass it through, so the turn goes out now and the call (which also
1715
+ // indexes it and reports the page and budget) runs in the background.
1716
+ compressInBackground({ opts, state, body, msgsForMemtree, hash, modelContextLimit, rec,
1717
+ sessionId, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
1718
+ // Never compress: the turn went out whole, so a compression the server's
1719
+ // own (more conservative) sizing chose could not be used, only billed.
1720
+ compaction: { target: NEVER_COMPRESS_TARGET_TOKENS },
1721
+ linkSeq: isAwaySummary ? undefined : nextMemtreeCallSeq(state) });
1722
+ releaseForOriginalFallback();
1723
+ sendingWhole();
1724
+ recordTurn(rec, "followup-under-budget", forwardBody);
1725
+ capture(opts, "anthropic-request", forwardBody);
1726
+ return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec)
1727
+ .then((delivered) => {
1728
+ noteWholeSize();
1729
+ if (delivered)
1730
+ markMainPromptDelivered();
1731
+ }));
1732
+ }
1670
1733
  // The complete request upload can outlive its downstream subscriber while
1671
1734
  // MemTree compression is in flight. Track that subscriber locally without
1672
1735
  // feeding its lifetime into MemtreeClient.compress(): compression promises
@@ -1716,7 +1779,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1716
1779
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
1717
1780
  .then((delivered) => {
1718
1781
  if (isMainRequest)
1719
- notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
1782
+ notePassthroughSize(state, sessionId, body.model, rec, rawBody);
1720
1783
  if (delivered)
1721
1784
  markMainPromptDelivered();
1722
1785
  }));
@@ -2315,7 +2378,8 @@ function rememberPage(pages, sessionId, page) {
2315
2378
  */
2316
2379
  function noteMemtreeHealth(state, compression) {
2317
2380
  if (compression.liveHealth === "failure") {
2318
- if (state.shutdownSignal.aborted)
2381
+ // Background calls the drain stopped (backgroundSignal) are no outage either.
2382
+ if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
2319
2383
  return;
2320
2384
  state.toolRecoveryCooldownUntil =
2321
2385
  Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
@@ -2398,7 +2462,7 @@ stable) {
2398
2462
  // Include trailing ambient role=system blocks: MemTree consolidated them
2399
2463
  // into compressedBody.system, so treating them as suffix would duplicate
2400
2464
  // those instructions on every tool request.
2401
- originalPrefixHashes: originalMessages.map(routeMessageHash),
2465
+ ...prefixIdentity(originalMessages, "route"),
2402
2466
  compressedMessages: cloneJson(compressedBody.messages),
2403
2467
  compressedSystem: cloneJson(compressedBody.system),
2404
2468
  hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
@@ -2437,7 +2501,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2437
2501
  const prefix = {
2438
2502
  sessionId,
2439
2503
  originalSystemHash: compressed.originalSystemHash,
2440
- originalPrefixHashes: originalMessages.map(stablePrefixMessageHash),
2504
+ ...prefixIdentity(originalMessages, "stable"),
2441
2505
  compressedMessages: compressed.compressedMessages,
2442
2506
  compressedSystem: compressed.compressedSystem,
2443
2507
  hasCompressedSystem: compressed.hasCompressedSystem,
@@ -2448,6 +2512,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2448
2512
  stable.installed = prefix;
2449
2513
  boundedSet(state.stablePrefixes, sessionId, prefix);
2450
2514
  state.compactNow.delete(sessionId);
2515
+ // Prepared against the old prefix's ride sizes: a fresh one is made when
2516
+ // the new prefix's rides near the budget again.
2517
+ state.prepared.discard(sessionId);
2451
2518
  return prefix;
2452
2519
  }
2453
2520
  /**
@@ -2468,6 +2535,7 @@ function persistStablePrefix(state, sessionId, prefix) {
2468
2535
  store.save(sessionId, owner, {
2469
2536
  originalSystemHash: prefix.originalSystemHash,
2470
2537
  originalPrefixHashes: prefix.originalPrefixHashes,
2538
+ originalMediaFreeHashes: prefix.originalMediaFreeHashes,
2471
2539
  compressedMessages: prefix.compressedMessages,
2472
2540
  compressedSystem: prefix.compressedSystem,
2473
2541
  hasCompressedSystem: prefix.hasCompressedSystem,
@@ -2520,7 +2588,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
2520
2588
  function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
2521
2589
  if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
2522
2590
  return "defer";
2523
- const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
2591
+ const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
2524
2592
  if (miss === null)
2525
2593
  return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
2526
2594
  const authoritative = miss === "prefix" &&
@@ -2550,7 +2618,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
2550
2618
  * at least one message after them. The suffix may be anything, human turns
2551
2619
  * included.
2552
2620
  */
2553
- function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMessageHash) {
2621
+ function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
2554
2622
  if (!sessionId || prefix.sessionId !== sessionId)
2555
2623
  return "session";
2556
2624
  if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
@@ -2560,7 +2628,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
2560
2628
  if (messages.length < prefixLength)
2561
2629
  return "prefix";
2562
2630
  for (let i = 0; i < prefixLength; i++) {
2563
- if (hashMessage(messages[i]) !== prefix.originalPrefixHashes[i]) {
2631
+ if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
2564
2632
  return "prefix";
2565
2633
  }
2566
2634
  }
@@ -2604,20 +2672,24 @@ function serverBudgetKey(model, modelContextLimit) {
2604
2672
  return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
2605
2673
  }
2606
2674
  /**
2607
- * The session's whole-request budget: CCC_BUDGET_TOKENS when set (tests),
2608
- * else the model budget the server last reported for this model and window,
2609
- * else fallbackBudgetTokens (servers that predate `model_budget_tokens`, and
2610
- * the first request before any response).
2675
+ * The session's whole-request budget, the size at which a request is
2676
+ * compressed: CCC_BUDGET_TOKENS when set (tests), else the model budget the
2677
+ * server last reported for this model and window, else fallbackBudgetTokens
2678
+ * (servers that predate `model_budget_tokens`, and the first request before
2679
+ * any response). Never above the window less the reply's room
2680
+ * (compressionTrigger): a request is compressed when its projected size plus
2681
+ * that reserve would exceed the window, or the budget.
2611
2682
  */
2612
2683
  function resolveBudget(opts, state, model, modelContextLimit) {
2684
+ const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
2613
2685
  if (opts.budgetTokensOverride !== undefined) {
2614
- return { tokens: opts.budgetTokensOverride, source: "override" };
2686
+ return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
2615
2687
  }
2616
2688
  const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
2617
2689
  if (reported !== undefined)
2618
- return { tokens: reported, source: "server" };
2690
+ return { tokens: trigger(reported), source: "server" };
2619
2691
  return {
2620
- tokens: fallbackBudgetTokens(model, modelContextLimit),
2692
+ tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
2621
2693
  source: "window-ratio",
2622
2694
  };
2623
2695
  }
@@ -2700,17 +2772,17 @@ function reportedInputTokens(rec) {
2700
2772
  (usage.cache_creation_input_tokens ?? 0));
2701
2773
  }
2702
2774
  /** Keep the newest (largest-body) reported size of a request on this prefix. */
2703
- function notePrefixSize(prefix, rec, bytes) {
2704
- const sample = reportedSample(rec, bytes);
2775
+ function notePrefixSize(prefix, rec, sent) {
2776
+ const sample = reportedSample(rec, sent);
2705
2777
  if (sample === undefined)
2706
2778
  return;
2707
- if (prefix.lastSize && bytes < prefix.lastSize.forwardedBytes)
2779
+ if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
2708
2780
  return;
2709
2781
  prefix.lastSize = sample;
2710
2782
  }
2711
2783
  /** Record the reported size of a main-thread request forwarded whole. */
2712
- function notePassthroughSize(state, sessionId, model, rec, bytes) {
2713
- const sample = reportedSample(rec, bytes);
2784
+ function notePassthroughSize(state, sessionId, model, rec, sent) {
2785
+ const sample = reportedSample(rec, sent);
2714
2786
  if (sessionId === undefined || sample === undefined)
2715
2787
  return;
2716
2788
  boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
@@ -2740,13 +2812,18 @@ function usedServerTools(rec) {
2740
2812
  * an implausible ratio would mis-size every later request it anchors, so it
2741
2813
  * is dropped and the previous sample kept.
2742
2814
  */
2743
- function reportedSample(rec, bytes) {
2815
+ function reportedSample(rec, sent) {
2744
2816
  const tokens = reportedBodyTokens(rec);
2745
2817
  if (tokens === undefined)
2746
2818
  return undefined;
2747
- const sample = { tokens, forwardedBytes: bytes };
2819
+ const sample = sizeSampleOf(sent, tokens, rec.model);
2748
2820
  return plausibleSample(sample) ? sample : undefined;
2749
2821
  }
2822
+ /** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
2823
+ function sizeSampleOf(sent, tokens, model) {
2824
+ const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
2825
+ return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
2826
+ }
2750
2827
  /**
2751
2828
  * Anthropic's input tokens for the body this request forwarded, when its usage
2752
2829
  * describes that body (not a changed delivery, no server tools). Not filtered
@@ -2793,12 +2870,12 @@ const noteRideSize = notePrefixSize;
2793
2870
  * Record the reported size of a request forwarded whole: the session's
2794
2871
  * passthrough size on the main thread, else its lane's.
2795
2872
  */
2796
- function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
2873
+ function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
2797
2874
  if (isMainRequest && sessionId !== undefined) {
2798
- notePassthroughSize(state, sessionId, model, rec, bytes);
2875
+ notePassthroughSize(state, sessionId, model, rec, sent);
2799
2876
  return;
2800
2877
  }
2801
- const sample = reportedSample(rec, bytes);
2878
+ const sample = reportedSample(rec, sent);
2802
2879
  if (sample === undefined)
2803
2880
  return;
2804
2881
  boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
@@ -2859,7 +2936,7 @@ async function planEdgeCompaction(args) {
2859
2936
  };
2860
2937
  let prefix = state.stablePrefixes.get(sessionId);
2861
2938
  if (prefix) {
2862
- const miss = prefixMismatch(body, messages, prefix, sessionId, stablePrefixMessageHash);
2939
+ const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
2863
2940
  if (miss) {
2864
2941
  // Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
2865
2942
  // longer stands for this conversation. Start over from passthrough.
@@ -2895,8 +2972,13 @@ async function planEdgeCompaction(args) {
2895
2972
  (explicitTarget && prefix.targetTokens !== targetTokens)) {
2896
2973
  return forced("target-change", fallback);
2897
2974
  }
2898
- if (size.tokens >= budget.tokens || overWindow)
2975
+ if (size.tokens >= budget.tokens || overWindow) {
2976
+ const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
2977
+ waitMs: opts.memtree.longCompressBudgetMs });
2978
+ if (adopted)
2979
+ return { kind: "ride", ...adopted };
2899
2980
  return forced("budget", fallback);
2981
+ }
2900
2982
  return { kind: "ride", prefix, routed };
2901
2983
  }
2902
2984
  if (state.compactNow.has(sessionId))
@@ -2909,12 +2991,18 @@ async function planEdgeCompaction(args) {
2909
2991
  compaction.sizeSource = size.source;
2910
2992
  noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
2911
2993
  if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
2994
+ const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
2995
+ waitMs: opts.memtree.longCompressBudgetMs });
2996
+ if (adopted)
2997
+ return { kind: "ride", ...adopted };
2912
2998
  return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
2913
2999
  }
2914
3000
  if (state.serverReportsBudget) {
2915
3001
  // The server measures the request and compresses only past the budget.
2916
3002
  const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
2917
3003
  compaction.thresholdTokens = threshold;
3004
+ if (clearlyUnderBudget(opts, state, budget, size, compaction))
3005
+ return { kind: "pass" };
2918
3006
  return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
2919
3007
  }
2920
3008
  // Without calibrated usage or threshold support, let the server decide
@@ -2955,7 +3043,7 @@ async function planToolCompaction(args) {
2955
3043
  const stableLane = args.isMainRequest && sessionId !== undefined;
2956
3044
  const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
2957
3045
  let ride = args.ride;
2958
- if (!ride && current && !prefixMismatch(body, messages, current, sessionId, stablePrefixMessageHash)) {
3046
+ if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
2959
3047
  const routed = prefixRoutedBody(body, messages, current);
2960
3048
  if (routed) {
2961
3049
  const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
@@ -2993,12 +3081,27 @@ async function planToolCompaction(args) {
2993
3081
  if (size.tokens < budget.tokens && !overWindow) {
2994
3082
  return ride ? { kind: "ride", ride } : { kind: "pass" };
2995
3083
  }
3084
+ const adopted = stableLane
3085
+ ? await adoptPreparedPrefix({ ...args, sessionId: sessionId, budgetTokens: budget.tokens,
3086
+ compaction, waitMs: opts.memtree.longCompressBudgetMs })
3087
+ : undefined;
3088
+ if (adopted) {
3089
+ return { kind: "ride",
3090
+ ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
3091
+ }
3092
+ // Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
3093
+ const modeNow = compactionMode(opts, state, sessionId);
3094
+ if (modeNow.mode === "off")
3095
+ return { kind: "off" };
3096
+ const targetNow = compactionTarget(modeNow, budget.tokens);
3097
+ compaction.mode = modeNow.mode;
3098
+ compaction.targetTokens = targetNow;
2996
3099
  compaction.reason = "budget";
2997
3100
  return {
2998
3101
  kind: "compress",
2999
- ...budgetCompaction(state, compaction, targetTokens, body, modelContextLimit),
3000
- targetTokens,
3001
- explicitTarget: mode.mode === "explicit",
3102
+ ...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
3103
+ targetTokens: targetNow,
3104
+ explicitTarget: modeNow.mode === "explicit",
3002
3105
  estimateTokens: size.tokens,
3003
3106
  budgetTokens: budget.tokens,
3004
3107
  overWindow,
@@ -3007,6 +3110,224 @@ async function planToolCompaction(args) {
3007
3110
  ...(stableLane ? { replaces: current ?? null } : {}),
3008
3111
  };
3009
3112
  }
3113
+ /** Server `compression_target_tokens` sentinel: never compress, always pass through. */
3114
+ const NEVER_COMPRESS_TARGET_TOKENS = -1;
3115
+ /** Share of the budget under which a counted human turn skips its blocking call. */
3116
+ const CLEARLY_UNDER_BUDGET_RATIO = 0.85;
3117
+ /**
3118
+ * Whether a main human turn with no prefix is clearly under the budget by its
3119
+ * token count (an exact count, or a projection from Anthropic's reported
3120
+ * usage; never a byte guess) against a budget the server reported. The
3121
+ * compress call could then only pass it through, so it need not wait for it.
3122
+ */
3123
+ function clearlyUnderBudget(opts, state, budget, size, compaction) {
3124
+ // A known payment or key failure keeps the blocking path: it gives the notice.
3125
+ if (opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
3126
+ return false;
3127
+ return budget.source !== "window-ratio" && size.source === "reported" &&
3128
+ compaction.uncountedOverWindow !== true &&
3129
+ size.tokens <= Math.floor(budget.tokens * CLEARLY_UNDER_BUDGET_RATIO);
3130
+ }
3131
+ /**
3132
+ * Whether this model and window prepare compressions: a Claude model on a 1M
3133
+ * window whose budget is the near-window one (not a Sonnet ceiling).
3134
+ */
3135
+ function preparesCompression(model, modelContextLimit) {
3136
+ return modelContextLimit >= ONE_MILLION_TOKENS &&
3137
+ fallbackBudgetTokens(model, modelContextLimit) === nearWindowBudgetTokens(modelContextLimit) &&
3138
+ typeof model === "string" && CLAUDE_FAMILY_RE.test(model);
3139
+ }
3140
+ const ONE_MILLION_TOKENS = 1_000_000;
3141
+ /**
3142
+ * The session's prepared prefix (prepared-compression.ts) as this turn's
3143
+ * ride, now the session's stable prefix, when it still covers this history
3144
+ * and prefix + the turns after it fit under the budget. Undefined otherwise,
3145
+ * and the prepared prefix is dropped: the turn compresses as before.
3146
+ */
3147
+ async function adoptPreparedPrefix(args) {
3148
+ const { state, sessionId } = args;
3149
+ const base = state.stablePrefixes.get(sessionId);
3150
+ await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
3151
+ const entry = state.prepared.get(sessionId);
3152
+ if (!entry)
3153
+ return undefined;
3154
+ state.prepared.discard(sessionId);
3155
+ const fit = await preparedFit(args, entry);
3156
+ // Another request installed a prefix, or `/memtree-compact` asked for its
3157
+ // own target, while this one waited or counted: that wins.
3158
+ const kind = fit.kind === "used" && (state.stablePrefixes.get(sessionId) !== base ||
3159
+ state.compactNow.has(sessionId) || compactionMode(args.opts, state, sessionId).mode !== "auto")
3160
+ ? "stale" : fit.kind;
3161
+ args.compaction.prepared = kind;
3162
+ if (kind !== "used" || fit.kind !== "used") {
3163
+ // A preparation not adopted is never sent, not even as a fallback.
3164
+ if (fit.candidate)
3165
+ args.sizing.excludeFallback(fit.candidate);
3166
+ if (kind === "too-large" && fit.candidate) {
3167
+ notePreparedTruncationBase(args, entry, base, fit.candidate);
3168
+ }
3169
+ return undefined;
3170
+ }
3171
+ args.compaction.estimatedTokens = fit.tokens;
3172
+ args.compaction.estimatedBytes = fit.routed.raw.length;
3173
+ installPreparedPrefix(state, sessionId, entry);
3174
+ return { prefix: entry.prefix, routed: fit.routed };
3175
+ }
3176
+ /** An adopted preparation becomes the session's stable prefix (and its page, persisted). */
3177
+ function installPreparedPrefix(state, sessionId, entry) {
3178
+ boundedSet(state.stablePrefixes, sessionId, entry.prefix);
3179
+ state.compactNow.delete(sessionId);
3180
+ if (entry.source)
3181
+ notePreparedSource(state, sessionId, entry.source);
3182
+ persistStablePrefix(state, sessionId, entry.prefix);
3183
+ }
3184
+ const preparedTruncationBases = new WeakMap();
3185
+ function notePreparedTruncationBase(args, entry, base, candidate) {
3186
+ const { state, sessionId } = args;
3187
+ preparedTruncationBases.set(args.sizing, { candidate, install: () => {
3188
+ if (state.stablePrefixes.get(sessionId) !== base || state.compactNow.has(sessionId) ||
3189
+ compactionMode(args.opts, state, sessionId).mode !== "auto")
3190
+ return;
3191
+ args.compaction.prepared = "truncated";
3192
+ installPreparedPrefix(state, sessionId, entry);
3193
+ } });
3194
+ }
3195
+ /**
3196
+ * The turn that crosses the budget while its session's preparation is still
3197
+ * in flight waits for it (it started earlier than a fresh compression could),
3198
+ * for at most `waitMs`, rather than making a second, concurrent compress call.
3199
+ */
3200
+ async function awaitPreparation(state, sessionId, waitMs, compaction) {
3201
+ const pending = state.preparing.get(sessionId);
3202
+ if (!pending)
3203
+ return;
3204
+ const started = Date.now();
3205
+ let timer;
3206
+ await Promise.race([pending, new Promise((resolve) => {
3207
+ timer = setTimeout(resolve, Math.max(0, waitMs));
3208
+ })]).finally(() => clearTimeout(timer));
3209
+ compaction.preparedWaitMs = Date.now() - started;
3210
+ }
3211
+ /** An adopted preparation's MemTree page becomes the session's memory source and page. */
3212
+ function notePreparedSource(state, sessionId, source) {
3213
+ const result = { messages: [], compressed: true, memtreeUrl: source.url,
3214
+ memtreeIndex: source.index };
3215
+ noteMemtreePage(state, source.seq, sessionId, result);
3216
+ noteMemorySource(state, source.seq, sessionId, result);
3217
+ }
3218
+ /** Whether a prepared prefix can carry this request (adoptPreparedPrefix). */
3219
+ async function preparedFit(args, entry) {
3220
+ const { sizing, body, messages, modelContextLimit, budgetTokens, compaction } = args;
3221
+ if (entry.prefix.modelContextLimit !== modelContextLimit || entry.triggerTokens !== budgetTokens ||
3222
+ entry.prefix.targetTokens !== compaction.targetTokens ||
3223
+ entry.prefix.explicitTarget !== (compaction.mode === "explicit")) {
3224
+ return { kind: "stale" };
3225
+ }
3226
+ if (prefixMismatch(body, messages, entry.prefix, args.sessionId, "stable")) {
3227
+ return { kind: "mismatch" };
3228
+ }
3229
+ const routed = prefixRoutedBody(body, messages, entry.prefix);
3230
+ if (!routed)
3231
+ return { kind: "mismatch" };
3232
+ const candidate = sizing.register(routed.raw, "ride", entry.prefix.lastSize);
3233
+ candidateSizeOwners.set(candidate, entry.prefix);
3234
+ await countIfOverBound(sizing, candidate, body, modelContextLimit);
3235
+ const tokens = sizing.plan(candidate).tokens;
3236
+ const fits = tokens < budgetTokens && fitsNative(tokens, { budget: budgetTokens,
3237
+ context: modelContextLimit, output: bodyOutputReservation(body, sizing.isExact(candidate)) }) &&
3238
+ (sizing.isExact(candidate) || sizing.boundFits(candidate, { budget: budgetTokens, context: modelContextLimit, output: bodyOutputReservation(body, false) }));
3239
+ // Too large, the turn compresses as before; the ride stays registered, so
3240
+ // if that cannot fit either, delivery's safety net may shorten its newest
3241
+ // tool result instead (truncatedToFit).
3242
+ return fits ? { kind: "used", routed, tokens, candidate } : { kind: "too-large", candidate };
3243
+ }
3244
+ /**
3245
+ * After a main-thread turn's plan: once its size nears the budget, ask
3246
+ * MemTree in the background for the compressed prefix the turn that crosses
3247
+ * it will ride (prepared-compression.ts). Fire and forget, one call per
3248
+ * session at a time; never delays this turn.
3249
+ */
3250
+ function maybePrepareCompression(args) {
3251
+ const { state, sessionId, rec } = args;
3252
+ const compaction = rec.compaction;
3253
+ // Automatic compaction only: an explicit target (/memtree-compact N) is the
3254
+ // user's own schedule, and a high one would leave nothing to prepare for.
3255
+ if (!compaction || compaction.mode !== "auto" || compaction.estimatedTokens === undefined ||
3256
+ compaction.targetTokens === undefined || state.keyInvalid() ||
3257
+ state.backgroundSignal.aborted ||
3258
+ state.pausedDetail() !== null ||
3259
+ !preparesCompression(args.body.model, args.modelContextLimit))
3260
+ return;
3261
+ const tokens = compaction.estimatedTokens;
3262
+ if (tokens < prepareStartTokens(compaction.budgetTokens))
3263
+ return;
3264
+ const decision = state.prepared.decide(sessionId, tokens, compaction.budgetTokens);
3265
+ rec.prepare = decision;
3266
+ if (decision !== "prepare" ||
3267
+ !state.prepared.begin(sessionId, tokens, compaction.budgetTokens))
3268
+ return;
3269
+ const base = state.stablePrefixes.get(sessionId);
3270
+ const pending = prepareInBackground({ ...args, tokens, targetTokens: compaction.targetTokens,
3271
+ explicitTarget: false, triggerTokens: compaction.budgetTokens,
3272
+ linkSeq: nextMemtreeCallSeq(state) })
3273
+ .catch(() => undefined)
3274
+ .then((prepared) => {
3275
+ // A prefix installed meanwhile (a blocking compaction) supersedes it.
3276
+ const current = state.stablePrefixes.get(sessionId) === base;
3277
+ state.prepared.finish(sessionId, current ? prepared : undefined);
3278
+ if (state.preparing.get(sessionId) === pending)
3279
+ state.preparing.delete(sessionId);
3280
+ });
3281
+ boundedSet(state.preparing, sessionId, pending);
3282
+ }
3283
+ /** The background prepare call and the prefix it yields (maybePrepareCompression). */
3284
+ async function prepareInBackground(args) {
3285
+ const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
3286
+ const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
3287
+ noteServerBudget(state, body.model, modelContextLimit, result);
3288
+ if (!result)
3289
+ return undefined;
3290
+ state.prepared.noteServerAnswer(result.prepared === true);
3291
+ if (result.prepared !== true || !didMemtreeCompress(result) ||
3292
+ !checkCompressedHistory(result, msgsForMemtree).usable)
3293
+ return undefined;
3294
+ const built = buildCompressedBody(body, result);
3295
+ if (!built)
3296
+ return undefined;
3297
+ const prefix = {
3298
+ sessionId,
3299
+ originalSystemHash: routeValueHash(normalizeRouteSystem(body.system)),
3300
+ ...prefixIdentity(args.messages, "stable"),
3301
+ compressedMessages: cloneJson(built.compressedBody.messages),
3302
+ compressedSystem: cloneJson(built.compressedBody.system),
3303
+ hasCompressedSystem: Object.prototype.hasOwnProperty.call(built.compressedBody, "system"),
3304
+ targetTokens: args.targetTokens,
3305
+ explicitTarget: args.explicitTarget,
3306
+ modelContextLimit,
3307
+ };
3308
+ const source = result.memtreeUrl && result.memtreeIndex
3309
+ ? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
3310
+ return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
3311
+ ...(source ? { source } : {}) };
3312
+ }
3313
+ /** What a prepare call sends: a forced compression to the target, no threshold. */
3314
+ function preparedCompressMeta(args) {
3315
+ const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
3316
+ const messageUsage = opts.transcriptUsage?.usageFor(sessionId, msgsForMemtree, undefined);
3317
+ const messageTimes = transcriptTimesFor(opts, sessionId, undefined)?.(msgsForMemtree);
3318
+ return {
3319
+ model: modelForMemtree(typeof body.model === "string" ? body.model : undefined, modelContextLimit),
3320
+ tools: Array.isArray(body.tools) ? body.tools : undefined,
3321
+ compressionTargetTokens: args.targetTokens,
3322
+ prepare: true,
3323
+ ...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
3324
+ ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3325
+ sessionId,
3326
+ ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3327
+ // Nothing waits on it: the long budget, so a slow server still prepares.
3328
+ timeoutMs: opts.memtree.longCompressBudgetMs,
3329
+ };
3330
+ }
3010
3331
  function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
3011
3332
  // Model is deliberately not route identity: Claude Code switches models
3012
3333
  // mid-loop (overload fallback, /model, /fast), and the compressed prefix is
@@ -3024,7 +3345,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
3024
3345
  if (messages.length < prefixLength)
3025
3346
  return null;
3026
3347
  for (let i = 0; i < prefixLength; i++) {
3027
- if (routeMessageHash(messages[i]) !== route.originalPrefixHashes[i]) {
3348
+ if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
3028
3349
  return null;
3029
3350
  }
3030
3351
  }
@@ -3407,8 +3728,9 @@ async function runBlockingCompression(args) {
3407
3728
  : undefined;
3408
3729
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
3409
3730
  const compressStarted = Date.now();
3410
- const budgetMs = args.longBudget
3411
- ? opts.memtree.longCompressBudgetMs : opts.memtree.compressBudgetMs;
3731
+ // The upload is about the request's size: the budget grows with it.
3732
+ const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? opts.memtree.longCompressBudgetMs
3733
+ : opts.memtree.compressBudgetForBytes(rec.requestBytes));
3412
3734
  const compressMeta = {
3413
3735
  // Model + tools drive the server's model-based memory budget
3414
3736
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3436,7 +3758,9 @@ async function runBlockingCompression(args) {
3436
3758
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3437
3759
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3438
3760
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3439
- ...(args.longBudget ? { timeoutMs: budgetMs } : {}),
3761
+ // The deadline this wait uses (the upload-scaled budget included), so the
3762
+ // call and its waiters end together.
3763
+ timeoutMs: budgetMs,
3440
3764
  };
3441
3765
  // Sampled BEFORE the call, while it still describes this call: after the
3442
3766
  // await the hash is in the cache regardless of who put it there.
@@ -3444,9 +3768,9 @@ async function runBlockingCompression(args) {
3444
3768
  // compress() maps every failure to a resolved null (it never rejects).
3445
3769
  // Keep foreground work finite even if a transport ignores its abort signal.
3446
3770
  let deadline;
3447
- const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : 30_000, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
3771
+ const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
3448
3772
  const result = await Promise.race([
3449
- opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
3773
+ opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
3450
3774
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
3451
3775
  ]).finally(() => { if (deadline)
3452
3776
  clearTimeout(deadline); });
@@ -3456,19 +3780,27 @@ async function runBlockingCompression(args) {
3456
3780
  // another class can overwrite the complete-key entry.
3457
3781
  const failureArming = result === null &&
3458
3782
  opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
3783
+ const failureStatus = result === null
3784
+ ? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
3459
3785
  const hadLiveSuccess = !cached && result !== null;
3460
3786
  // Only arming-class live failures (network error, timeout, 5xx, 402) are
3461
3787
  // fuse evidence. A responsive server's other 4xx failed THIS call but says
3462
3788
  // nothing about MemTree's health, so it contributes neither failure nor
3463
3789
  // success — the fuse is untouched.
3464
- const hadLiveFailure = !cached && result === null && failureArming;
3790
+ // Budget-consumed heuristic: the client maps every failure to null, so a
3791
+ // null that took (roughly) the whole abort budget was almost certainly
3792
+ // the AbortSignal timeout, not a fast server error.
3793
+ const timedOut = result === null && compressMs >= Math.min(budgetMs, maximumWait);
3794
+ // A timeout while other compress calls answered (one succeeded after this
3795
+ // one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
3796
+ // healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
3797
+ const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
3798
+ const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered;
3465
3799
  rec.compress = {
3466
3800
  ms: compressMs,
3467
3801
  ok: result !== null,
3468
- // Budget-consumed heuristic: the client maps every failure to null, so a
3469
- // null that took (roughly) the whole abort budget was almost certainly
3470
- // the AbortSignal timeout, not a fast server error.
3471
- timedOut: result === null && compressMs >= budgetMs,
3802
+ timedOut,
3803
+ ...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
3472
3804
  };
3473
3805
  return {
3474
3806
  result,
@@ -3480,6 +3812,61 @@ async function runBlockingCompression(args) {
3480
3812
  : "none",
3481
3813
  };
3482
3814
  }
3815
+ /**
3816
+ * A clearly under-budget human turn's compress call, not awaited: its answer
3817
+ * (a passthrough) only updates the server budget, MemTree health and the
3818
+ * session's page link. Its telemetry goes to a copy of the turn's record.
3819
+ */
3820
+ function compressInBackground(args) {
3821
+ if (args.state.backgroundSignal.aborted)
3822
+ return;
3823
+ const lanes = args.state.backgroundCompress;
3824
+ const lane = backgroundCompressLane(args);
3825
+ if (lane === undefined) {
3826
+ void runBackgroundCompress(args);
3827
+ return;
3828
+ }
3829
+ const busy = lanes.get(lane);
3830
+ if (busy) {
3831
+ // One whole-history call at a time per conversation: under the budget each turn
3832
+ // started its own (16 to 22 s each on staging in the long-session test, about 12 at
3833
+ // once beside the index uploads). A waiting call is replaced only by one whose
3834
+ // history contains all of it; otherwise it goes now.
3835
+ if (busy.waiting && !extendsHistory(args.msgsForMemtree, busy.waiting.msgsForMemtree)) {
3836
+ void runBackgroundCompress(busy.waiting);
3837
+ }
3838
+ busy.waiting = args;
3839
+ return;
3840
+ }
3841
+ const owner = {};
3842
+ lanes.set(lane, owner);
3843
+ void runBackgroundCompress(args).finally(() => {
3844
+ if (lanes.get(lane) === owner)
3845
+ lanes.delete(lane);
3846
+ if (owner.waiting)
3847
+ compressInBackground(owner.waiting);
3848
+ });
3849
+ }
3850
+ function runBackgroundCompress(args) {
3851
+ const { state, sessionId, linkSeq } = args;
3852
+ return runBlockingCompression({ ...args, rec: { ...args.rec }, signal: state.backgroundSignal })
3853
+ .then((compression) => {
3854
+ noteMemtreeHealth(state, compression);
3855
+ if (linkSeq !== undefined)
3856
+ noteMemtreePage(state, linkSeq, sessionId, compression.result);
3857
+ })
3858
+ .catch(() => undefined);
3859
+ }
3860
+ /**
3861
+ * A conversation for compressInBackground: session and first message, media left out (Claude
3862
+ * Code's media eviction keeps it, as indexLaneKey does); none without a session.
3863
+ */
3864
+ function backgroundCompressLane(args) {
3865
+ if (args.sessionId === undefined)
3866
+ return undefined;
3867
+ const first = args.msgsForMemtree.find((message) => message.role !== "system");
3868
+ return JSON.stringify([args.sessionId, first ? mediaFreeMessageHash(first) : ""]);
3869
+ }
3483
3870
  /**
3484
3871
  * Shared final request shape for a validated compression result: lift any
3485
3872
  * returned system message to `body.system`, and forward the SERVER's flatten
@@ -3579,14 +3966,14 @@ async function recoverToolRouteMiss(args) {
3579
3966
  capture(opts, "anthropic-request-memory-tool", fallback.raw);
3580
3967
  return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3581
3968
  toolForwarding.settle(delivered);
3582
- noteRideSize(fallback.sizeHolder, rec, fallback.raw.length);
3969
+ noteRideSize(fallback.sizeHolder, rec, fallback.raw);
3583
3970
  });
3584
3971
  }
3585
3972
  recordTurn(rec, "tool", args.originalBody);
3586
3973
  capture(opts, "anthropic-request", args.originalBody);
3587
3974
  return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3588
3975
  toolForwarding.settle(delivered);
3589
- noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
3976
+ noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
3590
3977
  });
3591
3978
  };
3592
3979
  // Same downstream-close tracking as the followup path: compression
@@ -3727,6 +4114,7 @@ async function recoverToolRouteMiss(args) {
3727
4114
  const { compressedBody, compressedRaw } = built;
3728
4115
  if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
3729
4116
  rec.routeRecovery = { conversationBytes, outcome: "unusable" };
4117
+ noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
3730
4118
  releaseOwnReservation();
3731
4119
  if (!noteIndexLag(opts, rec, result))
3732
4120
  return forwardOriginal();
@@ -3873,7 +4261,7 @@ async function recoverToolRouteMiss(args) {
3873
4261
  // The compacted request's reported size anchors the next budget check.
3874
4262
  const sizeHolder = stable?.installed ?? installedRoute;
3875
4263
  if (sizeHolder)
3876
- notePrefixSize(sizeHolder, rec, compressedRaw.length);
4264
+ notePrefixSize(sizeHolder, rec, compressedRaw);
3877
4265
  // Nothing to ride next turn, or a result that still does not fit the
3878
4266
  // window (the next turn would reject it and compress again): wait for
3879
4267
  // growth before the lane tries again.
@@ -3972,18 +4360,29 @@ const recapLinkAppenders = new WeakMap();
3972
4360
  * miss the tree, index a divergent conversation and bill for it.
3973
4361
  */
3974
4362
  async function recoverLateCount(args) {
4363
+ let generation;
3975
4364
  try {
3976
4365
  const { body, msgsForMemtree: messages } = args;
3977
4366
  const budget = resolveBudget(args.opts, args.state, body.model, args.modelContextLimit).tokens;
3978
- const target = lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
4367
+ const target = args.target ?? lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
4368
+ const sessionId = requestSessionId(args.req);
4369
+ const linkSeq = args.route.lane === "main" ? nextMemtreeCallSeq(args.state) : undefined;
4370
+ const replaces = sessionId !== undefined
4371
+ ? args.state.stablePrefixes.get(sessionId) ?? null : null;
4372
+ // Reserved before the compress call: a newer same-lane decision made
4373
+ // while it runs outranks this one, so this never overwrites its route.
4374
+ generation = ++args.state.mainRouteDecisionGeneration;
4375
+ reserveRouteDecision(args.state, args.route.key, generation);
3979
4376
  const outcome = await runBlockingCompression({
3980
4377
  opts: args.opts, state: args.state, body, msgsForMemtree: messages,
3981
4378
  hash: args.hash, modelContextLimit: args.modelContextLimit,
3982
- rec: args.rec, sessionId: requestSessionId(args.req), agentId: agentAttributionId(args.req),
4379
+ rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
3983
4380
  clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
3984
4381
  compaction: { target },
3985
4382
  });
3986
4383
  noteMemtreeHealth(args.state, outcome);
4384
+ if (linkSeq !== undefined)
4385
+ noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
3987
4386
  // Like every compress path: a refusal after a lagging answer waits for
3988
4387
  // the index, and the cached answer expires so a retry asks again.
3989
4388
  if (outcome.result)
@@ -3994,11 +4393,63 @@ async function recoverLateCount(args) {
3994
4393
  const built = buildCompressedBody(body, outcome.result);
3995
4394
  if (!built)
3996
4395
  return undefined;
4396
+ lateRouteInstalls.set(args.rec, lateRouteInstall(args, built.compressedBody, outcome.result, {
4397
+ targetTokens: target, explicitTarget: true, modelContextLimit: args.modelContextLimit,
4398
+ replaces
4399
+ }, linkSeq, generation));
4400
+ generation = undefined; // the install owns the reservation now
3997
4401
  return built.compressedRaw;
3998
4402
  }
3999
4403
  catch {
4000
4404
  return undefined;
4001
4405
  }
4406
+ finally {
4407
+ if (generation !== undefined)
4408
+ releaseRouteDecision(args.state, args.route.key, generation);
4409
+ }
4410
+ }
4411
+ /** Late recoveries' installs, per request, until delivery settles. */
4412
+ const lateRouteInstalls = new WeakMap();
4413
+ function lateRouteInstall(args, compressedBody, result, stable, linkSeq,
4414
+ /** Reserved (reserveRouteDecision) before the compress call; released here if unused. */
4415
+ generation) {
4416
+ const { state, route } = args;
4417
+ const sessionId = requestSessionId(args.req);
4418
+ let done = false;
4419
+ let installed;
4420
+ return {
4421
+ activate: () => {
4422
+ if (done)
4423
+ return;
4424
+ done = true;
4425
+ if (sessionId !== undefined && route.lane !== "away") {
4426
+ installed = installMemoryRoute(state, route.key, args.req, args.body, route.messages, compressedBody, route.epoch, generation, route.lane === "main" ? stable : undefined);
4427
+ }
4428
+ if (!installed)
4429
+ releaseRouteDecision(state, route.key, generation);
4430
+ else if (linkSeq !== undefined)
4431
+ noteMemorySource(state, linkSeq, sessionId, result);
4432
+ },
4433
+ settle(delivered, sent) {
4434
+ if (delivered)
4435
+ this.activate();
4436
+ // The replacement is a changed delivery (reportedSample skips those):
4437
+ // its reported size is this very body's, so it is noted directly.
4438
+ const holder = stable.installed ?? installed;
4439
+ const tokens = reportedInputTokens(args.rec);
4440
+ if (holder && sent && delivered && tokens !== undefined &&
4441
+ !serverToolRecords.has(args.rec) && !usedServerTools(args.rec)) {
4442
+ const sample = sizeSampleOf(sent, tokens, args.rec.model);
4443
+ if (plausibleSample(sample) && !(holder.lastSize && sent.length < holder.lastSize.forwardedBytes)) {
4444
+ holder.lastSize = sample;
4445
+ }
4446
+ }
4447
+ if (!installed) {
4448
+ done = true;
4449
+ releaseRouteDecision(state, route.key, generation);
4450
+ }
4451
+ },
4452
+ };
4002
4453
  }
4003
4454
  /**
4004
4455
  * The ride compressed to `target` still counted `rideTokens` (over the
@@ -4018,16 +4469,25 @@ async function selectForwardCandidate(args) {
4018
4469
  const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
4019
4470
  const measured = await policy.sizing.measure(preferred, args.limits);
4020
4471
  let target = preferred;
4021
- if (preferred.kind === "ride" && measured.source === "exact" &&
4022
- measured.tokens > args.limits.budget && !args.rec.compress && !args.rec.lateCountRecovery) {
4472
+ const late = lateRecoveryPlan(args.state, args.rec, preferred, measured, args.limits, args.routeKey);
4473
+ if (late) {
4023
4474
  args.rec.lateCountRecovery = { outcome: "forwarded" };
4024
- const replacement = await policy.recoverLate(preferred.body, measured.tokens);
4475
+ const replacement = await holdingLane(args.state, args.routeKey, () => policy.recoverLate(preferred.body, late.tokens, late.target));
4025
4476
  if (replacement)
4026
4477
  target = policy.sizing.register(replacement, "replacement");
4027
4478
  }
4028
- const selection = await policy.sizing.select(target, args.limits);
4479
+ const selection = await policy.sizing.select(target, args.limits) ??
4480
+ await policy.sizing.retryTimedOutCount(args.limits) ??
4481
+ (args.rec.routeLane === "main" && args.opts.toolOutputs
4482
+ ? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
4483
+ : undefined);
4029
4484
  const quietDegrade = quietDegrades.get(args.rec);
4030
4485
  quietDegrades.delete(args.rec);
4486
+ const lateInstall = lateRouteInstalls.get(args.rec);
4487
+ if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
4488
+ lateRouteInstalls.delete(args.rec);
4489
+ lateInstall.settle(false);
4490
+ }
4031
4491
  if (!selection) {
4032
4492
  if (args.rec.lateCountRecovery)
4033
4493
  args.rec.lateCountRecovery.outcome = "refused";
@@ -4035,6 +4495,10 @@ async function selectForwardCandidate(args) {
4035
4495
  return undefined;
4036
4496
  }
4037
4497
  const { candidate, measurement } = selection;
4498
+ const preparedBase = preparedTruncationBases.get(policy.sizing);
4499
+ if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
4500
+ preparedBase.install();
4501
+ }
4038
4502
  const changed = candidate !== preferred || intended !== args.preferredBody;
4039
4503
  if (changed) {
4040
4504
  policy.allowed.delete(args.preferredBody);
@@ -4057,6 +4521,154 @@ async function selectForwardCandidate(args) {
4057
4521
  }
4058
4522
  return changed ? candidate.body : intended;
4059
4523
  }
4524
+ /** MemTree's answer when the conversation cannot fit the window even compressed. */
4525
+ const OVER_CONTEXT_STATUS = 413;
4526
+ /** Truncation attempts, each cutting more, before the request is refused. */
4527
+ const TRUNCATION_ATTEMPTS = 3;
4528
+ /** Cut this much more than the measured excess (tokens per character vary). */
4529
+ const TRUNCATION_MARGIN = 1.25;
4530
+ /**
4531
+ * The safety net for a main-thread request that does not fit even after
4532
+ * compression (tool-result-truncation.ts): its newest tool result saved whole
4533
+ * (tool-output-store.ts) and sent with the middle replaced by a pointer to
4534
+ * that copy, cut until the request leaves the reply's room below the window.
4535
+ * A compressed candidate first (a ride on a prefix, or a replacement whose
4536
+ * suffix is structured), else the whole history.
4537
+ */
4538
+ async function truncatedToFit(rec, sizing, preferred, limits, store) {
4539
+ if (!compressionSettled(rec))
4540
+ return undefined;
4541
+ const prepared = preparedTruncationBases.get(sizing);
4542
+ const rank = (item) => item.kind === "original" ? 1 : 0;
4543
+ const bases = [preferred, ...sizing.fallbacks().filter((item) => item !== preferred),
4544
+ ...(prepared && prepared.candidate !== preferred ? [prepared.candidate] : [])]
4545
+ .sort((a, b) => rank(a) - rank(b) || a.body.length - b.body.length);
4546
+ for (const base of bases) {
4547
+ const fitted = await truncateCandidate(rec, sizing, base, limits, store);
4548
+ if (fitted && prepared && base === prepared.candidate)
4549
+ prepared.derived = fitted.candidate;
4550
+ if (fitted)
4551
+ return fitted;
4552
+ }
4553
+ return undefined;
4554
+ }
4555
+ /**
4556
+ * Whether compression had its chance and the request still cannot fit: a ride
4557
+ * on a prepared prefix, a compress answer that compressed (sent or found too
4558
+ * large), or MemTree's 413 (it cannot fit the window even compressed). Not
4559
+ * after a failed or skipped compress call (an outage, an exception, an auth
4560
+ * or payment refusal, a backoff: the retryable refusal stands), a passthrough
4561
+ * of a warming index, or while the index is behind: waiting for it lets
4562
+ * compression summarize instead.
4563
+ */
4564
+ function compressionSettled(rec) {
4565
+ if (rec.indexBehind === true)
4566
+ return false;
4567
+ if (rec.compress?.refused === true || rec.compaction?.prepared === "used")
4568
+ return true;
4569
+ return rec.compress?.ok === true && rec.routeRecovery?.outcome !== "noop" &&
4570
+ rec.turnType !== "followup-noop";
4571
+ }
4572
+ /** truncatedToFit for one candidate; undefined when it has no tool result to
4573
+ * shorten, the copy could not be saved, or it cannot be made to fit. */
4574
+ async function truncateCandidate(rec, sizing, base, limits, store) {
4575
+ const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
4576
+ const parsed = JSON.parse(base.body.toString("utf-8"));
4577
+ const first = await sizing.measure(base, limits);
4578
+ const bytesPerToken = base.body.length / Math.max(1, first.tokens);
4579
+ for (let attempt = 1; attempt <= TRUNCATION_ATTEMPTS; attempt++) {
4580
+ const excess = Math.max(1_000, first.tokens - goal);
4581
+ const cut = Math.ceil(excess * bytesPerToken * TRUNCATION_MARGIN * attempt);
4582
+ // The store pins each copy it saves, so none this request points at is
4583
+ // evicted while it is counted and forwarded.
4584
+ const plan = truncateToolResults(parsed, cut, (text) => store.save(text));
4585
+ if (!plan)
4586
+ return undefined;
4587
+ const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
4588
+ const measurement = await sizing.measure(candidate, limits);
4589
+ if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
4590
+ rec.truncatedToolResult = { originalChars: plan.originalChars,
4591
+ omittedChars: plan.omittedChars };
4592
+ return { candidate, measurement };
4593
+ }
4594
+ }
4595
+ return undefined;
4596
+ }
4597
+ /**
4598
+ * Whether delivery recompresses before sending, and to what: a ride counted
4599
+ * over the budget (no compression yet); the whole history counted over the
4600
+ * window with no compression yet (outside an outage cooldown: a refusal is
4601
+ * all that is left otherwise); or a compression that came back too large for
4602
+ * the window (oversizedCompressions), redone once to a target smaller by the
4603
+ * excess, so the turn is compressed and sent in the same request instead of
4604
+ * being refused and retried.
4605
+ */
4606
+ function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
4607
+ if (rec.lateCountRecovery)
4608
+ return undefined;
4609
+ const oversized = oversizedCompressions.get(rec);
4610
+ // A lagging index kept history verbatim: the refusal waits for it instead.
4611
+ if (oversized && rec.indexBehind !== true) {
4612
+ const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
4613
+ const excess = oversized.tokens - goal;
4614
+ const target = Math.floor(oversized.target - excess - oversized.target * SMALLER_TARGET_MARGIN);
4615
+ return target >= SERVER_MIN_TARGET_TOKENS ? { tokens: oversized.tokens, target } : undefined;
4616
+ }
4617
+ if (rec.compress || measured.source !== "exact" || !(measured.tokens > limits.budget)) {
4618
+ return undefined;
4619
+ }
4620
+ // Another request of this lane is compressing (rec.routeRecovery "in-flight"):
4621
+ // a second compression of the same history at another target would miss its
4622
+ // dedupe and bill twice. This one waits for neither and goes as it is.
4623
+ if (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight)
4624
+ return undefined;
4625
+ if (preferred.kind === "ride")
4626
+ return { tokens: measured.tokens };
4627
+ // Over the window with nothing compressed, a refusal is all that is left:
4628
+ // an outage cooldown does not skip this one compress.
4629
+ return preferred.kind === "original" && !fitsMeasured(measured, limits)
4630
+ ? { tokens: measured.tokens } : undefined;
4631
+ }
4632
+ /**
4633
+ * Run a delivery-time compression as the lane's in-flight operation, so a
4634
+ * concurrent request of the lane neither starts its own (planning sees
4635
+ * "in-flight") nor races it; the lane's own attempt state is kept.
4636
+ */
4637
+ async function holdingLane(state, routeKey, run) {
4638
+ const lanes = state.toolRecoveryAttemptedLanes;
4639
+ const prior = lanes.get(routeKey);
4640
+ const attempt = prior ?? { modelContextLimit: 0, inFlight: true };
4641
+ // Clears only what it set: an attempt another request holds in flight
4642
+ // stays that request's to end.
4643
+ const setHere = !attempt.inFlight;
4644
+ attempt.inFlight = true;
4645
+ if (!prior)
4646
+ lanes.set(routeKey, attempt);
4647
+ try {
4648
+ return await run();
4649
+ }
4650
+ finally {
4651
+ if (setHere)
4652
+ attempt.inFlight = false;
4653
+ if (!prior && lanes.get(routeKey) === attempt)
4654
+ lanes.delete(routeKey);
4655
+ }
4656
+ }
4657
+ /** A recompression aims this share of its old target lower than the excess alone. */
4658
+ const SMALLER_TARGET_MARGIN = 0.1;
4659
+ /**
4660
+ * Compressions of this request that came back too large for the window: their
4661
+ * size (an exact count, else the conservative bound) and the target they had.
4662
+ */
4663
+ const oversizedCompressions = new WeakMap();
4664
+ /** Note a compression of `rec` that does not fit the window (lateRecoveryPlan). */
4665
+ function noteOversizedCompression(rec, sizing, body, compressedRaw, target) {
4666
+ if (target === undefined)
4667
+ return;
4668
+ const tokens = sizing.exactTokensFor(compressedRaw) ??
4669
+ conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
4670
+ oversizedCompressions.set(rec, { tokens, target });
4671
+ }
4060
4672
  /**
4061
4673
  * Whether delivery would refuse `candidate` sent whole, by delivery's own
4062
4674
  * admission: it is sendable only when its plan fits and either an exact count
@@ -4207,7 +4819,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
4207
4819
  const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
4208
4820
  if (!selected)
4209
4821
  return false;
4210
- const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : undefined);
4822
+ // A late recovery's replacement went out instead: it installs as its route.
4823
+ const lateInstall = rec && selected !== bodyBuffer ? lateRouteInstalls.get(rec) : undefined;
4824
+ if (rec)
4825
+ lateRouteInstalls.delete(rec);
4826
+ const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : lateInstall?.activate);
4827
+ lateInstall?.settle(delivered, selected);
4211
4828
  if (delivered && rec)
4212
4829
  noteSelectedCandidateSize(rec);
4213
4830
  return delivered;
@@ -4222,7 +4839,7 @@ function noteSelectedCandidateSize(rec) {
4222
4839
  // Server tools' injected results inflate the reported input (reportedSample).
4223
4840
  if (serverToolRecords.has(rec) || usedServerTools(rec))
4224
4841
  return;
4225
- const sample = { tokens, forwardedBytes: candidate.body.length };
4842
+ const sample = sizeSampleOf(candidate.body, tokens, rec.model);
4226
4843
  if (!plausibleSample(sample))
4227
4844
  return;
4228
4845
  if ("set" in owner)
@@ -4948,4 +5565,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
4948
5565
  return undefined;
4949
5566
  return (messages) => source.timesFor(sessionId, messages, agentId);
4950
5567
  }
5568
+ export const __testHoldingLane = holdingLane;
4951
5569
  //# sourceMappingURL=proxy.js.map