claude-code-infinite 1.0.28 → 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 (121) hide show
  1. package/README.md +96 -19
  2. package/dist/auto-update.d.ts +2 -0
  3. package/dist/auto-update.d.ts.map +1 -1
  4. package/dist/auto-update.js +4 -0
  5. package/dist/auto-update.js.map +1 -1
  6. package/dist/claude-auth.d.ts +27 -2
  7. package/dist/claude-auth.d.ts.map +1 -1
  8. package/dist/claude-auth.js +76 -4
  9. package/dist/claude-auth.js.map +1 -1
  10. package/dist/claude-install.d.ts +73 -0
  11. package/dist/claude-install.d.ts.map +1 -0
  12. package/dist/claude-install.js +142 -0
  13. package/dist/claude-install.js.map +1 -0
  14. package/dist/claude-plan-warning.d.ts +70 -7
  15. package/dist/claude-plan-warning.d.ts.map +1 -1
  16. package/dist/claude-plan-warning.js +152 -8
  17. package/dist/claude-plan-warning.js.map +1 -1
  18. package/dist/cli-lifecycle.d.ts +45 -1
  19. package/dist/cli-lifecycle.d.ts.map +1 -1
  20. package/dist/cli-lifecycle.js +59 -0
  21. package/dist/cli-lifecycle.js.map +1 -1
  22. package/dist/cli-main.js +136 -29
  23. package/dist/cli-main.js.map +1 -1
  24. package/dist/count-tokens.d.ts +15 -0
  25. package/dist/count-tokens.d.ts.map +1 -1
  26. package/dist/count-tokens.js +23 -1
  27. package/dist/count-tokens.js.map +1 -1
  28. package/dist/diagnostics-schema.d.ts +4 -0
  29. package/dist/diagnostics-schema.d.ts.map +1 -1
  30. package/dist/diagnostics-schema.js +25 -0
  31. package/dist/diagnostics-schema.js.map +1 -1
  32. package/dist/diagnostics.d.ts +24 -1
  33. package/dist/diagnostics.d.ts.map +1 -1
  34. package/dist/diagnostics.js +86 -5
  35. package/dist/diagnostics.js.map +1 -1
  36. package/dist/final-index.d.ts +7 -1
  37. package/dist/final-index.d.ts.map +1 -1
  38. package/dist/final-index.js +10 -3
  39. package/dist/final-index.js.map +1 -1
  40. package/dist/image-refs.d.ts +51 -0
  41. package/dist/image-refs.d.ts.map +1 -0
  42. package/dist/image-refs.js +169 -0
  43. package/dist/image-refs.js.map +1 -0
  44. package/dist/image-tokens.d.ts +37 -0
  45. package/dist/image-tokens.d.ts.map +1 -0
  46. package/dist/image-tokens.js +158 -0
  47. package/dist/image-tokens.js.map +1 -0
  48. package/dist/install-ping.d.ts +54 -0
  49. package/dist/install-ping.d.ts.map +1 -0
  50. package/dist/install-ping.js +177 -0
  51. package/dist/install-ping.js.map +1 -0
  52. package/dist/memtree-body.d.ts +52 -0
  53. package/dist/memtree-body.d.ts.map +1 -0
  54. package/dist/memtree-body.js +99 -0
  55. package/dist/memtree-body.js.map +1 -0
  56. package/dist/memtree-finder.d.ts +2 -1
  57. package/dist/memtree-finder.d.ts.map +1 -1
  58. package/dist/memtree-finder.js +4 -5
  59. package/dist/memtree-finder.js.map +1 -1
  60. package/dist/memtree-mcp.d.ts.map +1 -1
  61. package/dist/memtree-mcp.js +17 -7
  62. package/dist/memtree-mcp.js.map +1 -1
  63. package/dist/memtree-times.d.ts +35 -0
  64. package/dist/memtree-times.d.ts.map +1 -0
  65. package/dist/memtree-times.js +166 -0
  66. package/dist/memtree-times.js.map +1 -0
  67. package/dist/memtree-tools.d.ts +12 -0
  68. package/dist/memtree-tools.d.ts.map +1 -1
  69. package/dist/memtree-tools.js +63 -7
  70. package/dist/memtree-tools.js.map +1 -1
  71. package/dist/memtree.d.ts +77 -3
  72. package/dist/memtree.d.ts.map +1 -1
  73. package/dist/memtree.js +267 -53
  74. package/dist/memtree.js.map +1 -1
  75. package/dist/paused-session.d.ts.map +1 -1
  76. package/dist/paused-session.js +11 -8
  77. package/dist/paused-session.js.map +1 -1
  78. package/dist/postinstall.d.ts +2 -0
  79. package/dist/postinstall.d.ts.map +1 -0
  80. package/dist/postinstall.js +4 -0
  81. package/dist/postinstall.js.map +1 -0
  82. package/dist/prefix-store.d.ts +2 -0
  83. package/dist/prefix-store.d.ts.map +1 -1
  84. package/dist/prefix-store.js +4 -0
  85. package/dist/prefix-store.js.map +1 -1
  86. package/dist/prepared-compression.d.ts +78 -0
  87. package/dist/prepared-compression.d.ts.map +1 -0
  88. package/dist/prepared-compression.js +127 -0
  89. package/dist/prepared-compression.js.map +1 -0
  90. package/dist/proxy.d.ts +370 -13
  91. package/dist/proxy.d.ts.map +1 -1
  92. package/dist/proxy.js +790 -92
  93. package/dist/proxy.js.map +1 -1
  94. package/dist/rate-limit.d.ts +64 -0
  95. package/dist/rate-limit.d.ts.map +1 -0
  96. package/dist/rate-limit.js +126 -0
  97. package/dist/rate-limit.js.map +1 -0
  98. package/dist/reqlog.d.ts +66 -2
  99. package/dist/reqlog.d.ts.map +1 -1
  100. package/dist/reqlog.js.map +1 -1
  101. package/dist/request-sizing.d.ts +21 -2
  102. package/dist/request-sizing.d.ts.map +1 -1
  103. package/dist/request-sizing.js +57 -11
  104. package/dist/request-sizing.js.map +1 -1
  105. package/dist/route-identity.d.ts +27 -0
  106. package/dist/route-identity.d.ts.map +1 -1
  107. package/dist/route-identity.js +88 -8
  108. package/dist/route-identity.js.map +1 -1
  109. package/dist/token-estimate.d.ts +3 -20
  110. package/dist/token-estimate.d.ts.map +1 -1
  111. package/dist/token-estimate.js +9 -50
  112. package/dist/token-estimate.js.map +1 -1
  113. package/dist/tool-output-store.d.ts +12 -0
  114. package/dist/tool-output-store.d.ts.map +1 -0
  115. package/dist/tool-output-store.js +146 -0
  116. package/dist/tool-output-store.js.map +1 -0
  117. package/dist/tool-result-truncation.d.ts +32 -0
  118. package/dist/tool-result-truncation.d.ts.map +1 -0
  119. package/dist/tool-result-truncation.js +135 -0
  120. package/dist/tool-result-truncation.js.map +1 -0
  121. package/package.json +2 -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,7 +64,9 @@ 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";
