claude-code-infinite 1.0.20 → 1.0.21

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 (69) hide show
  1. package/README.md +91 -6
  2. package/dist/auto-update-runner.d.ts +52 -0
  3. package/dist/auto-update-runner.d.ts.map +1 -0
  4. package/dist/auto-update-runner.js +307 -0
  5. package/dist/auto-update-runner.js.map +1 -0
  6. package/dist/auto-update-setup.d.ts +38 -0
  7. package/dist/auto-update-setup.d.ts.map +1 -0
  8. package/dist/auto-update-setup.js +72 -0
  9. package/dist/auto-update-setup.js.map +1 -0
  10. package/dist/auto-update.d.ts +170 -0
  11. package/dist/auto-update.d.ts.map +1 -0
  12. package/dist/auto-update.js +401 -0
  13. package/dist/auto-update.js.map +1 -0
  14. package/dist/cli-main.js +22 -2
  15. package/dist/cli-main.js.map +1 -1
  16. package/dist/config.d.ts +2 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/count-tokens.d.ts +38 -3
  20. package/dist/count-tokens.d.ts.map +1 -1
  21. package/dist/count-tokens.js +88 -19
  22. package/dist/count-tokens.js.map +1 -1
  23. package/dist/memtree-finder.d.ts +11 -0
  24. package/dist/memtree-finder.d.ts.map +1 -1
  25. package/dist/memtree-finder.js +18 -0
  26. package/dist/memtree-finder.js.map +1 -1
  27. package/dist/memtree-mcp-config.d.ts.map +1 -1
  28. package/dist/memtree-mcp-config.js +4 -2
  29. package/dist/memtree-mcp-config.js.map +1 -1
  30. package/dist/memtree-mcp.d.ts +24 -4
  31. package/dist/memtree-mcp.d.ts.map +1 -1
  32. package/dist/memtree-mcp.js +47 -14
  33. package/dist/memtree-mcp.js.map +1 -1
  34. package/dist/memtree-tools.d.ts +17 -1
  35. package/dist/memtree-tools.d.ts.map +1 -1
  36. package/dist/memtree-tools.js +50 -2
  37. package/dist/memtree-tools.js.map +1 -1
  38. package/dist/memtree.d.ts +10 -1
  39. package/dist/memtree.d.ts.map +1 -1
  40. package/dist/memtree.js +54 -19
  41. package/dist/memtree.js.map +1 -1
  42. package/dist/notices.d.ts +6 -1
  43. package/dist/notices.d.ts.map +1 -1
  44. package/dist/notices.js +13 -1
  45. package/dist/notices.js.map +1 -1
  46. package/dist/proxy.d.ts +17 -1
  47. package/dist/proxy.d.ts.map +1 -1
  48. package/dist/proxy.js +130 -47
  49. package/dist/proxy.js.map +1 -1
  50. package/dist/reqlog.d.ts +10 -0
  51. package/dist/reqlog.d.ts.map +1 -1
  52. package/dist/reqlog.js.map +1 -1
  53. package/dist/request-sizing.d.ts +21 -2
  54. package/dist/request-sizing.d.ts.map +1 -1
  55. package/dist/request-sizing.js +60 -7
  56. package/dist/request-sizing.js.map +1 -1
  57. package/dist/token-estimate.d.ts +35 -0
  58. package/dist/token-estimate.d.ts.map +1 -0
  59. package/dist/token-estimate.js +97 -0
  60. package/dist/token-estimate.js.map +1 -0
  61. package/dist/update-check.d.ts +4 -3
  62. package/dist/update-check.d.ts.map +1 -1
  63. package/dist/update-check.js +4 -3
  64. package/dist/update-check.js.map +1 -1
  65. package/dist/update-lock.d.ts +68 -0
  66. package/dist/update-lock.d.ts.map +1 -0
  67. package/dist/update-lock.js +176 -0
  68. package/dist/update-lock.js.map +1 -0
  69. package/package.json +1 -1
package/dist/proxy.d.ts CHANGED
@@ -47,7 +47,7 @@ import type { StablePrefixStore } from "./prefix-store.js";
47
47
  import type { NewestTreeLookup } from "./memtree-newest.js";
