claude-code-infinite 1.0.29 → 1.0.31

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 (95) hide show
  1. package/README.md +65 -7
  2. package/dist/cli-args.d.ts +10 -0
  3. package/dist/cli-args.d.ts.map +1 -1
  4. package/dist/cli-args.js +30 -0
  5. package/dist/cli-args.js.map +1 -1
  6. package/dist/cli-lifecycle.d.ts +45 -1
  7. package/dist/cli-lifecycle.d.ts.map +1 -1
  8. package/dist/cli-lifecycle.js +59 -0
  9. package/dist/cli-lifecycle.js.map +1 -1
  10. package/dist/cli-main.js +61 -16
  11. package/dist/cli-main.js.map +1 -1
  12. package/dist/count-tokens.d.ts +15 -0
  13. package/dist/count-tokens.d.ts.map +1 -1
  14. package/dist/count-tokens.js +23 -1
  15. package/dist/count-tokens.js.map +1 -1
  16. package/dist/diagnostics-schema.d.ts.map +1 -1
  17. package/dist/diagnostics-schema.js +5 -0
  18. package/dist/diagnostics-schema.js.map +1 -1
  19. package/dist/diagnostics.d.ts.map +1 -1
  20. package/dist/diagnostics.js +18 -1
  21. package/dist/diagnostics.js.map +1 -1
  22. package/dist/final-index.d.ts +7 -1
  23. package/dist/final-index.d.ts.map +1 -1
  24. package/dist/final-index.js +10 -3
  25. package/dist/final-index.js.map +1 -1
  26. package/dist/hooks.d.ts +24 -1
  27. package/dist/hooks.d.ts.map +1 -1
  28. package/dist/hooks.js +24 -2
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/image-refs.d.ts +51 -0
  31. package/dist/image-refs.d.ts.map +1 -0
  32. package/dist/image-refs.js +169 -0
  33. package/dist/image-refs.js.map +1 -0
  34. package/dist/image-tokens.d.ts +37 -0
  35. package/dist/image-tokens.d.ts.map +1 -0
  36. package/dist/image-tokens.js +158 -0
  37. package/dist/image-tokens.js.map +1 -0
  38. package/dist/memtree-body.d.ts +52 -0
  39. package/dist/memtree-body.d.ts.map +1 -0
  40. package/dist/memtree-body.js +99 -0
  41. package/dist/memtree-body.js.map +1 -0
  42. package/dist/memtree-mcp.d.ts.map +1 -1
  43. package/dist/memtree-mcp.js +4 -0
  44. package/dist/memtree-mcp.js.map +1 -1
  45. package/dist/memtree.d.ts +86 -3
  46. package/dist/memtree.d.ts.map +1 -1
  47. package/dist/memtree.js +283 -53
  48. package/dist/memtree.js.map +1 -1
  49. package/dist/model-context.d.ts +80 -0
  50. package/dist/model-context.d.ts.map +1 -0
  51. package/dist/model-context.js +192 -0
  52. package/dist/model-context.js.map +1 -0
  53. package/dist/paused-session.d.ts.map +1 -1
  54. package/dist/paused-session.js +11 -8
  55. package/dist/paused-session.js.map +1 -1
  56. package/dist/payment-gate.d.ts +2 -0
  57. package/dist/payment-gate.d.ts.map +1 -1
  58. package/dist/payment-gate.js +1 -1
  59. package/dist/payment-gate.js.map +1 -1
  60. package/dist/prefix-store.d.ts +2 -0
  61. package/dist/prefix-store.d.ts.map +1 -1
  62. package/dist/prefix-store.js +4 -0
  63. package/dist/prefix-store.js.map +1 -1
  64. package/dist/prepared-compression.d.ts +78 -0
  65. package/dist/prepared-compression.d.ts.map +1 -0
  66. package/dist/prepared-compression.js +127 -0
  67. package/dist/prepared-compression.js.map +1 -0
  68. package/dist/proxy.d.ts +370 -14
  69. package/dist/proxy.d.ts.map +1 -1
  70. package/dist/proxy.js +797 -94
  71. package/dist/proxy.js.map +1 -1
  72. package/dist/reqlog.d.ts +55 -0
  73. package/dist/reqlog.d.ts.map +1 -1
  74. package/dist/reqlog.js.map +1 -1
  75. package/dist/request-sizing.d.ts +21 -2
  76. package/dist/request-sizing.d.ts.map +1 -1
  77. package/dist/request-sizing.js +57 -11
  78. package/dist/request-sizing.js.map +1 -1
  79. package/dist/route-identity.d.ts +27 -0
  80. package/dist/route-identity.d.ts.map +1 -1
  81. package/dist/route-identity.js +88 -8
  82. package/dist/route-identity.js.map +1 -1
  83. package/dist/token-estimate.d.ts +3 -20
  84. package/dist/token-estimate.d.ts.map +1 -1
  85. package/dist/token-estimate.js +9 -50
  86. package/dist/token-estimate.js.map +1 -1
  87. package/dist/tool-output-store.d.ts +12 -0
  88. package/dist/tool-output-store.d.ts.map +1 -0
  89. package/dist/tool-output-store.js +146 -0
  90. package/dist/tool-output-store.js.map +1 -0
  91. package/dist/tool-result-truncation.d.ts +32 -0
  92. package/dist/tool-result-truncation.d.ts.map +1 -0
  93. package/dist/tool-result-truncation.js +135 -0
  94. package/dist/tool-result-truncation.js.map +1 -0
  95. 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";
@@ -53,15 +55,17 @@ import { join } from "node:path";
53
55
  import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
54
56
  import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
55
57
  import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
56
- import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
58
+ import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH, } from "./hooks.js";
57
59
  import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
58
60
  import { hyperlink } from "./payment-gate.js";
59
61
  import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
62
+ import { KEY_INVALID_KIND, ModelContextNotices, PAUSED_KIND, keyInvalidModelContext, lastToldKindInFile, pausedModelContext } from "./model-context.js";
60
63
  import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
61
64
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
62
65
  import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
63
66
  import { isResumableSessionId } from "./resume-hint.js";
64
67
  import { conservativeBodyTokens } from "./token-estimate.js";
68
+ import { bodyImages } from "./image-tokens.js";
65
69
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
66
70
  import { rateLimitFromResponse } from "./rate-limit.js";
67
71
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
@@ -78,6 +82,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
78
82
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
79
83
  /** Safety cap on a long-budget compress, should a transport ignore its abort. */
80
84
  const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
85
+ /** The most a blocking compress with a fallback may wait (its budget grows with its upload). */
86
+ const MAX_COMPRESS_WAIT_MS = 60_000;
81
87
  const SKIP_REQUEST_HEADERS = new Set([
82
88
  "host",
83
89
  "connection",
@@ -96,20 +102,18 @@ const SKIP_RESPONSE_HEADERS = new Set([
96
102
  ]);
97
103
  /**
98
104
  * 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.
105
+ * server's ccc policy (polychat index_budget.py near_window_budget_tokens):
106
+ * every Claude model compresses near its window, at the window less the
107
+ * reply's room (COMPRESSION_RESERVE_TOKENS, 40k) and never below
108
+ * FALLBACK_BUDGET_WINDOW_RATIO of it: 960k of 1M, 160k of 200k; at most a
109
+ * Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
110
+ * FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts at
111
+ * window - 33k (967k of 1M), so MemTree compresses just before it would; ccc
112
+ * lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
113
+ * jumps past the budget reaches the proxy and is compressed here, on a
114
+ * prepared prefix when there is one (prepared-compression.ts).
108
115
  */
109
116
  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
117
  /** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
114
118
  const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
115
119
  /**
@@ -125,10 +129,14 @@ export function fallbackBudgetTokens(model, modelContextLimit) {
125
129
  if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
126
130
  return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
127
131
  }
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));
132
+ const nearWindow = nearWindowBudgetTokens(modelContextLimit);
129
133
  const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
130
134
  return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
131
135
  }
136
+ /** A Claude model's near-window budget before any ceiling (fallbackBudgetTokens). */
137
+ function nearWindowBudgetTokens(modelContextLimit) {
138
+ return Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), modelContextLimit - COMPRESSION_RESERVE_TOKENS);
139
+ }
132
140
  /**
133
141
  * An explicit target is capped at this share of the budget: a compaction to
134
142
  * the budget itself would be over budget again on the next turn and
@@ -320,6 +328,7 @@ export function startProxy(opts) {
320
328
  const activeRequests = new Set();
321
329
  const acceptedRequests = new Set();
322
330
  const shutdownAbort = new AbortController();
331
+ const backgroundAbort = new AbortController();
323
332
  const state = {
324
333
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
325
334
  keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
@@ -328,6 +337,9 @@ export function startProxy(opts) {
328
337
  pausedNearWindowWarned: new Set(),
329
338
  unpaidDetail: () => opts.memtree.paymentRequiredDetail,
330
339
  pausedDetail: () => opts.memtree.pausedDetail,
340
+ pausedReason: () => opts.memtree.pausedReason,
341
+ memtreeAnswering: () => opts.memtree.answering,
342
+ modelNotices: new ModelContextNotices(),
331
343
  usageUrl: opts.memtree.usageDashboardUrl,
332
344
  sessionStartNotice: opts.sessionStartNotice,
333
345
  promptSubmitNotice: opts.promptSubmitNotice,
@@ -347,6 +359,7 @@ export function startProxy(opts) {
347
359
  toolRecoveryAttemptedLanes: new Map(),
348
360
  toolRecoveryCooldownUntil: 0,
349
361
  shutdownSignal: shutdownAbort.signal,
362
+ backgroundSignal: backgroundAbort.signal,
350
363
  routeInstallFault: opts.routeInstallFault,
351
364
  memtreePages: new Map(),
352
365
  memorySourcePages: new Map(),
@@ -362,6 +375,9 @@ export function startProxy(opts) {
362
375
  defaultCompactOff: opts.defaultCompactTarget === null,
363
376
  compactNow: new Set(),
364
377
  stablePrefixes: new Map(),
378
+ prepared: new PreparedCompressions(),
379
+ preparing: new Map(),
380
+ backgroundCompress: new Map(),
365
381
  serverBudgets: new Map(),
366
382
  serverReportsBudget: false,
367
383
  countTokensCooldowns: new CountTokensCooldowns(),
@@ -412,6 +428,7 @@ export function startProxy(opts) {
412
428
  const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
413
429
  ? Math.floor(timeoutMs)
414
430
  : 5_000;
431
+ backgroundAbort.abort();
415
432
  const quiesced = (async () => {
416
433
  // Once close completes, every accepted request has entered the tracked
417
434
  // set.
@@ -476,6 +493,9 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
476
493
  res.end(JSON.stringify({ detail: "The ccc proxy only serves programs on this machine" }));
477
494
  return;
478
495
  }
496
+ if (url.pathname === PRECOMPACT_HOOK_PATH) {
497
+ return handleNoticeHook(req, res, state, opts.reqlog, undefined, "PreCompact");
498
+ }
479
499
  if (url.pathname === hookPath) {
480
500
  return handleNoticeHook(req, res, state, opts.reqlog, (sessionId) => opts.memtree.scheduleFinalIndex(sessionId, (signal) => opts.transcriptUsage?.finalReplyAsync
481
501
  ? opts.transcriptUsage.finalReplyAsync(sessionId, signal)
@@ -710,7 +730,9 @@ function noteMainSessionStart(state, source, sessionId) {
710
730
  /** Serve only validated Claude hook POSTs on the randomized localhost path. */
