claude-code-infinite 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proxy.js CHANGED
@@ -70,6 +70,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
70
70
  // Long enough that a real outage costs one stall, short enough that a
71
71
  // transient blip does not disable recovery for a working session.
72
72
  const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
73
+ /** Safety cap on a long-budget compress, should a transport ignore its abort. */
74
+ const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
73
75
  const SKIP_REQUEST_HEADERS = new Set([
74
76
  "host",
75
77
  "connection",
@@ -282,6 +284,10 @@ export function startProxy(opts) {
282
284
  memorySourcePages: new Map(),
283
285
  memtreeCallSeq: 0,
284
286
  memtreeLinkStore: opts.memtreeLinkStore,
287
+ stablePrefixStore: opts.stablePrefixStore,
288
+ storedPrefixLookups: new Map(),
289
+ storageOwner: typeof opts.memtree.storageIdentity === "string"
290
+ ? opts.memtree.storageIdentity : undefined,
285
291
  newestTrees: opts.newestTrees,
286
292
  memtreeLinkPlacement: opts.memtreeLinkPlacement ?? "turn",
287
293
  compactTargets: new Map(),
@@ -945,7 +951,9 @@ async function handleMessages(req, res, opts, upstream, state) {
945
951
  },
946
952
  }, state.countTokensCooldowns);
947
953
  const requestSizing = new RequestSizing(countSession, opts.countTokens === true);
948
- const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey);
954
+ if (carriesServerTools(body))
955
+ serverToolRecords.add(rec);
956
+ const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey, body.model);
949
957
  const originalCandidate = requestSizing.register(rawBody, "original", sizeSlot.get());
950
958
  const forwardCandidate = requestSizing.register(forwardBody, "original", sizeSlot.get());
951
959
  candidateSizeOwners.set(originalCandidate, sizeSlot);
@@ -956,6 +964,9 @@ async function handleMessages(req, res, opts, upstream, state) {
956
964
  output: bodyOutputReservation(body),
957
965
  recountOutput: outputReservation(undefined, body.max_tokens), // full max_tokens
958
966
  });
967
+ if (isMainRequest && requestSessionKey !== undefined) {
968
+ adoptStoredPrefix(opts, state, rec, requestSessionKey, body, messages, modelContextLimit);
969
+ }
959
970
  const rides = state.memoryRoutes.has(requestRouteKey) ||
960
971
  (isMainRequest && requestSessionKey !== undefined && state.stablePrefixes.has(requestSessionKey));
961
972
  if (!rides) {
@@ -1263,6 +1274,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1263
1274
  ...(plan.fallback ? { fallback: plan.fallback } : {}),
1264
1275
  retryAtTokens: plan.estimateTokens +
1265
1276
  Math.floor(plan.budgetTokens * TOOL_COMPACTION_RETRY_BUDGET_RATIO),
1277
+ ...(plan.fallback ? {} : { wholeRefused: () => wholeRequestRefused(requestSizing, originalCandidate, sizingLimits()) }),
1266
1278
  }).finally(() => {
1267
1279
  recoveryAttempt.inFlight = false;
1268
1280
  }));
@@ -1289,7 +1301,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1289
1301
  if (toolRide)
1290
1302
  noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
1291
1303
  else {
1292
- noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, rec, sendBody.length);
1304
+ noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
1293
1305
  }
1294
1306
  }));
1295
1307
  }
@@ -1528,7 +1540,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1528
1540
  const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
1529
1541
  const noteWholeSize = () => {
1530
1542
  if (isMainRequest)
1531
- notePassthroughSize(state, sessionId, rec, forwardBody.length);
1543
+ notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
1532
1544
  };
1533
1545
  // The complete request upload can outlive its downstream subscriber while
1534
1546
  // MemTree compression is in flight. Track that subscriber locally without
@@ -1563,6 +1575,10 @@ async function handleMessages(req, res, opts, upstream, state) {
1563
1575
  : edge?.kind === "off"
1564
1576
  ? { compaction: {} }
1565
1577
  : {}),
