claude-code-infinite 1.0.16 → 1.0.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
package/dist/proxy.js CHANGED
@@ -52,9 +52,10 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
52
52
  import { join } from "node:path";
53
53
  import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
54
54
  import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
55
- import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, PAYMENT_REQUIRED_NOTICE, SseNoticeRewriter, sanitizeNoticeDetail, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
56
- import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
55
+ 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";
57
57
  import { hyperlink } from "./payment-gate.js";
58
+ import { PaymentNoticeThrottle, paymentNoticeText } from "./payment-notice.js";
58
59
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
59
60
  import { describeClaudeCodeRequest, inspectMonitorTranscript, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
60
61
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
@@ -69,6 +70,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
69
70
  // Long enough that a real outage costs one stall, short enough that a
70
71
  // transient blip does not disable recovery for a working session.
71
72
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
73
+ /** Safety cap on a long-budget compress, should a transport ignore its abort. */
74
+ const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
72
75
  const SKIP_REQUEST_HEADERS = new Set([
73
76
  "host",
74
77
  "connection",
@@ -260,7 +263,8 @@ export function startProxy(opts) {
260
263
  const acceptedRequests = new Set();
261
264
  const shutdownAbort = new AbortController();
262
265
  const state = {
263
- paymentNoticeShown: false,
266
+ paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
267
+ unpaidDetail: () => opts.memtree.paymentRequiredDetail,
264
268
  notices: new NoticeDeliveryQueue(),
265
269
  mainNoticePending: false,
266
270
  mainPromptGeneration: 0,
@@ -277,8 +281,13 @@ export function startProxy(opts) {
277
281
  shutdownSignal: shutdownAbort.signal,
278
282
  routeInstallFault: opts.routeInstallFault,
279
283
  memtreePages: new Map(),
284
+ memorySourcePages: new Map(),
280
285
  memtreeCallSeq: 0,
281
286
  memtreeLinkStore: opts.memtreeLinkStore,
287
+ stablePrefixStore: opts.stablePrefixStore,
288
+ storedPrefixLookups: new Map(),
289
+ storageOwner: typeof opts.memtree.storageIdentity === "string"
290
+ ? opts.memtree.storageIdentity : undefined,
282
291
  newestTrees: opts.newestTrees,
283
292
  memtreeLinkPlacement: opts.memtreeLinkPlacement ?? "turn",
284
293
  compactTargets: new Map(),
@@ -547,7 +556,7 @@ async function handleMemTreeFinder(req, res, opts, upstreamPath, url) {
547
556
  */
548
557
  async function handleMemTreeCurrent(req, res, opts, state, url) {
549
558
  const sessionId = url.searchParams.get("session") || undefined;
550
- const page = currentMemtreePage(state, sessionId);
559
+ const page = currentMemorySourcePage(state, sessionId);
551
560
  const id = page ? memtreePageId(page.url) : undefined;
552
561
  if (!page || !id) {
553
562
  res.writeHead(404, { "content-type": "application/json" });
@@ -579,6 +588,12 @@ async function handleMemTreeCurrent(req, res, opts, state, url) {
579
588
  }
580
589
  }
581
590
  const MEMTREE_CURRENT_PATH = `${MEMTREE_PASSTHROUGH_PREFIX}current`;
591
+ /** The page whose memory is in the prompt, else (none adopted yet) the newest page. */
592
+ function currentMemorySourcePage(state, sessionId) {
593
+ if (!sessionId)
594
+ return undefined;
595
+ return state.memorySourcePages.get(sessionId) ?? currentMemtreePage(state, sessionId);
596
+ }
582
597
  function currentMemtreePage(state, sessionId) {
583
598
  if (!sessionId)
584
599
  return undefined;
@@ -640,7 +655,7 @@ onMainStop) {
640
655
  if (parsed.agent_id === undefined && parsed.source === "resume") {
641
656
  await state.newestTrees?.settle(parsed.session_id);
642
657
  }
643
- const line = resumeLinkLine(state, parsed);
658
+ const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed));
644
659
  if (!line) {
645
660
  res.writeHead(204);
646
661
  res.end();
@@ -659,16 +674,16 @@ onMainStop) {
659
674
  // `/memtree-view`: answered here and blocked, so no model turn runs and
660
675
  // the prompt never enters the conversation. Not a human turn either, so
661
676
  // none of the turn state below is touched.
662
- if (parsed.agent_id === undefined) {
663
- // `/memtree-view` waits briefly for the newest tree; a prompt only warms it.
664
- if (isMemtreeViewCommand(parsed.prompt))
665
- await state.newestTrees?.settle(parsed.session_id);
666
- else
667
- state.newestTrees?.peek(parsed.session_id);
668
- }
677
+ // `/memtree-view` waits briefly for the newest tree; a prompt only warms it.
678
+ // In a subagent's view the command is answered too (with the main
679
+ // session's page): unanswered, the plugin's fallback body would run.
680
+ if (isMemtreeViewCommand(parsed.prompt))
681
+ await state.newestTrees?.settle(parsed.session_id);
682
+ else if (parsed.agent_id === undefined)
683
+ state.newestTrees?.peek(parsed.session_id);
669
684
  const commandReply = parsed.agent_id === undefined
670
685
  ? sessionCommandReply(state, parsed.session_id, parsed.prompt)
671
- : undefined;
686
+ : subagentCommandReply(state, parsed.session_id, parsed.prompt);
672
687
  if (commandReply !== undefined) {
673
688
  const body = Buffer.from(JSON.stringify({
674
689
  decision: "block",
@@ -936,7 +951,9 @@ async function handleMessages(req, res, opts, upstream, state) {
936
951
  },
937
952
  }, state.countTokensCooldowns);
938
953
  const requestSizing = new RequestSizing(countSession, opts.countTokens === true);
939
- const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey);
954
+ if (carriesServerTools(body))
955
+ serverToolRecords.add(rec);
956
+ const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey, body.model);
940
957
  const originalCandidate = requestSizing.register(rawBody, "original", sizeSlot.get());
941
958
  const forwardCandidate = requestSizing.register(forwardBody, "original", sizeSlot.get());
942
959
  candidateSizeOwners.set(originalCandidate, sizeSlot);
@@ -947,6 +964,9 @@ async function handleMessages(req, res, opts, upstream, state) {
947
964
  output: bodyOutputReservation(body),
948
965
  recountOutput: outputReservation(undefined, body.max_tokens), // full max_tokens
949
966
  });
967
+ if (isMainRequest && requestSessionKey !== undefined) {
968
+ adoptStoredPrefix(opts, state, rec, requestSessionKey, body, messages, modelContextLimit);
969
+ }
950
970
  const rides = state.memoryRoutes.has(requestRouteKey) ||
951
971
  (isMainRequest && requestSessionKey !== undefined && state.stablePrefixes.has(requestSessionKey));
952
972
  if (!rides) {
@@ -1254,6 +1274,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1254
1274
  ...(plan.fallback ? { fallback: plan.fallback } : {}),
1255
1275
  retryAtTokens: plan.estimateTokens +
1256
1276
  Math.floor(plan.budgetTokens * TOOL_COMPACTION_RETRY_BUDGET_RATIO),
1277
+ ...(plan.fallback ? {} : { wholeRefused: () => wholeRequestRefused(requestSizing, originalCandidate, sizingLimits()) }),
1257
1278
  }).finally(() => {
1258
1279
  recoveryAttempt.inFlight = false;
1259
1280
  }));
@@ -1280,7 +1301,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1280
1301
  if (toolRide)
1281
1302
  noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
1282
1303
  else {
1283
- noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, rec, sendBody.length);
1304
+ noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
1284
1305
  }
1285
1306
  }));
1286
1307
  }