711
731
  async function handleNoticeHook(req, res, state, reqlog,
712
732
  /** A main-lane turn ended: the session may be over (its final index). */
713
- onMainStop) {
733
+ onMainStop,
734
+ /** The only event this path takes (the token-free PreCompact path). */
735
+ onlyEvent) {
714
736
  if (req.method !== "POST") {
715
737
  res.writeHead(405, { allow: "POST" });
716
738
  res.end();
@@ -744,19 +766,32 @@ onMainStop) {
744
766
  res.end();
745
767
  return;
746
768
  }
769
+ if (onlyEvent !== undefined && parsed.hook_event_name !== onlyEvent) {
770
+ res.writeHead(403);
771
+ res.end();
772
+ return;
773
+ }
747
774
  if (parsed.hook_event_name === "SessionStart") {
748
775
  if (parsed.agent_id === undefined) {
749
776
  recordSessionStart(state, parsed.source);
750
777
  noteMainSessionStart(state, parsed.source, parsed.session_id);
778
+ // Claude Code's compaction dropped any MemTree notice from the transcript.
779
+ if (parsed.source === "compact")
780
+ state.modelNotices.forget(parsed.session_id);
751
781
  }
752
782
  // Earlier trees in this project: asked now, so its wait overlaps the resume's.
753
783
  const contextPromise = projectTreesContext(state.projectTrees, parsed);
784
+ // A resumed or forked history may carry notices an earlier process gave.
785
+ const restored = parsed.agent_id === undefined &&
786
+ (parsed.source === "resume" || parsed.source === "fork")
787
+ ? restoreModelNotices(state, parsed) : Promise.resolve();
754
788
  // A resume waits briefly for the newest tree; a fresh session has none to wait for.
755
789
  if (parsed.agent_id === undefined && parsed.source === "resume") {
756
790
  await state.newestTrees?.settle(parsed.session_id);
757
791
  }
758
792
  const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
759
793
  const context = await contextPromise;
794
+ await restored;
760
795
  if (!line && !context) {
761
796
  res.writeHead(204);
762
797
  res.end();
@@ -771,6 +806,25 @@ onMainStop) {
771
806
  res.end(body);
772
807
  return;
773
808
  }
809
+ if (parsed.hook_event_name === "PreCompact") {
810
+ // A typed /compact while MemTree compresses: say why it isn't needed, once.
811
+ const reason = parsed.agent_id === undefined && parsed.trigger === "manual"
812
+ ? state.modelNotices.blockCompact(parsed.session_id, memtreeWorking(state))
813
+ : undefined;
814
+ if (reason === undefined) {
815
+ res.writeHead(204);
816
+ res.end();
817
+ return;
818
+ }
819
+ const body = Buffer.from(JSON.stringify({ decision: "block", reason }), "utf-8");
820
+ res.writeHead(200, {
821
+ "content-type": "application/json",
822
+ "content-length": String(body.length),
823
+ "cache-control": "no-store",
824
+ });
825
+ res.end(body);
826
+ return;
827
+ }
774
828
  if (parsed.hook_event_name === "UserPromptSubmit") {
775
829
  // `/memtree-view`: answered here and blocked, so no model turn runs and
776
830
  // the prompt never enters the conversation. Not a human turn either, so
@@ -809,8 +863,11 @@ onMainStop) {
809
863
  recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
810
864
  }
811
865
  const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
812
- if (line) {
813
- const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
866
+ const context = parsed.agent_id === undefined
867
+ ? state.modelNotices.next(parsed.session_id, currentModelNotice(state))
868
+ : undefined;
869
+ if (line || context) {
870
+ const body = Buffer.from(JSON.stringify(promptSubmitOutput(line, context)), "utf-8");
814
871
  res.writeHead(200, {
815
872
  "content-type": "application/json",
816
873
  "content-length": String(body.length),
@@ -1089,7 +1146,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1089
1146
  if (!rides) {
1090
1147
  const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
1091
1148
  if (measured.source === "exact") {
1092
- sizeSlot.set({ tokens: measured.tokens, forwardedBytes: forwardCandidate.body.length });
1149
+ sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
1093
1150
  }
1094
1151
  }
1095
1152
  rec.routeLane = requestRouteLane;
@@ -1150,12 +1207,18 @@ async function handleMessages(req, res, opts, upstream, state) {
1150
1207
  },
1151
1208
  // The ride's bytes are only what failed; recovery always compresses the
1152
1209
  // 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" ||
1210
+ recoverLate: async (_ride, rideTokens, target) => {
1211
+ // A compression already made may be redone only to a smaller target.
1212
+ if ((rec.compress && target === undefined) ||
1213
+ compactionMode(opts, state, requestSessionKey).mode === "off" ||
1214
+ !hasEarlierNonToolUserMessage(messages) ||
1155
1215
  (rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
1156
1216
  return undefined;
1157
1217
  return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
1158
- modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate });
1218
+ modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
1219
+ route: { key: requestRouteKey, messages, epoch: routeEpoch,
1220
+ lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
1221
+ ...(target !== undefined ? { target } : {}) });
1159
1222
  },
1160
1223
  delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
1161
1224
  });
@@ -1302,6 +1365,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1302
1365
  ride: toolRide,
1303
1366
  canCompress,
1304
1367
  });
1368
+ if (isMainRequest && toolSessionId !== undefined && canCompress &&
1369
+ (plan.kind === "ride" || plan.kind === "pass")) {
1370
+ maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
1371
+ sessionId: toolSessionId, modelContextLimit, rec, clientMeta });
1372
+ }
1305
1373
  if (plan.kind === "ride")
1306
1374
  toolRide = plan.ride;
1307
1375
  if (plan.kind === "pass" && plan.original) {
@@ -1341,9 +1409,13 @@ async function handleMessages(req, res, opts, upstream, state) {
1341
1409
  // for growth instead of paying a blocking call on every tool turn.
1342
1410
  rec.routeRecovery = { outcome: "backoff" };
1343
1411
  }
1344
- else if (Date.now() < state.toolRecoveryCooldownUntil) {
1412
+ else if (Date.now() < state.toolRecoveryCooldownUntil &&
1413
+ !(plan.overWindow && !plan.fallback && (!plan.uncountedOverWindowOnly ||
1414
+ await wholeRequestRefused(requestSizing, originalCandidate, sizingLimits())))) {
1345
1415
  // A recent attempt burned the full compress budget and still
1346
- // failed. Every tool turn appends a tool_result and rehashes, so
1416
+ // failed. (Not when this turn is over the window with no prefix to
1417
+ // ride and delivery would refuse it: skipping could only end in a
1418
+ // refusal, so it compresses anyway, on the long budget.) Every tool turn appends a tool_result and rehashes, so
1347
1419
  // compress() dedup can never absorb the repeat: without this
1348
1420
  // cooldown a MemTree outage would add the whole blocking budget to
1349
1421
  // every tool turn over the budget. A cooldown skip does not start
@@ -1424,9 +1496,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1424
1496
  return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1425
1497
  toolForwarding.settle(delivered);
1426
1498
  if (toolRide)
1427
- noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
1499
+ noteRideSize(toolRide.sizeHolder, rec, sendBody);
1428
1500
  else {
1429
- noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
1501
+ noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
1430
1502
  }
1431
1503
  }));
1432
1504
  }
@@ -1469,19 +1541,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1469
1541
  const sessionId = requestSessionId(req);
1470
1542
  // Stable-prefix (edge) compaction: the main thread's human turns only. The
1471
1543
  // 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;