48
48
  import type { TranscriptUsageSource } from "./transcript-usage.js";
49
49
  import type { ProjectMeta } from "./project-meta.js";
50
- import { type RequestLogSink } from "./reqlog.js";
50
+ import { type MessagesRecord, type RequestLogSink } from "./reqlog.js";
51
51
  export interface ProxyOptions {
52
52
  memtree: MemtreeClient;
53
53
  /**
@@ -171,6 +171,12 @@ export interface ProxyOptions {
171
171
  * user can find sessions by project. Omitted means none is sent.
172
172
  */
173
173
  projectMeta?: ProjectMeta;
174
+ /**
175
+ * An extra line for a main session's SessionStart (not compaction), e.g.
176
+ * the auto-update result (auto-update.ts). Called on every such hook; the
177
+ * provider decides how often it has something to say.
178
+ */
179
+ sessionStartNotice?: () => string | undefined;
174
180
  /** Test-only: forward to this origin instead of api.anthropic.com. */
175
181
  upstreamOrigin?: string;
176
182
  /** Test-only: dump each forwarded /v1/messages body to this directory. */
@@ -212,7 +218,17 @@ export declare const SUBAGENT_VIEW_NOTE = "(main session's page; subagent pages
212
218
  export declare const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main session only.";
213
219
  /** "50k", "50000", "1.5m" → tokens; undefined when not a positive count. */
214
220
  export declare function parseTokenCount(text: string): number | undefined;
221
+ /**
222
+ * Whether a forwarded compressed body still overflows the window: its exact
223
+ * count (`exactTokens`, made for delivery), else its reported input tokens,
224
+ * both Anthropic's own count, so the output floor applies; else its
225
+ * conservative image-aware bound (token-estimate.ts) with full max_tokens,
226
+ * never bytes/4, which reads base64 images as text.
227
+ */
228
+ declare function compressedResultExceedsWindow(body: Record<string, any>, rec: MessagesRecord, compressedRaw: Buffer, modelContextLimit: number, exactTokens?: number): boolean;
215
229
  /** Test seam. */
216
230
  export declare const __testCapCacheBreakpoints: typeof capCacheBreakpoints;
217
231
  export declare const __testEstimateRequestTokens: typeof estimateRequestTokens;
232
+ export declare const __testCompressedResultExceedsWindow: typeof compressedResultExceedsWindow;
233
+ export {};
218
234
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
package/dist/proxy.js CHANGED
@@ -59,6 +59,7 @@ import { hyperlink } from "./payment-gate.js";
59
59
  import { PaymentNoticeThrottle, paymentNoticeText } from "./payment-notice.js";
60
60
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
61
61
  import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
62
+ import { conservativeBodyTokens } from "./token-estimate.js";
62
63
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
63
64
  const DEFAULT_UPSTREAM = "https://api.anthropic.com";
64
65
  const HOOK_BODY_LIMIT = 64 * 1024;
@@ -267,6 +268,7 @@ export function startProxy(opts) {
267
268
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
268
269
  unpaidDetail: () => opts.memtree.paymentRequiredDetail,
269
270
  usageUrl: opts.memtree.usageDashboardUrl,
271
+ sessionStartNotice: opts.sessionStartNotice,
270
272
  notices: new NoticeDeliveryQueue(),
271
273
  mainNoticePending: false,
272
274
  mainPromptGeneration: 0,
@@ -657,7 +659,7 @@ onMainStop) {
657
659
  if (parsed.agent_id === undefined && parsed.source === "resume") {
658
660
  await state.newestTrees?.settle(parsed.session_id);
659
661
  }
660
- const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed));
662
+ const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
661
663
  if (!line) {
662
664
  res.writeHead(204);
663
665
  res.end();
@@ -950,10 +952,11 @@ async function handleMessages(req, res, opts, upstream, state) {
950
952
  upstream, headers: forwardableRequestHeaders(req), requestUrl: req.url,
951
953
  signal: state.shutdownSignal,
952
954
  onAttempt: (result) => {
953
- (rec.countTokens ??= []).push({ statusClass: result.outcome, ms: result.ms });
955
+ (rec.countTokens ??= []).push({ statusClass: result.outcome, ms: result.ms,
956
+ bytes: result.bytes ?? 0, timeoutMs: result.timeoutMs ?? 0 });
954
957
  },
955
958
  }, state.countTokensCooldowns);
