claude-code-infinite 1.0.32 → 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.
package/dist/proxy.js CHANGED
@@ -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
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, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH, } from "./hooks.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";
@@ -86,8 +86,11 @@ const HOOK_BODY_TIMEOUT_MS = 3_000;
86
86
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
87
87
  /** Safety cap on a long-budget compress, should a transport ignore its abort. */
88
88
  const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
89
- /** The most a blocking compress with a fallback may wait (its budget grows with its upload). */
90
- 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;
91
94
  const SKIP_REQUEST_HEADERS = new Set([
92
95
  "host",
93
96
  "connection",
@@ -347,6 +350,7 @@ export function startProxy(opts) {
347
350
  usageUrl: opts.memtree.usageDashboardUrl,
348
351
  sessionStartNotice: opts.sessionStartNotice,
349
352
  promptSubmitNotice: opts.promptSubmitNotice,
353
+ openUrl: opts.openUrl,
350
354
  projectTrees: opts.projectTrees,
351
355
  subagentHint: opts.subagentHint,
352
356
  resumeGuidance: opts.resumeGuidance,
@@ -358,6 +362,8 @@ export function startProxy(opts) {
358
362
  fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
359
363
  refusalStreaks: new RefusalStreaks(),
360
364
  activeSubagents: new Set(),
365
+ deferredAgentIndexes: new Map(),
366
+ adoptingPrepared: new Map(),
361
367
  memoryRoutes: new Map(),
362
368
  mainRouteEpoch: 0,
363
369
  mainRouteDecisionGeneration: 0,
@@ -453,8 +459,10 @@ export function startProxy(opts) {
453
459
  ]);
454
460
  if (timer)
455
461
  clearTimeout(timer);
456
- if (completed)
462
+ if (completed) {
463
+ sendDeferredAgentIndexes(state);
457
464
  return true;
465
+ }
458
466
  // The grace period protects useful in-flight delivery. Once it
459
467
  // expires, signal every forwarding path first so each can
460
468
  // classify the close as proxy-owned, then tear down any request that was
@@ -463,6 +471,7 @@ export function startProxy(opts) {
463
471
  for (const accepted of acceptedRequests)
464
472
  cancelAcceptedRequest(accepted);
465
473
  await quiesced;
474
+ sendDeferredAgentIndexes(state);
466
475
  return false;
467
476
  };
468
477
  return new Promise((resolve, reject) => {
@@ -942,6 +951,7 @@ onlyEvent) {
942
951
  }
943
952
  if (parsed.hook_event_name === "SubagentStop") {
944
953
  state.activeSubagents.delete(parsed.agent_id);
954
+ sendDeferredAgentIndex(state, parsed.agent_id);
945
955
  res.writeHead(204);
946
956
  res.end();
947
957
  return;
@@ -1409,7 +1419,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1409
1419
  // prefix, else whole) with no compress call; at it, compress once to
1410
1420
  // the target (planToolCompaction). A "replay" is a client retry of the
1411
1421
  // request that installed the route, forwarded verbatim.
1412
- const plan = await planToolCompaction({
1422
+ const planArgs = {
1413
1423
  opts,
1414
1424
  state,
1415
1425
  body,
@@ -1424,7 +1434,24 @@ async function handleMessages(req, res, opts, upstream, state) {
1424
1434
  rec,
1425
1435
  ride: toolRide,
1426
1436
  canCompress,
1427
- });
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
+ }
1428
1455
  if (isMainRequest && toolSessionId !== undefined && canCompress &&
1429
1456
  (plan.kind === "ride" || plan.kind === "pass")) {
1430
1457
  maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
@@ -1482,6 +1509,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1482
1509
  // the lane's backoff.
1483
1510
  rec.routeRecovery = { outcome: "cooldown" };
1484
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
+ }
1485
1516
  else {
1486
1517
  const recoveryAttempt = { modelContextLimit, inFlight: true };
1487
1518
  if (!prior && state.toolRecoveryAttemptedLanes.size >= 128) {
@@ -1548,7 +1579,8 @@ async function handleMessages(req, res, opts, upstream, state) {
1548
1579
  requestSendPolicies.get(req)?.allowed.add(toolRide.raw);
1549
1580
  const sendBody = toolRide?.raw ?? (toolNeedsOriginal ? rawBody : forwardBody);
1550
1581
  recordTurn(rec, toolRide ? toolRide.turnType : isUserTurn ? "first-user" : "tool", sendBody);
1551
- 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 });
1552
1584
  capture(opts, toolRide ? "anthropic-request-memory-tool" : "anthropic-request", sendBody);
1553
1585
  // The size Anthropic reports is the next budget check's anchor: a ride
1554
1586
  // reports to its prefix (or route), a whole request to the session's (or
@@ -1619,6 +1651,32 @@ async function handleMessages(req, res, opts, upstream, state) {
1619
1651
  rec.compaction.prepared ??= prepared;
1620
1652
  }
1621
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
+ }
1622
1680
  // Not beside a blocking compress call: the two would overlap on the server.
1623
1681
  if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
1624
1682
  maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
@@ -1847,9 +1905,17 @@ async function handleMessages(req, res, opts, upstream, state) {
1847
1905
  // The hidden away-summary request compresses too, but its page is not the
1848
1906
  // conversation the user is looking at.
1849
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;
1850
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();
1851
1917
  try {
1852
- compression = await runBlockingCompression({
1918
+ compression = await edgeLane(async () => runBlockingCompression({
1853
1919
  opts,
1854
1920
  state,
1855
1921
  body,
@@ -1871,7 +1937,8 @@ async function handleMessages(req, res, opts, upstream, state) {
1871
1937
  // short budget; with neither, a timeout could only end in a refusal.
1872
1938
  longBudget: !(edge?.kind === "compress" && edge.fallback && !edge.fallback.uncounted) &&
1873
1939
  await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
1874
- });
1940
+ ...(edgeCarry ? { carry: edgeCarry } : {}),
1941
+ }));
1875
1942
  }
1876
1943
  catch {
1877
1944
  const kept = rideOldPrefix(false);
@@ -1911,6 +1978,9 @@ async function handleMessages(req, res, opts, upstream, state) {
1911
1978
  if (downstreamClosedDuringCompression ||
1912
1979
  (res.destroyed && !res.writableFinished)) {
1913
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));
1914
1984
  recordTurn(rec, "followup-client-closed", Buffer.alloc(0));
1915
1985
  return logged(Promise.resolve());
1916
1986
  }
@@ -2353,15 +2423,14 @@ function sessionCommandReply(state, sessionId, prompt) {
2353
2423
  : pausedCompactionText(viewUrl(subscribeUrlFromDetail(unpaid)));
2354
2424
  if (sessionCommandArgs(prompt, MEMTREE_HELP_COMMAND) !== undefined) {
2355
2425
  return [
2356
- "• /memtree-view · show this session's MemTree page link",
2426
+ "• /memtree-view · open this session's MemTree page and show its link",
2357
2427
  paused !== undefined ? `• ${paused}`
2358
2428
  : `• /memtree-compact [tokens | off] · compact this session on your next message (default half the budget, at least ${MEMTREE_COMPACT_MIN_TOKENS / 1000}k)`,
2359
2429
  usageLines(state),
2360
2430
  ].filter((line) => line !== undefined).join("\n");
2361
2431
  }
2362
- if (isMemtreeViewCommand(prompt)) {
2363
- return joinNoticeLines(memtreeViewLine(state, sessionId), usageLines(state)) ?? "";
2364
- }
2432
+ if (isMemtreeViewCommand(prompt))
2433
+ return memtreeView(state, sessionId);
2365
2434
  const args = sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND);
2366
2435
  if (args === undefined)
2367
2436
  return undefined;
@@ -2405,10 +2474,8 @@ export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main sessi
2405
2474
  * is refused rather than applied to the main conversation from here.
2406
2475
  */
2407
2476
  function subagentCommandReply(state, sessionId, prompt) {
2408
- if (isMemtreeViewCommand(prompt)) {
2409
- const view = `${memtreeViewLine(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
2410
- return joinNoticeLines(view, usageLines(state)) ?? view;
2411
- }
2477
+ if (isMemtreeViewCommand(prompt))
2478
+ return `${memtreeView(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
2412
2479
  if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
2413
2480
  return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
2414
2481
  }
@@ -2423,8 +2490,13 @@ export function parseTokenCount(text) {
2423
2490
  const value = Math.round(Number(match[1]) * scale);
2424
2491
  return Number.isFinite(value) && value > 0 ? value : undefined;
2425
2492
  }
2426
- /** The `/memtree-view` answer: this session's newest page, or why there is none. */
2427
- 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) {
2428
2500
  const inMemory = state.memtreePages.get(sessionId);
2429
2501
  const page = inMemory && (inMemory.sessionId === undefined || inMemory.sessionId === sessionId)
2430
2502
  ? inMemory
@@ -2433,15 +2505,17 @@ function memtreeViewLine(state, sessionId) {
2433
2505
  if (!link) {
2434
2506
  return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
2435
2507
  }
2508
+ try {
2509
+ state.openUrl?.(link.link);
2510
+ }
2511
+ catch {
2512
+ // The printed link below is the fallback.
2513
+ }
2436
2514
  // Blocked-prompt reasons need an explicit hyperlink in styled terminals.
2437
- const lines = linkLines(LINK_LABEL, viewUrl(link.link), link.note);
2438
- // The memory the agent works from can be an older tree than the newest one.
2439
- return page?.compressed && page.url !== link.link
2440
- ? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
2441
- : lines;
2515
+ return linkLines(LINK_LABEL, viewUrl(link.link), link.note);
2442
2516
  }
2443
2517
  /**
2444
- * The usage dashboard link that `/memtree` and `/memtree-view` end with,
2518
+ * The usage dashboard link that `/memtree` ends with,
2445
2519
  * formatted like the MemTree link; undefined when the environment has none.
2446
2520
  */
2447
2521
  function usageLines(state) {
@@ -3292,6 +3366,16 @@ async function planToolCompaction(args) {
3292
3366
  return { kind: "ride",
3293
3367
  ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
3294
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
+ }
3295
3379
  // Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
3296
3380
  const modeNow = compactionMode(opts, state, sessionId);
3297
3381
  if (modeNow.mode === "off")
@@ -3351,10 +3435,49 @@ async function adoptPreparedPrefix(args) {
3351
3435
  const { state, sessionId } = args;
3352
3436
  const base = state.stablePrefixes.get(sessionId);
3353
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);
3354
3441
  const entry = state.prepared.get(sessionId);
3355
3442
  if (!entry)
3356
3443
  return undefined;
3357
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;
3358
3481
  const fit = await preparedFit(args, entry);
3359
3482
  // Another request installed a prefix, or `/memtree-compact` asked for its
3360
3483
  // own target, while this one waited or counted: that wins.
@@ -3483,17 +3606,98 @@ function maybePrepareCompression(args) {
3483
3606
  });
3484
3607
  boundedSet(state.preparing, sessionId, pending);
3485
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
+ }
3486
3683
  /** The background prepare call and the prefix it yields (maybePrepareCompression). */
3487
3684
  async function prepareInBackground(args) {
3488
- const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
3685
+ const { opts, state, body, msgsForMemtree, modelContextLimit } = args;
3489
3686
  const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
3490
3687
  noteServerBudget(state, body.model, modelContextLimit, result);
3491
3688
  if (!result)
3492
3689
  return undefined;
3493
3690
  state.prepared.noteServerAnswer(result.prepared === true);
3494
- if (result.prepared !== true || !didMemtreeCompress(result) ||
3495
- !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) {
3496
3699
  return undefined;
3700
+ }
3497
3701
  const built = buildCompressedBody(body, result);
3498
3702
  if (!built)
3499
3703
  return undefined;
@@ -3508,11 +3712,83 @@ async function prepareInBackground(args) {
3508
3712
  explicitTarget: args.explicitTarget,
3509
3713
  modelContextLimit,
3510
3714
  };
3511
- const source = result.memtreeUrl && result.memtreeIndex
3715
+ const source = result.memtreeUrl && result.memtreeIndex && args.linkSeq !== undefined
3512
3716
  ? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
3513
3717
  return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
3514
3718
  ...(source ? { source } : {}) };
3515
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
+ }
3516
3792
  /** What a prepare call sends: a forced compression to the target, no threshold. */
3517
3793
  function preparedCompressMeta(args) {
3518
3794
  const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
@@ -3931,9 +4207,13 @@ async function runBlockingCompression(args) {
3931
4207
  : undefined;
3932
4208
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
3933
4209
  const compressStarted = Date.now();
3934
- // The upload is about the request's size: the budget grows with it.
3935
- const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? opts.memtree.longCompressBudgetMs
3936
- : 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;
3937
4217
  const compressMeta = {
3938
4218
  // Model + tools drive the server's model-based memory budget
3939
4219
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3962,8 +4242,8 @@ async function runBlockingCompression(args) {
3962
4242
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3963
4243
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3964
4244
  // The deadline this wait uses (the upload-scaled budget included), so the
3965
- // call and its waiters end together.
3966
- 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,
3967
4247
  };
3968
4248
  // Sampled BEFORE the call, while it still describes this call: after the
3969
4249
  // await the hash is in the cache regardless of who put it there.
@@ -3972,12 +4252,21 @@ async function runBlockingCompression(args) {
3972
4252
  // Keep foreground work finite even if a transport ignores its abort signal.
3973
4253
  let deadline;
3974
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);
3975
4261
  const result = await Promise.race([
3976
- opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
4262
+ call,
3977
4263
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
3978
4264
  ]).finally(() => { if (deadline)
3979
4265
  clearTimeout(deadline); });
3980
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);
3981
4270
  noteServerBudget(state, body.model, modelContextLimit, result);
3982
4271
  // Sampled at this call's settle, before a concurrent same-request failure of
3983
4272
  // another class can overwrite the complete-key entry.
@@ -3993,16 +4282,20 @@ async function runBlockingCompression(args) {
3993
4282
  // Budget-consumed heuristic: the client maps every failure to null, so a
3994
4283
  // null that took (roughly) the whole abort budget was almost certainly
3995
4284
  // the AbortSignal timeout, not a fast server error.
3996
- 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);
3997
4288
  // A timeout while other compress calls answered (one succeeded after this
3998
4289
  // one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
3999
4290
  // healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
4000
4291
  const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
4001
- 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;
4002
4294
  rec.compress = {
4003
4295
  ms: compressMs,
4004
4296
  ok: result !== null,
4005
4297
  timedOut,
4298
+ ...(carried ? { carriedOver: true } : {}),
4006
4299
  ...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
4007
4300
  };
4008
4301
  return {
@@ -4189,6 +4482,10 @@ async function recoverToolRouteMiss(args) {
4189
4482
  };
4190
4483
  res.once("close", markDownstreamClosed);
4191
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;
4192
4489
  let compression;
4193
4490
  try {
4194
4491
  compression = await runBlockingCompression({
@@ -4206,6 +4503,7 @@ async function recoverToolRouteMiss(args) {
4206
4503
  inputCandidate: args.inputCandidate,
4207
4504
  ...(args.compaction ? { compaction: args.compaction } : {}),
4208
4505
  longBudget: args.wholeRefused ? await args.wholeRefused() : false,
4506
+ ...(toolCarry ? { carry: toolCarry } : {}),
4209
4507
  });
4210
4508
  }
4211
4509
  catch {
@@ -4227,9 +4525,11 @@ async function recoverToolRouteMiss(args) {
4227
4525
  if (downstreamClosedDuringCompression ||
4228
4526
  (res.destroyed && !res.writableFinished)) {
4229
4527
  // The MemTree work keeps its cache/index value, but a dead client gets
4230
- // no Anthropic request and no route.
4528
+ // no Anthropic request and no route. The session's next turn rides it.
4231
4529
  rec.routeRecovery = { conversationBytes, outcome: "client-closed" };
4232
4530
  releaseOwnReservation();
4531
+ if (result && toolCarry)
4532
+ carryOverCompression(state, toolCarry, Promise.resolve(result));
4233
4533
  // Same hazard class as the post-forward fates "upstream-failed" /
4234
4534
  // "client-aborted": no route was installed and the client's identical
4235
4535
  // retry is imminent, so no backoff is set and the attempt mark is
@@ -4249,7 +4549,9 @@ async function recoverToolRouteMiss(args) {
4249
4549
  // submission: its longer independent budget can still warm the index for
4250
4550
  // a later turn. An unpaid key (402, possibly set by this very compress)
4251
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.
4252
4553
  if (!state.shutdownSignal.aborted &&
4554
+ rec.compress?.carriedOver !== true &&
4253
4555
  opts.memtree.paymentRequiredDetail === null &&
4254
4556
  !opts.memtree.keyInvalid // a refused key refuses the index too
4255
4557
  ) {
@@ -4850,6 +5152,40 @@ function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
4850
5152
  return preferred.kind === "original" && !fitsMeasured(measured, limits)
4851
5153
  ? { tokens: measured.tokens } : undefined;
4852
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;
4853
5189
  /**
4854
5190
  * Run a delivery-time compression as the lane's in-flight operation, so a
4855
5191
  * concurrent request of the lane neither starts its own (planning sees