claude-code-infinite 1.0.31 → 1.0.33

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 (79) hide show
  1. package/README.md +71 -56
  2. package/dist/claude-auth-status.d.ts +50 -0
  3. package/dist/claude-auth-status.d.ts.map +1 -0
  4. package/dist/claude-auth-status.js +213 -0
  5. package/dist/claude-auth-status.js.map +1 -0
  6. package/dist/claude-auth.d.ts +2 -1
  7. package/dist/claude-auth.d.ts.map +1 -1
  8. package/dist/claude-auth.js +2 -1
  9. package/dist/claude-auth.js.map +1 -1
  10. package/dist/claude-plan-warning.d.ts +13 -40
  11. package/dist/claude-plan-warning.d.ts.map +1 -1
  12. package/dist/claude-plan-warning.js +24 -75
  13. package/dist/claude-plan-warning.js.map +1 -1
  14. package/dist/cli-args.d.ts +7 -0
  15. package/dist/cli-args.d.ts.map +1 -1
  16. package/dist/cli-args.js +19 -0
  17. package/dist/cli-args.js.map +1 -1
  18. package/dist/cli-main.js +117 -17
  19. package/dist/cli-main.js.map +1 -1
  20. package/dist/count-tokens.d.ts +7 -3
  21. package/dist/count-tokens.d.ts.map +1 -1
  22. package/dist/count-tokens.js +6 -2
  23. package/dist/count-tokens.js.map +1 -1
  24. package/dist/diagnostics-schema.d.ts +5 -2
  25. package/dist/diagnostics-schema.d.ts.map +1 -1
  26. package/dist/diagnostics-schema.js +26 -6
  27. package/dist/diagnostics-schema.js.map +1 -1
  28. package/dist/diagnostics.d.ts +5 -1
  29. package/dist/diagnostics.d.ts.map +1 -1
  30. package/dist/diagnostics.js +7 -2
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/hooks.d.ts +18 -1
  33. package/dist/hooks.d.ts.map +1 -1
  34. package/dist/hooks.js +57 -4
  35. package/dist/hooks.js.map +1 -1
  36. package/dist/memtree-mcp.d.ts +7 -0
  37. package/dist/memtree-mcp.d.ts.map +1 -1
  38. package/dist/memtree-mcp.js +12 -1
  39. package/dist/memtree-mcp.js.map +1 -1
  40. package/dist/memtree.d.ts +86 -3
  41. package/dist/memtree.d.ts.map +1 -1
  42. package/dist/memtree.js +305 -18
  43. package/dist/memtree.js.map +1 -1
  44. package/dist/notices.d.ts +2 -0
  45. package/dist/notices.d.ts.map +1 -1
  46. package/dist/notices.js +2 -0
  47. package/dist/notices.js.map +1 -1
  48. package/dist/open-url.d.ts +49 -0
  49. package/dist/open-url.d.ts.map +1 -0
  50. package/dist/open-url.js +100 -0
  51. package/dist/open-url.js.map +1 -0
  52. package/dist/proxy.d.ts +77 -1
  53. package/dist/proxy.d.ts.map +1 -1
  54. package/dist/proxy.js +618 -77
  55. package/dist/proxy.js.map +1 -1
  56. package/dist/reqlog.d.ts +21 -2
  57. package/dist/reqlog.d.ts.map +1 -1
  58. package/dist/reqlog.js.map +1 -1
  59. package/dist/request-sizing.d.ts +2 -0
  60. package/dist/request-sizing.d.ts.map +1 -1
  61. package/dist/request-sizing.js +5 -0
  62. package/dist/request-sizing.js.map +1 -1
  63. package/dist/resume-guidance.d.ts +102 -0
  64. package/dist/resume-guidance.d.ts.map +1 -0
  65. package/dist/resume-guidance.js +182 -0
  66. package/dist/resume-guidance.js.map +1 -0
  67. package/dist/route-fallback.d.ts +23 -0
  68. package/dist/route-fallback.d.ts.map +1 -1
  69. package/dist/route-fallback.js +44 -0
  70. package/dist/route-fallback.js.map +1 -1
  71. package/dist/sign-in-notice.d.ts +137 -0
  72. package/dist/sign-in-notice.d.ts.map +1 -0
  73. package/dist/sign-in-notice.js +300 -0
  74. package/dist/sign-in-notice.js.map +1 -0
  75. package/dist/subagent-hint.d.ts +31 -0
  76. package/dist/subagent-hint.d.ts.map +1 -0
  77. package/dist/subagent-hint.js +47 -0
  78. package/dist/subagent-hint.js.map +1 -0
  79. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { capCacheBreakpoints } from "./route-cache.js";
42
42
  import { PromptAccounting } from "./prompt-accounting.js";
43
- import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures, pausedRefusal } from "./route-fallback.js";
43
+ import { INDEX_WAIT_RETRY_AFTER_SECONDS, RefusalStreaks, 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
46
  import { extendsHistory, mediaFreeMessageHash, prefixIdentity, prefixMessageMatches, } from "./route-identity.js";
@@ -52,10 +52,10 @@ import { createHash, randomBytes } from "node:crypto";
52
52
  import { brotliDecompressSync, createBrotliDecompress, createGunzip, createInflate, gunzipSync, inflateSync, } from "node:zlib";
53
53
  import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
54
54
  import { join } from "node:path";
55
- import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
55
+ import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, losesMedia, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
56
56
  import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
57
- import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.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
+ import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, SLOW_RETRY_NOTICE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
58
+ import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH, } from "./hooks.js";
59
59
  import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
60
60
  import { hyperlink } from "./payment-gate.js";
61
61
  import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
@@ -64,6 +64,8 @@ import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paym
64
64
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
65
65
  import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
66
66
  import { isResumableSessionId } from "./resume-hint.js";
67
+ import { subagentMemoryHintText, subagentStartOutput, } from "./subagent-hint.js";
68
+ import { RESUME_GUIDANCE_HOOK_SUFFIX } from "./hooks.js";
67
69
  import { conservativeBodyTokens } from "./token-estimate.js";
68
70
  import { bodyImages } from "./image-tokens.js";
69
71
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
@@ -71,6 +73,8 @@ import { rateLimitFromResponse } from "./rate-limit.js";
71
73
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
72
74
  const DEFAULT_UPSTREAM = "https://api.anthropic.com";
73
75
  const HOOK_BODY_LIMIT = 64 * 1024;
76
+ /** A hook body still arriving after this is dropped (the hook itself waits 5 s). */
77
+ const HOOK_BODY_TIMEOUT_MS = 3_000;
74
78
  // After a recovery attempt returns null (MemTree down, 5xx, or a burned
75
79
  // timeout budget), suppress the blocking attempt for this long, across every
76
80
  // lane. A lane's own growth backoff alone would let each over-budget tool turn
@@ -82,8 +86,11 @@ const HOOK_BODY_LIMIT = 64 * 1024;
82
86
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
83
87
  /** Safety cap on a long-budget compress, should a transport ignore its abort. */
84
88
  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;