1578
+ // Riding the old prefix, or sending a raw request that fits, keeps the
1579
+ // short budget; with neither, a timeout could only end in a refusal.
1580
+ longBudget: !(edge?.kind === "compress" && edge.fallback) &&
1581
+ await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
1566
1582
  });
1567
1583
  }
1568
1584
  catch {
@@ -1575,7 +1591,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1575
1591
  return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
1576
1592
  .then((delivered) => {
1577
1593
  if (isMainRequest)
1578
- notePassthroughSize(state, sessionId, rec, rawBody.length);
1594
+ notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
1579
1595
  if (delivered)
1580
1596
  markMainPromptDelivered();
1581
1597
  }));
@@ -1880,6 +1896,9 @@ function sessionCommandReply(state, sessionId, prompt) {
1880
1896
  state.memoryRoutes.delete(JSON.stringify([sessionId, "main"]));
1881
1897
  state.compactNow.delete(sessionId);
1882
1898
  state.stablePrefixes.delete(sessionId);
1899
+ if (state.storageOwner)
1900
+ state.stablePrefixStore?.delete(sessionId, state.storageOwner);
1901
+ state.storedPrefixLookups.set(sessionId, null);
1883
1902
  return `${TRAILER_LABEL} compaction off: the conversation is sent whole, and MemTree compresses only when it outgrows the model's budget.`;
1884
1903
  }
1885
1904
  if (args === "") {
@@ -2151,6 +2170,9 @@ stable) {
2151
2170
  // request carried, and the compressed bytes sent for them. Later requests
2152
2171
  // reuse these bytes unchanged, so the prefix stays a cache read.
2153
2172
  route.stablePrefix = storeStablePrefix(state, sessionId, originalMessages, route, stable);
2173
+ if (route.stablePrefix && key === JSON.stringify([sessionId, "main"])) {
2174
+ persistStablePrefix(state, sessionId, route.stablePrefix);
2175
+ }
2154
2176
  }
2155
2177
  setMemoryRoute(state, key, route);
2156
2178
  state.routeDecisionsLive.set(key, new Set([decisionGeneration]));
@@ -2186,6 +2208,83 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
2186
2208
  state.compactNow.delete(sessionId);
2187
2209
  return prefix;
2188
2210
  }