@@ -1390,6 +1411,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1390
1411
  return;
1391
1412
  }
1392
1413
  const installed = installMemoryRoute(state, requestRouteKey, req, body, messages, compressedBody, routeEpoch, routeDecisionGeneration, stable);
1414
+ // The answer's memory is now what future requests reuse; a ride keeps
1415
+ // the prior source, and a failed or stale forward adopts nothing.
1416
+ if (installed && result && linkSeq !== undefined) {
1417
+ noteMemorySource(state, linkSeq, sessionId, result);
1418
+ }
1393
1419
  commitRouteDecision();
1394
1420
  // Leave this false if installMemoryRoute unexpectedly throws: the
1395
1421
  // delivery-complete fallback then gets one safe retry.
@@ -1514,7 +1540,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1514
1540
  const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
1515
1541
  const noteWholeSize = () => {
1516
1542
  if (isMainRequest)
1517
- notePassthroughSize(state, sessionId, rec, forwardBody.length);
1543
+ notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
1518
1544
  };
1519
1545
  // The complete request upload can outlive its downstream subscriber while
1520
1546
  // MemTree compression is in flight. Track that subscriber locally without
@@ -1549,6 +1575,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1549
1575
  : edge?.kind === "off"
1550
1576
  ? { compaction: {} }