956
- const requestSizing = new RequestSizing(countSession, opts.countTokens === true);
959
+ const requestSizing = new RequestSizing(countSession, opts.countTokens === true, typeof body.model === "string" ? body.model : undefined);
957
960
  if (carriesServerTools(body))
958
961
  serverToolRecords.add(rec);
959
962
  const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey, body.model);
@@ -1170,7 +1173,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1170
1173
  // prefix, else whole) with no compress call; at it, compress once to
1171
1174
  // the target (planToolCompaction). A "replay" is a client retry of the
1172
1175
  // request that installed the route, forwarded verbatim.
1173
- const plan = planToolCompaction({
1176
+ const plan = await planToolCompaction({
1174
1177
  opts,
1175
1178
  state,
1176
1179
  body,
@@ -1207,11 +1210,15 @@ async function handleMessages(req, res, opts, upstream, state) {
1207
1210
  // A concurrent request of this lane is already compressing.
1208
1211
  rec.routeRecovery = { outcome: "in-flight" };
1209
1212
  }
1210
- else if (prior?.awaitingIndex && !plan.overWindow) {
1213
+ else if (prior?.awaitingIndex && (!plan.overWindow || plan.uncountedOverWindowOnly)) {
1211
1214
  // The last attempt found no tree for this conversation, so a
1212
1215
  // compress call could only pass everything through again. Forward
1213
1216
  // now; the page check lets the next tool turn try once it exists.
1217
+ // Over the window only by its uncounted bound, delivery may still
1218
+ // count it; if it refuses, that is an index wait, not a strike.
1214
1219
  rec.routeRecovery = { outcome: "awaiting-index" };
1220
+ if (plan.uncountedOverWindowOnly)
1221
+ rec.indexBehind = true;
1215
1222
  checkAwaitedIndex(opts, prior, state.shutdownSignal);
1216
1223
  }
1217
1224
  else if (prior?.retryAtTokens !== undefined &&
@@ -1350,7 +1357,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1350
1357
  // Stable-prefix (edge) compaction: the main thread's human turns only. The
1351
1358
  // recap rides the main route above; subagents keep per-turn compression.
1352
1359
  const edge = isMainRequest && sessionId !== undefined
1353
- ? planEdgeCompaction({
1360
+ ? await planEdgeCompaction({
1354
1361
  sizing: requestSizing,
1355
1362
  opts,
1356
1363
  state,
@@ -1877,6 +1884,17 @@ function resumePaymentNotice(state, input) {
1877
1884
  state.paymentNotice.markShown();
1878
1885
  return state.notices.warningText(paymentNoticeText(detail));
1879
1886
  }
1887
+ /** The caller's extra SessionStart line; a throwing provider shows nothing. */
1888
+ function extraSessionStartLine(provider, input) {
1889
+ if (!provider || input.agent_id !== undefined || input.source === "compact")
1890
+ return undefined;
1891
+ try {
1892
+ return provider();
1893
+ }
1894
+ catch {
1895
+ return undefined;
1896
+ }
1897
+ }
1880
1898
  function joinNoticeLines(...parts) {
1881
1899
  const present = parts.filter((part) => !!part);
1882
1900
  return present.length ? present.join("\n") : undefined;
@@ -2442,10 +2460,11 @@ function laneCompaction(opts, state, sessionId, model, modelContextLimit) {
2442
2460
  }
2443
2461
  /** Calibrated input must leave room for the requested output as well. */
2444
2462
  function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
2445
- // A planning estimate, so the full max_tokens (the floor needs an exact count).
2446
- const outputTokens = bodyOutputReservation(body, false);
2447
- return record.sizeSource === "reported" &&
2448
- (record.estimatedTokens ?? 0) + outputTokens > modelContextLimit;
2463
+ // An exact count reserves the model's output floor, as delivery does; an
2464
+ // estimate reserves the full max_tokens.
2465
+ const outputTokens = bodyOutputReservation(body, exactSizedRecords.has(record));
2466
+ return record.uncountedOverWindow === true || (record.sizeSource === "reported" &&
2467
+ (record.estimatedTokens ?? 0) + outputTokens > modelContextLimit);
2449
2468
  }
2450
2469
  function calibratedSizeExceedsLimits(record, body, modelContextLimit) {
2451
2470
  return (record.sizeSource === "reported" &&
@@ -2515,16 +2534,24 @@ function usedServerTools(rec) {
2515
2534
  * is dropped and the previous sample kept.
2516
2535
  */
2517
2536
  function reportedSample(rec, bytes) {
2518
- if (changedDeliveryRecords.has(rec))
2519
- return undefined;
2520
- if (serverToolRecords.has(rec) || usedServerTools(rec))
2521
- return undefined;
2522
- const tokens = reportedInputTokens(rec);
2537
+ const tokens = reportedBodyTokens(rec);
2523
2538
  if (tokens === undefined)
2524
2539
  return undefined;
2525
2540
  const sample = { tokens, forwardedBytes: bytes };
2526
2541
  return plausibleSample(sample) ? sample : undefined;
2527
2542
  }
2543
+ /**
2544
+ * Anthropic's input tokens for the body this request forwarded, when its usage
2545
+ * describes that body (not a changed delivery, no server tools). Not filtered
2546
+ * by plausibility: a body that is mostly images is truly cheap per byte.
2547
+ */
2548
+ function reportedBodyTokens(rec) {
2549
+ if (changedDeliveryRecords.has(rec))
2550
+ return undefined;
2551
+ if (serverToolRecords.has(rec) || usedServerTools(rec))
2552
+ return undefined;
2553
+ return reportedInputTokens(rec);
2554
+ }
2528
2555
  /**
2529
2556
  * Key of a non-main request's size sample (ProxyState.laneSizes): its lane,
2530
2557
  * its own agent id (helpers that carry only a parent id stay apart from the
@@ -2592,7 +2619,7 @@ function boundedSet(map, key, value, limit = STABLE_PREFIX_MAX_SESSIONS) {
2592
2619
  *
2593
2620
  * Fills rec.compaction with the budget, estimate and reason.
2594
2621
  */
2595
- function planEdgeCompaction(args) {
2622
+ async function planEdgeCompaction(args) {
2596
2623
  const { opts, state, body, messages, sessionId, modelContextLimit, forwardBody, rec } = args;
2597
2624
  const mode = compactionMode(opts, state, sessionId);
2598
2625
  const budget = resolveBudget(opts, state, body.model, modelContextLimit);
@@ -2644,12 +2671,16 @@ function planEdgeCompaction(args) {
2644
2671
  }
2645
2672
  const prefixCandidate = args.sizing.register(routed.raw, "ride", prefix.lastSize);
2646
2673
  candidateSizeOwners.set(prefixCandidate, prefix);
2674
+ await countIfOverBound(args.sizing, prefixCandidate, body, modelContextLimit);
2647
2675
  const size = args.sizing.plan(prefixCandidate);
2648
2676
  compaction.estimatedTokens = size.tokens;
2649
2677
  compaction.estimatedBytes = routed.raw.length;
2650
2678
  compaction.sizeSource = size.source;
2651
- const overWindow = routedBodyExceedsContext(body, routed.raw, modelContextLimit) ||
2652
- calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
2679
+ noteUncountedBound(compaction, args.sizing, prefixCandidate, body, modelContextLimit);
2680
+ // Request-local sizing (an exact count, a calibrated sample, or the
2681
+ // image-aware bound), not bytes: a flatten that kept images is mostly
2682
+ // base64 that costs a fixed amount per image.
2683
+ const overWindow = calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
2653
2684
  const fallback = overWindow ? undefined : { prefix, routed };
2654
2685
  if (state.compactNow.has(sessionId))
2655
2686
  return forced("manual", fallback);
@@ -2663,10 +2694,13 @@ function planEdgeCompaction(args) {
2663
2694
  }
2664
2695
  if (state.compactNow.has(sessionId))
2665
2696
  return forced("manual");
2666
- const size = args.sizing.plan(args.sizing.register(forwardBody, "original"));
2697
+ const originalCandidate = args.sizing.register(forwardBody, "original");
2698
+ await countIfOverBound(args.sizing, originalCandidate, body, modelContextLimit);
2699
+ const size = args.sizing.plan(originalCandidate);
2667
2700
  compaction.estimatedTokens = size.tokens;
2668
2701
  compaction.estimatedBytes = forwardBody.length;
2669
2702
  compaction.sizeSource = size.source;
2703
+ noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
2670
2704
  if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
2671
2705
  return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
2672
2706
  }
@@ -2697,7 +2731,7 @@ function planEdgeCompaction(args) {
2697
2731
  *
2698
2732
  * Fills rec.compaction.
2699
2733
  */
2700
- function planToolCompaction(args) {
2734
+ async function planToolCompaction(args) {
2701
2735
  const { opts, state, body, messages, sessionId, modelContextLimit, forwardBody, rec } = args;
2702
2736
  const mode = compactionMode(opts, state, sessionId);
2703
2737
  const budget = resolveBudget(opts, state, body.model, modelContextLimit);
@@ -2714,23 +2748,16 @@ function planToolCompaction(args) {
2714
2748
  const stableLane = args.isMainRequest && sessionId !== undefined;
2715
2749
  const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
2716
2750
  let ride = args.ride;
2717
- let overWindow = false;
2718
2751
  if (!ride && current && !prefixMismatch(body, messages, current, sessionId, stablePrefixMessageHash)) {
2719
2752
  const routed = prefixRoutedBody(body, messages, current);
2720
2753
  if (routed) {
2721
2754
  const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
2722
2755
  candidateSizeOwners.set(prefixCandidate, current);
2723
- if (routedBodyExceedsContext(body, routed.raw, modelContextLimit))
2724
- overWindow = true;
2725
- else
2726
- ride = { raw: routed.raw, turnType: "tool-prefix", sizeHolder: current };
2756
+ // Sized below with the request-local sizing; an over-window ride is not
2757
+ // kept as a fallback.
2758
+ ride = { raw: routed.raw, turnType: "tool-prefix", sizeHolder: current };
2727
2759
  }
2728
2760
  }
2729
- if (!ride) {
2730
- const original = args.sizing.register(forwardBody, "original");
2731
- overWindow ||= args.sizing.plan(original).tokens +
2732
- bodyOutputReservation(body, args.sizing.isExact(original)) > modelContextLimit;
2733
- }
2734
2761
  const sample = ride
2735
2762
  ? ride.sizeHolder.lastSize
2736
2763
  : stableLane
@@ -2739,12 +2766,18 @@ function planToolCompaction(args) {
2739
2766
  const candidate = args.sizing.register(ride?.raw ?? forwardBody, ride ? "ride" : "original", sample);
2740
2767
  if (ride)
2741
2768
  candidateSizeOwners.set(candidate, ride.sizeHolder);
2769
+ await countIfOverBound(args.sizing, candidate, body, modelContextLimit);
2742
2770
  const size = args.sizing.plan(candidate);
2743
2771
  compaction.estimatedTokens = size.tokens;
2744
2772
  compaction.estimatedBytes = (ride?.raw ?? forwardBody).length;
2745
2773
  compaction.sizeSource = size.source;
2746
- overWindow ||= !fitsNative(size.tokens, { budget: budget.tokens, context: modelContextLimit,
2774
+ noteUncountedBound(compaction, args.sizing, candidate, body, modelContextLimit);
2775
+ // Judged after any planning count, so a fitting exact count is not
2776
+ // overridden by the estimate it replaced.
2777
+ let overWindow = !fitsNative(size.tokens, { budget: budget.tokens, context: modelContextLimit,
2747
2778
  output: bodyOutputReservation(body, args.sizing.isExact(candidate)) });
2779
+ const uncountedOnly = !overWindow && compaction.uncountedOverWindow === true;
2780
+ overWindow ||= uncountedOnly;
2748
2781
  if (!args.canCompress) {
2749
2782
  if (overWindow)
2750
2783
  return { kind: "pass", original: true };
@@ -2762,6 +2795,7 @@ function planToolCompaction(args) {
2762
2795
  estimateTokens: size.tokens,
2763
2796
  budgetTokens: budget.tokens,
2764
2797
  overWindow,
2798
+ ...(uncountedOnly ? { uncountedOverWindowOnly: true } : {}),
2765
2799
  ...(ride && !overWindow ? { fallback: ride } : {}),
2766
2800
  ...(stableLane ? { replaces: current ?? null } : {}),
2767
2801
  };
@@ -2806,12 +2840,46 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
2806
2840
  return null;
2807
2841
  return Buffer.from(JSON.stringify(routed), "utf-8");
2808
2842
  }
2809
- /** Size the assembled input (including system/tools) and reserve output room. */
2810
- function routedBodyExceedsContext(body, routedBody, modelContextLimit) {
2811
- const outputTokens = bodyOutputReservation(body, false);
2812
- // This is the existing local byte estimate, not a tokenizer or a hard cap.
2813
- // Final forwarding checks can replace this estimate with Count Tokens.
2814
- return approxTokensFromBytes(routedBody.length) + outputTokens > modelContextLimit;
2843
+ /**
2844
+ * Count the candidate a planner is about to judge (ride or original, sampled
2845
+ * or not) when only a count could admit it: no exact count, and its per-model
2846
+ * conservative bound plus max_tokens exceeds the window. Delivery reuses the
2847
+ * result.
2848
+ */
2849
+ async function countIfOverBound(sizing, candidate, body, modelContextLimit) {
2850
+ await sizing.countIfOverBound(candidate, { budget: modelContextLimit, context: modelContextLimit, output: bodyOutputReservation(body, false) });
2851
+ }
2852
+ /** Compaction records whose estimatedTokens is an exact count (noteUncountedBound). */
2853
+ const exactSizedRecords = new WeakSet();
2854
+ /**
2855
+ * Note an exactly counted candidate (exactSizedRecords), or mark a planned
2856
+ * candidate still without an exact count (counting off, or its
2857
+ * planning count failed: countIfOverBound ran first) whose conservative bound
2858
+ * plus max_tokens exceeds the window: delivery would refuse it uncounted, so
2859
+ * it is over the window for planning and takes the normal gated compression.
2860
+ */
2861
+ function noteUncountedBound(record, sizing, candidate, body, modelContextLimit) {
2862
+ if (sizing.isExact(candidate)) {
2863
+ exactSizedRecords.add(record);
2864
+ return;
2865
+ }
2866
+ if (sizing.exceedsRequestLimit(candidate) ||
2867
+ candidate.conservativeTokens + bodyOutputReservation(body, false) > modelContextLimit) {
2868
+ record.uncountedOverWindow = true;
2869
+ }
2870
+ }
2871
+ /**
2872
+ * Whether a forwarded compressed body still overflows the window: its exact
2873
+ * count (`exactTokens`, made for delivery), else its reported input tokens,
2874
+ * both Anthropic's own count, so the output floor applies; else its
2875
+ * conservative image-aware bound (token-estimate.ts) with full max_tokens,
2876
+ * never bytes/4, which reads base64 images as text.
2877
+ */
2878
+ function compressedResultExceedsWindow(body, rec, compressedRaw, modelContextLimit, exactTokens) {
2879
+ const counted = exactTokens ?? reportedBodyTokens(rec);
2880
+ const tokens = counted ??
2881
+ conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
2882
+ return tokens + bodyOutputReservation(body, counted !== undefined) > modelContextLimit;
2815
2883
  }
2816
2884
  /** Output room a native-fit check reserves: the model's floor for an exact
2817
2885
  * count, else (an estimate) the full max_tokens. */
@@ -2998,12 +3066,18 @@ function withFlattenCacheBreakpoint(body, ttl) {
2998
3066
  if (messages.length !== 1)
2999
3067
  return messages;
3000
3068
  const only = messages[0];
3001
- const text = typeof only.content === "string" ? only.content : undefined;
3002
- if (text === undefined)
3003
- return messages;
3004
3069
  const cacheControl = { type: "ephemeral" };
3005
3070
  if (ttl !== undefined)
3006
3071
  cacheControl.ttl = ttl;
3072
+ if (Array.isArray(only.content) && only.content.length > 0) {
3073
+ // A flatten that kept images: the breakpoint goes on its last block.
3074
+ const blocks = only.content.map((block) => ({ ...block }));
3075
+ blocks[blocks.length - 1].cache_control = cacheControl;
3076
+ return [{ ...only, content: blocks }];
3077
+ }
3078
+ const text = typeof only.content === "string" ? only.content : undefined;
3079
+ if (text === undefined)
3080
+ return messages;
3007
3081
  return [{ ...only, content: [{ type: "text", text, cache_control: cacheControl }] }];
3008
3082
  }
3009
3083
  /**
@@ -3147,7 +3221,9 @@ async function runBlockingCompression(args) {
3147
3221
  // whatever the fallback gate would refuse to send uncompressed; a bytes/4
3148
3222
  // guess is not sent, as it would force needless compression.
3149
3223
  ...(args.inputCandidate && args.sizing.clientInputTokens(args.inputCandidate) !== undefined
3150
- ? { clientInputTokens: args.sizing.clientInputTokens(args.inputCandidate) }
3224
+ ? { clientInputTokens: args.sizing.clientInputTokens(args.inputCandidate),
3225
+ // Only an exact count of these bytes may loosen the server's sizing.
3226
+ clientInputTokensExact: args.sizing.isExact(args.inputCandidate) }
3151
3227
  : {}),
3152
3228
  ...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
3153
3229
  ...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
@@ -3592,8 +3668,11 @@ async function recoverToolRouteMiss(args) {
3592
3668
  // Nothing to ride next turn, or a result that still does not fit the
3593
3669
  // window (the next turn would reject it and compress again): wait for
3594
3670
  // growth before the lane tries again.
3595
- if (!sizeHolder || routedBodyExceedsContext(body, compressedRaw, modelContextLimit))
3671
+ const exactTokens = args.sizing.exactTokensFor(compressedRaw);
3672
+ if (!sizeHolder ||
3673
+ compressedResultExceedsWindow(body, rec, compressedRaw, modelContextLimit, exactTokens)) {
3596
3674
  backOff();
3675
+ }
3597
3676
  // A reservation that lost its race (stale) or never reached
3598
3677
  // protocol-complete (upstream 5xx, truncated stream) installed nothing, so
3599
3678
  // it must stop suppressing whoever is still trying to install.
@@ -3766,13 +3845,16 @@ async function selectForwardCandidate(args) {
3766
3845
  return changed ? candidate.body : intended;
3767
3846
  }
3768
3847
  /**
3769
- * Whether delivery would refuse `candidate` sent whole: it is planned over the
3770
- * window, and the measurement selection would use (an exact count, else the
3771
- * same estimate or bytes/4 fallback) does not fit either. Only then is there
3772
- * no fallback, and only then may a compress take the long budget.
3848
+ * Whether delivery would refuse `candidate` sent whole, by delivery's own
3849
+ * admission: it is sendable only when its plan fits and either an exact count
3850
+ * or its conservative per-model bound (with the full max_tokens) fits too;
3851
+ * otherwise the measurement delivery would use (a count, else that bound)
3852
+ * decides. Only when it is refused is there no fallback, and only then may a
3853
+ * compress take the long budget.
3773
3854
  */
3774
3855
  async function wholeRequestRefused(sizing, candidate, limits) {
3775
- if (fitsNative(sizing.plan(candidate).tokens, limits))
3856
+ if (fitsNative(sizing.plan(candidate).tokens, limits) &&
3857
+ (sizing.isExact(candidate) || sizing.boundFits(candidate, limits)))
3776
3858
  return false;
3777
3859
  return !fitsMeasured(await sizing.measure(candidate, limits), limits);
3778
3860
  }
@@ -4529,6 +4611,7 @@ function readAll(stream) {
4529
4611
  /** Test seam. */
4530
4612
  export const __testCapCacheBreakpoints = capCacheBreakpoints;
4531
4613
  export const __testEstimateRequestTokens = estimateRequestTokens;
4614
+ export const __testCompressedResultExceedsWindow = compressedResultExceedsWindow;
4532
4615
  /** Reads message times from the session's transcript, when there is one. */
4533
4616
  function transcriptTimesFor(opts, sessionId, agentId) {
4534
4617
  const source = opts.transcriptUsage;