2211
+ /**
2212
+ * Persist the main lane's newly adopted prefix with its memory-source page.
2213
+ * Deferred one tick so the page noted right after the install is included,
2214
+ * and skipped when a newer prefix replaced this one meanwhile.
2215
+ */
2216
+ function persistStablePrefix(state, sessionId, prefix) {
2217
+ const store = state.stablePrefixStore;
2218
+ const owner = state.storageOwner;
2219
+ if (!store || !owner)
2220
+ return;
2221
+ state.storedPrefixLookups.set(sessionId, null);
2222
+ setImmediate(() => {
2223
+ if (state.stablePrefixes.get(sessionId) !== prefix)
2224
+ return;
2225
+ const page = state.memorySourcePages.get(sessionId);
2226
+ store.save(sessionId, owner, {
2227
+ originalSystemHash: prefix.originalSystemHash,
2228
+ originalPrefixHashes: prefix.originalPrefixHashes,
2229
+ compressedMessages: prefix.compressedMessages,
2230
+ compressedSystem: prefix.compressedSystem,
2231
+ hasCompressedSystem: prefix.hasCompressedSystem,
2232
+ targetTokens: prefix.targetTokens,
2233
+ explicitTarget: prefix.explicitTarget,
2234
+ modelContextLimit: prefix.modelContextLimit,
2235
+ }, page && { url: page.url, index: page.index, compressed: page.compressed });
2236
+ });
2237
+ }
2238
+ /**
2239
+ * A fresh process's main-lane requests for a session: adopt the session's
2240
+ * persisted prefix when this history passes the checks the in-memory prefix
2241
+ * would (same session, system and covered messages; same window; a routable
2242
+ * body). Only an authoritative main history may discard it: one with the same
2243
+ * system and window, no server tools, and at least the covered length, whose
2244
+ * covered messages differ. Anything else (a side or helper request such as
2245
+ * the Haiku WebSearch call) leaves it for a later request. The disk is read
2246
+ * once per session; the store checks the backend/account owner.
2247
+ */
2248
+ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelContextLimit) {
2249
+ const { stablePrefixStore: store, storageOwner: owner } = state;
2250
+ if (!store || !owner || state.stablePrefixes.has(sessionId))
2251
+ return;
2252
+ if (compactionMode(opts, state, sessionId).mode === "off")
2253
+ return;
2254
+ let entry = state.storedPrefixLookups.get(sessionId);
2255
+ if (entry === undefined) {
2256
+ entry = store.load(sessionId, owner) ?? null;
2257
+ boundedSet(state.storedPrefixLookups, sessionId, entry);
2258
+ }
2259
+ if (entry === null)
2260
+ return;
2261
+ const prefix = { sessionId, ...entry.prefix };
2262
+ const verdict = storedPrefixVerdict(body, messages, prefix, modelContextLimit);
2263
+ if (verdict === "defer")
2264
+ return;
2265
+ state.storedPrefixLookups.set(sessionId, null);
2266
+ if (verdict === "discard") {
2267
+ store.delete(sessionId, owner);
2268
+ rec.storedPrefix = "discarded";
2269
+ return;
2270
+ }
2271
+ boundedSet(state.stablePrefixes, sessionId, prefix);
2272
+ if (entry.source && !state.memorySourcePages.has(sessionId)) {
2273
+ rememberPage(state.memorySourcePages, sessionId, { sessionId, ...entry.source, seq: 0 });
2274
+ }
2275
+ rec.storedPrefix = "adopted";
2276
+ }
2277
+ /** What this request says about a persisted prefix (adoptStoredPrefix). */
2278
+ function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
2279
+ if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
2280
+ return "defer";
2281
+ const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
2282
+ if (miss === null)
2283
+ return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
2284
+ const authoritative = miss === "prefix" &&
2285
+ messages.length >= prefix.originalPrefixHashes.length;
2286
+ return authoritative ? "discard" : "defer";
2287
+ }
2189
2288
  /**
2190
2289
  * A fork of the main conversation (the away recap) sent on the main thread's
2191
2290
  * last compressed prefix: that prefix, then the fork's own messages after it.
@@ -2367,11 +2466,31 @@ function notePrefixSize(prefix, rec, bytes) {
2367
2466
  prefix.lastSize = sample;
2368
2467
  }
2369
2468
  /** Record the reported size of a main-thread request forwarded whole. */