1544
+ const planEdge = () => planEdgeCompaction({ sizing: requestSizing, opts, state, body, messages,
1545
+ sessionId: sessionId, modelContextLimit, forwardBody, rec });
1546
+ let edge = isMainRequest && sessionId !== undefined ? await planEdge() : undefined;
1547
+ if (edge?.kind === "compress" && (state.preparing.has(sessionId) ||
1548
+ rec.compaction?.preparedWaitMs !== undefined || rec.compaction?.prepared !== undefined)) {
1549
+ // Never beside the session's preparation (both would be billed): wait for
1550
+ // it, then plan again with the settings now current (a /memtree-compact
1551
+ // that arrived while it waited or counted) and the preparation it left.
1552
+ const waited = rec.compaction?.preparedWaitMs ?? 0;
1553
+ const prepared = rec.compaction?.prepared;
1554
+ await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
1555
+ const total = waited + (rec.compaction?.preparedWaitMs ?? 0);
1556
+ edge = await planEdge();
1557
+ if (rec.compaction) {
1558
+ rec.compaction.preparedWaitMs = total;
1559
+ rec.compaction.prepared ??= prepared;
1560
+ }
1561
+ }
1562
+ // Not beside a blocking compress call: the two would overlap on the server.
1563
+ if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
1564
+ maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
1565
+ modelContextLimit, rec, clientMeta });
1566
+ }
1485
1567
  // An active subagent can repeat/embed the human prompt in its own request;
1486
1568
  // producer suppression must also preserve the arm for the later main call.
1487
1569
  if (displayForThisTurn)
@@ -1496,6 +1578,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1496
1578
  // A lagging index left history verbatim: a refusal waits for indexing.
1497
1579
  if (result)
1498
1580
  noteIndexLag(opts, rec, result);
1581
+ // Too large for the window: delivery recompresses it once, smaller.
1582
+ if (result && !rec.indexBehind) {
1583
+ noteOversizedCompression(rec, requestSizing, body, compressedRaw, rec.compaction?.targetTokens);
1584
+ }
1499
1585
  releaseUncommittedRouteDecision();
1500
1586
  recordTurn(rec, "followup-degraded", rawBody);
1501
1587
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
@@ -1596,7 +1682,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1596
1682
  : stable.installed
1597
1683
  : undefined;
1598
1684
  if (sizePrefix)
1599
- notePrefixSize(sizePrefix, rec, compressedRaw.length);
1685
+ notePrefixSize(sizePrefix, rec, compressedRaw);
1600
1686
  // A route already installed at protocol-complete deliberately survives
1601
1687
  // delivered=false: that settle may be the fast-tool abort (client
1602
1688
  // consumed message_stop, closed the SSE response, and its immediate
@@ -1633,7 +1719,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1633
1719
  }
1634
1720
  }));
1635
1721
  };
1636
- if (existingRide && (!edge || edge.kind === "off")) {
1722
+ if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
1637
1723
  const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
1638
1724
  const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
1639
1725
  candidateSizeOwners.set(candidate, owner);
@@ -1665,8 +1751,29 @@ async function handleMessages(req, res, opts, upstream, state) {
1665
1751
  const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
1666
1752
  const noteWholeSize = () => {
1667
1753
  if (isMainRequest)
1668
- notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
1754
+ notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
1669
1755
  };
1756
+ if (edge?.kind === "pass") {
1757
+ // Clearly under the budget by its own count: the compress call could only
1758
+ // pass it through, so the turn goes out now and the call (which also
1759
+ // indexes it and reports the page and budget) runs in the background.
1760
+ compressInBackground({ opts, state, body, msgsForMemtree, hash, modelContextLimit, rec,
1761
+ sessionId, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
1762
+ // Never compress: the turn went out whole, so a compression the server's
1763
+ // own (more conservative) sizing chose could not be used, only billed.
1764
+ compaction: { target: NEVER_COMPRESS_TARGET_TOKENS },
1765
+ linkSeq: isAwaySummary ? undefined : nextMemtreeCallSeq(state) });
1766
+ releaseForOriginalFallback();
1767
+ sendingWhole();
1768
+ recordTurn(rec, "followup-under-budget", forwardBody);
1769
+ capture(opts, "anthropic-request", forwardBody);
1770
+ return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec)
1771
+ .then((delivered) => {
1772
+ noteWholeSize();
1773
+ if (delivered)
1774
+ markMainPromptDelivered();
1775
+ }));
1776
+ }
1670
1777
  // The complete request upload can outlive its downstream subscriber while
1671
1778
  // MemTree compression is in flight. Track that subscriber locally without
1672
1779
  // feeding its lifetime into MemtreeClient.compress(): compression promises
@@ -1716,7 +1823,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1716
1823
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
1717
1824
  .then((delivered) => {
1718
1825
  if (isMainRequest)
1719
- notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
1826
+ notePassthroughSize(state, sessionId, body.model, rec, rawBody);
1720
1827
  if (delivered)
1721
1828
  markMainPromptDelivered();
1722
1829
  }));
@@ -2051,6 +2158,47 @@ function promptSubmitLine(state) {
2051
2158
  return undefined;
2052
2159
  }
2053
2160
  }
2161
+ /**
2162
+ * Whether MemTree is known to be compressing: its latest answer a success and
2163
+ * no pause (402) remembered. Before any answer, or after an outage, it is not.
2164
+ */
2165
+ function memtreeWorking(state) {
2166
+ return state.memtreeAnswering() === true && state.unpaidDetail() === null;
2167
+ }
2168
+ /**
2169
+ * What the model should know about MemTree right now: a notice, undefined
2170
+ * when it is known to work, null when neither is known (no answer, an outage).
2171
+ */
2172
+ function currentModelNotice(state) {
2173
+ if (state.keyInvalid()) {
2174
+ return { kind: KEY_INVALID_KIND, text: keyInvalidModelContext(state.keyPageUrl) };
2175
+ }
2176
+ const detail = state.unpaidDetail();
2177
+ if (detail !== null) {
2178
+ return { kind: PAUSED_KIND, text: pausedModelContext(detail, state.pausedReason()) };
2179
+ }
2180
+ return memtreeWorking(state) ? undefined : null;
2181
+ }
2182
+ /**
2183
+ * A resumed session's transcript may hold a notice from an earlier ccc process
2184
+ * (paused, then paid in between): start from it, so the change is announced.
2185
+ * A missing or unreadable transcript, or a scan past its time box, starts from
2186
+ * nothing. Never throws.
2187
+ */
2188
+ async function restoreModelNotices(state, input) {
2189
+ if (!input.transcript_path)
2190
+ return;
2191
+ state.modelNotices.restore(input.session_id, await lastToldKindInFile(input.transcript_path));
2192
+ }
2193
+ /** UserPromptSubmit's answer: a line for the user, context for the model, or both. */
2194
+ function promptSubmitOutput(line, context) {
2195
+ return {
2196
+ ...(line ? { systemMessage: line } : {}),
2197
+ ...(context
2198
+ ? { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }
2199
+ : {}),
2200
+ };
2201
+ }
2054
2202
  /** The caller's extra SessionStart line; a throwing provider shows nothing. */