89
+ /**
90
+ * The most a blocking compress with a fallback may wait (its budget grows with its upload):
91
+ * a 30 MB upload's scaled budget (15 s + 2.5 ms/KiB, ~90 s) fits under it.
92
+ */
93
+ const MAX_COMPRESS_WAIT_MS = 120_000;
87
94
  const SKIP_REQUEST_HEADERS = new Set([
88
95
  "host",
89
96
  "connection",
@@ -343,14 +350,20 @@ export function startProxy(opts) {
343
350
  usageUrl: opts.memtree.usageDashboardUrl,
344
351
  sessionStartNotice: opts.sessionStartNotice,
345
352
  promptSubmitNotice: opts.promptSubmitNotice,
353
+ openUrl: opts.openUrl,
346
354
  projectTrees: opts.projectTrees,
355
+ subagentHint: opts.subagentHint,
356
+ resumeGuidance: opts.resumeGuidance,
347
357
  diagnostics: opts.diagnostics,
348
358
  notices: new NoticeDeliveryQueue(),
349
359
  mainNoticePending: false,
350
360
  mainPromptGeneration: 0,
351
361
  promptAccounting: new PromptAccounting(),
352
362
  fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
363
+ refusalStreaks: new RefusalStreaks(),
353
364
  activeSubagents: new Set(),
365
+ deferredAgentIndexes: new Map(),
366
+ adoptingPrepared: new Map(),
354
367
  memoryRoutes: new Map(),
355
368
  mainRouteEpoch: 0,
356
369
  mainRouteDecisionGeneration: 0,
@@ -446,8 +459,10 @@ export function startProxy(opts) {
446
459
  ]);
447
460
  if (timer)
448
461
  clearTimeout(timer);
449
- if (completed)
462
+ if (completed) {
463
+ sendDeferredAgentIndexes(state);
450
464
  return true;
465
+ }
451
466
  // The grace period protects useful in-flight delivery. Once it
452
467
  // expires, signal every forwarding path first so each can
453
468
  // classify the close as proxy-owned, then tear down any request that was
@@ -456,6 +471,7 @@ export function startProxy(opts) {
456
471
  for (const accepted of acceptedRequests)
457
472
  cancelAcceptedRequest(accepted);
458
473
  await quiesced;
474
+ sendDeferredAgentIndexes(state);
459
475
  return false;
460
476
  };
461
477
  return new Promise((resolve, reject) => {
@@ -496,6 +512,9 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
496
512
  if (url.pathname === PRECOMPACT_HOOK_PATH) {
497
513
  return handleNoticeHook(req, res, state, opts.reqlog, undefined, "PreCompact");
498
514
  }
515
+ if (url.pathname === `${hookPath}${RESUME_GUIDANCE_HOOK_SUFFIX}`) {
516
+ return handleResumeGuidanceHook(req, res, state);
517
+ }
499
518
  if (url.pathname === hookPath) {
500
519
  return handleNoticeHook(req, res, state, opts.reqlog, (sessionId) => opts.memtree.scheduleFinalIndex(sessionId, (signal) => opts.transcriptUsage?.finalReplyAsync
501
520
  ? opts.transcriptUsage.finalReplyAsync(sessionId, signal)
@@ -727,6 +746,39 @@ function noteMainSessionStart(state, source, sessionId) {
727
746
  state.resumableMainSession = undefined;
728
747
  }
729
748
  }
749
+ /**
750
+ * SessionStart on its own hook (resume-guidance.ts): only the current MemTree
751
+ * guidance, so Claude Code sees it as one context of its own and adds an
752
+ * identical one only once. Never fails the hook: anything unexpected is 204.
753
+ */
754
+ async function handleResumeGuidanceHook(req, res, state) {
755
+ const raw = req.method === "POST" ? await readBodyWithin(req, HOOK_BODY_LIMIT) : undefined;
756
+ if (req.method !== "POST")
757
+ req.resume();
758
+ let input = null;
759
+ try {
760
+ if (raw !== undefined && raw.length > 0) {
761
+ input = parseNoticeHookInput(JSON.parse(raw.toString("utf-8")));
762
+ }
763
+ }
764
+ catch {
765
+ input = null;
766
+ }
767
+ const context = input?.hook_event_name === "SessionStart"
768
+ ? await resumeGuidanceContext(state, input) : undefined;
769
+ if (!context) {
770
+ res.writeHead(204);
771
+ res.end();
772
+ return;
773
+ }
774
+ const body = Buffer.from(JSON.stringify(sessionStartOutput(undefined, context)), "utf-8");
775
+ res.writeHead(200, {
776
+ "content-type": "application/json",
777
+ "content-length": String(body.length),
778
+ "cache-control": "no-store",
779
+ });
780
+ res.end(body);
781
+ }
730
782
  /** Serve only validated Claude hook POSTs on the randomized localhost path. */
731
783
  async function handleNoticeHook(req, res, state, reqlog,
732
784
  /** A main-lane turn ended: the session may be over (its final index). */
@@ -882,12 +934,24 @@ onlyEvent) {
882
934
  }
883
935
  if (parsed.hook_event_name === "SubagentStart") {
884
936
  state.activeSubagents.add(parsed.agent_id);
885
- res.writeHead(204);
886
- res.end();
937
+ const hint = await subagentHintContext(state, parsed.session_id);
938
+ if (hint === undefined) {
939
+ res.writeHead(204);
940
+ res.end();
941
+ return;
942
+ }
943
+ const body = Buffer.from(JSON.stringify(subagentStartOutput(hint)), "utf-8");
944
+ res.writeHead(200, {
945
+ "content-type": "application/json",
946
+ "content-length": String(body.length),
947
+ "cache-control": "no-store",
948
+ });
949
+ res.end(body);
887
950
  return;
888
951
  }
889
952
  if (parsed.hook_event_name === "SubagentStop") {
890
953
  state.activeSubagents.delete(parsed.agent_id);
954
+ sendDeferredAgentIndex(state, parsed.agent_id);
891
955
  res.writeHead(204);
892
956
  res.end();
893
957
  return;
@@ -981,6 +1045,8 @@ async function handleMessages(req, res, opts, upstream, state) {
981
1045
  requestBytes: rawBody.length,
982
1046
  claudeAuth: claudeAuthFromHeaders(req.headers),
983
1047
  };
1048
+ // The prompt this request was made for, before any await (Codex r2 P2).
1049
+ noticeContexts.set(rec, { promptId: state.mainPromptId, generation: state.mainPromptGeneration });
984
1050
  const logged = async (forward) => {
985
1051
  try {
986
1052
  await forward;
@@ -1220,7 +1286,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1220
1286
  lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
1221
1287
  ...(target !== undefined ? { target } : {}) });
1222
1288
  },
1223
- delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
1289
+ delivered: () => {
1290
+ state.fallbackFailures.succeeded(requestRouteKey);
1291
+ state.refusalStreaks.delivered(requestRouteKey);
1292
+ toolForwarding.settle(true);
1293
+ },
1224
1294
  });
1225
1295
  const rawMsgsForMemtree = messagesWithSystem(messages, body.system);
1226
1296
  const msgsForMemtree = normalizeMessagesForMemtree(rawMsgsForMemtree);
@@ -1349,7 +1419,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1349
1419
  // prefix, else whole) with no compress call; at it, compress once to
1350
1420
  // the target (planToolCompaction). A "replay" is a client retry of the
1351
1421
  // request that installed the route, forwarded verbatim.
1352
- const plan = await planToolCompaction({
1422
+ const planArgs = {
1353
1423
  opts,
1354
1424
  state,
1355
1425
  body,
@@ -1364,7 +1434,24 @@ async function handleMessages(req, res, opts, upstream, state) {
1364
1434
  rec,
1365
1435
  ride: toolRide,
1366
1436
  canCompress,
1367
- });
1437
+ };
1438
+ let plan = await planToolCompaction(planArgs);
1439
+ // Over the window with nothing to ride while the lane compresses: going without it
1440
+ // can only be refused (staging 2026-10-30: a 120 s compress, every tool turn meanwhile
1441
+ // refused). Wait for it, then plan again on what it installed.
1442
+ if (plan.kind === "compress" && plan.overWindow && !plan.fallback &&
1443
+ state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight) {
1444
+ // Stops early if Claude Code drops this request meanwhile.
1445
+ const clientGone = new AbortController();
1446
+ const onClose = () => clientGone.abort();
1447
+ res.once("close", onClose);
1448
+ rec.laneWaitMs = await waitForLaneIdle(state, requestRouteKey, Math.min(MAX_LONG_COMPRESS_WAIT_MS, opts.memtree.longCompressBudgetMs), clientGone.signal);
1449
+ res.off("close", onClose);
1450
+ // Plan on what the lane holds now: the route its compression installed (or, on the
1451
+ // main lane, the session's new stable prefix), never the ride read before the wait.
1452
+ plan = await planToolCompaction({ ...planArgs,
1453
+ ride: currentLaneRide(state, requestRouteKey, body, messages, toolSessionId, isMainRequest) });
1454
+ }
1368
1455
  if (isMainRequest && toolSessionId !== undefined && canCompress &&
1369
1456
  (plan.kind === "ride" || plan.kind === "pass")) {
1370
1457
  maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
@@ -1422,6 +1509,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1422
1509
  // the lane's backoff.
1423
1510
  rec.routeRecovery = { outcome: "cooldown" };
1424
1511
  }
1512
+ else if (state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight) {
1513
+ // Claimed while the cooldown check above awaited a count (a crossing prompt).
1514
+ rec.routeRecovery = { outcome: "in-flight" };
1515
+ }
1425
1516
  else {
1426
1517
  const recoveryAttempt = { modelContextLimit, inFlight: true };
1427
1518
  if (!prior && state.toolRecoveryAttemptedLanes.size >= 128) {
@@ -1488,7 +1579,8 @@ async function handleMessages(req, res, opts, upstream, state) {
1488
1579
  requestSendPolicies.get(req)?.allowed.add(toolRide.raw);
1489
1580
  const sendBody = toolRide?.raw ?? (toolNeedsOriginal ? rawBody : forwardBody);
1490
1581
  recordTurn(rec, toolRide ? toolRide.turnType : isUserTurn ? "first-user" : "tool", sendBody);
1491
- opts.memtree.indexInBackground(hash, msgsForMemtree, modelContextLimit, toolSessionId, clientMeta, transcriptTimesFor(opts, toolSessionId, agentAttributionId(req)));
1582
+ indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
1583
+ sessionId: toolSessionId, clientMeta });
1492
1584
  capture(opts, toolRide ? "anthropic-request-memory-tool" : "anthropic-request", sendBody);
1493
1585
  // The size Anthropic reports is the next budget check's anchor: a ride
1494
1586
  // reports to its prefix (or route), a whole request to the session's (or
@@ -1559,6 +1651,32 @@ async function handleMessages(req, res, opts, upstream, state) {
1559
1651
  rec.compaction.prepared ??= prepared;
1560
1652
  }
1561
1653
  }
1654
+ // Never beside the main lane's compression in flight either (a tool turn's that Claude
1655
+ // Code dropped, or another prompt's, both after the preparation they waited for failed):
1656
+ // wait for it, then plan again on what it left: a prefix to ride, or a preparation.
1657
+ // Bounded; stops early if Claude Code drops this request (release 1.0.33 E2E).
1658
+ const crossing = () => edge?.kind === "compress" && isMainRequest &&
1659
+ isAutoCrossing(rec, body.model, modelContextLimit);
1660
+ for (let i = 0; i < 2 && crossing() &&
1661
+ state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight; i++) {
1662
+ const clientGone = new AbortController();
1663
+ const onClose = () => clientGone.abort();
1664
+ res.once("close", onClose);
1665
+ if (res.destroyed)
1666
+ clientGone.abort();
1667
+ rec.laneWaitMs = (rec.laneWaitMs ?? 0) + await waitForLaneIdle(state, requestRouteKey, Math.min(MAX_LONG_COMPRESS_WAIT_MS, opts.memtree.longCompressBudgetMs), clientGone.signal);
1668
+ res.off("close", onClose);
1669
+ await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
1670
+ edge = await planEdge();
1671
+ }
1672
+ // Claude Code dropped it while it waited: the lane's compression in flight already carries
1673
+ // this session's crossing (the next turn rides it), so none beside it for nobody (r3 P1).
1674
+ if (crossing() && state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight &&
1675
+ res.destroyed && !res.writableFinished) {
1676
+ releaseUncommittedRouteDecision();
1677
+ recordTurn(rec, "followup-client-closed", Buffer.alloc(0));
1678
+ return logged(Promise.resolve());
1679
+ }
1562
1680
  // Not beside a blocking compress call: the two would overlap on the server.
1563
1681
  if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
1564
1682
  maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
@@ -1787,9 +1905,17 @@ async function handleMessages(req, res, opts, upstream, state) {
1787
1905
  // The hidden away-summary request compresses too, but its page is not the
1788
1906
  // conversation the user is looking at.
1789
1907
  const linkSeq = isMainRequest && !isAwaySummary ? nextMemtreeCallSeq(state) : undefined;
1908
+ const edgeCarry = edge?.kind === "compress" && !isAwaySummary
1909
+ ? carryFor({ state, isMainRequest, sessionId, body, messages, msgsForMemtree,
1910
+ modelContextLimit, rec,
1911
+ targetTokens: edge.targetTokens, explicitTarget: edge.explicitTarget, linkSeq })
1912
+ : undefined;
1790
1913
  let compression;
1914
+ // A main-thread compaction holds its lane, claimed with no await since the check above:
1915
+ // a concurrent turn of the lane waits for it or rides its fallback, never compresses beside it.
1916
+ const edgeLane = (run) => crossing() ? holdingLane(state, requestRouteKey, run) : run();
1791
1917
  try {
1792
- compression = await runBlockingCompression({
1918
+ compression = await edgeLane(async () => runBlockingCompression({
1793
1919
  opts,
1794
1920
  state,
1795
1921
  body,
@@ -1809,9 +1935,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1809
1935
  : {}),
1810
1936
  // Riding the old prefix, or sending a raw request that fits, keeps the
1811
1937
  // short budget; with neither, a timeout could only end in a refusal.
1812
- longBudget: !(edge?.kind === "compress" && edge.fallback) &&
1938
+ longBudget: !(edge?.kind === "compress" && edge.fallback && !edge.fallback.uncounted) &&
1813
1939
  await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
1814
- });
1940
+ ...(edgeCarry ? { carry: edgeCarry } : {}),
1941
+ }));
1815
1942
  }
1816
1943
  catch {
1817
1944
  const kept = rideOldPrefix(false);
@@ -1851,6 +1978,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1851
1978
  if (downstreamClosedDuringCompression ||
1852
1979
  (res.destroyed && !res.writableFinished)) {
1853
1980
  releaseUncommittedRouteDecision();
1981
+ // Claude Code gave up on the turn, not on the compaction: the next turn rides it.
1982
+ if (result && edgeCarry)
1983
+ carryOverCompression(state, edgeCarry, Promise.resolve(result));
1854
1984
  recordTurn(rec, "followup-client-closed", Buffer.alloc(0));
1855
1985
  return logged(Promise.resolve());
1856
1986
  }
@@ -1858,6 +1988,28 @@ async function handleMessages(req, res, opts, upstream, state) {
1858
1988
  // Each forwarder installs its own close listener synchronously, so this is
1859
1989
  // an event-loop-atomic handoff of downstream-close ownership.
1860
1990
  if (!result) {
1991
+ const paymentDetail = opts.memtree.paymentRequiredDetail;
1992
+ const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
1993
+ if (mayQueueNotice && paymentDetail === null && !state.keyInvalid()) {
1994
+ // Under the budget the whole request is what compression would have
1995
+ // sent: the turn is not degraded, so the failure stays in the logs.
1996
+ // Delivery decides the rest (selectForwardCandidate): only a whole
1997
+ // history actually sent warns. A refusal (Claude Code retries it) or a
1998
+ // ride on a compressed prefix did not run uncompressed (2026-10-28).
1999
+ const eager = compressionNeeded(rec.compaction);
2000
+ // Delivery sizes the request again (an exact count, else a
2001
+ // conservative bound): a planned fit it finds over the budget warns then.
2002
+ if (!eager)
2003
+ rec.degradedNotice = "quiet-under-budget";
2004
+ degradedNotices.set(rec, { eager, warn: () => {
2005
+ // A 402 or refused key meanwhile keeps its own, actionable notice (Codex r2 P2).
2006
+ if (state.mainPromptGeneration !== noticePromptGeneration ||
2007
+ opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
2008
+ return;
2009
+ state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
2010
+ rec.degradedNotice = "shown";
2011
+ } });
2012
+ }
1861
2013
  const kept = rideOldPrefix(false);
1862
2014
  if (kept)
1863
2015
  return kept;
@@ -1872,8 +2024,6 @@ async function handleMessages(req, res, opts, upstream, state) {
1872
2024
  releaseForOriginalFallback();
1873
2025
  sendingWhole();
1874
2026
  capture(opts, "anthropic-request", forwardBody);
1875
- const paymentDetail = opts.memtree.paymentRequiredDetail;
1876
- const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
1877
2027
  const overBudget = reachedBudget(rec.compaction);
1878
2028
  if (state.keyInvalid()) {
1879
2029
  // A refused key (401/403) says why MemTree is off and how to fix it;
@@ -1897,26 +2047,6 @@ async function handleMessages(req, res, opts, upstream, state) {
1897
2047
  if (overBudget)
1898
2048
  state.nearWindowNotice = handle;
1899
2049
  }
1900
- else if (mayQueueNotice && paymentDetail === null) {
1901
- // Under the budget the whole request is what compression would have
1902
- // sent: the turn is not degraded, so the failure stays in the logs.
1903
- const warn = () => {
1904
- state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
1905
- rec.degradedNotice = "shown";
1906
- };
1907
- if (compressionNeeded(rec.compaction)) {
1908
- warn();
1909
- }
1910
- else {
1911
- // Delivery sizes the request again (an exact count, else a
1912
- // conservative bound): a turn it finds over the budget warns then.
1913
- rec.degradedNotice = "quiet-under-budget";
1914
- quietDegrades.set(rec, () => {
1915
- if (state.mainPromptGeneration === noticePromptGeneration)
1916
- warn();
1917
- });
1918
- }
1919
- }
1920
2050
  return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1921
2051
  noteWholeSize();
1922
2052
  if (delivered)
@@ -2224,6 +2354,48 @@ async function projectTreesContext(hint, input) {
2224
2354
  return undefined;
2225
2355
  }
2226
2356
  }
2357
+ /**
2358
+ * The parent's memory hint for a starting subagent: only while MemTree works
2359
+ * and the session has the tools, naming the parent's newest tree (a bounded
2360
+ * wait when no answer is cached). Undefined otherwise; never throws.
2361
+ */
2362
+ async function subagentHintContext(state, sessionId) {
2363
+ if (!state.subagentHint?.enabled || !memtreeWorking(state))
2364
+ return undefined;
2365
+ try {
2366
+ // A cached tree will do (peek refreshes it in the background); wait only for none.
2367
+ const tree = state.newestTrees?.peek(sessionId) ?? await state.newestTrees?.settle(sessionId);
2368
+ // MemTree may have paused or failed while the lookup was on its way.
2369
+ if (!tree || !state.subagentHint.enabled || !memtreeWorking(state))
2370
+ return undefined;
2371
+ return subagentMemoryHintText(sessionId, tree.key);
2372
+ }
2373
+ catch {
2374
+ return undefined;
2375
+ }
2376
+ }
2377
+ /** The current MemTree guidance for a resumed main session (resume-guidance.ts). */
2378
+ async function resumeGuidanceContext(state, input) {
2379
+ try {
2380
+ return await state.resumeGuidance?.contextFor(input, {
2381
+ working: () => resumeMemtreeWorking(state),
2382
+ compressed: () => currentMemtreePage(state, input.session_id)?.compressed,
2383
+ });
2384
+ }
2385
+ catch {
2386
+ return undefined;
2387
+ }
2388
+ }
2389
+ /**
2390
+ * Whether MemTree works as a session resumes: not paused, key not refused,
2391
+ * and its latest answer, if any, a success. A new `ccc --resume` process has
2392
+ * no answer yet; its startup key check already turned a refused or unpaid
2393
+ * key into no guidance (resumeGuidanceFor).
2394
+ */
2395
+ function resumeMemtreeWorking(state) {
2396
+ return state.memtreeAnswering() !== false && state.unpaidDetail() === null &&
2397
+ !state.keyInvalid();
2398
+ }
2227
2399
  /**
2228
2400
  * The SessionStart hook's answer: the banner line for the user
2229
2401
  * (`systemMessage`), model context (`additionalContext`), or both.
@@ -2251,15 +2423,14 @@ function sessionCommandReply(state, sessionId, prompt) {
2251
2423
  : pausedCompactionText(viewUrl(subscribeUrlFromDetail(unpaid)));
2252
2424
  if (sessionCommandArgs(prompt, MEMTREE_HELP_COMMAND) !== undefined) {
2253
2425
  return [
2254
- "• /memtree-view · show this session's MemTree page link",
2426
+ "• /memtree-view · open this session's MemTree page and show its link",
2255
2427
  paused !== undefined ? `• ${paused}`
2256
2428
  : `• /memtree-compact [tokens | off] · compact this session on your next message (default half the budget, at least ${MEMTREE_COMPACT_MIN_TOKENS / 1000}k)`,
2257
2429
  usageLines(state),
2258
2430
  ].filter((line) => line !== undefined).join("\n");
2259
2431
  }
2260
- if (isMemtreeViewCommand(prompt)) {
2261
- return joinNoticeLines(memtreeViewLine(state, sessionId), usageLines(state)) ?? "";
2262
- }
2432
+ if (isMemtreeViewCommand(prompt))
2433
+ return memtreeView(state, sessionId);
2263
2434
  const args = sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND);
2264
2435
  if (args === undefined)
2265
2436
  return undefined;
@@ -2303,10 +2474,8 @@ export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main sessi
2303
2474
  * is refused rather than applied to the main conversation from here.
2304
2475
  */
2305
2476
  function subagentCommandReply(state, sessionId, prompt) {
2306
- if (isMemtreeViewCommand(prompt)) {
2307
- const view = `${memtreeViewLine(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
2308
- return joinNoticeLines(view, usageLines(state)) ?? view;
2309
- }
2477
+ if (isMemtreeViewCommand(prompt))
2478
+ return `${memtreeView(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
2310
2479
  if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
2311
2480
  return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
2312
2481
  }
@@ -2321,8 +2490,13 @@ export function parseTokenCount(text) {
2321
2490
  const value = Math.round(Number(match[1]) * scale);
2322
2491
  return Number.isFinite(value) && value > 0 ? value : undefined;
2323
2492
  }
2324
- /** The `/memtree-view` answer: this session's newest page, or why there is none. */
2325
- function memtreeViewLine(state, sessionId) {
2493
+ /**
2494
+ * The `/memtree-view` answer: one link, this session's newest page (the
2495
+ * newest completed tree, which covers the whole session, rather than the
2496
+ * possibly stale page its memory was compressed from), opened in the browser
2497
+ * where the CLI allows it; or why there is none.
2498
+ */
2499
+ function memtreeView(state, sessionId) {
2326
2500
  const inMemory = state.memtreePages.get(sessionId);
2327
2501
  const page = inMemory && (inMemory.sessionId === undefined || inMemory.sessionId === sessionId)
2328
2502
  ? inMemory
@@ -2331,15 +2505,17 @@ function memtreeViewLine(state, sessionId) {
2331
2505
  if (!link) {
2332
2506
  return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
2333
2507
  }
2508
+ try {
2509
+ state.openUrl?.(link.link);
2510
+ }
2511
+ catch {
2512
+ // The printed link below is the fallback.
2513
+ }
2334
2514
  // Blocked-prompt reasons need an explicit hyperlink in styled terminals.
2335
- const lines = linkLines(LINK_LABEL, viewUrl(link.link), link.note);
2336
- // The memory the agent works from can be an older tree than the newest one.
2337
- return page?.compressed && page.url !== link.link
2338
- ? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
2339
- : lines;
2515
+ return linkLines(LINK_LABEL, viewUrl(link.link), link.note);
2340
2516
  }
2341
2517
  /**
2342
- * The usage dashboard link that `/memtree` and `/memtree-view` end with,
2518
+ * The usage dashboard link that `/memtree` ends with,
2343
2519
  * formatted like the MemTree link; undefined when the environment has none.
2344
2520
  */
2345
2521
  function usageLines(state) {
@@ -2830,6 +3006,15 @@ function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
2830
3006
  return record.uncountedOverWindow === true || (record.sizeSource === "reported" &&
2831
3007
  (record.estimatedTokens ?? 0) + outputTokens > modelContextLimit);
2832
3008
  }
3009
+ /**
3010
+ * Over the window (calibratedSizeExceedsWindow) only by the uncounted
3011
+ * conservative bound: a calibrated estimate that fits the window with the
3012
+ * full max_tokens says otherwise.
3013
+ */
3014
+ function uncountedOnlyOverWindow(record, body, modelContextLimit) {
3015
+ return record.uncountedOverWindow === true && record.sizeSource === "reported" &&
3016
+ (record.estimatedTokens ?? 0) + bodyOutputReservation(body, false) <= modelContextLimit;
3017
+ }
2833
3018
  function calibratedSizeExceedsLimits(record, body, modelContextLimit) {
2834
3019
  return (record.sizeSource === "reported" &&
2835
3020
  (record.estimatedTokens ?? 0) >= record.budgetTokens) ||
@@ -3050,7 +3235,14 @@ async function planEdgeCompaction(args) {
3050
3235
  // image-aware bound), not bytes: a flatten that kept images is mostly
3051
3236
  // base64 that costs a fixed amount per image.
3052
3237
  const overWindow = calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
3053
- const fallback = overWindow ? undefined : { prefix, routed };
3238
+ // Over the window only by the conservative bound of a ride whose planning
3239
+ // count timed out: delivery counts it again before sending it, so it stays
3240
+ // the fallback a failed compress rides (2026-10-28: a 528k ride whose
3241
+ // count timed out was dropped, and the failed compress refused the turn).
3242
+ const uncounted = overWindow && uncountedOnlyOverWindow(compaction, body, modelContextLimit) &&
3243
+ args.sizing.canRetryCount(prefixCandidate);
3244
+ const fallback = overWindow && !uncounted
3245
+ ? undefined : { prefix, routed, ...(uncounted ? { uncounted: true } : {}) };
3054
3246
  if (state.compactNow.has(sessionId))
3055
3247
  return forced("manual", fallback);
3056
3248
  if (prefix.modelContextLimit !== modelContextLimit ||
@@ -3174,6 +3366,16 @@ async function planToolCompaction(args) {
3174
3366
  return { kind: "ride",
3175
3367
  ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
3176
3368
  }
3369
+ // Another turn that waited for the same preparation installed it meanwhile: plan again on
3370
+ // it (once) rather than compress beside it (Codex r1 P1, release 1.0.33).
3371
+ if (stableLane && !args.replanned && state.stablePrefixes.get(sessionId) !== current) {
3372
+ const waited = compaction.preparedWaitMs;
3373
+ const plan = await planToolCompaction({ ...args, replanned: true,
3374
+ ride: currentLaneRide(state, args.routeKey, body, messages, sessionId, true) });
3375
+ if (waited !== undefined && rec.compaction)
3376
+ rec.compaction.preparedWaitMs = waited;
3377
+ return plan;
3378
+ }
3177
3379
  // Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
3178
3380
  const modeNow = compactionMode(opts, state, sessionId);
3179
3381
  if (modeNow.mode === "off")
@@ -3233,10 +3435,49 @@ async function adoptPreparedPrefix(args) {
3233
3435
  const { state, sessionId } = args;
3234
3436
  const base = state.stablePrefixes.get(sessionId);
3235
3437
  await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
3438
+ // Checked and taken with no await between: two turns resuming together see one owner.
3439
+ while (state.adoptingPrepared.has(sessionId))
3440
+ await awaitAdoption(state, sessionId, args.compaction);
3236
3441
  const entry = state.prepared.get(sessionId);
3237
3442
  if (!entry)
3238
3443
  return undefined;
3239
3444
  state.prepared.discard(sessionId);
3445
+ const release = holdAdoption(state, sessionId);
3446
+ try {
3447
+ return await adoptEntry(args, entry, base);
3448
+ }
3449
+ finally {
3450
+ release();
3451
+ }
3452
+ }
3453
+ /**
3454
+ * Another turn of the session is adopting its prepared prefix (two turns waited for the same
3455
+ * preparation): wait for it to finish, so this turn plans on the prefix it installs instead of
3456
+ * compressing again (Codex r1 P1, release 1.0.33). Counted as preparation wait, so a human
3457
+ * turn plans again after it.
3458
+ */
3459
+ async function awaitAdoption(state, sessionId, compaction) {
3460
+ const adopting = state.adoptingPrepared.get(sessionId);
3461
+ if (!adopting)
3462
+ return;
3463
+ const started = Date.now();
3464
+ await adopting;
3465
+ compaction.preparedWaitMs = (compaction.preparedWaitMs ?? 0) + (Date.now() - started);
3466
+ }
3467
+ /** Mark the session's prepared prefix as being adopted; the returned function ends it. */
3468
+ function holdAdoption(state, sessionId) {
3469
+ let done;
3470
+ const adopting = new Promise((resolve) => { done = resolve; });
3471
+ state.adoptingPrepared.set(sessionId, adopting);
3472
+ return () => {
3473
+ if (state.adoptingPrepared.get(sessionId) === adopting)
3474
+ state.adoptingPrepared.delete(sessionId);
3475
+ done();
3476
+ };
3477
+ }
3478
+ /** adoptPreparedPrefix's check and install of the entry it took. */
3479
+ async function adoptEntry(args, entry, base) {
3480
+ const { state, sessionId } = args;
3240
3481
  const fit = await preparedFit(args, entry);
3241
3482
  // Another request installed a prefix, or `/memtree-compact` asked for its
3242
3483
  // own target, while this one waited or counted: that wins.
@@ -3365,17 +3606,98 @@ function maybePrepareCompression(args) {
3365
3606
  });
3366
3607
  boundedSet(state.preparing, sessionId, pending);
3367
3608
  }
3609
+ /**
3610
+ * A subagent's share of its budget from which it is indexed on every tool turn, as the
3611
+ * main thread is; below it, only once, when it ends. 2026-10-30: 29 subagents ran ~32
3612
+ * index jobs each ($45.63 a week for one session), and none came near its budget, so no
3613
+ * subagent tree was ever used to compress. Its tree is still built (at SubagentStop, or
3614
+ * at exit), so MemTree search and the page still find it.
3615
+ */
3616
+ const AGENT_PER_TURN_INDEX_SHARE = 0.5;
3617
+ /** Subagents whose unsent index is kept; the oldest past this is sent at once. */
3618
+ const DEFERRED_AGENT_INDEX_MAX = 64;
3619
+ /**
3620
+ * A tool turn's background index call: now on the main thread and for a subagent past
3621
+ * AGENT_PER_TURN_INDEX_SHARE of its budget; otherwise kept, replacing the subagent's
3622
+ * previous one, until it ends (sendDeferredAgentIndex).
3623
+ */
3624
+ function indexTurn(args) {
3625
+ const { opts, state, req, sessionId } = args;
3626
+ const send = (mode) => opts.memtree.indexInBackground(args.hash, args.msgsForMemtree, args.modelContextLimit, sessionId, args.clientMeta, transcriptTimesFor(opts, sessionId, agentAttributionId(req)), mode === "final" ? { finalIndex: true } : mode === "flush" ? { force: true } : {});
3627
+ // Only a request with its own agent id (the one SubagentStop names) waits for its end; one
3628
+ // attributed by its parent's id only is indexed per turn as before.
3629
+ const agentId = firstNonEmptyHeader(req, "x-claude-code-agent-id");
3630
+ if (agentId === undefined || sessionId === undefined)
3631
+ return send();
3632
+ const compaction = args.rec.compaction;
3633
+ if (compaction?.estimatedTokens !== undefined &&
3634
+ compaction.estimatedTokens >= compaction.budgetTokens * AGENT_PER_TURN_INDEX_SHARE) {
3635
+ send();
3636
+ // Kept too: its last turns under the text minimum still go at its end (forced).
3637
+ keepDeferredIndex(state, deferredIndexKey(agentId, args.msgsForMemtree), { messages: args.msgsForMemtree, send });
3638
+ return;
3639
+ }
3640
+ // Per agent and conversation: a side request made under the subagent's (or its parent's)
3641
+ // id is another conversation and must not replace the subagent's own.
3642
+ keepDeferredIndex(state, deferredIndexKey(agentId, args.msgsForMemtree), { messages: args.msgsForMemtree, send });
3643
+ }
3644
+ /** Keep `entry` as its key's newest unsent index call (bounded). */
3645
+ function keepDeferredIndex(state, key, entry) {
3646
+ const kept = state.deferredAgentIndexes.get(key);
3647
+ // A rewind or another branch: the kept history is not part of this one, so it goes now.
3648
+ // So does one holding media Claude Code has evicted since (never uploaded otherwise).
3649
+ if (kept && (!extendsHistory(entry.messages, kept.messages) ||
3650
+ losesMedia(kept.messages, entry.messages)))
3651
+ sendDeferredIndex(state, key, "flush");
3652
+ state.deferredAgentIndexes.delete(key);
3653
+ state.deferredAgentIndexes.set(key, entry);
3654
+ while (state.deferredAgentIndexes.size > DEFERRED_AGENT_INDEX_MAX) {
3655
+ const oldest = state.deferredAgentIndexes.keys().next().value;
3656
+ if (oldest === undefined)
3657
+ break;
3658
+ sendDeferredIndex(state, oldest, "flush");
3659
+ }
3660
+ }
3661
+ /** A deferred index's key: the agent id and its conversation's first message. */
3662
+ function deferredIndexKey(agentId, messages) {
3663
+ const first = messages.find((message) => message.role !== "system");
3664
+ return JSON.stringify([agentId, first ? mediaFreeMessageHash(first) : ""]);
3665
+ }
3666
+ /** A subagent ended: send each of its conversations' kept index call, whatever its size. */
3667
+ function sendDeferredAgentIndex(state, agentId) {
3668
+ for (const key of [...state.deferredAgentIndexes.keys()]) {
3669
+ if (JSON.parse(key)[0] === agentId)
3670
+ sendDeferredIndex(state, key, "final");
3671
+ }
3672
+ }
3673
+ function sendDeferredIndex(state, key, mode) {
3674
+ const kept = state.deferredAgentIndexes.get(key);
3675
+ state.deferredAgentIndexes.delete(key);
3676
+ kept?.send(mode);
3677
+ }
3678
+ /** At exit: every subagent's kept index calls. */
3679
+ function sendDeferredAgentIndexes(state) {
3680
+ for (const key of [...state.deferredAgentIndexes.keys()])
3681
+ sendDeferredIndex(state, key, "final");
3682
+ }
3368
3683
  /** The background prepare call and the prefix it yields (maybePrepareCompression). */
3369
3684
  async function prepareInBackground(args) {
3370
- const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
3685
+ const { opts, state, body, msgsForMemtree, modelContextLimit } = args;
3371
3686
  const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
3372
3687
  noteServerBudget(state, body.model, modelContextLimit, result);
3373
3688
  if (!result)
3374
3689
  return undefined;
3375
3690
  state.prepared.noteServerAnswer(result.prepared === true);
3376
- if (result.prepared !== true || !didMemtreeCompress(result) ||
3377
- !checkCompressedHistory(result, msgsForMemtree).usable)
3691
+ if (result.prepared !== true)
3692
+ return undefined;
3693
+ return preparedFromResult(args, result);
3694
+ }
3695
+ /** A compressed answer for a session's history, as its prepared prefix (or undefined). */
3696
+ function preparedFromResult(args, result) {
3697
+ const { body, sessionId, modelContextLimit } = args;
3698
+ if (!didMemtreeCompress(result) || !checkCompressedHistory(result, args.msgsForMemtree).usable) {
3378
3699
  return undefined;
3700
+ }
3379
3701
  const built = buildCompressedBody(body, result);
3380
3702
  if (!built)
3381
3703
  return undefined;
@@ -3390,11 +3712,83 @@ async function prepareInBackground(args) {
3390
3712
  explicitTarget: args.explicitTarget,
3391
3713
  modelContextLimit,
3392
3714
  };
3393
- const source = result.memtreeUrl && result.memtreeIndex
3715
+ const source = result.memtreeUrl && result.memtreeIndex && args.linkSeq !== undefined
3394
3716
  ? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
3395
3717
  return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
3396
3718
  ...(source ? { source } : {}) };
3397
3719
  }
3720
+ /**
3721
+ * Whether a main-thread compaction is a budget crossing at the automatic target where
3722
+ * preparations exist: the turns that may race one (a dropped crossing turn and the next
3723
+ * prompt) are serialized on its lane, as carryFor carries it.
3724
+ */
3725
+ function isAutoCrossing(rec, model, modelContextLimit) {
3726
+ const compaction = rec.compaction;
3727
+ return compaction?.mode === "auto" &&
3728
+ (compaction.reason === "budget" || compaction.reason === "prefix-mismatch") &&
3729
+ preparesCompression(model, modelContextLimit);
3730
+ }
3731
+ /** A main-thread compaction's CarryOver, when its plan has the automatic target. */
3732
+ function carryFor(args) {
3733
+ const compaction = args.rec.compaction;
3734
+ // Where prepared prefixes exist (preparesCompression): the turn after rides it. Not a
3735
+ // /memtree-compact (adoption refuses it) nor beside a preparation (the carry would be
3736
+ // refused, its call left running for nothing).
3737
+ if (!args.isMainRequest || args.sessionId === undefined || args.explicitTarget !== false ||
3738
+ args.targetTokens === undefined || compaction?.mode !== "auto" ||
3739
+ // A crossing only: a call the server decides (maybe a passthrough) is not waited for.
3740
+ (compaction.reason !== "budget" && compaction.reason !== "prefix-mismatch") || args.state.compactNow.has(args.sessionId) ||
3741
+ args.state.preparing.has(args.sessionId) || args.state.prepared.isInFlight(args.sessionId) ||
3742
+ args.state.backgroundSignal.aborted ||
3743
+ !preparesCompression(args.body.model, args.modelContextLimit))
3744
+ return undefined;
3745
+ return { body: args.body, messages: args.messages, msgsForMemtree: args.msgsForMemtree,
3746
+ sessionId: args.sessionId, modelContextLimit: args.modelContextLimit,
3747
+ tokens: compaction.estimatedTokens ?? 0, triggerTokens: compaction.budgetTokens,
3748
+ targetTokens: args.targetTokens, explicitTarget: false,
3749
+ ...(args.linkSeq !== undefined ? { linkSeq: args.linkSeq } : {}),
3750
+ base: args.state.stablePrefixes.get(args.sessionId) };
3751
+ }
3752
+ /**
3753
+ * A main-thread compaction its turn stopped waiting for (its budget ran out) becomes the
3754
+ * session's preparation: it runs on to the long budget, the session's next turn at the
3755
+ * budget waits for it (awaitPreparation) instead of starting another, and rides its
3756
+ * answer as a prepared prefix. Before, each turn gave up on its call and started a new
3757
+ * one beside it: 2026-10-30, eleven 26 MB compressions in ten minutes, each billed, none
3758
+ * used. False when the session already has a preparation in flight (it serves instead).
3759
+ */
3760
+ function carryOverCompression(state, carry, pending) {
3761
+ const { sessionId, base } = carry;
3762
+ // A prefix installed since the call started (another turn's) supersedes it already.
3763
+ if (state.backgroundSignal.aborted || state.preparing.has(sessionId) ||
3764
+ state.stablePrefixes.get(sessionId) !== base ||
3765
+ !state.prepared.begin(sessionId, carry.tokens, carry.triggerTokens))
3766
+ return false;
3767
+ const done = pending
3768
+ .then((result) => {
3769
+ // A late answer clears the cooldown; a late failure is no new outage evidence (a
3770
+ // refusal or a slow server's timeout must not arm it, as runBlockingCompression).
3771
+ if (result)
3772
+ noteMemtreeHealth(state, { result, liveHealth: "success" });
3773
+ return result ? preparedFromResult(carry, result) : undefined;
3774
+ })
3775
+ .catch(() => undefined)
3776
+ .then((prepared) => {
3777
+ // A prefix installed meanwhile supersedes it.
3778
+ const current = state.stablePrefixes.get(sessionId) === base;
3779
+ state.prepared.finish(sessionId, current ? prepared : undefined);
3780
+ if (state.preparing.get(sessionId) === done)
3781
+ state.preparing.delete(sessionId);
3782
+ });
3783
+ boundedSet(state.preparing, sessionId, done);
3784
+ return true;
3785
+ }
3786
+ /** How long a carried compaction may run: the long budget, or its upload's if longer. */
3787
+ /** How much longer than its turn's wait a carried call must be allowed to run. */
3788
+ const CARRY_MIN_EXTRA_MS = 5_000;
3789
+ function carriedBudgetMs(opts, scaledMs) {
3790
+ return Math.min(MAX_LONG_COMPRESS_WAIT_MS, Math.max(opts.memtree.longCompressBudgetMs, scaledMs));
3791
+ }
3398
3792
  /** What a prepare call sends: a forced compression to the target, no threshold. */
3399
3793
  function preparedCompressMeta(args) {
3400
3794
  const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
@@ -3813,9 +4207,13 @@ async function runBlockingCompression(args) {
3813
4207
  : undefined;
3814
4208
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
3815
4209
  const compressStarted = Date.now();
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));
4210
+ // The upload is about the request's size: the budget grows with it (the long one
4211
+ // too, never below the short one's).
4212
+ const scaledMs = opts.memtree.compressBudgetForBytes(rec.requestBytes);
4213
+ const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? Math.max(opts.memtree.longCompressBudgetMs, scaledMs) : scaledMs);
4214
+ // Carried only when running on gives it meaningfully more time than this wait.
4215
+ const carry = args.carry && carriedBudgetMs(opts, scaledMs) >= budgetMs + CARRY_MIN_EXTRA_MS
4216
+ ? args.carry : undefined;
3819
4217
  const compressMeta = {
3820
4218
  // Model + tools drive the server's model-based memory budget
3821
4219
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3844,8 +4242,8 @@ async function runBlockingCompression(args) {
3844
4242
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3845
4243
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3846
4244
  // The deadline this wait uses (the upload-scaled budget included), so the
3847
- // call and its waiters end together.
3848
- timeoutMs: budgetMs,
4245
+ // call and its waiters end together; a carried call runs on to the long one.
4246
+ timeoutMs: carry ? Math.max(budgetMs, carriedBudgetMs(opts, scaledMs)) : budgetMs,
3849
4247
  };
3850
4248
  // Sampled BEFORE the call, while it still describes this call: after the
3851
4249
  // await the hash is in the cache regardless of who put it there.
@@ -3854,12 +4252,21 @@ async function runBlockingCompression(args) {
3854
4252
  // Keep foreground work finite even if a transport ignores its abort signal.
3855
4253
  let deadline;
3856
4254
  const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
4255
+ let settled = false;
4256
+ // A refused carry is not aborted: the call may be shared (in-flight join) with a
4257
+ // request that carried it.
4258
+ const call = opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta);
4259
+ const settle = () => { settled = true; };
4260
+ call.then(settle, settle);
3857
4261
  const result = await Promise.race([
3858
- opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
4262
+ call,
3859
4263
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
3860
4264
  ]).finally(() => { if (deadline)
3861
4265
  clearTimeout(deadline); });
3862
4266
  const compressMs = Date.now() - compressStarted;
4267
+ // Given up on but still running: the session's next turn waits for it, not a new call.
4268
+ const carried = result === null && !settled && carry !== undefined &&
4269
+ carryOverCompression(state, carry, call);
3863
4270
  noteServerBudget(state, body.model, modelContextLimit, result);
3864
4271
  // Sampled at this call's settle, before a concurrent same-request failure of
3865
4272
  // another class can overwrite the complete-key entry.
@@ -3875,16 +4282,20 @@ async function runBlockingCompression(args) {
3875
4282
  // Budget-consumed heuristic: the client maps every failure to null, so a
3876
4283
  // null that took (roughly) the whole abort budget was almost certainly
3877
4284
  // the AbortSignal timeout, not a fast server error.
3878
- const timedOut = result === null && compressMs >= Math.min(budgetMs, maximumWait);
4285
+ // A few ms of slack: a timer can fire a millisecond early by the wall clock (CI flake).
4286
+ const timedOut = result === null &&
4287
+ compressMs + TIMER_SLACK_MS >= Math.min(budgetMs, maximumWait);
3879
4288
  // A timeout while other compress calls answered (one succeeded after this
3880
4289
  // one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
3881
4290
  // healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
3882
4291
  const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
3883
- const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered;
4292
+ // A carried call has not failed: it is still running (its answer reports its health).
4293
+ const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered && !carried;
3884
4294
  rec.compress = {
3885
4295
  ms: compressMs,
3886
4296
  ok: result !== null,
3887
4297
  timedOut,
4298
+ ...(carried ? { carriedOver: true } : {}),
3888
4299
  ...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
3889
4300
  };
3890
4301
  return {
@@ -4071,6 +4482,10 @@ async function recoverToolRouteMiss(args) {
4071
4482
  };
4072
4483
  res.once("close", markDownstreamClosed);
4073
4484
  const linkSeq = isMainRequest ? nextMemtreeCallSeq(state) : undefined;
4485
+ const toolCarry = stable ? carryFor({ state, isMainRequest, sessionId, body, messages,
4486
+ msgsForMemtree,
4487
+ modelContextLimit, rec, targetTokens: stable.targetTokens, explicitTarget: stable.explicitTarget,
4488
+ linkSeq }) : undefined;
4074
4489
  let compression;
4075
4490
  try {
4076
4491
  compression = await runBlockingCompression({
@@ -4088,6 +4503,7 @@ async function recoverToolRouteMiss(args) {
4088
4503
  inputCandidate: args.inputCandidate,
4089
4504
  ...(args.compaction ? { compaction: args.compaction } : {}),
4090
4505
  longBudget: args.wholeRefused ? await args.wholeRefused() : false,
4506
+ ...(toolCarry ? { carry: toolCarry } : {}),
4091
4507
  });
4092
4508
  }
4093
4509
  catch {
@@ -4109,9 +4525,11 @@ async function recoverToolRouteMiss(args) {
4109
4525
  if (downstreamClosedDuringCompression ||
4110
4526
  (res.destroyed && !res.writableFinished)) {
4111
4527
  // The MemTree work keeps its cache/index value, but a dead client gets
4112
- // no Anthropic request and no route.
4528
+ // no Anthropic request and no route. The session's next turn rides it.
4113
4529
  rec.routeRecovery = { conversationBytes, outcome: "client-closed" };
4114
4530
  releaseOwnReservation();
4531
+ if (result && toolCarry)
4532
+ carryOverCompression(state, toolCarry, Promise.resolve(result));
4115
4533
  // Same hazard class as the post-forward fates "upstream-failed" /
4116
4534
  // "client-aborted": no route was installed and the client's identical
4117
4535
  // retry is imminent, so no backoff is set and the attempt mark is
@@ -4131,7 +4549,9 @@ async function recoverToolRouteMiss(args) {
4131
4549
  // submission: its longer independent budget can still warm the index for
4132
4550
  // a later turn. An unpaid key (402, possibly set by this very compress)
4133
4551
  // or a shutting-down proxy gets no retry.
4552
+ // A carried compression still runs on this history (and indexes it): no upload beside it.
4134
4553
  if (!state.shutdownSignal.aborted &&
4554
+ rec.compress?.carriedOver !== true &&
4135
4555
  opts.memtree.paymentRequiredDetail === null &&
4136
4556
  !opts.memtree.keyInvalid // a refused key refuses the index too
4137
4557
  ) {
@@ -4566,8 +4986,8 @@ async function selectForwardCandidate(args) {
4566
4986
  (args.rec.routeLane === "main" && args.opts.toolOutputs
4567
4987
  ? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
4568
4988
  : undefined);
4569
- const quietDegrade = quietDegrades.get(args.rec);
4570
- quietDegrades.delete(args.rec);
4989
+ const degraded = degradedNotices.get(args.rec);
4990
+ degradedNotices.delete(args.rec);
4571
4991
  const lateInstall = lateRouteInstalls.get(args.rec);
4572
4992
  if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
4573
4993
  lateRouteInstalls.delete(args.rec);
@@ -4595,14 +5015,29 @@ async function selectForwardCandidate(args) {
4595
5015
  args.rec.lateCountRecovery.outcome =
4596
5016
  changed && candidate.kind === "replacement" ? "compressed" : "forwarded";
4597
5017
  delete args.rec.overBudgetForward;
4598
- if (measurement.tokens > args.limits.budget ||
5018
+ const overBudget = measurement.tokens > args.limits.budget ||
4599
5019
  (measurement.source !== "exact" && candidate.kind === "original" &&
4600
- measurement.estimatedTokens > args.limits.budget)) {
5020
+ measurement.estimatedTokens > args.limits.budget);
5021
+ if (overBudget) {
4601
5022
  args.rec.overBudgetForward = { reason: measurement.source === "exact"
4602
5023
  ? "exact-count-fits-window" : measurement.source === "bytes"
4603
5024
  ? "bytes-fallback-fits-window" : "estimate-fits-window" };
4604
5025
  warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
4605
- quietDegrade?.();
5026
+ }
5027
+ // Uncompressed only when the whole history goes out (or a truncation of
5028
+ // it), not a compressed ride.
5029
+ const whole = candidate.kind === "original" || wholeHistoryCandidates.has(candidate);
5030
+ if (degraded && whole) {
5031
+ // A failed compress's ride that delivery could not send went out whole.
5032
+ if (args.rec.turnType === "followup-prefix") {
5033
+ args.rec.turnType = "followup-degraded";
5034
+ delete args.rec.compaction?.keptPrefix;
5035
+ }
5036
+ if (degraded.eager || overBudget)
5037
+ degraded.warn();
5038
+ }
5039
+ else if (degraded) {
5040
+ delete args.rec.degradedNotice;
4606
5041
  }
4607
5042
  return changed ? candidate.body : intended;
4608
5043
  }
@@ -4670,6 +5105,9 @@ async function truncateCandidate(rec, sizing, base, limits, store) {
4670
5105
  if (!plan)
4671
5106
  return undefined;
4672
5107
  const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
5108
+ if (base.kind === "original" || wholeHistoryCandidates.has(base)) {
5109
+ wholeHistoryCandidates.add(candidate);
5110
+ }
4673
5111
  const measurement = await sizing.measure(candidate, limits);
4674
5112
  if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
4675
5113
  rec.truncatedToolResult = { originalChars: plan.originalChars,
@@ -4714,6 +5152,40 @@ function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
4714
5152
  return preferred.kind === "original" && !fitsMeasured(measured, limits)
4715
5153
  ? { tokens: measured.tokens } : undefined;
4716
5154
  }
5155
+ /**
5156
+ * Wait (up to `maxMs`, or until `signal` aborts) for the lane's in-flight compression to
5157
+ * end; the milliseconds waited.
5158
+ */
5159
+ async function waitForLaneIdle(state, routeKey, maxMs, signal) {
5160
+ const started = Date.now();
5161
+ while (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight && !signal.aborted &&
5162
+ !state.shutdownSignal.aborted && !state.backgroundSignal.aborted &&
5163
+ Date.now() - started < maxMs) {
5164
+ // Unref'd: a wait never holds the process open.
5165
+ await new Promise((resolve) => setTimeout(resolve, LANE_WAIT_POLL_MS).unref());
5166
+ }
5167
+ return Date.now() - started;
5168
+ }
5169
+ /**
5170
+ * The ride the lane's current memory route gives this request, read afresh (after a wait):
5171
+ * undefined when there is none, it does not match this history, or (main lane) a newer
5172
+ * stable prefix superseded it, which the planner then rides itself.
5173
+ */
5174
+ function currentLaneRide(state, routeKey, body, messages, sessionId, isMainRequest) {
5175
+ const route = getMemoryRoute(state, routeKey);
5176
+ if (!route)
5177
+ return undefined;
5178
+ if (isMainRequest && sessionId !== undefined &&
5179
+ route.stablePrefix !== state.stablePrefixes.get(sessionId))
5180
+ return undefined;
5181
+ const raw = memoryRoutedToolBody(body, messages, route, state.mainRouteEpoch, sessionId);
5182
+ return raw ? { raw, turnType: "tool-memory", sizeHolder: route.stablePrefix ?? route }
5183
+ : undefined;
5184
+ }
5185
+ /** Wall-clock slack when judging whether a call ran its whole budget (timers fire early). */
5186
+ const TIMER_SLACK_MS = 5;
5187
+ /** How often a turn waiting for its lane's compression looks again. */
5188
+ const LANE_WAIT_POLL_MS = 100;
4717
5189
  /**
4718
5190
  * Run a delivery-time compression as the lane's in-flight operation, so a
4719
5191
  * concurrent request of the lane neither starts its own (planning sees
@@ -4778,6 +5250,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
4778
5250
  : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
4779
5251
  args.rec.forwardedBytes = 0;
4780
5252
  args.rec.approxInputTokens = 0;
5253
+ // Nothing went out uncompressed: Claude Code retries it (2026-10-28).
5254
+ // A failed compress's ride that could not be sent either (review r2 P3).
5255
+ const refusedRide = args.rec.turnType === "followup-prefix" &&
5256
+ args.rec.compaction?.keptPrefix === true;
5257
+ if (args.rec.turnType === "followup-degraded" || refusedRide) {
5258
+ args.rec.turnType = "followup-refused";
5259
+ delete args.rec.compaction?.keptPrefix;
5260
+ }
5261
+ delete args.rec.degradedNotice;
5262
+ if (unpaid === null && !keyInvalid)
5263
+ noteSlowRetry(args, failure);
4781
5264
  args.state.diagnostics?.record("refusal", {
4782
5265
  status: failure.status, attempt: failure.attempt,
4783
5266
  input_ktokens: Math.round(inputTokens / 1000), budget_ktokens: Math.round(budgetTokens / 1000),
@@ -4804,6 +5287,24 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
4804
5287
  args.state.keyNotice.markShown();
4805
5288
  return false;
4806
5289
  }
5290
+ /**
5291
+ * A repeated retryable refusal on the main thread: say once per streak that
5292
+ * MemTree is slow and Claude Code is retrying, unless another notice waits.
5293
+ */
5294
+ function noteSlowRetry(args, failure) {
5295
+ const { state, rec } = args;
5296
+ const prompt = noticeContexts.get(rec);
5297
+ // Not for a stale prompt or a closed response, nor an index wait (its 503
5298
+ // says why), nor a final refusal (its own message shows).
5299
+ const canShow = failure.status === 503 && rec.routeLane === "main" &&
5300
+ rec.indexBehind !== true && prompt !== undefined &&
5301
+ prompt.generation === state.mainPromptGeneration && !args.res.destroyed &&
5302
+ !state.notices.hasPending();
5303
+ if (!state.refusalStreaks.refused(args.routeKey, Date.now(), canShow))
5304
+ return;
5305
+ state.notices.queueSuffix(SLOW_RETRY_NOTICE, undefined, prompt.promptId);
5306
+ rec.slowRetryNotice = true;
5307
+ }
4807
5308
  /**
4808
5309
  * The refusal text already says MemTree is paused and how to resume, so it
4809
5310
  * counts as the paused notice: the throttle restarts, and this request's
@@ -4879,7 +5380,7 @@ function reachedBudget(compaction) {
4879
5380
  * forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
4880
5381
  * size at or over the budget, or a size nobody measured. A planned fit is
4881
5382
  * provisional: delivery's own sizing can still find the turn over the budget
4882
- * (quietDegrades), since a plan can undercount.
5383
+ * (degradedNotices), since a plan can undercount.
4883
5384
  */
4884
5385
  function compressionNeeded(compaction) {
4885
5386
  if (compaction === undefined || compaction.reason !== undefined)
@@ -4889,10 +5390,15 @@ function compressionNeeded(compaction) {
4889
5390
  return compaction.estimatedTokens >= compaction.budgetTokens;
4890
5391
  }
4891
5392
  /**
4892
- * A degraded turn whose plan fit the budget, so it queued no notice: the
4893
- * notice to queue if delivery's sizing finds it over the budget after all.
5393
+ * A degraded turn's notice, queued by delivery only when the whole request
5394
+ * goes out: at once when planning said it needed compression (`eager`), else
5395
+ * when delivery's sizing finds it over the budget after all.
4894
5396
  */
4895
- const quietDegrades = new WeakMap();
5397
+ const degradedNotices = new WeakMap();
5398
+ /** Truncations (truncatedToFit) of the whole history: they still run uncompressed. */
5399
+ const wholeHistoryCandidates = new WeakSet();
5400
+ /** The prompt a request was made for, for notices its refusal may queue. */
5401
+ const noticeContexts = new WeakMap();
4896
5402
  /** The paused notice a degraded request queued, until it is refused or settles. */
4897
5403
  const pausedNotices = new WeakMap();
4898
5404
  const changedDeliveryRecords = new WeakSet();
@@ -5580,6 +6086,41 @@ function capture(opts, kind, body) {
5580
6086
  // diagnostics only — never break the proxy path
5581
6087
  }
5582
6088
  }
6089
+ /**
6090
+ * The body, or undefined once it passes `limit` bytes (declared or streamed)
6091
+ * or takes longer than HOOK_BODY_TIMEOUT_MS: the rest is drained unkept.
6092
+ */
6093
+ function readBodyWithin(req, limit) {
6094
+ return new Promise((resolve) => {
6095
+ const chunks = [];
6096
+ let size = 0;
6097
+ let done = false;
6098
+ const finish = (body) => {
6099
+ if (done)
6100
+ return;
6101
+ done = true;
6102
+ clearTimeout(timer);
6103
+ if (body === undefined) {
6104
+ chunks.length = 0;
6105
+ req.removeAllListeners("data");
6106
+ req.resume();
6107
+ }
6108
+ resolve(body);
6109
+ };
6110
+ const timer = setTimeout(() => finish(undefined), HOOK_BODY_TIMEOUT_MS);
6111
+ if (Number(req.headers["content-length"] ?? 0) > limit)
6112
+ return finish(undefined);
6113
+ req.on("data", (chunk) => {
6114
+ size += chunk.length;
6115
+ if (size > limit)
6116
+ finish(undefined);
6117
+ else
6118
+ chunks.push(chunk);
6119
+ });
6120
+ req.on("end", () => finish(Buffer.concat(chunks)));
6121
+ req.on("error", () => finish(undefined));
6122
+ });
6123
+ }
5583
6124
  function readBody(req) {
5584
6125
  return readAll(req);
5585
6126
  }