2370
- function notePassthroughSize(state, sessionId, rec, bytes) {
2469
+ function notePassthroughSize(state, sessionId, model, rec, bytes) {
2371
2470
  const sample = reportedSample(rec, bytes);
2372
2471
  if (sessionId === undefined || sample === undefined)
2373
2472
  return;
2374
- boundedSet(state.passthroughSizes, sessionId, sample);
2473
+ boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
2474
+ }
2475
+ /** Key of a main-thread size sample (ProxyState.passthroughSizes). */
2476
+ function sessionSizeKey(sessionId, model) {
2477
+ return JSON.stringify([sessionId, typeof model === "string" ? model : null]);
2478
+ }
2479
+ /** Requests whose reported input includes server-injected tool results. */
2480
+ const serverToolRecords = new WeakSet();
2481
+ /**
2482
+ * Whether the request offers server tools (web search, web fetch, code
2483
+ * execution, tool search): their results are added to the input server-side,
2484
+ * so the reported size says nothing about the body's bytes.
2485
+ */
2486
+ function carriesServerTools(body) {
2487
+ return Array.isArray(body.tools) && body.tools.some((tool) => typeof tool?.type === "string" && SERVER_TOOL_TYPE.test(tool.type));
2488
+ }
2489
+ const SERVER_TOOL_TYPE = /^(web_search|web_fetch|code_execution|tool_search_tool)_/;
2490
+ /** Whether the response reports any server tool use (server_tool_use counts). */
2491
+ function usedServerTools(rec) {
2492
+ const used = rec.usage?.server_tool_use;
2493
+ return !!used && Object.values(used).some((count) => count > 0);
2375
2494
  }
2376
2495
  /**
2377
2496
  * The size Anthropic reported for a body of `bytes`, if it can describe one:
@@ -2381,6 +2500,8 @@ function notePassthroughSize(state, sessionId, rec, bytes) {
2381
2500
  function reportedSample(rec, bytes) {
2382
2501
  if (changedDeliveryRecords.has(rec))
2383
2502
  return undefined;
2503
+ if (serverToolRecords.has(rec) || usedServerTools(rec))
2504
+ return undefined;
2384
2505
  const tokens = reportedInputTokens(rec);
2385
2506
  if (tokens === undefined)
2386
2507
  return undefined;
@@ -2400,15 +2521,16 @@ function laneSizeKey(req, routeKey, model) {
2400
2521
  ]);
2401
2522
  }
2402
2523
  /** The session's passthrough size on the main thread, else the lane's. */
2403
- function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey) {
2524
+ function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey, model) {
2404
2525
  const main = isMainRequest && sessionId !== undefined;
2526
+ const mainKey = main ? sessionSizeKey(sessionId, model) : "";
2405
2527
  return {
2406
- get: () => (main ? state.passthroughSizes.get(sessionId) : state.laneSizes.get(sizeKey)),
2528
+ get: () => (main ? state.passthroughSizes.get(mainKey) : state.laneSizes.get(sizeKey)),
2407
2529
  set: (sample) => {
2408
2530
  if (!plausibleSample(sample))
2409
2531
  return;
2410
2532
  if (main)
2411
- boundedSet(state.passthroughSizes, sessionId, sample);
2533
+ boundedSet(state.passthroughSizes, mainKey, sample);
2412
2534
  else
2413
2535
  boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
2414
2536
  },
@@ -2420,9 +2542,9 @@ const noteRideSize = notePrefixSize;
2420
2542
  * Record the reported size of a request forwarded whole: the session's
2421
2543
  * passthrough size on the main thread, else its lane's.
2422
2544
  */
2423
- function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, rec, bytes) {
2545
+ function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
2424
2546
  if (isMainRequest && sessionId !== undefined) {
2425
- notePassthroughSize(state, sessionId, rec, bytes);
2547
+ notePassthroughSize(state, sessionId, model, rec, bytes);
2426
2548
  return;
2427
2549
  }
2428
2550
  const sample = reportedSample(rec, bytes);
@@ -2595,7 +2717,7 @@ function planToolCompaction(args) {
2595
2717
  const sample = ride
2596
2718
  ? ride.sizeHolder.lastSize
2597
2719
  : stableLane
2598
- ? state.passthroughSizes.get(sessionId)
2720
+ ? state.passthroughSizes.get(sessionSizeKey(sessionId, body.model))
2599
2721
  : state.laneSizes.get(args.sizeKey);
2600
2722
  const candidate = args.sizing.register(ride?.raw ?? forwardBody, ride ? "ride" : "original", sample);
2601
2723
  if (ride)
@@ -2987,6 +3109,8 @@ async function runBlockingCompression(args) {
2987
3109
  : undefined;
2988
3110
  const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
2989
3111
  const compressStarted = Date.now();
3112
+ const budgetMs = args.longBudget
3113
+ ? opts.memtree.longCompressBudgetMs : opts.memtree.compressBudgetMs;
2990
3114
  const compressMeta = {
2991
3115
  // Model + tools drive the server's model-based memory budget
2992
3116
  // (e.g. 800k whole-request target for Fable / Opus 5). Omitting
@@ -3012,6 +3136,7 @@ async function runBlockingCompression(args) {
3012
3136
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
3013
3137
  ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
3014
3138
  ...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
3139
+ ...(args.longBudget ? { timeoutMs: budgetMs } : {}),
3015
3140
  };
3016
3141
  // Sampled BEFORE the call, while it still describes this call: after the
3017
3142
  // await the hash is in the cache regardless of who put it there.
@@ -3019,7 +3144,7 @@ async function runBlockingCompression(args) {
3019
3144
  // compress() maps every failure to a resolved null (it never rejects).
3020
3145
  // Keep foreground work finite even if a transport ignores its abort signal.
3021
3146
  let deadline;
3022
- const maximumWait = Math.min(30_000, Math.max(1, Number.isFinite(opts.memtree.compressBudgetMs) ? opts.memtree.compressBudgetMs : 15_000));
3147
+ const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : 30_000, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
3023
3148
  const result = await Promise.race([
3024
3149
  opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
3025
3150
  new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
@@ -3043,7 +3168,7 @@ async function runBlockingCompression(args) {
3043
3168
  // Budget-consumed heuristic: the client maps every failure to null, so a
3044
3169
  // null that took (roughly) the whole abort budget was almost certainly
3045
3170
  // the AbortSignal timeout, not a fast server error.
3046
- timedOut: result === null && compressMs >= opts.memtree.compressBudgetMs,
3171
+ timedOut: result === null && compressMs >= budgetMs,
3047
3172
  };
3048
3173
  return {
3049
3174
  result,
@@ -3161,7 +3286,7 @@ async function recoverToolRouteMiss(args) {
3161
3286
  capture(opts, "anthropic-request", args.originalBody);
3162
3287
  return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
3163
3288
  toolForwarding.settle(delivered);
3164
- noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, rec, args.originalBody.length);
3289
+ noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
3165
3290
  });
3166
3291
  };
3167
3292
  // Same downstream-close tracking as the followup path: compression
@@ -3190,6 +3315,7 @@ async function recoverToolRouteMiss(args) {
3190
3315
  sizing: args.sizing,
3191
3316
  inputCandidate: args.inputCandidate,
3192
3317
  ...(args.compaction ? { compaction: args.compaction } : {}),
3318
+ longBudget: args.wholeRefused ? await args.wholeRefused() : false,
3193
3319
  });
3194
3320
  }
3195
3321
  catch {
@@ -3609,6 +3735,17 @@ async function selectForwardCandidate(args) {
3609
3735
  }
3610
3736
  return changed ? candidate.body : intended;
3611
3737
  }
3738
+ /**
3739
+ * Whether delivery would refuse `candidate` sent whole: it is planned over the
3740
+ * window, and the measurement selection would use (an exact count, else the
3741
+ * same estimate or bytes/4 fallback) does not fit either. Only then is there
3742
+ * no fallback, and only then may a compress take the long budget.
3743
+ */
3744
+ async function wholeRequestRefused(sizing, candidate, limits) {
3745
+ if (fitsNative(sizing.plan(candidate).tokens, limits))
3746
+ return false;
3747
+ return !fitsMeasured(await sizing.measure(candidate, limits), limits);
3748
+ }
3612
3749
  /** Write the bounded fallback refusal (route-fallback.ts); always false. */
3613
3750
  function refuseWholeRequest(args, inputTokens, budgetTokens) {
3614
3751
  const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
@@ -3647,6 +3784,9 @@ function noteSelectedCandidateSize(rec) {
3647
3784
  const tokens = reportedInputTokens(rec);
3648
3785
  if (!candidate || !owner || tokens === undefined)
3649
3786
  return;
3787
+ // Server tools' injected results inflate the reported input (reportedSample).
3788
+ if (serverToolRecords.has(rec) || usedServerTools(rec))
3789
+ return;
3650
3790
  const sample = { tokens, forwardedBytes: candidate.body.length };
3651
3791
  if (!plausibleSample(sample))
3652
3792
  return;