2055
2203
  function extraSessionStartLine(provider, input) {
2056
2204
  if (!provider || input.agent_id !== undefined || input.source === "compact")
@@ -2315,7 +2463,8 @@ function rememberPage(pages, sessionId, page) {
2315
2463
  */
2316
2464
  function noteMemtreeHealth(state, compression) {
2317
2465
  if (compression.liveHealth === "failure") {
2318
- if (state.shutdownSignal.aborted)
2466
+ // Background calls the drain stopped (backgroundSignal) are no outage either.
2467
+ if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
2319
2468
  return;
2320
2469
  state.toolRecoveryCooldownUntil =
2321
2470
  Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
@@ -2398,7 +2547,7 @@ stable) {
2398
2547
  // Include trailing ambient role=system blocks: MemTree consolidated them
2399
2548
  // into compressedBody.system, so treating them as suffix would duplicate
2400
2549
  // those instructions on every tool request.
2401
- originalPrefixHashes: originalMessages.map(routeMessageHash),
2550
+ ...prefixIdentity(originalMessages, "route"),
2402
2551
  compressedMessages: cloneJson(compressedBody.messages),
2403
2552
  compressedSystem: cloneJson(compressedBody.system),
2404
2553
  hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
@@ -2437,7 +2586,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2437
2586
  const prefix = {
2438
2587
  sessionId,
2439
2588
  originalSystemHash: compressed.originalSystemHash,
2440
- originalPrefixHashes: originalMessages.map(stablePrefixMessageHash),
2589
+ ...prefixIdentity(originalMessages, "stable"),
2441
2590
  compressedMessages: compressed.compressedMessages,
2442
2591
  compressedSystem: compressed.compressedSystem,
2443
2592
  hasCompressedSystem: compressed.hasCompressedSystem,
@@ -2448,6 +2597,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2448
2597
  stable.installed = prefix;
2449
2598
  boundedSet(state.stablePrefixes, sessionId, prefix);
2450
2599
  state.compactNow.delete(sessionId);
2600
+ // Prepared against the old prefix's ride sizes: a fresh one is made when
2601
+ // the new prefix's rides near the budget again.
2602
+ state.prepared.discard(sessionId);
2451
2603
  return prefix;
2452
2604
  }
2453
2605
  /**
@@ -2468,6 +2620,7 @@ function persistStablePrefix(state, sessionId, prefix) {
2468
2620
  store.save(sessionId, owner, {
2469
2621
  originalSystemHash: prefix.originalSystemHash,
2470
2622
  originalPrefixHashes: prefix.originalPrefixHashes,
2623
+ originalMediaFreeHashes: prefix.originalMediaFreeHashes,
2471
2624
  compressedMessages: prefix.compressedMessages,
2472
2625
  compressedSystem: prefix.compressedSystem,
2473
2626
  hasCompressedSystem: prefix.hasCompressedSystem,
@@ -2520,7 +2673,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
2520
2673
  function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
2521
2674
  if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
2522
2675
  return "defer";
2523
- const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
2676
+ const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
2524
2677
  if (miss === null)
2525
2678
  return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
2526
2679
  const authoritative = miss === "prefix" &&
@@ -2550,7 +2703,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
2550
2703
  * at least one message after them. The suffix may be anything, human turns
2551
2704
  * included.
2552
2705
  */
2553
- function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMessageHash) {
2706
+ function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
2554
2707
  if (!sessionId || prefix.sessionId !== sessionId)
2555
2708
  return "session";
2556
2709
  if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
@@ -2560,7 +2713,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
2560
2713
  if (messages.length < prefixLength)
2561
2714
  return "prefix";
2562
2715
  for (let i = 0; i < prefixLength; i++) {
2563
- if (hashMessage(messages[i]) !== prefix.originalPrefixHashes[i]) {
2716
+ if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
2564
2717
  return "prefix";
2565
2718
  }
2566
2719
  }
@@ -2604,20 +2757,24 @@ function serverBudgetKey(model, modelContextLimit) {
2604
2757
  return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
2605
2758
  }
2606
2759
  /**
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).
2760
+ * The session's whole-request budget, the size at which a request is
2761
+ * compressed: CCC_BUDGET_TOKENS when set (tests), else the model budget the
2762
+ * server last reported for this model and window, else fallbackBudgetTokens
2763
+ * (servers that predate `model_budget_tokens`, and the first request before
2764
+ * any response). Never above the window less the reply's room
2765
+ * (compressionTrigger): a request is compressed when its projected size plus
2766
+ * that reserve would exceed the window, or the budget.
2611
2767
  */
2612
2768
  function resolveBudget(opts, state, model, modelContextLimit) {
2769
+ const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
2613
2770
  if (opts.budgetTokensOverride !== undefined) {
2614
- return { tokens: opts.budgetTokensOverride, source: "override" };
2771
+ return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
2615
2772
  }
2616
2773
  const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
2617
2774
  if (reported !== undefined)
2618
- return { tokens: reported, source: "server" };
2775
+ return { tokens: trigger(reported), source: "server" };
2619
2776
  return {
2620
- tokens: fallbackBudgetTokens(model, modelContextLimit),
2777
+ tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
2621
2778
  source: "window-ratio",
2622
2779
  };
2623
2780
  }
@@ -2700,17 +2857,17 @@ function reportedInputTokens(rec) {
2700
2857
  (usage.cache_creation_input_tokens ?? 0));
2701
2858
  }
2702
2859
  /** 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);
2860
+ function notePrefixSize(prefix, rec, sent) {
2861
+ const sample = reportedSample(rec, sent);
2705
2862
  if (sample === undefined)
2706
2863
  return;
2707
- if (prefix.lastSize && bytes < prefix.lastSize.forwardedBytes)
2864
+ if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
2708
2865
  return;
2709
2866
  prefix.lastSize = sample;
2710
2867
  }
2711
2868
  /** 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);
2869
+ function notePassthroughSize(state, sessionId, model, rec, sent) {
2870
+ const sample = reportedSample(rec, sent);
2714
2871
  if (sessionId === undefined || sample === undefined)
2715
2872
  return;
2716
2873
  boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
@@ -2740,13 +2897,18 @@ function usedServerTools(rec) {
2740
2897
  * an implausible ratio would mis-size every later request it anchors, so it
2741
2898
  * is dropped and the previous sample kept.
2742
2899
  */
2743
- function reportedSample(rec, bytes) {
2900
+ function reportedSample(rec, sent) {
2744
2901
  const tokens = reportedBodyTokens(rec);
2745
2902
  if (tokens === undefined)
2746
2903
  return undefined;
2747
- const sample = { tokens, forwardedBytes: bytes };
2904
+ const sample = sizeSampleOf(sent, tokens, rec.model);
2748
2905
  return plausibleSample(sample) ? sample : undefined;
2749
2906
  }
2907
+ /** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
2908
+ function sizeSampleOf(sent, tokens, model) {
2909
+ const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
2910
+ return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
2911
+ }
2750
2912
  /**
2751
2913
  * Anthropic's input tokens for the body this request forwarded, when its usage
2752
2914
  * describes that body (not a changed delivery, no server tools). Not filtered
@@ -2793,12 +2955,12 @@ const noteRideSize = notePrefixSize;
2793
2955
  * Record the reported size of a request forwarded whole: the session's
2794
2956
  * passthrough size on the main thread, else its lane's.
2795
2957
  */
2796
- function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
2958
+ function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
2797
2959
  if (isMainRequest && sessionId !== undefined) {
2798
- notePassthroughSize(state, sessionId, model, rec, bytes);
2960
+ notePassthroughSize(state, sessionId, model, rec, sent);
2799
2961
  return;
2800
2962
  }
2801
- const sample = reportedSample(rec, bytes);
2963
+ const sample = reportedSample(rec, sent);
2802
2964
  if (sample === undefined)
2803
2965
  return;
2804
2966
  boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
@@ -2859,7 +3021,7 @@ async function planEdgeCompaction(args) {
2859
3021
  };
2860
3022
  let prefix = state.stablePrefixes.get(sessionId);
2861
3023
  if (prefix) {
2862
- const miss = prefixMismatch(body, messages, prefix, sessionId, stablePrefixMessageHash);
3024
+ const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
2863
3025
  if (miss) {
2864
3026
  // Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
2865
3027
  // longer stands for this conversation. Start over from passthrough.
@@ -2895,8 +3057,13 @@ async function planEdgeCompaction(args) {
2895
3057
  (explicitTarget && prefix.targetTokens !== targetTokens)) {
2896
3058
  return forced("target-change", fallback);
2897
3059
  }
2898
- if (size.tokens >= budget.tokens || overWindow)
3060
+ if (size.tokens >= budget.tokens || overWindow) {
3061
+ const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
3062
+ waitMs: opts.memtree.longCompressBudgetMs });
3063
+ if (adopted)
3064
+ return { kind: "ride", ...adopted };
2899
3065
  return forced("budget", fallback);
3066
+ }
2900
3067
  return { kind: "ride", prefix, routed };
2901
3068
  }
2902
3069
  if (state.compactNow.has(sessionId))
@@ -2909,12 +3076,18 @@ async function planEdgeCompaction(args) {
2909
3076
  compaction.sizeSource = size.source;
2910
3077
  noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
2911
3078
  if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
3079
+ const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
3080
+ waitMs: opts.memtree.longCompressBudgetMs });
3081
+ if (adopted)
3082
+ return { kind: "ride", ...adopted };
2912
3083
  return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
2913
3084
  }
2914
3085
  if (state.serverReportsBudget) {
2915
3086
  // The server measures the request and compresses only past the budget.
2916
3087
  const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
2917
3088
  compaction.thresholdTokens = threshold;
3089
+ if (clearlyUnderBudget(opts, state, budget, size, compaction))
3090
+ return { kind: "pass" };
2918
3091
  return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
2919
3092
  }
2920
3093
  // Without calibrated usage or threshold support, let the server decide
@@ -2955,7 +3128,7 @@ async function planToolCompaction(args) {
2955
3128
  const stableLane = args.isMainRequest && sessionId !== undefined;
2956
3129
  const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
2957
3130
  let ride = args.ride;
2958
- if (!ride && current && !prefixMismatch(body, messages, current, sessionId, stablePrefixMessageHash)) {
3131
+ if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
2959
3132
  const routed = prefixRoutedBody(body, messages, current);
2960
3133
  if (routed) {
2961
3134
  const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
@@ -2993,12 +3166,27 @@ async function planToolCompaction(args) {
2993
3166
  if (size.tokens < budget.tokens && !overWindow) {
2994
3167
  return ride ? { kind: "ride", ride } : { kind: "pass" };
2995
3168
  }
3169
+ const adopted = stableLane
3170
+ ? await adoptPreparedPrefix({ ...args, sessionId: sessionId, budgetTokens: budget.tokens,
3171
+ compaction, waitMs: opts.memtree.longCompressBudgetMs })
3172
+ : undefined;
3173
+ if (adopted) {
3174
+ return { kind: "ride",
3175
+ ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
3176
+ }
3177
+ // Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
3178
+ const modeNow = compactionMode(opts, state, sessionId);
3179
+ if (modeNow.mode === "off")
3180
+ return { kind: "off" };
3181
+ const targetNow = compactionTarget(modeNow, budget.tokens);
3182
+ compaction.mode = modeNow.mode;
3183
+ compaction.targetTokens = targetNow;
2996
3184
  compaction.reason = "budget";
2997
3185
  return {
2998
3186
  kind: "compress",
2999
- ...budgetCompaction(state, compaction, targetTokens, body, modelContextLimit),
3000
- targetTokens,
3001
- explicitTarget: mode.mode === "explicit",
3187
+ ...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
3188
+ targetTokens: targetNow,
3189
+ explicitTarget: modeNow.mode === "explicit",
3002
3190
  estimateTokens: size.tokens,
3003
3191
  budgetTokens: budget.tokens,
3004
3192
  overWindow,
@@ -3007,6 +3195,224 @@ async function planToolCompaction(args) {
3007
3195
  ...(stableLane ? { replaces: current ?? null } : {}),
3008
3196
  };
3009
3197
  }
3198
+ /** Server `compression_target_tokens` sentinel: never compress, always pass through. */
3199
+ const NEVER_COMPRESS_TARGET_TOKENS = -1;
3200
+ /** Share of the budget under which a counted human turn skips its blocking call. */
3201
+ const CLEARLY_UNDER_BUDGET_RATIO = 0.85;
3202
+ /**
3203
+ * Whether a main human turn with no prefix is clearly under the budget by its
3204
+ * token count (an exact count, or a projection from Anthropic's reported
3205
+ * usage; never a byte guess) against a budget the server reported. The
3206
+ * compress call could then only pass it through, so it need not wait for it.
3207
+ */
3208
+ function clearlyUnderBudget(opts, state, budget, size, compaction) {
3209
+ // A known payment or key failure keeps the blocking path: it gives the notice.
3210
+ if (opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
3211
+ return false;
3212
+ return budget.source !== "window-ratio" && size.source === "reported" &&
3213
+ compaction.uncountedOverWindow !== true &&
3214
+ size.tokens <= Math.floor(budget.tokens * CLEARLY_UNDER_BUDGET_RATIO);
3215
+ }
3216
+ /**
3217
+ * Whether this model and window prepare compressions: a Claude model on a 1M
3218
+ * window whose budget is the near-window one (not a Sonnet ceiling).
3219
+ */
3220
+ function preparesCompression(model, modelContextLimit) {
3221
+ return modelContextLimit >= ONE_MILLION_TOKENS &&
3222
+ fallbackBudgetTokens(model, modelContextLimit) === nearWindowBudgetTokens(modelContextLimit) &&
3223
+ typeof model === "string" && CLAUDE_FAMILY_RE.test(model);
3224
+ }
3225
+ const ONE_MILLION_TOKENS = 1_000_000;
3226
+ /**
3227
+ * The session's prepared prefix (prepared-compression.ts) as this turn's
3228
+ * ride, now the session's stable prefix, when it still covers this history
3229
+ * and prefix + the turns after it fit under the budget. Undefined otherwise,
3230
+ * and the prepared prefix is dropped: the turn compresses as before.
3231
+ */
3232
+ async function adoptPreparedPrefix(args) {
3233
+ const { state, sessionId } = args;
3234
+ const base = state.stablePrefixes.get(sessionId);
3235
+ await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
3236
+ const entry = state.prepared.get(sessionId);
3237
+ if (!entry)
3238
+ return undefined;
3239
+ state.prepared.discard(sessionId);
3240
+ const fit = await preparedFit(args, entry);
3241
+ // Another request installed a prefix, or `/memtree-compact` asked for its
3242
+ // own target, while this one waited or counted: that wins.
3243
+ const kind = fit.kind === "used" && (state.stablePrefixes.get(sessionId) !== base ||
3244
+ state.compactNow.has(sessionId) || compactionMode(args.opts, state, sessionId).mode !== "auto")
3245
+ ? "stale" : fit.kind;
3246
+ args.compaction.prepared = kind;
3247
+ if (kind !== "used" || fit.kind !== "used") {
3248
+ // A preparation not adopted is never sent, not even as a fallback.
3249
+ if (fit.candidate)
3250
+ args.sizing.excludeFallback(fit.candidate);
3251
+ if (kind === "too-large" && fit.candidate) {
3252
+ notePreparedTruncationBase(args, entry, base, fit.candidate);
3253
+ }
3254
+ return undefined;
3255
+ }
3256
+ args.compaction.estimatedTokens = fit.tokens;
3257
+ args.compaction.estimatedBytes = fit.routed.raw.length;
3258
+ installPreparedPrefix(state, sessionId, entry);
3259
+ return { prefix: entry.prefix, routed: fit.routed };
3260
+ }
3261
+ /** An adopted preparation becomes the session's stable prefix (and its page, persisted). */
3262
+ function installPreparedPrefix(state, sessionId, entry) {
3263
+ boundedSet(state.stablePrefixes, sessionId, entry.prefix);
3264
+ state.compactNow.delete(sessionId);
3265
+ if (entry.source)
3266
+ notePreparedSource(state, sessionId, entry.source);
3267
+ persistStablePrefix(state, sessionId, entry.prefix);
3268
+ }
3269
+ const preparedTruncationBases = new WeakMap();
3270
+ function notePreparedTruncationBase(args, entry, base, candidate) {
3271
+ const { state, sessionId } = args;
3272
+ preparedTruncationBases.set(args.sizing, { candidate, install: () => {
3273
+ if (state.stablePrefixes.get(sessionId) !== base || state.compactNow.has(sessionId) ||
3274
+ compactionMode(args.opts, state, sessionId).mode !== "auto")
3275
+ return;
3276
+ args.compaction.prepared = "truncated";
3277
+ installPreparedPrefix(state, sessionId, entry);
3278
+ } });
3279
+ }
3280
+ /**
3281
+ * The turn that crosses the budget while its session's preparation is still
3282
+ * in flight waits for it (it started earlier than a fresh compression could),
3283
+ * for at most `waitMs`, rather than making a second, concurrent compress call.
3284
+ */
3285
+ async function awaitPreparation(state, sessionId, waitMs, compaction) {
3286
+ const pending = state.preparing.get(sessionId);
3287
+ if (!pending)
3288
+ return;
3289
+ const started = Date.now();
3290
+ let timer;
3291
+ await Promise.race([pending, new Promise((resolve) => {
3292
+ timer = setTimeout(resolve, Math.max(0, waitMs));
3293
+ })]).finally(() => clearTimeout(timer));
3294
+ compaction.preparedWaitMs = Date.now() - started;
3295
+ }
3296
+ /** An adopted preparation's MemTree page becomes the session's memory source and page. */
3297
+ function notePreparedSource(state, sessionId, source) {
3298
+ const result = { messages: [], compressed: true, memtreeUrl: source.url,
3299
+ memtreeIndex: source.index };
3300
+ noteMemtreePage(state, source.seq, sessionId, result);
3301
+ noteMemorySource(state, source.seq, sessionId, result);
3302
+ }
3303
+ /** Whether a prepared prefix can carry this request (adoptPreparedPrefix). */
3304
+ async function preparedFit(args, entry) {
3305
+ const { sizing, body, messages, modelContextLimit, budgetTokens, compaction } = args;
3306
+ if (entry.prefix.modelContextLimit !== modelContextLimit || entry.triggerTokens !== budgetTokens ||
3307
+ entry.prefix.targetTokens !== compaction.targetTokens ||
3308
+ entry.prefix.explicitTarget !== (compaction.mode === "explicit")) {
3309
+ return { kind: "stale" };
3310
+ }
3311
+ if (prefixMismatch(body, messages, entry.prefix, args.sessionId, "stable")) {
3312
+ return { kind: "mismatch" };
3313
+ }
3314
+ const routed = prefixRoutedBody(body, messages, entry.prefix);
3315
+ if (!routed)
3316
+ return { kind: "mismatch" };
3317
+ const candidate = sizing.register(routed.raw, "ride", entry.prefix.lastSize);
3318
+ candidateSizeOwners.set(candidate, entry.prefix);
3319
+ await countIfOverBound(sizing, candidate, body, modelContextLimit);
3320
+ const tokens = sizing.plan(candidate).tokens;
3321
+ const fits = tokens < budgetTokens && fitsNative(tokens, { budget: budgetTokens,
3322
+ context: modelContextLimit, output: bodyOutputReservation(body, sizing.isExact(candidate)) }) &&
3323
+ (sizing.isExact(candidate) || sizing.boundFits(candidate, { budget: budgetTokens, context: modelContextLimit, output: bodyOutputReservation(body, false) }));
3324
+ // Too large, the turn compresses as before; the ride stays registered, so
3325
+ // if that cannot fit either, delivery's safety net may shorten its newest
3326
+ // tool result instead (truncatedToFit).
3327
+ return fits ? { kind: "used", routed, tokens, candidate } : { kind: "too-large", candidate };
3328
+ }
3329
+ /**
3330
+ * After a main-thread turn's plan: once its size nears the budget, ask
3331
+ * MemTree in the background for the compressed prefix the turn that crosses
3332
+ * it will ride (prepared-compression.ts). Fire and forget, one call per
3333
+ * session at a time; never delays this turn.
3334
+ */
3335
+ function maybePrepareCompression(args) {
3336
+ const { state, sessionId, rec } = args;
3337
+ const compaction = rec.compaction;
3338
+ // Automatic compaction only: an explicit target (/memtree-compact N) is the
3339
+ // user's own schedule, and a high one would leave nothing to prepare for.
3340
+ if (!compaction || compaction.mode !== "auto" || compaction.estimatedTokens === undefined ||
3341
+ compaction.targetTokens === undefined || state.keyInvalid() ||
3342
+ state.backgroundSignal.aborted ||
3343
+ state.pausedDetail() !== null ||
3344
+ !preparesCompression(args.body.model, args.modelContextLimit))
3345
+ return;
3346
+ const tokens = compaction.estimatedTokens;
3347
+ if (tokens < prepareStartTokens(compaction.budgetTokens))
3348
+ return;
3349
+ const decision = state.prepared.decide(sessionId, tokens, compaction.budgetTokens);
3350
+ rec.prepare = decision;
3351
+ if (decision !== "prepare" ||
3352
+ !state.prepared.begin(sessionId, tokens, compaction.budgetTokens))
3353
+ return;
3354
+ const base = state.stablePrefixes.get(sessionId);
3355
+ const pending = prepareInBackground({ ...args, tokens, targetTokens: compaction.targetTokens,
3356
+ explicitTarget: false, triggerTokens: compaction.budgetTokens,
3357
+ linkSeq: nextMemtreeCallSeq(state) })
3358
+ .catch(() => undefined)
3359
+ .then((prepared) => {
3360
+ // A prefix installed meanwhile (a blocking compaction) supersedes it.
3361
+ const current = state.stablePrefixes.get(sessionId) === base;
3362
+ state.prepared.finish(sessionId, current ? prepared : undefined);
3363
+ if (state.preparing.get(sessionId) === pending)
3364
+ state.preparing.delete(sessionId);
3365
+ });
3366
+ boundedSet(state.preparing, sessionId, pending);
3367
+ }
3368
+ /** The background prepare call and the prefix it yields (maybePrepareCompression). */
3369
+ async function prepareInBackground(args) {
3370
+ const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
3371
+ const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
3372
+ noteServerBudget(state, body.model, modelContextLimit, result);
3373
+ if (!result)
3374
+ return undefined;
3375
+ state.prepared.noteServerAnswer(result.prepared === true);
3376
+ if (result.prepared !== true || !didMemtreeCompress(result) ||
3377
+ !checkCompressedHistory(result, msgsForMemtree).usable)
3378
+ return undefined;
3379
+ const built = buildCompressedBody(body, result);
3380
+ if (!built)
3381
+ return undefined;
3382
+ const prefix = {
3383
+ sessionId,
3384
+ originalSystemHash: routeValueHash(normalizeRouteSystem(body.system)),
3385
+ ...prefixIdentity(args.messages, "stable"),
3386
+ compressedMessages: cloneJson(built.compressedBody.messages),
3387
+ compressedSystem: cloneJson(built.compressedBody.system),
3388
+ hasCompressedSystem: Object.prototype.hasOwnProperty.call(built.compressedBody, "system"),
3389
+ targetTokens: args.targetTokens,
3390
+ explicitTarget: args.explicitTarget,
3391
+ modelContextLimit,
3392
+ };
3393
+ const source = result.memtreeUrl && result.memtreeIndex
3394
+ ? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
3395
+ return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
3396
+ ...(source ? { source } : {}) };
3397
+ }
3398
+ /** What a prepare call sends: a forced compression to the target, no threshold. */
3399
+ function preparedCompressMeta(args) {
3400
+ const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
3401
+ const messageUsage = opts.transcriptUsage?.usageFor(sessionId, msgsForMemtree, undefined);
3402
+ const messageTimes = transcriptTimesFor(opts, sessionId, undefined)?.(msgsForMemtree);
3403
+ return {
3404
+ model: modelForMemtree(typeof body.model === "string" ? body.model : undefined, modelContextLimit),
3405
+ tools: Array.isArray(body.tools) ? body.tools : undefined,
3406
+ compressionTargetTokens: args.targetTokens,
3407
+ prepare: true,
3408
+ ...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
3409
+ ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3410
+ sessionId,
3411
+ ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3412
+ // Nothing waits on it: the long budget, so a slow server still prepares.
3413
+ timeoutMs: opts.memtree.longCompressBudgetMs,
3414
+ };
3415
+ }
3010
3416
  function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
3011
3417
  // Model is deliberately not route identity: Claude Code switches models
3012
3418
  // mid-loop (overload fallback, /model, /fast), and the compressed prefix is
@@ -3024,7 +3430,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
3024
3430
  if (messages.length < prefixLength)
3025
3431
  return null;
3026
3432
  for (let i = 0; i < prefixLength; i++) {
3027
- if (routeMessageHash(messages[i]) !== route.originalPrefixHashes[i]) {
3433
+ if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
3028
3434
  return null;
3029
3435
  }
3030
3436
  }
@@ -3407,8 +3813,9 @@ async function runBlockingCompression(args) {
3407
3813
  : undefined;
3408
3814
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
3409
3815
  const compressStarted = Date.now();
3410
- const budgetMs = args.longBudget
3411
- ? opts.memtree.longCompressBudgetMs : opts.memtree.compressBudgetMs;
3816
+ // The upload is about the request's size: the budget grows with it.
3817
+ const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? opts.memtree.longCompressBudgetMs
3818
+ : opts.memtree.compressBudgetForBytes(rec.requestBytes));
3412
3819
  const compressMeta = {
3413
3820
  // Model + tools drive the server's model-based memory budget
3414
3821
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3436,7 +3843,9 @@ async function runBlockingCompression(args) {
3436
3843
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3437
3844
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3438
3845
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3439
- ...(args.longBudget ? { timeoutMs: budgetMs } : {}),
3846
+ // The deadline this wait uses (the upload-scaled budget included), so the
3847
+ // call and its waiters end together.
3848
+ timeoutMs: budgetMs,
3440
3849
  };
3441
3850
  // Sampled BEFORE the call, while it still describes this call: after the
3442
3851
  // await the hash is in the cache regardless of who put it there.
@@ -3444,9 +3853,9 @@ async function runBlockingCompression(args) {
3444
3853
  // compress() maps every failure to a resolved null (it never rejects).
3445
3854
  // Keep foreground work finite even if a transport ignores its abort signal.
3446
3855
  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));
3856
+ 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
3857
  const result = await Promise.race([
3449
- opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
3858
+ opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
3450
3859
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
3451
3860
  ]).finally(() => { if (deadline)
3452
3861
  clearTimeout(deadline); });
@@ -3456,19 +3865,27 @@ async function runBlockingCompression(args) {
3456
3865
  // another class can overwrite the complete-key entry.
3457
3866
  const failureArming = result === null &&
3458
3867
  opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
3868
+ const failureStatus = result === null
3869
+ ? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
3459
3870
  const hadLiveSuccess = !cached && result !== null;
3460
3871
  // Only arming-class live failures (network error, timeout, 5xx, 402) are
3461
3872
  // fuse evidence. A responsive server's other 4xx failed THIS call but says
3462
3873
  // nothing about MemTree's health, so it contributes neither failure nor
3463
3874
  // success — the fuse is untouched.
3464
- const hadLiveFailure = !cached && result === null && failureArming;
3875
+ // Budget-consumed heuristic: the client maps every failure to null, so a
3876
+ // null that took (roughly) the whole abort budget was almost certainly
3877
+ // the AbortSignal timeout, not a fast server error.
3878
+ const timedOut = result === null && compressMs >= Math.min(budgetMs, maximumWait);
3879
+ // A timeout while other compress calls answered (one succeeded after this
3880
+ // one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
3881
+ // healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
3882
+ const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
3883
+ const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered;
3465
3884
  rec.compress = {
3466
3885
  ms: compressMs,
3467
3886
  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,
3887
+ timedOut,
3888
+ ...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
3472
3889
  };
3473
3890
  return {
3474
3891
  result,
@@ -3480,6 +3897,61 @@ async function runBlockingCompression(args) {
3480
3897
  : "none",
3481
3898
  };
3482
3899
  }
3900
+ /**
3901
+ * A clearly under-budget human turn's compress call, not awaited: its answer
3902
+ * (a passthrough) only updates the server budget, MemTree health and the
3903
+ * session's page link. Its telemetry goes to a copy of the turn's record.
3904
+ */
3905
+ function compressInBackground(args) {
3906
+ if (args.state.backgroundSignal.aborted)
3907
+ return;
3908
+ const lanes = args.state.backgroundCompress;
3909
+ const lane = backgroundCompressLane(args);
3910
+ if (lane === undefined) {
3911
+ void runBackgroundCompress(args);
3912
+ return;
3913
+ }
3914
+ const busy = lanes.get(lane);
3915
+ if (busy) {
3916
+ // One whole-history call at a time per conversation: under the budget each turn
3917
+ // started its own (16 to 22 s each on staging in the long-session test, about 12 at
3918
+ // once beside the index uploads). A waiting call is replaced only by one whose
3919
+ // history contains all of it; otherwise it goes now.
3920
+ if (busy.waiting && !extendsHistory(args.msgsForMemtree, busy.waiting.msgsForMemtree)) {
3921
+ void runBackgroundCompress(busy.waiting);
3922
+ }
3923
+ busy.waiting = args;
3924
+ return;
3925
+ }
3926
+ const owner = {};
3927
+ lanes.set(lane, owner);
3928
+ void runBackgroundCompress(args).finally(() => {
3929
+ if (lanes.get(lane) === owner)
3930
+ lanes.delete(lane);
3931
+ if (owner.waiting)
3932
+ compressInBackground(owner.waiting);
3933
+ });
3934
+ }
3935
+ function runBackgroundCompress(args) {
3936
+ const { state, sessionId, linkSeq } = args;
3937
+ return runBlockingCompression({ ...args, rec: { ...args.rec }, signal: state.backgroundSignal })
3938
+ .then((compression) => {
3939
+ noteMemtreeHealth(state, compression);
3940
+ if (linkSeq !== undefined)
3941
+ noteMemtreePage(state, linkSeq, sessionId, compression.result);
3942
+ })
3943
+ .catch(() => undefined);
3944
+ }
3945
+ /**
3946
+ * A conversation for compressInBackground: session and first message, media left out (Claude
3947
+ * Code's media eviction keeps it, as indexLaneKey does); none without a session.
3948
+ */
3949
+ function backgroundCompressLane(args) {
3950
+ if (args.sessionId === undefined)
3951
+ return undefined;
3952
+ const first = args.msgsForMemtree.find((message) => message.role !== "system");
3953
+ return JSON.stringify([args.sessionId, first ? mediaFreeMessageHash(first) : ""]);
3954
+ }
3483
3955
  /**
3484
3956
  * Shared final request shape for a validated compression result: lift any
3485
3957
  * returned system message to `body.system`, and forward the SERVER's flatten
@@ -3579,14 +4051,14 @@ async function recoverToolRouteMiss(args) {
3579
4051
  capture(opts, "anthropic-request-memory-tool", fallback.raw);
3580
4052
  return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3581
4053
  toolForwarding.settle(delivered);
3582
- noteRideSize(fallback.sizeHolder, rec, fallback.raw.length);
4054
+ noteRideSize(fallback.sizeHolder, rec, fallback.raw);
3583
4055
  });
3584
4056
  }
3585
4057
  recordTurn(rec, "tool", args.originalBody);
3586
4058
  capture(opts, "anthropic-request", args.originalBody);
3587
4059
  return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3588
4060
  toolForwarding.settle(delivered);
3589
- noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
4061
+ noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
3590
4062
  });
3591
4063
  };
3592
4064
  // Same downstream-close tracking as the followup path: compression
@@ -3727,6 +4199,7 @@ async function recoverToolRouteMiss(args) {
3727
4199
  const { compressedBody, compressedRaw } = built;
3728
4200
  if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
3729
4201
  rec.routeRecovery = { conversationBytes, outcome: "unusable" };
4202
+ noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
3730
4203
  releaseOwnReservation();
3731
4204
  if (!noteIndexLag(opts, rec, result))
3732
4205
  return forwardOriginal();
@@ -3873,7 +4346,7 @@ async function recoverToolRouteMiss(args) {
3873
4346
  // The compacted request's reported size anchors the next budget check.
3874
4347
  const sizeHolder = stable?.installed ?? installedRoute;
3875
4348
  if (sizeHolder)
3876
- notePrefixSize(sizeHolder, rec, compressedRaw.length);
4349
+ notePrefixSize(sizeHolder, rec, compressedRaw);
3877
4350
  // Nothing to ride next turn, or a result that still does not fit the
3878
4351
  // window (the next turn would reject it and compress again): wait for
3879
4352
  // growth before the lane tries again.
@@ -3972,18 +4445,29 @@ const recapLinkAppenders = new WeakMap();
3972
4445
  * miss the tree, index a divergent conversation and bill for it.
3973
4446
  */
3974
4447
  async function recoverLateCount(args) {
4448
+ let generation;
3975
4449
  try {
3976
4450
  const { body, msgsForMemtree: messages } = args;
3977
4451
  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);
4452
+ const target = args.target ?? lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
4453
+ const sessionId = requestSessionId(args.req);
4454
+ const linkSeq = args.route.lane === "main" ? nextMemtreeCallSeq(args.state) : undefined;
4455
+ const replaces = sessionId !== undefined
4456
+ ? args.state.stablePrefixes.get(sessionId) ?? null : null;
4457
+ // Reserved before the compress call: a newer same-lane decision made
4458
+ // while it runs outranks this one, so this never overwrites its route.
4459
+ generation = ++args.state.mainRouteDecisionGeneration;
4460
+ reserveRouteDecision(args.state, args.route.key, generation);
3979
4461
  const outcome = await runBlockingCompression({
3980
4462
  opts: args.opts, state: args.state, body, msgsForMemtree: messages,
3981
4463
  hash: args.hash, modelContextLimit: args.modelContextLimit,
3982
- rec: args.rec, sessionId: requestSessionId(args.req), agentId: agentAttributionId(args.req),
4464
+ rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
3983
4465
  clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
3984
4466
  compaction: { target },
3985
4467
  });
3986
4468
  noteMemtreeHealth(args.state, outcome);
4469
+ if (linkSeq !== undefined)
4470
+ noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
3987
4471
  // Like every compress path: a refusal after a lagging answer waits for
3988
4472
  // the index, and the cached answer expires so a retry asks again.
3989
4473
  if (outcome.result)
@@ -3994,11 +4478,63 @@ async function recoverLateCount(args) {
3994
4478
  const built = buildCompressedBody(body, outcome.result);
3995
4479
  if (!built)
3996
4480
  return undefined;
4481
+ lateRouteInstalls.set(args.rec, lateRouteInstall(args, built.compressedBody, outcome.result, {
4482
+ targetTokens: target, explicitTarget: true, modelContextLimit: args.modelContextLimit,
4483
+ replaces
4484
+ }, linkSeq, generation));
4485
+ generation = undefined; // the install owns the reservation now
3997
4486
  return built.compressedRaw;
3998
4487
  }
3999
4488
  catch {
4000
4489
  return undefined;
4001
4490
  }
4491
+ finally {
4492
+ if (generation !== undefined)
4493
+ releaseRouteDecision(args.state, args.route.key, generation);
4494
+ }
4495
+ }
4496
+ /** Late recoveries' installs, per request, until delivery settles. */
4497
+ const lateRouteInstalls = new WeakMap();
4498
+ function lateRouteInstall(args, compressedBody, result, stable, linkSeq,
4499
+ /** Reserved (reserveRouteDecision) before the compress call; released here if unused. */
4500
+ generation) {
4501
+ const { state, route } = args;
4502
+ const sessionId = requestSessionId(args.req);
4503
+ let done = false;
4504
+ let installed;
4505
+ return {
4506
+ activate: () => {
4507
+ if (done)
4508
+ return;
4509
+ done = true;
4510
+ if (sessionId !== undefined && route.lane !== "away") {
4511
+ installed = installMemoryRoute(state, route.key, args.req, args.body, route.messages, compressedBody, route.epoch, generation, route.lane === "main" ? stable : undefined);
4512
+ }
4513
+ if (!installed)
4514
+ releaseRouteDecision(state, route.key, generation);
4515
+ else if (linkSeq !== undefined)
4516
+ noteMemorySource(state, linkSeq, sessionId, result);
4517
+ },
4518
+ settle(delivered, sent) {
4519
+ if (delivered)
4520
+ this.activate();
4521
+ // The replacement is a changed delivery (reportedSample skips those):
4522
+ // its reported size is this very body's, so it is noted directly.
4523
+ const holder = stable.installed ?? installed;
4524
+ const tokens = reportedInputTokens(args.rec);
4525
+ if (holder && sent && delivered && tokens !== undefined &&
4526
+ !serverToolRecords.has(args.rec) && !usedServerTools(args.rec)) {
4527
+ const sample = sizeSampleOf(sent, tokens, args.rec.model);
4528
+ if (plausibleSample(sample) && !(holder.lastSize && sent.length < holder.lastSize.forwardedBytes)) {
4529
+ holder.lastSize = sample;
4530
+ }
4531
+ }
4532
+ if (!installed) {
4533
+ done = true;
4534
+ releaseRouteDecision(state, route.key, generation);
4535
+ }
4536
+ },
4537
+ };
4002
4538
  }
4003
4539
  /**
4004
4540
  * The ride compressed to `target` still counted `rideTokens` (over the
@@ -4018,16 +4554,25 @@ async function selectForwardCandidate(args) {
4018
4554
  const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
4019
4555
  const measured = await policy.sizing.measure(preferred, args.limits);
4020
4556
  let target = preferred;
4021
- if (preferred.kind === "ride" && measured.source === "exact" &&
4022
- measured.tokens > args.limits.budget && !args.rec.compress && !args.rec.lateCountRecovery) {
4557
+ const late = lateRecoveryPlan(args.state, args.rec, preferred, measured, args.limits, args.routeKey);
4558
+ if (late) {
4023
4559
  args.rec.lateCountRecovery = { outcome: "forwarded" };
4024
- const replacement = await policy.recoverLate(preferred.body, measured.tokens);
4560
+ const replacement = await holdingLane(args.state, args.routeKey, () => policy.recoverLate(preferred.body, late.tokens, late.target));
4025
4561
  if (replacement)
4026
4562
  target = policy.sizing.register(replacement, "replacement");
4027
4563
  }
4028
- const selection = await policy.sizing.select(target, args.limits);
4564
+ const selection = await policy.sizing.select(target, args.limits) ??
4565
+ await policy.sizing.retryTimedOutCount(args.limits) ??
4566
+ (args.rec.routeLane === "main" && args.opts.toolOutputs
4567
+ ? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
4568
+ : undefined);
4029
4569
  const quietDegrade = quietDegrades.get(args.rec);
4030
4570
  quietDegrades.delete(args.rec);
4571
+ const lateInstall = lateRouteInstalls.get(args.rec);
4572
+ if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
4573
+ lateRouteInstalls.delete(args.rec);
4574
+ lateInstall.settle(false);
4575
+ }
4031
4576
  if (!selection) {
4032
4577
  if (args.rec.lateCountRecovery)
4033
4578
  args.rec.lateCountRecovery.outcome = "refused";
@@ -4035,6 +4580,10 @@ async function selectForwardCandidate(args) {
4035
4580
  return undefined;
4036
4581
  }
4037
4582
  const { candidate, measurement } = selection;
4583
+ const preparedBase = preparedTruncationBases.get(policy.sizing);
4584
+ if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
4585
+ preparedBase.install();
4586
+ }
4038
4587
  const changed = candidate !== preferred || intended !== args.preferredBody;
4039
4588
  if (changed) {
4040
4589
  policy.allowed.delete(args.preferredBody);
@@ -4057,6 +4606,154 @@ async function selectForwardCandidate(args) {
4057
4606
  }
4058
4607
  return changed ? candidate.body : intended;
4059
4608
  }
4609
+ /** MemTree's answer when the conversation cannot fit the window even compressed. */
4610
+ const OVER_CONTEXT_STATUS = 413;
4611
+ /** Truncation attempts, each cutting more, before the request is refused. */
4612
+ const TRUNCATION_ATTEMPTS = 3;
4613
+ /** Cut this much more than the measured excess (tokens per character vary). */
4614
+ const TRUNCATION_MARGIN = 1.25;
4615
+ /**
4616
+ * The safety net for a main-thread request that does not fit even after
4617
+ * compression (tool-result-truncation.ts): its newest tool result saved whole
4618
+ * (tool-output-store.ts) and sent with the middle replaced by a pointer to
4619
+ * that copy, cut until the request leaves the reply's room below the window.
4620
+ * A compressed candidate first (a ride on a prefix, or a replacement whose
4621
+ * suffix is structured), else the whole history.
4622
+ */
4623
+ async function truncatedToFit(rec, sizing, preferred, limits, store) {
4624
+ if (!compressionSettled(rec))
4625
+ return undefined;
4626
+ const prepared = preparedTruncationBases.get(sizing);
4627
+ const rank = (item) => item.kind === "original" ? 1 : 0;
4628
+ const bases = [preferred, ...sizing.fallbacks().filter((item) => item !== preferred),
4629
+ ...(prepared && prepared.candidate !== preferred ? [prepared.candidate] : [])]
4630
+ .sort((a, b) => rank(a) - rank(b) || a.body.length - b.body.length);
4631
+ for (const base of bases) {
4632
+ const fitted = await truncateCandidate(rec, sizing, base, limits, store);
4633
+ if (fitted && prepared && base === prepared.candidate)
4634
+ prepared.derived = fitted.candidate;
4635
+ if (fitted)
4636
+ return fitted;
4637
+ }
4638
+ return undefined;
4639
+ }
4640
+ /**
4641
+ * Whether compression had its chance and the request still cannot fit: a ride
4642
+ * on a prepared prefix, a compress answer that compressed (sent or found too
4643
+ * large), or MemTree's 413 (it cannot fit the window even compressed). Not
4644
+ * after a failed or skipped compress call (an outage, an exception, an auth
4645
+ * or payment refusal, a backoff: the retryable refusal stands), a passthrough
4646
+ * of a warming index, or while the index is behind: waiting for it lets
4647
+ * compression summarize instead.
4648
+ */
4649
+ function compressionSettled(rec) {
4650
+ if (rec.indexBehind === true)
4651
+ return false;
4652
+ if (rec.compress?.refused === true || rec.compaction?.prepared === "used")
4653
+ return true;
4654
+ return rec.compress?.ok === true && rec.routeRecovery?.outcome !== "noop" &&
4655
+ rec.turnType !== "followup-noop";
4656
+ }
4657
+ /** truncatedToFit for one candidate; undefined when it has no tool result to
4658
+ * shorten, the copy could not be saved, or it cannot be made to fit. */
4659
+ async function truncateCandidate(rec, sizing, base, limits, store) {
4660
+ const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
4661
+ const parsed = JSON.parse(base.body.toString("utf-8"));
4662
+ const first = await sizing.measure(base, limits);
4663
+ const bytesPerToken = base.body.length / Math.max(1, first.tokens);
4664
+ for (let attempt = 1; attempt <= TRUNCATION_ATTEMPTS; attempt++) {
4665
+ const excess = Math.max(1_000, first.tokens - goal);
4666
+ const cut = Math.ceil(excess * bytesPerToken * TRUNCATION_MARGIN * attempt);
4667
+ // The store pins each copy it saves, so none this request points at is
4668
+ // evicted while it is counted and forwarded.
4669
+ const plan = truncateToolResults(parsed, cut, (text) => store.save(text));
4670
+ if (!plan)
4671
+ return undefined;
4672
+ const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
4673
+ const measurement = await sizing.measure(candidate, limits);
4674
+ if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
4675
+ rec.truncatedToolResult = { originalChars: plan.originalChars,
4676
+ omittedChars: plan.omittedChars };
4677
+ return { candidate, measurement };
4678
+ }
4679
+ }
4680
+ return undefined;
4681
+ }
4682
+ /**
4683
+ * Whether delivery recompresses before sending, and to what: a ride counted
4684
+ * over the budget (no compression yet); the whole history counted over the
4685
+ * window with no compression yet (outside an outage cooldown: a refusal is
4686
+ * all that is left otherwise); or a compression that came back too large for
4687
+ * the window (oversizedCompressions), redone once to a target smaller by the
4688
+ * excess, so the turn is compressed and sent in the same request instead of
4689
+ * being refused and retried.
4690
+ */
4691
+ function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
4692
+ if (rec.lateCountRecovery)
4693
+ return undefined;
4694
+ const oversized = oversizedCompressions.get(rec);
4695
+ // A lagging index kept history verbatim: the refusal waits for it instead.
4696
+ if (oversized && rec.indexBehind !== true) {
4697
+ const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
4698
+ const excess = oversized.tokens - goal;
4699
+ const target = Math.floor(oversized.target - excess - oversized.target * SMALLER_TARGET_MARGIN);
4700
+ return target >= SERVER_MIN_TARGET_TOKENS ? { tokens: oversized.tokens, target } : undefined;
4701
+ }
4702
+ if (rec.compress || measured.source !== "exact" || !(measured.tokens > limits.budget)) {
4703
+ return undefined;
4704
+ }
4705
+ // Another request of this lane is compressing (rec.routeRecovery "in-flight"):
4706
+ // a second compression of the same history at another target would miss its
4707
+ // dedupe and bill twice. This one waits for neither and goes as it is.
4708
+ if (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight)
4709
+ return undefined;
4710
+ if (preferred.kind === "ride")
4711
+ return { tokens: measured.tokens };
4712
+ // Over the window with nothing compressed, a refusal is all that is left:
4713
+ // an outage cooldown does not skip this one compress.
4714
+ return preferred.kind === "original" && !fitsMeasured(measured, limits)
4715
+ ? { tokens: measured.tokens } : undefined;
4716
+ }
4717
+ /**
4718
+ * Run a delivery-time compression as the lane's in-flight operation, so a
4719
+ * concurrent request of the lane neither starts its own (planning sees
4720
+ * "in-flight") nor races it; the lane's own attempt state is kept.
4721
+ */
4722
+ async function holdingLane(state, routeKey, run) {
4723
+ const lanes = state.toolRecoveryAttemptedLanes;
4724
+ const prior = lanes.get(routeKey);
4725
+ const attempt = prior ?? { modelContextLimit: 0, inFlight: true };
4726
+ // Clears only what it set: an attempt another request holds in flight
4727
+ // stays that request's to end.
4728
+ const setHere = !attempt.inFlight;
4729
+ attempt.inFlight = true;
4730
+ if (!prior)
4731
+ lanes.set(routeKey, attempt);
4732
+ try {
4733
+ return await run();
4734
+ }
4735
+ finally {
4736
+ if (setHere)
4737
+ attempt.inFlight = false;
4738
+ if (!prior && lanes.get(routeKey) === attempt)
4739
+ lanes.delete(routeKey);
4740
+ }
4741
+ }
4742
+ /** A recompression aims this share of its old target lower than the excess alone. */
4743
+ const SMALLER_TARGET_MARGIN = 0.1;
4744
+ /**
4745
+ * Compressions of this request that came back too large for the window: their
4746
+ * size (an exact count, else the conservative bound) and the target they had.
4747
+ */
4748
+ const oversizedCompressions = new WeakMap();
4749
+ /** Note a compression of `rec` that does not fit the window (lateRecoveryPlan). */
4750
+ function noteOversizedCompression(rec, sizing, body, compressedRaw, target) {
4751
+ if (target === undefined)
4752
+ return;
4753
+ const tokens = sizing.exactTokensFor(compressedRaw) ??
4754
+ conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
4755
+ oversizedCompressions.set(rec, { tokens, target });
4756
+ }
4060
4757
  /**
4061
4758
  * Whether delivery would refuse `candidate` sent whole, by delivery's own
4062
4759
  * admission: it is sendable only when its plan fits and either an exact count
@@ -4207,7 +4904,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
4207
4904
  const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
4208
4905
  if (!selected)
4209
4906
  return false;
4210
- const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : undefined);
4907
+ // A late recovery's replacement went out instead: it installs as its route.
4908
+ const lateInstall = rec && selected !== bodyBuffer ? lateRouteInstalls.get(rec) : undefined;
4909
+ if (rec)
4910
+ lateRouteInstalls.delete(rec);
4911
+ const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : lateInstall?.activate);
4912
+ lateInstall?.settle(delivered, selected);
4211
4913
  if (delivered && rec)
4212
4914
  noteSelectedCandidateSize(rec);
4213
4915
  return delivered;
@@ -4222,7 +4924,7 @@ function noteSelectedCandidateSize(rec) {
4222
4924
  // Server tools' injected results inflate the reported input (reportedSample).
4223
4925
  if (serverToolRecords.has(rec) || usedServerTools(rec))
4224
4926
  return;
4225
- const sample = { tokens, forwardedBytes: candidate.body.length };
4927
+ const sample = sizeSampleOf(candidate.body, tokens, rec.model);
4226
4928
  if (!plausibleSample(sample))
4227
4929
  return;
4228
4930
  if ("set" in owner)
@@ -4948,4 +5650,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
4948
5650
  return undefined;
4949
5651
  return (messages) => source.timesFor(sessionId, messages, agentId);
4950
5652
  }
5653
+ export const __testHoldingLane = holdingLane;
4951
5654
  //# sourceMappingURL=proxy.js.map