1551
1577
  : {}),
1578
+ // Riding the old prefix, or sending a raw request that fits, keeps the
1579
+ // short budget; with neither, a timeout could only end in a refusal.
1580
+ longBudget: !(edge?.kind === "compress" && edge.fallback) &&
1581
+ await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
1552
1582
  });
1553
1583
  }
1554
1584
  catch {
@@ -1561,7 +1591,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1561
1591
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
1562
1592
  .then((delivered) => {
1563
1593
  if (isMainRequest)
1564
- notePassthroughSize(state, sessionId, rec, rawBody.length);
1594
+ notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
1565
1595
  if (delivered)
1566
1596
  markMainPromptDelivered();
1567
1597
  }));
@@ -1596,21 +1626,17 @@ async function handleMessages(req, res, opts, upstream, state) {
1596
1626
  // it. Degrade to passthrough and queue a display-only hook notice for a
1597
1627
  // visible turn. The hidden away-summary request deliberately stays quiet.
1598
1628
  // Unpaid key (402, from this compress OR an earlier background index) gets
1599
- // a payment-specific notice instead of the generic degraded one, at most
1600
- // once per proxy process after it has actually been delivered.
1629
+ // a payment-specific notice with the subscribe link instead of the generic
1630
+ // degraded one: on the first 402, then at most once per throttle interval
1631
+ // (counted from actual delivery). In between, unpaid turns stay quiet.
1601
1632
  recordTurn(rec, "followup-degraded", forwardBody);
1602
1633
  releaseForOriginalFallback();
1603
1634
  sendingWhole();
1604
1635
  capture(opts, "anthropic-request", forwardBody);
1605
1636
  const paymentDetail = opts.memtree.paymentRequiredDetail;
1606
1637
  const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
1607
- if (mayQueueNotice && paymentDetail !== null && !state.paymentNoticeShown) {
1608
- const detailFirstLine = sanitizeNoticeDetail(paymentDetail.split(/[\r\n]/, 1)[0]);
1609
- state.notices.queueSuffix(detailFirstLine
1610
- ? `${PAYMENT_REQUIRED_NOTICE}\n${detailFirstLine}`
1611
- : PAYMENT_REQUIRED_NOTICE, () => {
1612
- state.paymentNoticeShown = true;
1613
- }, noticePromptId);
1638
+ if (mayQueueNotice && paymentDetail !== null && state.paymentNotice.due()) {
1639
+ state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () => state.paymentNotice.markShown(), noticePromptId);
1614
1640
  }
1615
1641
  else if (mayQueueNotice && paymentDetail === null) {
1616
1642
  state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
@@ -1831,6 +1857,25 @@ function resumeLinkLine(state, input) {
1831
1857
  }
1832
1858
  return state.notices.resumeLine(link, sessionId);
1833
1859
  }
1860
+ /**
1861
+ * The unpaid-key notice for a resumed main session whose key the server has
1862
+ * already answered 402 for: a resume is a fresh look at the session, so it
1863
+ * says again why it runs uncompressed, regardless of the throttle. The link
1864
+ * line above it already names the session's MemTree page.
1865
+ */
1866
+ function resumePaymentNotice(state, input) {
1867
+ if (input.agent_id !== undefined || input.source !== "resume")
1868
+ return undefined;
1869
+ const detail = state.unpaidDetail();
1870
+ if (detail === null)
1871
+ return undefined;
1872
+ state.paymentNotice.markShown();
1873
+ return state.notices.warningText(paymentNoticeText(detail));
1874
+ }
1875
+ function joinNoticeLines(...parts) {
1876
+ const present = parts.filter((part) => !!part);
1877
+ return present.length ? present.join("\n") : undefined;
1878
+ }
1834
1879
  export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
1835
1880
  /** The reply to a ccc slash command, or undefined for an ordinary prompt. */
1836
1881
  function sessionCommandReply(state, sessionId, prompt) {
@@ -1851,6 +1896,9 @@ function sessionCommandReply(state, sessionId, prompt) {
1851
1896
  state.memoryRoutes.delete(JSON.stringify([sessionId, "main"]));
1852
1897
  state.compactNow.delete(sessionId);
1853
1898
  state.stablePrefixes.delete(sessionId);
1899
+ if (state.storageOwner)
1900
+ state.stablePrefixStore?.delete(sessionId, state.storageOwner);
1901
+ state.storedPrefixLookups.set(sessionId, null);
1854
1902
  return `${TRAILER_LABEL} compaction off: the conversation is sent whole, and MemTree compresses only when it outgrows the model's budget.`;
1855
1903
  }
1856
1904
  if (args === "") {
@@ -1872,6 +1920,22 @@ function sessionCommandReply(state, sessionId, prompt) {
1872
1920
  state.compactNow.add(sessionId);
1873
1921
  return `${TRAILER_LABEL} compacting: your next message is sent compressed to about ${Math.round(target / 1000)}k tokens, ${keep}`;
1874
1922
  }
1923
+ export const SUBAGENT_VIEW_NOTE = "(main session's page; subagent pages aren't linked yet)";
1924
+ export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main session only.";
1925
+ /**
1926
+ * The reply to a ccc slash command typed in a subagent's view. `sessionId` is
1927
+ * the parent session's: the view shows its page, and compaction (per session)
1928
+ * is refused rather than applied to the main conversation from here.
1929
+ */
1930
+ function subagentCommandReply(state, sessionId, prompt) {
1931
+ if (isMemtreeViewCommand(prompt)) {
1932
+ return `${memtreeViewLine(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
1933
+ }
1934
+ if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
1935
+ return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
1936
+ }
1937
+ return sessionCommandReply(state, sessionId, prompt);
1938
+ }
1875
1939
  /** "50k", "50000", "1.5m" → tokens; undefined when not a positive count. */
1876
1940
  export function parseTokenCount(text) {
1877
1941
  const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(text.trim());
@@ -1892,13 +1956,16 @@ function memtreeViewLine(state, sessionId) {
1892
1956
  return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
1893
1957
  }
1894
1958
  // Blocked-prompt reasons need an explicit hyperlink in styled terminals.
1895
- const url = terminalSupportsHyperlinks() ? hyperlink(link.link, link.link) : link.link;
1896
- const lines = linkLines(LINK_LABEL, url, link.note);
1959
+ const lines = linkLines(LINK_LABEL, viewUrl(link.link), link.note);
1897
1960
  // The memory the agent works from can be an older tree than the newest one.
1898
1961
  return page?.compressed && page.url !== link.link
1899
- ? `${lines}\n memory from ${page.url}`
1962
+ ? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
1900
1963
  : lines;
1901
1964
  }
1965
+ /** A `/memtree-view` URL: an explicit OSC 8 link where the terminal has them. */
1966
+ function viewUrl(url) {
1967
+ return terminalSupportsHyperlinks() ? hyperlink(url, url) : url;
1968
+ }
1902
1969
  /**
1903
1970
  * The link shown for a session: its newest completed tree when the server
1904
1971
  * named one (newestTrees), else `source`, the page its memory came from. The
@@ -1947,12 +2014,30 @@ function noteMemtreePage(state, seq, sessionId, result) {
1947
2014
  // A cached newest tree may predate this page; ask again.
1948
2015
  state.newestTrees?.invalidate(sessionId);
1949
2016
  }
1950
- /** Bound optional page state independently of how many sessions use the proxy. */
2017
+ /**
2018
+ * Record a compress answer's page as the source of the memory now forwarded
2019
+ * (memorySourcePages), unless a later-submitted call's memory already is.
2020
+ */
2021
+ function noteMemorySource(state, seq, sessionId, result) {
2022
+ const url = result.memtreeUrl;
2023
+ const index = result.memtreeIndex;
2024
+ if (!sessionId || !url || !index)
2025
+ return;
2026
+ const latest = state.memorySourcePages.get(sessionId);
2027
+ if (latest && latest.seq >= seq)
2028
+ return;
2029
+ const page = { sessionId, url, index, compressed: didMemtreeCompress(result), seq };
2030
+ rememberPage(state.memorySourcePages, sessionId, page);
2031
+ }
1951
2032
  function rememberMemtreePage(state, sessionId, page) {
1952
- state.memtreePages.delete(sessionId);
1953
- state.memtreePages.set(sessionId, page);
1954
- while (state.memtreePages.size > MEMTREE_LINKS_MAX_SESSIONS) {
1955
- state.memtreePages.delete(state.memtreePages.keys().next().value);
2033
+ rememberPage(state.memtreePages, sessionId, page);
2034
+ }
2035
+ /** Bound optional page state independently of how many sessions use the proxy. */
2036
+ function rememberPage(pages, sessionId, page) {
2037
+ pages.delete(sessionId);
2038
+ pages.set(sessionId, page);
2039
+ while (pages.size > MEMTREE_LINKS_MAX_SESSIONS) {
2040
+ pages.delete(pages.keys().next().value);
1956
2041
  }
1957
2042
  }
1958
2043
  /**
@@ -2085,6 +2170,9 @@ stable) {
2085
2170
  // request carried, and the compressed bytes sent for them. Later requests
2086
2171
  // reuse these bytes unchanged, so the prefix stays a cache read.
2087
2172
  route.stablePrefix = storeStablePrefix(state, sessionId, originalMessages, route, stable);
2173
+ if (route.stablePrefix && key === JSON.stringify([sessionId, "main"])) {
2174
+ persistStablePrefix(state, sessionId, route.stablePrefix);
2175
+ }
2088
2176
  }
2089
2177
  setMemoryRoute(state, key, route);
2090
2178
  state.routeDecisionsLive.set(key, new Set([decisionGeneration]));
@@ -2120,6 +2208,83 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2120
2208
  state.compactNow.delete(sessionId);
2121
2209
  return prefix;
2122
2210
  }
2211
+ /**
2212
+ * Persist the main lane's newly adopted prefix with its memory-source page.
2213
+ * Deferred one tick so the page noted right after the install is included,
2214
+ * and skipped when a newer prefix replaced this one meanwhile.
2215
+ */
2216
+ function persistStablePrefix(state, sessionId, prefix) {
2217
+ const store = state.stablePrefixStore;
2218
+ const owner = state.storageOwner;
2219
+ if (!store || !owner)
2220
+ return;
2221
+ state.storedPrefixLookups.set(sessionId, null);
2222
+ setImmediate(() => {
2223
+ if (state.stablePrefixes.get(sessionId) !== prefix)
2224
+ return;
2225
+ const page = state.memorySourcePages.get(sessionId);
2226
+ store.save(sessionId, owner, {
2227
+ originalSystemHash: prefix.originalSystemHash,
2228
+ originalPrefixHashes: prefix.originalPrefixHashes,
2229
+ compressedMessages: prefix.compressedMessages,
2230
+ compressedSystem: prefix.compressedSystem,
2231
+ hasCompressedSystem: prefix.hasCompressedSystem,
2232
+ targetTokens: prefix.targetTokens,
2233
+ explicitTarget: prefix.explicitTarget,
2234
+ modelContextLimit: prefix.modelContextLimit,
2235
+ }, page && { url: page.url, index: page.index, compressed: page.compressed });
2236
+ });
2237
+ }
2238
+ /**
2239
+ * A fresh process's main-lane requests for a session: adopt the session's
2240
+ * persisted prefix when this history passes the checks the in-memory prefix
2241
+ * would (same session, system and covered messages; same window; a routable
2242
+ * body). Only an authoritative main history may discard it: one with the same
2243
+ * system and window, no server tools, and at least the covered length, whose
2244
+ * covered messages differ. Anything else (a side or helper request such as
2245
+ * the Haiku WebSearch call) leaves it for a later request. The disk is read
2246
+ * once per session; the store checks the backend/account owner.
2247
+ */
2248
+ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelContextLimit) {
2249
+ const { stablePrefixStore: store, storageOwner: owner } = state;
2250
+ if (!store || !owner || state.stablePrefixes.has(sessionId))
2251
+ return;
2252
+ if (compactionMode(opts, state, sessionId).mode === "off")
2253
+ return;
2254
+ let entry = state.storedPrefixLookups.get(sessionId);
2255
+ if (entry === undefined) {
2256
+ entry = store.load(sessionId, owner) ?? null;
2257
+ boundedSet(state.storedPrefixLookups, sessionId, entry);
2258
+ }
2259
+ if (entry === null)
2260
+ return;
2261
+ const prefix = { sessionId, ...entry.prefix };
2262
+ const verdict = storedPrefixVerdict(body, messages, prefix, modelContextLimit);
2263
+ if (verdict === "defer")
2264
+ return;
2265
+ state.storedPrefixLookups.set(sessionId, null);
2266
+ if (verdict === "discard") {
2267
+ store.delete(sessionId, owner);
2268
+ rec.storedPrefix = "discarded";
2269
+ return;
2270
+ }
2271
+ boundedSet(state.stablePrefixes, sessionId, prefix);
2272
+ if (entry.source && !state.memorySourcePages.has(sessionId)) {
2273
+ rememberPage(state.memorySourcePages, sessionId, { sessionId, ...entry.source, seq: 0 });
2274
+ }
2275
+ rec.storedPrefix = "adopted";
2276
+ }
2277
+ /** What this request says about a persisted prefix (adoptStoredPrefix). */
2278
+ function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
2279
+ if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
2280
+ return "defer";
2281
+ const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
2282
+ if (miss === null)
2283
+ return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
2284
+ const authoritative = miss === "prefix" &&
2285
+ messages.length >= prefix.originalPrefixHashes.length;
2286
+ return authoritative ? "discard" : "defer";
2287
+ }
2123
2288
  /**
2124
2289
  * A fork of the main conversation (the away recap) sent on the main thread's
2125
2290
  * last compressed prefix: that prefix, then the fork's own messages after it.
@@ -2301,11 +2466,31 @@ function notePrefixSize(prefix, rec, bytes) {
2301
2466
  prefix.lastSize = sample;
2302
2467
  }
2303
2468
  /** Record the reported size of a main-thread request forwarded whole. */
2304
- function notePassthroughSize(state, sessionId, rec, bytes) {
2469
+ function notePassthroughSize(state, sessionId, model, rec, bytes) {
2305
2470
  const sample = reportedSample(rec, bytes);
2306
2471
  if (sessionId === undefined || sample === undefined)
2307
2472
  return;
2308
- boundedSet(state.passthroughSizes, sessionId, sample);
2473
+ boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
2474
+ }
2475
+ /** Key of a main-thread size sample (ProxyState.passthroughSizes). */
2476
+ function sessionSizeKey(sessionId, model) {
2477
+ return JSON.stringify([sessionId, typeof model === "string" ? model : null]);
2478
+ }
2479
+ /** Requests whose reported input includes server-injected tool results. */
2480
+ const serverToolRecords = new WeakSet();
2481
+ /**
2482
+ * Whether the request offers server tools (web search, web fetch, code
2483
+ * execution, tool search): their results are added to the input server-side,
2484
+ * so the reported size says nothing about the body's bytes.
2485
+ */
2486
+ function carriesServerTools(body) {
2487
+ return Array.isArray(body.tools) && body.tools.some((tool) => typeof tool?.type === "string" && SERVER_TOOL_TYPE.test(tool.type));
2488
+ }
2489
+ const SERVER_TOOL_TYPE = /^(web_search|web_fetch|code_execution|tool_search_tool)_/;
2490
+ /** Whether the response reports any server tool use (server_tool_use counts). */
2491
+ function usedServerTools(rec) {
2492
+ const used = rec.usage?.server_tool_use;
2493
+ return !!used && Object.values(used).some((count) => count > 0);
2309
2494
  }
2310
2495
  /**
2311
2496
  * The size Anthropic reported for a body of `bytes`, if it can describe one:
@@ -2315,6 +2500,8 @@ function notePassthroughSize(state, sessionId, rec, bytes) {
2315
2500
  function reportedSample(rec, bytes) {
2316
2501
  if (changedDeliveryRecords.has(rec))
2317
2502
  return undefined;
2503
+ if (serverToolRecords.has(rec) || usedServerTools(rec))
2504
+ return undefined;
2318
2505
  const tokens = reportedInputTokens(rec);
2319
2506
  if (tokens === undefined)
2320
2507
  return undefined;
@@ -2334,15 +2521,16 @@ function laneSizeKey(req, routeKey, model) {
2334
2521
  ]);
2335
2522
  }
2336
2523
  /** The session's passthrough size on the main thread, else the lane's. */
2337
- function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey) {
2524
+ function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey, model) {
2338
2525
  const main = isMainRequest && sessionId !== undefined;
2526
+ const mainKey = main ? sessionSizeKey(sessionId, model) : "";
2339
2527
  return {
2340
- get: () => (main ? state.passthroughSizes.get(sessionId) : state.laneSizes.get(sizeKey)),
2528
+ get: () => (main ? state.passthroughSizes.get(mainKey) : state.laneSizes.get(sizeKey)),
2341
2529
  set: (sample) => {
2342
2530
  if (!plausibleSample(sample))
2343
2531
  return;
2344
2532
  if (main)
2345
- boundedSet(state.passthroughSizes, sessionId, sample);
2533
+ boundedSet(state.passthroughSizes, mainKey, sample);
2346
2534
  else
2347
2535
  boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
2348
2536
  },
@@ -2354,9 +2542,9 @@ const noteRideSize = notePrefixSize;
2354
2542
  * Record the reported size of a request forwarded whole: the session's
2355
2543
  * passthrough size on the main thread, else its lane's.
2356
2544
  */
2357
- function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, rec, bytes) {
2545
+ function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
2358
2546
  if (isMainRequest && sessionId !== undefined) {
2359
- notePassthroughSize(state, sessionId, rec, bytes);
2547
+ notePassthroughSize(state, sessionId, model, rec, bytes);
2360
2548
  return;
2361
2549
  }
2362
2550
  const sample = reportedSample(rec, bytes);
@@ -2529,7 +2717,7 @@ function planToolCompaction(args) {
2529
2717
  const sample = ride
2530
2718
  ? ride.sizeHolder.lastSize
2531
2719
  : stableLane
2532
- ? state.passthroughSizes.get(sessionId)
2720
+ ? state.passthroughSizes.get(sessionSizeKey(sessionId, body.model))
2533
2721
  : state.laneSizes.get(args.sizeKey);
2534
2722
  const candidate = args.sizing.register(ride?.raw ?? forwardBody, ride ? "ride" : "original", sample);
2535
2723
  if (ride)
@@ -2921,6 +3109,8 @@ async function runBlockingCompression(args) {
2921
3109
  : undefined;
2922
3110
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
2923
3111
  const compressStarted = Date.now();
3112
+ const budgetMs = args.longBudget
3113
+ ? opts.memtree.longCompressBudgetMs : opts.memtree.compressBudgetMs;
2924
3114
  const compressMeta = {
2925
3115
  // Model + tools drive the server's model-based memory budget
2926
3116
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -2946,6 +3136,7 @@ async function runBlockingCompression(args) {
2946
3136
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
2947
3137
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
2948
3138
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3139
+ ...(args.longBudget ? { timeoutMs: budgetMs } : {}),
2949
3140
  };
2950
3141
  // Sampled BEFORE the call, while it still describes this call: after the
2951
3142
  // await the hash is in the cache regardless of who put it there.
@@ -2953,7 +3144,7 @@ async function runBlockingCompression(args) {
2953
3144
  // compress() maps every failure to a resolved null (it never rejects).
2954
3145
  // Keep foreground work finite even if a transport ignores its abort signal.
2955
3146
  let deadline;
2956
- const maximumWait = Math.min(30_000, Math.max(1, Number.isFinite(opts.memtree.compressBudgetMs) ? opts.memtree.compressBudgetMs : 15_000));
3147
+ const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : 30_000, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
2957
3148
  const result = await Promise.race([
2958
3149
  opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
2959
3150
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
@@ -2977,7 +3168,7 @@ async function runBlockingCompression(args) {
2977
3168
  // Budget-consumed heuristic: the client maps every failure to null, so a
2978
3169
  // null that took (roughly) the whole abort budget was almost certainly
2979
3170
  // the AbortSignal timeout, not a fast server error.
2980
- timedOut: result === null && compressMs >= opts.memtree.compressBudgetMs,
3171
+ timedOut: result === null && compressMs >= budgetMs,
2981
3172
  };
2982
3173
  return {
2983
3174
  result,
@@ -3095,7 +3286,7 @@ async function recoverToolRouteMiss(args) {
3095
3286
  capture(opts, "anthropic-request", args.originalBody);
3096
3287
  return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3097
3288
  toolForwarding.settle(delivered);
3098
- noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, rec, args.originalBody.length);
3289
+ noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
3099
3290
  });
3100
3291
  };
3101
3292
  // Same downstream-close tracking as the followup path: compression
@@ -3124,6 +3315,7 @@ async function recoverToolRouteMiss(args) {
3124
3315
  sizing: args.sizing,
3125
3316
  inputCandidate: args.inputCandidate,
3126
3317
  ...(args.compaction ? { compaction: args.compaction } : {}),
3318
+ longBudget: args.wholeRefused ? await args.wholeRefused() : false,
3127
3319
  });
3128
3320
  }
3129
3321
  catch {
@@ -3275,6 +3467,10 @@ async function recoverToolRouteMiss(args) {
3275
3467
  activationAttempted = true;
3276
3468
  installedRoute = installed;
3277
3469
  installFate = installed ? "installed" : "stale";
3470
+ // Only an installed route makes this answer's memory the reused one.
3471
+ if (installed && linkSeq !== undefined) {
3472
+ noteMemorySource(state, linkSeq, requestSessionId(req), result);
3473
+ }
3278
3474
  // A tool continuation can arrive after message_stop while this HTTP
3279
3475
  // stream is still draining. Its route is already complete, so a smaller
3280
3476
  // window may buy recovery now. Mutate only this attempt's object: a late
@@ -3539,6 +3735,17 @@ async function selectForwardCandidate(args) {
3539
3735
  }
3540
3736
  return changed ? candidate.body : intended;
3541
3737
  }
3738
+ /**
3739
+ * Whether delivery would refuse `candidate` sent whole: it is planned over the
3740
+ * window, and the measurement selection would use (an exact count, else the
3741
+ * same estimate or bytes/4 fallback) does not fit either. Only then is there
3742
+ * no fallback, and only then may a compress take the long budget.
3743
+ */
3744
+ async function wholeRequestRefused(sizing, candidate, limits) {
3745
+ if (fitsNative(sizing.plan(candidate).tokens, limits))
3746
+ return false;
3747
+ return !fitsMeasured(await sizing.measure(candidate, limits), limits);
3748
+ }
3542
3749
  /** Write the bounded fallback refusal (route-fallback.ts); always false. */
3543
3750
  function refuseWholeRequest(args, inputTokens, budgetTokens) {
3544
3751
  const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
@@ -3577,6 +3784,9 @@ function noteSelectedCandidateSize(rec) {
3577
3784
  const tokens = reportedInputTokens(rec);
3578
3785
  if (!candidate || !owner || tokens === undefined)
3579
3786
  return;
3787
+ // Server tools' injected results inflate the reported input (reportedSample).
3788
+ if (serverToolRecords.has(rec) || usedServerTools(rec))
3789
+ return;
3580
3790
  const sample = { tokens, forwardedBytes: candidate.body.length };
3581
3791
  if (!plausibleSample(sample))
3582
3792
  return;