69
+ import { rateLimitFromResponse } from "./rate-limit.js";
66
70
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
67
71
  const DEFAULT_UPSTREAM = "https://api.anthropic.com";
68
72
  const HOOK_BODY_LIMIT = 64 * 1024;
@@ -77,6 +81,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
77
81
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
78
82
  /** Safety cap on a long-budget compress, should a transport ignore its abort. */
79
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;
80
86
  const SKIP_REQUEST_HEADERS = new Set([
81
87
  "host",
82
88
  "connection",
@@ -94,23 +100,41 @@ const SKIP_RESPONSE_HEADERS = new Set([
94
100
  "transfer-encoding",
95
101
  ]);
96
102
  /**
97
- * Budget fallback until the server reports `model_budget_tokens`: this share
98
- * of the model's context window. 0.8 by default; NEAR_WINDOW_FALLBACK_RATIO
99
- * (900k of 1M) for the 1M-window models the server budgets at 900k for ccc
100
- * (NEAR_WINDOW_MODEL_RE). Plain Claude Code 2.1.302 auto-compacts a 1M window
101
- * at 967k, so MemTree compresses no earlier than ~70k before it would; ccc
103
+ * Budget fallback until the server reports `model_budget_tokens`, mirroring the
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
102
111
  * lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
103
- * jumps past the budget reaches the proxy and is compressed here.
112
+ * jumps past the budget reaches the proxy and is compressed here, on a
113
+ * prepared prefix when there is one (prepared-compression.ts).
104
114
  */
105
115
  export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
106
- export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
107
- /** Opus 5.5, Opus 5, Opus 4.8 and Fable 5 / 5.1 (polychat LARGE_CONTEXT_MODELS). */
108
- const NEAR_WINDOW_MODEL_RE = /claude-(?:opus-(?:4-8|5-5|5)|fable-5(?:-1)?)(?![0-9])/i;
109
- const ONE_MILLION_TOKENS = 1_000_000;
110
- /** The fallback share of the window for `model` (see FALLBACK_BUDGET_WINDOW_RATIO). */
111
- export function fallbackBudgetRatio(model, modelContextLimit) {
112
- return typeof model === "string" && modelContextLimit >= ONE_MILLION_TOKENS &&
113
- NEAR_WINDOW_MODEL_RE.test(model) ? NEAR_WINDOW_FALLBACK_RATIO : FALLBACK_BUDGET_WINDOW_RATIO;
116
+ /** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
117
+ const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
118
+ /**
119
+ * polychat USABLE_CONTEXT_CEILING_TOKENS_BY_MODEL: the Sonnets degrade well
120
+ * before their window. Sonnet 5 / 5.5 550k; every other Sonnet 400k. First match wins.
121
+ */
122
+ const SONNET_CEILINGS = [
123
+ [/sonnet-5(?![0-9])/i, 550_000],
124
+ [/sonnet/i, 400_000],
125
+ ];
126
+ /** The fallback whole-request budget for `model` on this window (see above). */
127
+ export function fallbackBudgetTokens(model, modelContextLimit) {
128
+ if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
129
+ return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
130
+ }
131
+ const nearWindow = nearWindowBudgetTokens(modelContextLimit);
132
+ const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
133
+ return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
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);
114
138
  }
115
139
  /**
116
140
  * An explicit target is capped at this share of the budget: a compaction to
@@ -303,6 +327,7 @@ export function startProxy(opts) {
303
327
  const activeRequests = new Set();
304
328
  const acceptedRequests = new Set();
305
329
  const shutdownAbort = new AbortController();
330
+ const backgroundAbort = new AbortController();
306
331
  const state = {
307
332
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
308
333
  keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
@@ -313,6 +338,7 @@ export function startProxy(opts) {
313
338
  pausedDetail: () => opts.memtree.pausedDetail,
314
339
  usageUrl: opts.memtree.usageDashboardUrl,
315
340
  sessionStartNotice: opts.sessionStartNotice,
341
+ promptSubmitNotice: opts.promptSubmitNotice,
316
342
  projectTrees: opts.projectTrees,
317
343
  diagnostics: opts.diagnostics,
318
344
  notices: new NoticeDeliveryQueue(),
@@ -329,6 +355,7 @@ export function startProxy(opts) {
329
355
  toolRecoveryAttemptedLanes: new Map(),
330
356
  toolRecoveryCooldownUntil: 0,
331
357
  shutdownSignal: shutdownAbort.signal,
358
+ backgroundSignal: backgroundAbort.signal,
332
359
  routeInstallFault: opts.routeInstallFault,
333
360
  memtreePages: new Map(),
334
361
  memorySourcePages: new Map(),
@@ -344,6 +371,9 @@ export function startProxy(opts) {
344
371
  defaultCompactOff: opts.defaultCompactTarget === null,
345
372
  compactNow: new Set(),
346
373
  stablePrefixes: new Map(),
374
+ prepared: new PreparedCompressions(),
375
+ preparing: new Map(),
376
+ backgroundCompress: new Map(),
347
377
  serverBudgets: new Map(),
348
378
  serverReportsBudget: false,
349
379
  countTokensCooldowns: new CountTokensCooldowns(),
@@ -394,6 +424,7 @@ export function startProxy(opts) {
394
424
  const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
395
425
  ? Math.floor(timeoutMs)
396
426
  : 5_000;
427
+ backgroundAbort.abort();
397
428
  const quiesced = (async () => {
398
429
  // Once close completes, every accepted request has entered the tracked
399
430
  // set.
@@ -790,6 +821,17 @@ onMainStop) {
790
821
  state.promptAccounting.add(parsed.session_id, parsed.prompt_id, parsed.prompt);
791
822
  recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
792
823
  }
824
+ const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
825
+ if (line) {
826
+ const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
827
+ res.writeHead(200, {
828
+ "content-type": "application/json",
829
+ "content-length": String(body.length),
830
+ "cache-control": "no-store",
831
+ });
832
+ res.end(body);
833
+ return;
834
+ }
793
835
  res.writeHead(204);
794
836
  res.end();
795
837
  return;
@@ -1060,7 +1102,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1060
1102
  if (!rides) {
1061
1103
  const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
1062
1104
  if (measured.source === "exact") {
1063
- sizeSlot.set({ tokens: measured.tokens, forwardedBytes: forwardCandidate.body.length });
1105
+ sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
1064
1106
  }
1065
1107
  }
1066
1108
  rec.routeLane = requestRouteLane;
@@ -1121,12 +1163,18 @@ async function handleMessages(req, res, opts, upstream, state) {
1121
1163
  },
1122
1164
  // The ride's bytes are only what failed; recovery always compresses the
1123
1165
  // ORIGINAL conversation (the one MemTree indexed), never the ride itself.
1124
- recoverLate: async (_ride, rideTokens) => {
1125
- 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) ||
1126
1171
  (rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
1127
1172
  return undefined;
1128
1173
  return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
1129
- 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 } : {}) });
1130
1178
  },
1131
1179
  delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
1132
1180
  });
@@ -1273,6 +1321,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1273
1321
  ride: toolRide,
1274
1322
  canCompress,
1275
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
+ }
1276
1329
  if (plan.kind === "ride")
1277
1330
  toolRide = plan.ride;
1278
1331
  if (plan.kind === "pass" && plan.original) {
@@ -1312,9 +1365,13 @@ async function handleMessages(req, res, opts, upstream, state) {
1312
1365
  // for growth instead of paying a blocking call on every tool turn.
1313
1366
  rec.routeRecovery = { outcome: "backoff" };
1314
1367
  }
1315
- 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())))) {
1316
1371
  // A recent attempt burned the full compress budget and still
1317
- // 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
1318
1375
  // compress() dedup can never absorb the repeat: without this
1319
1376
  // cooldown a MemTree outage would add the whole blocking budget to
1320
1377
  // every tool turn over the budget. A cooldown skip does not start
@@ -1395,9 +1452,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1395
1452
  return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1396
1453
  toolForwarding.settle(delivered);
1397
1454
  if (toolRide)
1398
- noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
1455
+ noteRideSize(toolRide.sizeHolder, rec, sendBody);
1399
1456
  else {
1400
- noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
1457
+ noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
1401
1458
  }
1402
1459
  }));
1403
1460
  }
@@ -1440,19 +1497,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1440
1497
  const sessionId = requestSessionId(req);
1441
1498
  // Stable-prefix (edge) compaction: the main thread's human turns only. The
1442
1499
  // recap rides the main route above; subagents keep per-turn compression.
1443
- const edge = isMainRequest && sessionId !== undefined
1444
- ? await planEdgeCompaction({
1445
- sizing: requestSizing,
1446
- opts,
1447
- state,
1448
- body,
1449
- messages,
1450
- sessionId,
1451
- modelContextLimit,
1452
- forwardBody,
1453
- rec,
1454
- })
1455
- : 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
+ }
1456
1523
  // An active subagent can repeat/embed the human prompt in its own request;
1457
1524
  // producer suppression must also preserve the arm for the later main call.
1458
1525
  if (displayForThisTurn)
@@ -1467,6 +1534,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1467
1534
  // A lagging index left history verbatim: a refusal waits for indexing.
1468
1535
  if (result)
1469
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
+ }
1470
1541
  releaseUncommittedRouteDecision();
1471
1542
  recordTurn(rec, "followup-degraded", rawBody);
1472
1543
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
@@ -1567,7 +1638,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1567
1638
  : stable.installed
1568
1639
  : undefined;
1569
1640
  if (sizePrefix)
1570
- notePrefixSize(sizePrefix, rec, compressedRaw.length);
1641
+ notePrefixSize(sizePrefix, rec, compressedRaw);
1571
1642
  // A route already installed at protocol-complete deliberately survives
1572
1643
  // delivered=false: that settle may be the fast-tool abort (client
1573
1644
  // consumed message_stop, closed the SSE response, and its immediate
@@ -1604,7 +1675,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1604
1675
  }
1605
1676
  }));
1606
1677
  };
1607
- if (existingRide && (!edge || edge.kind === "off")) {
1678
+ if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
1608
1679
  const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
1609
1680
  const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
1610
1681
  candidateSizeOwners.set(candidate, owner);
@@ -1636,8 +1707,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1636
1707
  const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
1637
1708
  const noteWholeSize = () => {
1638
1709
  if (isMainRequest)
1639
- notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
1710
+ notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
1640
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
+ }
1641
1733
  // The complete request upload can outlive its downstream subscriber while
1642
1734
  // MemTree compression is in flight. Track that subscriber locally without
1643
1735
  // feeding its lifetime into MemtreeClient.compress(): compression promises
@@ -1687,7 +1779,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1687
1779
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
1688
1780
  .then((delivered) => {
1689
1781
  if (isMainRequest)
1690
- notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
1782
+ notePassthroughSize(state, sessionId, body.model, rec, rawBody);
1691
1783
  if (delivered)
1692
1784
  markMainPromptDelivered();
1693
1785
  }));
@@ -1762,7 +1854,24 @@ async function handleMessages(req, res, opts, upstream, state) {
1762
1854
  state.nearWindowNotice = handle;
1763
1855
  }
1764
1856
  else if (mayQueueNotice && paymentDetail === null) {
1765
- state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
1857
+ // Under the budget the whole request is what compression would have
1858
+ // sent: the turn is not degraded, so the failure stays in the logs.
1859
+ const warn = () => {
1860
+ state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
1861
+ rec.degradedNotice = "shown";
1862
+ };
1863
+ if (compressionNeeded(rec.compaction)) {
1864
+ warn();
1865
+ }
1866
+ else {
1867
+ // Delivery sizes the request again (an exact count, else a
1868
+ // conservative bound): a turn it finds over the budget warns then.
1869
+ rec.degradedNotice = "quiet-under-budget";
1870
+ quietDegrades.set(rec, () => {
1871
+ if (state.mainPromptGeneration === noticePromptGeneration)
1872
+ warn();
1873
+ });
1874
+ }
1766
1875
  }
1767
1876
  return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1768
1877
  noteWholeSize();
@@ -1996,6 +2105,15 @@ function resumePaymentNotice(state, input) {
1996
2105
  state.diagnostics?.record("paused_notice", { trigger: "resume" });
1997
2106
  return state.notices.warningText(paymentNoticeText(detail));
1998
2107
  }
2108
+ /** ProxyOptions.promptSubmitNotice's line for this prompt; never throws. */
2109
+ function promptSubmitLine(state) {
2110
+ try {
2111
+ return state.promptSubmitNotice?.();
2112
+ }
2113
+ catch {
2114
+ return undefined;
2115
+ }
2116
+ }
1999
2117
  /** The caller's extra SessionStart line; a throwing provider shows nothing. */
2000
2118
  function extraSessionStartLine(provider, input) {
2001
2119
  if (!provider || input.agent_id !== undefined || input.source === "compact")
@@ -2260,7 +2378,8 @@ function rememberPage(pages, sessionId, page) {
2260
2378
  */
2261
2379
  function noteMemtreeHealth(state, compression) {
2262
2380
  if (compression.liveHealth === "failure") {
2263
- if (state.shutdownSignal.aborted)
2381
+ // Background calls the drain stopped (backgroundSignal) are no outage either.
2382
+ if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
2264
2383
  return;
2265
2384
  state.toolRecoveryCooldownUntil =
2266
2385
  Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
@@ -2343,7 +2462,7 @@ stable) {
2343
2462
  // Include trailing ambient role=system blocks: MemTree consolidated them
2344
2463
  // into compressedBody.system, so treating them as suffix would duplicate
2345
2464
  // those instructions on every tool request.
2346
- originalPrefixHashes: originalMessages.map(routeMessageHash),
2465
+ ...prefixIdentity(originalMessages, "route"),
2347
2466
  compressedMessages: cloneJson(compressedBody.messages),
2348
2467
  compressedSystem: cloneJson(compressedBody.system),
2349
2468
  hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
@@ -2382,7 +2501,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2382
2501
  const prefix = {
2383
2502
  sessionId,
2384
2503
  originalSystemHash: compressed.originalSystemHash,
2385
- originalPrefixHashes: originalMessages.map(stablePrefixMessageHash),
2504
+ ...prefixIdentity(originalMessages, "stable"),
2386
2505
  compressedMessages: compressed.compressedMessages,
2387
2506
  compressedSystem: compressed.compressedSystem,
2388
2507
  hasCompressedSystem: compressed.hasCompressedSystem,
@@ -2393,6 +2512,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2393
2512
  stable.installed = prefix;
2394
2513
  boundedSet(state.stablePrefixes, sessionId, prefix);
2395
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);
2396
2518
  return prefix;
2397
2519
  }
2398
2520
  /**
@@ -2413,6 +2535,7 @@ function persistStablePrefix(state, sessionId, prefix) {
2413
2535
  store.save(sessionId, owner, {
2414
2536
  originalSystemHash: prefix.originalSystemHash,
2415
2537
  originalPrefixHashes: prefix.originalPrefixHashes,
2538
+ originalMediaFreeHashes: prefix.originalMediaFreeHashes,
2416
2539
  compressedMessages: prefix.compressedMessages,
2417
2540
  compressedSystem: prefix.compressedSystem,
2418
2541
  hasCompressedSystem: prefix.hasCompressedSystem,
@@ -2465,7 +2588,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
2465
2588
  function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
2466
2589
  if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
2467
2590
  return "defer";
2468
- const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
2591
+ const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
2469
2592
  if (miss === null)
2470
2593
  return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
2471
2594
  const authoritative = miss === "prefix" &&
@@ -2495,7 +2618,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
2495
2618
  * at least one message after them. The suffix may be anything, human turns
2496
2619
  * included.
2497
2620
  */
2498
- function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMessageHash) {
2621
+ function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
2499
2622
  if (!sessionId || prefix.sessionId !== sessionId)
2500
2623
  return "session";
2501
2624
  if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
@@ -2505,7 +2628,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
2505
2628
  if (messages.length < prefixLength)
2506
2629
  return "prefix";
2507
2630
  for (let i = 0; i < prefixLength; i++) {
2508
- if (hashMessage(messages[i]) !== prefix.originalPrefixHashes[i]) {
2631
+ if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
2509
2632
  return "prefix";
2510
2633
  }
2511
2634
  }
@@ -2549,20 +2672,24 @@ function serverBudgetKey(model, modelContextLimit) {
2549
2672
  return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
2550
2673
  }
2551
2674
  /**
2552
- * The session's whole-request budget: CCC_BUDGET_TOKENS when set (tests),
2553
- * else the model budget the server last reported for this model and window,
2554
- * else the window times fallbackBudgetRatio (servers that predate
2555
- * `model_budget_tokens`, and 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.
2556
2682
  */
2557
2683
  function resolveBudget(opts, state, model, modelContextLimit) {
2684
+ const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
2558
2685
  if (opts.budgetTokensOverride !== undefined) {
2559
- return { tokens: opts.budgetTokensOverride, source: "override" };
2686
+ return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
2560
2687
  }
2561
2688
  const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
2562
2689
  if (reported !== undefined)
2563
- return { tokens: reported, source: "server" };
2690
+ return { tokens: trigger(reported), source: "server" };
2564
2691
  return {
2565
- tokens: Math.floor(modelContextLimit * fallbackBudgetRatio(model, modelContextLimit)),
2692
+ tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
2566
2693
  source: "window-ratio",
2567
2694
  };
2568
2695
  }
@@ -2645,17 +2772,17 @@ function reportedInputTokens(rec) {
2645
2772
  (usage.cache_creation_input_tokens ?? 0));
2646
2773
  }
2647
2774
  /** Keep the newest (largest-body) reported size of a request on this prefix. */
2648
- function notePrefixSize(prefix, rec, bytes) {
2649
- const sample = reportedSample(rec, bytes);
2775
+ function notePrefixSize(prefix, rec, sent) {
2776
+ const sample = reportedSample(rec, sent);
2650
2777
  if (sample === undefined)
2651
2778
  return;
2652
- if (prefix.lastSize && bytes < prefix.lastSize.forwardedBytes)
2779
+ if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
2653
2780
  return;
2654
2781
  prefix.lastSize = sample;
2655
2782
  }
2656
2783
  /** Record the reported size of a main-thread request forwarded whole. */
2657
- function notePassthroughSize(state, sessionId, model, rec, bytes) {
2658
- const sample = reportedSample(rec, bytes);
2784
+ function notePassthroughSize(state, sessionId, model, rec, sent) {
2785
+ const sample = reportedSample(rec, sent);
2659
2786
  if (sessionId === undefined || sample === undefined)
2660
2787
  return;
2661
2788
  boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
@@ -2685,13 +2812,18 @@ function usedServerTools(rec) {
2685
2812
  * an implausible ratio would mis-size every later request it anchors, so it
2686
2813
  * is dropped and the previous sample kept.
2687
2814
  */
2688
- function reportedSample(rec, bytes) {
2815
+ function reportedSample(rec, sent) {
2689
2816
  const tokens = reportedBodyTokens(rec);
2690
2817
  if (tokens === undefined)
2691
2818
  return undefined;
2692
- const sample = { tokens, forwardedBytes: bytes };
2819
+ const sample = sizeSampleOf(sent, tokens, rec.model);
2693
2820
  return plausibleSample(sample) ? sample : undefined;
2694
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
+ }
2695
2827
  /**
2696
2828
  * Anthropic's input tokens for the body this request forwarded, when its usage
2697
2829
  * describes that body (not a changed delivery, no server tools). Not filtered
@@ -2738,12 +2870,12 @@ const noteRideSize = notePrefixSize;
2738
2870
  * Record the reported size of a request forwarded whole: the session's
2739
2871
  * passthrough size on the main thread, else its lane's.
2740
2872
  */
2741
- function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
2873
+ function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
2742
2874
  if (isMainRequest && sessionId !== undefined) {
2743
- notePassthroughSize(state, sessionId, model, rec, bytes);
2875
+ notePassthroughSize(state, sessionId, model, rec, sent);
2744
2876
  return;
2745
2877
  }
2746
- const sample = reportedSample(rec, bytes);
2878
+ const sample = reportedSample(rec, sent);
2747
2879
  if (sample === undefined)
2748
2880
  return;
2749
2881
  boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
@@ -2804,7 +2936,7 @@ async function planEdgeCompaction(args) {
2804
2936
  };
2805
2937
  let prefix = state.stablePrefixes.get(sessionId);
2806
2938
  if (prefix) {
2807
- const miss = prefixMismatch(body, messages, prefix, sessionId, stablePrefixMessageHash);
2939
+ const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
2808
2940
  if (miss) {
2809
2941
  // Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
2810
2942
  // longer stands for this conversation. Start over from passthrough.
@@ -2840,8 +2972,13 @@ async function planEdgeCompaction(args) {
2840
2972
  (explicitTarget && prefix.targetTokens !== targetTokens)) {
2841
2973
  return forced("target-change", fallback);
2842
2974
  }
2843
- 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 };
2844
2980
  return forced("budget", fallback);
2981
+ }
2845
2982
  return { kind: "ride", prefix, routed };
2846
2983
  }
2847
2984
  if (state.compactNow.has(sessionId))
@@ -2854,12 +2991,18 @@ async function planEdgeCompaction(args) {
2854
2991
  compaction.sizeSource = size.source;
2855
2992
  noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
2856
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 };
2857
2998
  return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
2858
2999
  }
2859
3000
  if (state.serverReportsBudget) {
2860
3001
  // The server measures the request and compresses only past the budget.
2861
3002
  const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
2862
3003
  compaction.thresholdTokens = threshold;
3004
+ if (clearlyUnderBudget(opts, state, budget, size, compaction))
3005
+ return { kind: "pass" };
2863
3006
  return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
2864
3007
  }
2865
3008
  // Without calibrated usage or threshold support, let the server decide
@@ -2900,7 +3043,7 @@ async function planToolCompaction(args) {
2900
3043
  const stableLane = args.isMainRequest && sessionId !== undefined;
2901
3044
  const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
2902
3045
  let ride = args.ride;
2903
- if (!ride && current && !prefixMismatch(body, messages, current, sessionId, stablePrefixMessageHash)) {
3046
+ if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
2904
3047
  const routed = prefixRoutedBody(body, messages, current);
2905
3048
  if (routed) {
2906
3049
  const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
@@ -2938,12 +3081,27 @@ async function planToolCompaction(args) {
2938
3081
  if (size.tokens < budget.tokens && !overWindow) {
2939
3082
  return ride ? { kind: "ride", ride } : { kind: "pass" };
2940
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;
2941
3099
  compaction.reason = "budget";
2942
3100
  return {
2943
3101
  kind: "compress",
2944
- ...budgetCompaction(state, compaction, targetTokens, body, modelContextLimit),
2945
- targetTokens,
2946
- explicitTarget: mode.mode === "explicit",
3102
+ ...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
3103
+ targetTokens: targetNow,
3104
+ explicitTarget: modeNow.mode === "explicit",
2947
3105
  estimateTokens: size.tokens,
2948
3106
  budgetTokens: budget.tokens,
2949
3107
  overWindow,
@@ -2952,6 +3110,224 @@ async function planToolCompaction(args) {
2952
3110
  ...(stableLane ? { replaces: current ?? null } : {}),
2953
3111
  };
2954
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
+ }
2955
3331
  function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
2956
3332
  // Model is deliberately not route identity: Claude Code switches models
2957
3333
  // mid-loop (overload fallback, /model, /fast), and the compressed prefix is
@@ -2969,7 +3345,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
2969
3345
  if (messages.length < prefixLength)
2970
3346
  return null;
2971
3347
  for (let i = 0; i < prefixLength; i++) {
2972
- if (routeMessageHash(messages[i]) !== route.originalPrefixHashes[i]) {
3348
+ if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
2973
3349
  return null;
2974
3350
  }
2975
3351
  }
@@ -3352,8 +3728,9 @@ async function runBlockingCompression(args) {
3352
3728
  : undefined;
3353
3729
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
3354
3730
  const compressStarted = Date.now();
3355
- const budgetMs = args.longBudget
3356
- ? 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));
3357
3734
  const compressMeta = {
3358
3735
  // Model + tools drive the server's model-based memory budget
3359
3736
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3381,7 +3758,9 @@ async function runBlockingCompression(args) {
3381
3758
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3382
3759
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3383
3760
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3384
- ...(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,
3385
3764
  };
3386
3765
  // Sampled BEFORE the call, while it still describes this call: after the
3387
3766
  // await the hash is in the cache regardless of who put it there.
@@ -3389,9 +3768,9 @@ async function runBlockingCompression(args) {
3389
3768
  // compress() maps every failure to a resolved null (it never rejects).
3390
3769
  // Keep foreground work finite even if a transport ignores its abort signal.
3391
3770
  let deadline;
3392
- 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));
3393
3772
  const result = await Promise.race([
3394
- opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
3773
+ opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
3395
3774
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
3396
3775
  ]).finally(() => { if (deadline)
3397
3776
  clearTimeout(deadline); });
@@ -3401,19 +3780,27 @@ async function runBlockingCompression(args) {
3401
3780
  // another class can overwrite the complete-key entry.
3402
3781
  const failureArming = result === null &&
3403
3782
  opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
3783
+ const failureStatus = result === null
3784
+ ? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
3404
3785
  const hadLiveSuccess = !cached && result !== null;
3405
3786
  // Only arming-class live failures (network error, timeout, 5xx, 402) are
3406
3787
  // fuse evidence. A responsive server's other 4xx failed THIS call but says
3407
3788
  // nothing about MemTree's health, so it contributes neither failure nor
3408
3789
  // success — the fuse is untouched.
3409
- 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;
3410
3799
  rec.compress = {
3411
3800
  ms: compressMs,
3412
3801
  ok: result !== null,
3413
- // Budget-consumed heuristic: the client maps every failure to null, so a
3414
- // null that took (roughly) the whole abort budget was almost certainly
3415
- // the AbortSignal timeout, not a fast server error.
3416
- timedOut: result === null && compressMs >= budgetMs,
3802
+ timedOut,
3803
+ ...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
3417
3804
  };
3418
3805
  return {
3419
3806
  result,
@@ -3425,6 +3812,61 @@ async function runBlockingCompression(args) {
3425
3812
  : "none",
3426
3813
  };
3427
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
+ }
3428
3870
  /**
3429
3871
  * Shared final request shape for a validated compression result: lift any
3430
3872
  * returned system message to `body.system`, and forward the SERVER's flatten
@@ -3524,14 +3966,14 @@ async function recoverToolRouteMiss(args) {
3524
3966
  capture(opts, "anthropic-request-memory-tool", fallback.raw);
3525
3967
  return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3526
3968
  toolForwarding.settle(delivered);
3527
- noteRideSize(fallback.sizeHolder, rec, fallback.raw.length);
3969
+ noteRideSize(fallback.sizeHolder, rec, fallback.raw);
3528
3970
  });
3529
3971
  }
3530
3972
  recordTurn(rec, "tool", args.originalBody);
3531
3973
  capture(opts, "anthropic-request", args.originalBody);
3532
3974
  return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3533
3975
  toolForwarding.settle(delivered);
3534
- noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
3976
+ noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
3535
3977
  });
3536
3978
  };
3537
3979
  // Same downstream-close tracking as the followup path: compression
@@ -3672,6 +4114,7 @@ async function recoverToolRouteMiss(args) {
3672
4114
  const { compressedBody, compressedRaw } = built;
3673
4115
  if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
3674
4116
  rec.routeRecovery = { conversationBytes, outcome: "unusable" };
4117
+ noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
3675
4118
  releaseOwnReservation();
3676
4119
  if (!noteIndexLag(opts, rec, result))
3677
4120
  return forwardOriginal();
@@ -3818,7 +4261,7 @@ async function recoverToolRouteMiss(args) {
3818
4261
  // The compacted request's reported size anchors the next budget check.
3819
4262
  const sizeHolder = stable?.installed ?? installedRoute;
3820
4263
  if (sizeHolder)
3821
- notePrefixSize(sizeHolder, rec, compressedRaw.length);
4264
+ notePrefixSize(sizeHolder, rec, compressedRaw);
3822
4265
  // Nothing to ride next turn, or a result that still does not fit the
3823
4266
  // window (the next turn would reject it and compress again): wait for
3824
4267
  // growth before the lane tries again.
@@ -3917,18 +4360,29 @@ const recapLinkAppenders = new WeakMap();
3917
4360
  * miss the tree, index a divergent conversation and bill for it.
3918
4361
  */
3919
4362
  async function recoverLateCount(args) {
4363
+ let generation;
3920
4364
  try {
3921
4365
  const { body, msgsForMemtree: messages } = args;
3922
4366
  const budget = resolveBudget(args.opts, args.state, body.model, args.modelContextLimit).tokens;
3923
- 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);
3924
4376
  const outcome = await runBlockingCompression({
3925
4377
  opts: args.opts, state: args.state, body, msgsForMemtree: messages,
3926
4378
  hash: args.hash, modelContextLimit: args.modelContextLimit,
3927
- rec: args.rec, sessionId: requestSessionId(args.req), agentId: agentAttributionId(args.req),
4379
+ rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
3928
4380
  clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
3929
4381
  compaction: { target },
3930
4382
  });
3931
4383
  noteMemtreeHealth(args.state, outcome);
4384
+ if (linkSeq !== undefined)
4385
+ noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
3932
4386
  // Like every compress path: a refusal after a lagging answer waits for
3933
4387
  // the index, and the cached answer expires so a retry asks again.
3934
4388
  if (outcome.result)
@@ -3939,11 +4393,63 @@ async function recoverLateCount(args) {
3939
4393
  const built = buildCompressedBody(body, outcome.result);
3940
4394
  if (!built)
3941
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
3942
4401
  return built.compressedRaw;
3943
4402
  }
3944
4403
  catch {
3945
4404
  return undefined;
3946
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
+ };
3947
4453
  }
3948
4454
  /**
3949
4455
  * The ride compressed to `target` still counted `rideTokens` (over the
@@ -3963,14 +4469,25 @@ async function selectForwardCandidate(args) {
3963
4469
  const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
3964
4470
  const measured = await policy.sizing.measure(preferred, args.limits);
3965
4471
  let target = preferred;
3966
- if (preferred.kind === "ride" && measured.source === "exact" &&
3967
- 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) {
3968
4474
  args.rec.lateCountRecovery = { outcome: "forwarded" };
3969
- 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));
3970
4476
  if (replacement)
3971
4477
  target = policy.sizing.register(replacement, "replacement");
3972
4478
  }
3973
- 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);
4484
+ const quietDegrade = quietDegrades.get(args.rec);
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
+ }
3974
4491
  if (!selection) {
3975
4492
  if (args.rec.lateCountRecovery)
3976
4493
  args.rec.lateCountRecovery.outcome = "refused";
@@ -3978,6 +4495,10 @@ async function selectForwardCandidate(args) {
3978
4495
  return undefined;
3979
4496
  }
3980
4497
  const { candidate, measurement } = selection;
4498
+ const preparedBase = preparedTruncationBases.get(policy.sizing);
4499
+ if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
4500
+ preparedBase.install();
4501
+ }
3981
4502
  const changed = candidate !== preferred || intended !== args.preferredBody;
3982
4503
  if (changed) {
3983
4504
  policy.allowed.delete(args.preferredBody);
@@ -3996,9 +4517,158 @@ async function selectForwardCandidate(args) {
3996
4517
  ? "exact-count-fits-window" : measurement.source === "bytes"
3997
4518
  ? "bytes-fallback-fits-window" : "estimate-fits-window" };
3998
4519
  warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
4520
+ quietDegrade?.();
3999
4521
  }
4000
4522
  return changed ? candidate.body : intended;
4001
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
+ }
4002
4672
  /**
4003
4673
  * Whether delivery would refuse `candidate` sent whole, by delivery's own
4004
4674
  * admission: it is sendable only when its plan fits and either an exact count
@@ -4119,6 +4789,25 @@ function nearWindowWarned(state, sessionId) {
4119
4789
  function reachedBudget(compaction) {
4120
4790
  return compaction !== undefined && (compaction.estimatedTokens ?? 0) >= compaction.budgetTokens;
4121
4791
  }
4792
+ /**
4793
+ * Whether the planned size says a turn needed MemTree to compress it: a
4794
+ * forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
4795
+ * size at or over the budget, or a size nobody measured. A planned fit is
4796
+ * provisional: delivery's own sizing can still find the turn over the budget
4797
+ * (quietDegrades), since a plan can undercount.
4798
+ */
4799
+ function compressionNeeded(compaction) {
4800
+ if (compaction === undefined || compaction.reason !== undefined)
4801
+ return true;
4802
+ if (compaction.estimatedTokens === undefined)
4803
+ return true;
4804
+ return compaction.estimatedTokens >= compaction.budgetTokens;
4805
+ }
4806
+ /**
4807
+ * A degraded turn whose plan fit the budget, so it queued no notice: the
4808
+ * notice to queue if delivery's sizing finds it over the budget after all.
4809
+ */
4810
+ const quietDegrades = new WeakMap();
4122
4811
  /** The paused notice a degraded request queued, until it is refused or settles. */
4123
4812
  const pausedNotices = new WeakMap();
4124
4813
  const changedDeliveryRecords = new WeakSet();
@@ -4130,7 +4819,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
4130
4819
  const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
4131
4820
  if (!selected)
4132
4821
  return false;
4133
- 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);
4134
4828
  if (delivered && rec)
4135
4829
  noteSelectedCandidateSize(rec);
4136
4830
  return delivered;
@@ -4145,7 +4839,7 @@ function noteSelectedCandidateSize(rec) {
4145
4839
  // Server tools' injected results inflate the reported input (reportedSample).
4146
4840
  if (serverToolRecords.has(rec) || usedServerTools(rec))
4147
4841
  return;
4148
- const sample = { tokens, forwardedBytes: candidate.body.length };
4842
+ const sample = sizeSampleOf(candidate.body, tokens, rec.model);
4149
4843
  if (!plausibleSample(sample))
4150
4844
  return;
4151
4845
  if ("set" in owner)
@@ -4329,6 +5023,9 @@ function forwardAccepted(req, res, bodyBuffer, opts, upstream, shutdownSignal, r
4329
5023
  };
4330
5024
  if (rec) {
4331
5025
  rec.upstreamStatus = upstreamRes.statusCode ?? 502;
5026
+ const rateLimit = rateLimitFromResponse(rec.upstreamStatus, upstreamRes.headers);
5027
+ if (rateLimit)
5028
+ rec.rateLimit = rateLimit;
4332
5029
  }
4333
5030
  successfulStatus =
4334
5031
  typeof upstreamRes.statusCode === "number" &&
@@ -4868,4 +5565,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
4868
5565
  return undefined;
4869
5566
  return (messages) => source.timesFor(sessionId, messages, agentId);
4870
5567
  }
5568
+ export const __testHoldingLane = holdingLane;
4871
5569
  //# sourceMappingURL=proxy.js.map