claude-code-infinite 1.0.34 → 1.0.36

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 (82) hide show
  1. package/README.md +56 -0
  2. package/dist/body-shape.d.ts +49 -0
  3. package/dist/body-shape.d.ts.map +1 -0
  4. package/dist/body-shape.js +232 -0
  5. package/dist/body-shape.js.map +1 -0
  6. package/dist/cli-main.js +19 -2
  7. package/dist/cli-main.js.map +1 -1
  8. package/dist/cli.js +23 -10
  9. package/dist/cli.js.map +1 -1
  10. package/dist/count-tokens.d.ts +15 -2
  11. package/dist/count-tokens.d.ts.map +1 -1
  12. package/dist/count-tokens.js +13 -4
  13. package/dist/count-tokens.js.map +1 -1
  14. package/dist/financial-redaction.d.ts +12 -0
  15. package/dist/financial-redaction.d.ts.map +1 -0
  16. package/dist/financial-redaction.js +509 -0
  17. package/dist/financial-redaction.js.map +1 -0
  18. package/dist/hooks.d.ts +2 -0
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +4 -0
  21. package/dist/hooks.js.map +1 -1
  22. package/dist/image-tokens.d.ts +6 -1
  23. package/dist/image-tokens.d.ts.map +1 -1
  24. package/dist/image-tokens.js +75 -14
  25. package/dist/image-tokens.js.map +1 -1
  26. package/dist/lane-activity.d.ts +42 -0
  27. package/dist/lane-activity.d.ts.map +1 -0
  28. package/dist/lane-activity.js +79 -0
  29. package/dist/lane-activity.js.map +1 -0
  30. package/dist/memtree-newest.d.ts +11 -3
  31. package/dist/memtree-newest.d.ts.map +1 -1
  32. package/dist/memtree-newest.js +24 -4
  33. package/dist/memtree-newest.js.map +1 -1
  34. package/dist/memtree.d.ts +10 -0
  35. package/dist/memtree.d.ts.map +1 -1
  36. package/dist/memtree.js +14 -0
  37. package/dist/memtree.js.map +1 -1
  38. package/dist/proxy.d.ts +21 -0
  39. package/dist/proxy.d.ts.map +1 -1
  40. package/dist/proxy.js +153 -12
  41. package/dist/proxy.js.map +1 -1
  42. package/dist/refusal-reason.d.ts +70 -0
  43. package/dist/refusal-reason.d.ts.map +1 -0
  44. package/dist/refusal-reason.js +132 -0
  45. package/dist/refusal-reason.js.map +1 -0
  46. package/dist/reqlog.d.ts +25 -2
  47. package/dist/reqlog.d.ts.map +1 -1
  48. package/dist/reqlog.js +1 -1
  49. package/dist/reqlog.js.map +1 -1
  50. package/dist/request-sizing.d.ts +38 -2
  51. package/dist/request-sizing.d.ts.map +1 -1
  52. package/dist/request-sizing.js +72 -6
  53. package/dist/request-sizing.js.map +1 -1
  54. package/dist/route-fallback.d.ts +2 -0
  55. package/dist/route-fallback.d.ts.map +1 -1
  56. package/dist/route-fallback.js +8 -8
  57. package/dist/route-fallback.js.map +1 -1
  58. package/dist/secret-redaction.d.ts +25 -0
  59. package/dist/secret-redaction.d.ts.map +1 -0
  60. package/dist/secret-redaction.js +492 -0
  61. package/dist/secret-redaction.js.map +1 -0
  62. package/dist/slow-upload.d.ts +40 -0
  63. package/dist/slow-upload.d.ts.map +1 -0
  64. package/dist/slow-upload.js +69 -0
  65. package/dist/slow-upload.js.map +1 -0
  66. package/dist/status-link.d.ts +18 -0
  67. package/dist/status-link.d.ts.map +1 -0
  68. package/dist/status-link.js +103 -0
  69. package/dist/status-link.js.map +1 -0
  70. package/dist/statusline-auto.d.ts +15 -0
  71. package/dist/statusline-auto.d.ts.map +1 -0
  72. package/dist/statusline-auto.js +125 -0
  73. package/dist/statusline-auto.js.map +1 -0
  74. package/dist/statusline.d.ts +51 -0
  75. package/dist/statusline.d.ts.map +1 -0
  76. package/dist/statusline.js +380 -0
  77. package/dist/statusline.js.map +1 -0
  78. package/dist/utf8-bytes.d.ts +10 -0
  79. package/dist/utf8-bytes.d.ts.map +1 -0
  80. package/dist/utf8-bytes.js +107 -0
  81. package/dist/utf8-bytes.js.map +1 -0
  82. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -42,6 +42,9 @@ import { UNKNOWN_INDEX_WAIT_SECONDS } from "./index-wait-413.js";
42
42
  import { capCacheBreakpoints } from "./route-cache.js";
43
43
  import { PromptAccounting } from "./prompt-accounting.js";
44
44
  import { INDEX_WAIT_RETRY_AFTER_SECONDS, RefusalStreaks, RouteFallbackFailures, pausedRefusal, unreachableRefusal } from "./route-fallback.js";
45
+ import { refusalCause, refusalMessage } from "./refusal-reason.js";
46
+ import { AGENT_RESULT_WAITING_LINE, LaneActivity, heavyRequestNoticeText } from "./lane-activity.js";
47
+ import { SlowUploads } from "./slow-upload.js";
45
48
  import { freshFailure, networkFailureOf, unreachableMessage, } from "./network-failure.js";
46
49
  import { createCountTokensSession, CountTokensCooldowns, plausibleSample, } from "./count-tokens.js";
47
50
  import { RequestSizing, estimateRequestTokens, fitsMeasured, fitsNative, outputReservation } from "./request-sizing.js";
@@ -55,7 +58,7 @@ import { CLAUDE_PID_HEADER, HOST_TOKEN_HEADER } from "./host-session.js";
55
58
  import { brotliDecompressSync, createBrotliDecompress, createGunzip, createInflate, gunzipSync, inflateSync, } from "node:zlib";
56
59
  import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
57
60
  import { join } from "node:path";
58
- import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, losesMedia, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
61
+ import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, losesMedia, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, memoryReadySeconds, unindexedTokenCount, } from "./memtree.js";
59
62
  import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
60
63
  import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, SLOW_RETRY_NOTICE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
61
64
  import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH } from "./hooks.js";
@@ -71,6 +74,7 @@ import { isSubagentCall, postToolUseOutput, SUBAGENT_RESULT_HINT, subagentMemory
71
74
  import { PROJECT_TREES_HOOK_SUFFIX, RESUME_GUIDANCE_HOOK_SUFFIX, SUBAGENT_RESULT_HOOK_SUFFIX, } from "./hooks.js";
72
75
  import { conservativeBodyTokens } from "./token-estimate.js";
73
76
  import { bodyImages } from "./image-tokens.js";
77
+ import { bodyShape, prefixDigest } from "./body-shape.js";
74
78
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
75
79
  import { rateLimitFromResponse } from "./rate-limit.js";
76
80
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
@@ -229,6 +233,12 @@ function noteIndexLag(opts, rec, result) {
229
233
  if (!indexLagEvidence(result))
230
234
  return false;
231
235
  rec.indexBehind = true;
236
+ const etaSeconds = memoryReadySeconds(result);
237
+ const unindexedTokens = unindexedTokenCount(result);
238
+ if (etaSeconds !== undefined || unindexedTokens !== undefined) {
239
+ rec.indexLag = { ...(etaSeconds !== undefined ? { etaSeconds } : {}),
240
+ ...(unindexedTokens !== undefined ? { unindexedTokens } : {}) };
241
+ }
232
242
  opts.memtree.expireCompressResult?.(result, opts.indexWaitRefreshMs ?? INDEX_WAIT_RETRY_AFTER_SECONDS * 1000);
233
243
  return true;
234
244
  }
@@ -370,9 +380,11 @@ export function startProxy(opts) {
370
380
  mainNoticePending: false,
371
381
  mainPromptGeneration: 0,
372
382
  promptAccounting: new PromptAccounting(),
383
+ slowUploads: new SlowUploads(undefined, opts.slowUploadMinBytes),
373
384
  fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs, opts.indexWaitMaxMs),
374
385
  refusalStreaks: new RefusalStreaks(),
375
386
  activeSubagents: new Set(),
387
+ laneActivity: new LaneActivity(),
376
388
  deferredAgentIndexes: new Map(),
377
389
  adoptingPrepared: new Map(),
378
390
  memoryRoutes: new Map(),
@@ -395,6 +407,8 @@ export function startProxy(opts) {
395
407
  storageOwner: typeof opts.memtree.storageIdentity === "string"
396
408
  ? opts.memtree.storageIdentity : undefined,
397
409
  newestTrees: opts.newestTrees,
410
+ statusLinks: opts.statusLinks,
411
+ statusLinkRefresh: opts.statusLinkRefresh === true,
398
412
  memtreeLinkPlacement: opts.memtreeLinkPlacement ?? "turn",
399
413
  compactTargets: new Map(),
400
414
  defaultCompactOff: opts.defaultCompactTarget === null,
@@ -410,6 +424,8 @@ export function startProxy(opts) {
410
424
  laneSizes: new Map(),
411
425
  };
412
426
  installMemtreeLink(state, state.memtreeLinkPlacement);
427
+ // A newer tree from the background lookup reaches the status line without a turn ending.
428
+ opts.newestTrees?.onTree((sessionId) => publishStatusLink(state, sessionId));
413
429
  state.notices.setFirstTreeIntro(opts.firstTreeIntro ?? null);
414
430
  const server = http.createServer((req, res) => {
415
431
  const accepted = { req, res };
@@ -1243,6 +1259,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1243
1259
  finally {
1244
1260
  if (rec.totalMs === undefined)
1245
1261
  rec.totalMs = Date.now() - received;
1262
+ noteForwardedSize(state, rec, requestSessionId(req));
1246
1263
  opts.reqlog?.log(rec);
1247
1264
  }
1248
1265
  };
@@ -1354,6 +1371,11 @@ async function handleMessages(req, res, opts, upstream, state) {
1354
1371
  return link ? recapLinkText(link.link, streamedTextChars, link.note) : undefined;
1355
1372
  });
1356
1373
  }
1374
+ else if (state.statusLinks && state.statusLinkRefresh) {
1375
+ // Keeps the status line's link fresh through long tool runs: answers from the cache,
1376
+ // refreshing in the background at most once a minute (NEWEST_TREE_TTL_MS).
1377
+ state.newestTrees?.peek(requestSessionId(req));
1378
+ }
1357
1379
  const isLocalBashCommand = isLocalBashCommandTurn(messages);
1358
1380
  // CC 2.1.207 identifies agent API calls explicitly. Use that wire-level
1359
1381
  // attribution before lifecycle-hook state so an agent request cannot claim
@@ -1413,6 +1435,7 @@ async function handleMessages(req, res, opts, upstream, state) {
1413
1435
  }
1414
1436
  }
1415
1437
  rec.routeLane = requestRouteLane;
1438
+ noteLaneActivity(state, rec, requestSessionId(req));
1416
1439
  // Stored by the server on the usage row (client_meta): which Claude Code,
1417
1440
  // lane, agent and model produced this request.
1418
1441
  const clientMeta = memtreeClientMeta({
@@ -2773,10 +2796,11 @@ function noteMemtreePage(state, seq, sessionId, result) {
2773
2796
  if (latest && latest.seq >= seq)
2774
2797
  return;
2775
2798
  const compressed = didMemtreeCompress(result);
2799
+ // A cached newest tree may predate this page; ask again (first, so the status line's
2800
+ // link is this page until the lookup answers).
2801
+ state.newestTrees?.invalidate(sessionId);
2776
2802
  rememberMemtreePage(state, sessionId, { sessionId, url, index, compressed, seq });
2777
2803
  state.memtreeLinkStore?.put(sessionId, { url, index, compressed });
2778
- // A cached newest tree may predate this page; ask again.
2779
- state.newestTrees?.invalidate(sessionId);
2780
2804
  }
2781
2805
  /**
2782
2806
  * Record a compress answer's page as the source of the memory now forwarded
@@ -2795,6 +2819,13 @@ function noteMemorySource(state, seq, sessionId, result) {
2795
2819
  }
2796
2820
  function rememberMemtreePage(state, sessionId, page) {
2797
2821
  rememberPage(state.memtreePages, sessionId, page);
2822
+ publishStatusLink(state, sessionId);
2823
+ }
2824
+ /** Write the session's current link (as the trailers would show it) for the status line. */
2825
+ function publishStatusLink(state, sessionId) {
2826
+ if (!state.statusLinks)
2827
+ return;
2828
+ state.statusLinks.write(sessionId, currentSessionLink(state, sessionId));
2798
2829
  }
2799
2830
  /** Bound optional page state independently of how many sessions use the proxy. */
2800
2831
  function rememberPage(pages, sessionId, page) {
@@ -3300,8 +3331,16 @@ function reportedSample(rec, sent) {
3300
3331
  }
3301
3332
  /** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
3302
3333
  function sizeSampleOf(sent, tokens, model) {
3303
- const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
3304
- return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
3334
+ const id = typeof model === "string" ? model : undefined;
3335
+ const { imageBytes, imageTokens } = bodyImages(sent, id);
3336
+ return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens, ...sampleLineage(sent, id) };
3337
+ }
3338
+ /** The sampled body's lineage (body-shape.ts) for anchoredBound; none for a small body. */
3339
+ function sampleLineage(sent, model) {
3340
+ const shape = bodyShape(sent);
3341
+ const digest = shape && prefixDigest(shape, shape.messages.length);
3342
+ return shape && digest && model
3343
+ ? { model, messageCount: shape.messages.length, prefixDigest: digest } : {};
3305
3344
  }
3306
3345
  /**
3307
3346
  * Anthropic's input tokens for the body this request forwarded, when its usage
@@ -3966,7 +4005,7 @@ function carryFor(args) {
3966
4005
  * one beside it: 2026-10-30, eleven 26 MB compressions in ten minutes, each billed, none
3967
4006
  * used. False when the session already has a preparation in flight (it serves instead).
3968
4007
  */
3969
- function carryOverCompression(state, carry, pending) {
4008
+ function carryOverCompression(state, carry, pending, bytes = 0) {
3970
4009
  const { sessionId, base } = carry;
3971
4010
  // A prefix installed since the call started (another turn's) supersedes it already.
3972
4011
  if (state.backgroundSignal.aborted || state.preparing.has(sessionId) ||
@@ -3979,6 +4018,9 @@ function carryOverCompression(state, carry, pending) {
3979
4018
  // refusal or a slow server's timeout must not arm it, as runBlockingCompression).
3980
4019
  if (result)
3981
4020
  noteMemtreeHealth(state, { result, liveHealth: "success" });
4021
+ // Its upload got through: a slow-upload episode is over (Codex r4 P2).
4022
+ if (result)
4023
+ state.slowUploads.settled(sessionId, bytes, "ok");
3982
4024
  return result ? preparedFromResult(carry, result) : undefined;
3983
4025
  })
3984
4026
  .catch(() => undefined)
@@ -3995,6 +4037,14 @@ function carryOverCompression(state, carry, pending) {
3995
4037
  /** How long a carried compaction may run: the long budget, or its upload's if longer. */
3996
4038
  /** How much longer than its turn's wait a carried call must be allowed to run. */
3997
4039
  const CARRY_MIN_EXTRA_MS = 5_000;
4040
+ /**
4041
+ * The long compress budget for an upload of `bytes`: the long budget or the upload's
4042
+ * scaled one if longer, raised after the session's last large upload timed out.
4043
+ */
4044
+ function longCompressBudgetFor(opts, state, sessionId, bytes) {
4045
+ const longMs = Math.max(opts.memtree.longCompressBudgetMs, opts.memtree.compressBudgetForBytes(bytes));
4046
+ return state.slowUploads.budgetMs(sessionId, bytes, longMs);
4047
+ }
3998
4048
  function carriedBudgetMs(opts, scaledMs) {
3999
4049
  return Math.min(MAX_LONG_COMPRESS_WAIT_MS, Math.max(opts.memtree.longCompressBudgetMs, scaledMs));
4000
4050
  }
@@ -4075,6 +4125,8 @@ const exactSizedRecords = new WeakSet();
4075
4125
  * it is over the window for planning and takes the normal gated compression.
4076
4126
  */
4077
4127
  function noteUncountedBound(record, sizing, candidate, body, modelContextLimit) {
4128
+ if (candidate.anchored)
4129
+ record.anchoredBound = true;
4078
4130
  if (sizing.isExact(candidate)) {
4079
4131
  exactSizedRecords.add(record);
4080
4132
  return;
@@ -4567,7 +4619,11 @@ async function runBlockingCompressionOnce(args) {
4567
4619
  // The upload is about the request's size: the budget grows with it (the long one
4568
4620
  // too, never below the short one's).
4569
4621
  const scaledMs = opts.memtree.compressBudgetForBytes(rec.requestBytes);
4570
- const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? Math.max(opts.memtree.longCompressBudgetMs, scaledMs) : scaledMs, args.capMs ?? Infinity);
4622
+ // A session whose last large upload ran out of the long budget gets more (slow-upload.ts).
4623
+ const extendedBudget = args.longBudget === true &&
4624
+ state.slowUploads.extended(args.sessionId, rec.requestBytes);
4625
+ const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? longCompressBudgetFor(opts, state, args.sessionId, rec.requestBytes)
4626
+ : scaledMs, args.capMs ?? Infinity);
4571
4627
  // Carried only when running on gives it meaningfully more time than this wait.
4572
4628
  const carry = args.carry && args.capMs === undefined &&
4573
4629
  carriedBudgetMs(opts, scaledMs) >= budgetMs + CARRY_MIN_EXTRA_MS ? args.carry : undefined;
@@ -4623,7 +4679,7 @@ async function runBlockingCompressionOnce(args) {
4623
4679
  const compressMs = Date.now() - compressStarted;
4624
4680
  // Given up on but still running: the session's next turn waits for it, not a new call.
4625
4681
  const carried = result === null && !settled && carry !== undefined &&
4626
- carryOverCompression(state, carry, call);
4682
+ carryOverCompression(state, carry, call, rec.requestBytes);
4627
4683
  noteServerBudget(state, body.model, modelContextLimit, result);
4628
4684
  // Sampled at this call's settle, before a concurrent same-request failure of
4629
4685
  // another class can overwrite the complete-key entry.
@@ -4650,10 +4706,21 @@ async function runBlockingCompressionOnce(args) {
4650
4706
  const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
4651
4707
  // A carried call has not failed: it is still running (its answer reports its health).
4652
4708
  const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered && !carried;
4709
+ // Only a whole long budget spent is the upload's slowness (a hold's cap is not).
4710
+ // A large upload's live answer (any budget, not a cache hit) ends a slow-upload episode.
4711
+ if (result !== null && !cached)
4712
+ state.slowUploads.settled(args.sessionId, rec.requestBytes, "ok");
4713
+ const uploadTimeout = result === null && args.longBudget === true &&
4714
+ args.capMs === undefined && !carried &&
4715
+ state.slowUploads.settled(args.sessionId, rec.requestBytes, timedOut ? "timeout" : "other");
4653
4716
  rec.compress = {
4654
4717
  ms: compressMs,
4655
4718
  ok: result !== null,
4656
4719
  timedOut,
4720
+ budgetMs,
4721
+ ...(extendedBudget ? { extendedBudget: true } : {}),
4722
+ ...(uploadTimeout ? { uploadTimeout: true } : {}),
4723
+ ...(failureStatus !== undefined ? { status: failureStatus } : {}),
4657
4724
  ...(carried ? { carriedOver: true } : {}),
4658
4725
  // A joined call's indexing 413 is MemTree's refusal of this request too.
4659
4726
  ...((!cached && failureStatus === OVER_CONTEXT_STATUS) || indexWaitSeconds !== undefined
@@ -5175,7 +5242,23 @@ async function recoverToolRouteMiss(args) {
5175
5242
  function recordTurn(rec, turnType, forwardBody) {
5176
5243
  rec.turnType = turnType;
5177
5244
  rec.forwardedBytes = forwardBody.length;
5178
- rec.approxInputTokens = approxTokensFromBytes(forwardBody.length);
5245
+ rec.approxInputTokens = approxBodyTokens(forwardBody, rec.model);
5246
+ forwardedImageBytes.set(rec, bodyImages(forwardBody, typeof rec.model === "string" ? rec.model : undefined).imageBytes);
5247
+ }
5248
+ /** The forwarded body's inline image bytes (recordTurn), for the heavy-request notice. */
5249
+ const forwardedImageBytes = new WeakMap();
5250
+ /** What this request uploaded to Anthropic, for the session's heavy-request notice. */
5251
+ function noteForwardedSize(state, rec, sessionId) {
5252
+ const bytes = rec.forwardedBytes ?? 0;
5253
+ if (bytes > 0) {
5254
+ state.laneActivity.sent(sessionId, { bytes, imageBytes: forwardedImageBytes.get(rec) ?? 0 });
5255
+ }
5256
+ }
5257
+ /** A body's text at bytes/4 plus its inline images at their visual tokens (log only):
5258
+ * bytes/4 alone logged a 22 MB contact-sheet lane at 5.5M tokens (Anthropic: 491k). */
5259
+ function approxBodyTokens(body, model) {
5260
+ const images = bodyImages(body, typeof model === "string" ? model : undefined);
5261
+ return approxTokensFromBytes(body.length - images.imageBytes) + images.imageTokens;
5179
5262
  }
5180
5263
  function headerText(headers, name) {
5181
5264
  const value = headers[name];
@@ -5617,6 +5700,30 @@ async function wholeRequestRefused(sizing, candidate, limits) {
5617
5700
  return false;
5618
5701
  return !fitsMeasured(await sizing.measure(candidate, limits), limits);
5619
5702
  }
5703
+ /** What explains refusing this request (refusal-reason.ts), from its record and the lane. */
5704
+ function refusalCauseFor(args, inputTokens) {
5705
+ const { state, rec } = args;
5706
+ const sessionId = requestSessionId(args.req);
5707
+ const bytes = rec.requestBytes;
5708
+ // After a slow upload: what the session's next long-budget try gets.
5709
+ const nextCompressBudgetMs = rec.compress?.uploadTimeout
5710
+ ? Math.min(MAX_LONG_COMPRESS_WAIT_MS, longCompressBudgetFor(args.opts, state, sessionId, bytes))
5711
+ : undefined;
5712
+ // This request's own 413, then its own answer's ETA, then the lane's last stated time.
5713
+ const known = (seconds) => seconds === UNKNOWN_INDEX_WAIT_SECONDS ? undefined : seconds;
5714
+ return refusalCause(rec, {
5715
+ inputTokens, modelContextLimit: args.modelContextLimit,
5716
+ // The 60 s stand-in for an indexing 413 that stated no time is no estimate.
5717
+ indexSecondsLeft: known(activeIndexWaits.get(rec)) ?? rec.indexLag?.etaSeconds ??
5718
+ known(state.fallbackFailures.indexSecondsLeft(args.routeKey)),
5719
+ nextCompressBudgetMs,
5720
+ });
5721
+ }
5722
+ /** The session's MemTree page (refusal line, status line), as the trailers would show it. */
5723
+ function currentSessionLink(state, sessionId) {
5724
+ return sessionId === undefined ? undefined
5725
+ : sessionLink(state, sessionId, currentMemtreePage(state, sessionId))?.link;
5726
+ }
5620
5727
  /** Write the bounded fallback refusal (route-fallback.ts); always false. */
5621
5728
  function refuseWholeRequest(args, inputTokens, budgetTokens) {
5622
5729
  // A refused key or a paused one: a retry cannot fix either, so it is final at once.
@@ -5624,11 +5731,14 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
5624
5731
  const unpaid = keyInvalid ? null : args.state.pausedDetail();
5625
5732
  // MemTree can't be reached: nothing says the request can't fit, so no strike (2026-11-09).
5626
5733
  const offline = keyInvalid || unpaid !== null ? undefined : unreachableNow(args.state, args.rec);
5734
+ const cause = keyInvalid || unpaid !== null || offline ? undefined
5735
+ : refusalCauseFor(args, inputTokens);
5627
5736
  const failure = keyInvalid ? pausedRefusal(keyInvalidRefusalMessage(args.state.keyPageUrl))
5628
5737
  : unpaid !== null ? pausedRefusal(pausedRefusalMessage(unpaid))
5629
5738
  : offline ? unreachableRefusal(offline.message)
5630
5739
  : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true,
5631
- indexSecondsLeft: activeIndexWaits.get(args.rec) });
5740
+ indexSecondsLeft: activeIndexWaits.get(args.rec),
5741
+ refusalMessage: refusalMessage(cause, currentSessionLink(args.state, requestSessionId(args.req)), agentResultNote(args)) });
5632
5742
  args.rec.forwardedBytes = 0;
5633
5743
  args.rec.approxInputTokens = 0;
5634
5744
  // Nothing went out uncompressed: Claude Code retries it (2026-10-28).
@@ -5655,6 +5765,7 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
5655
5765
  budgetTokens, modelContextLimit: args.modelContextLimit, ...(unpaid !== null ? { paused: true } : {}),
5656
5766
  ...(keyInvalid ? { keyInvalid: true } : {}),
5657
5767
  ...(offline ? { unreachable: offline.what } : {}),
5768
+ ...(cause && failure.status === 400 ? { cause: cause.kind } : {}),
5658
5769
  };
5659
5770
  if (unpaid !== null || keyInvalid)
5660
5771
  cancelPausedNotice(args.state, args.rec);
@@ -5670,6 +5781,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
5670
5781
  args.state.keyNotice.markShown();
5671
5782
  return false;
5672
5783
  }
5784
+ /**
5785
+ * A main-lane refusal while an agent lane of this session was recently active: a
5786
+ * background agent's finished result reaches the main session only as part of its
5787
+ * next turn, so say it may be waiting (2026-11-13: a refused main lane held one back).
5788
+ */
5789
+ function agentResultNote(args) {
5790
+ // This session's own agent traffic only: activeSubagents is process-wide (review r1 P3).
5791
+ return args.rec.routeLane === "main" &&
5792
+ args.state.laneActivity.agentRecent(requestSessionId(args.req))
5793
+ ? AGENT_RESULT_WAITING_LINE : undefined;
5794
+ }
5673
5795
  /** Requests held back by a lane backoff whose attempt MemTree answered (unreachableNow). */
5674
5796
  const answeredBackoffs = new WeakSet();
5675
5797
  /**
@@ -5701,6 +5823,25 @@ function unreachableNow(state, rec) {
5701
5823
  return { message: unreachableMessage(memtree, anthropic),
5702
5824
  what: anthropic ? "internet" : "memtree" };
5703
5825
  }
5826
+ /**
5827
+ * Note this request's lane (lane-activity.ts) and, on a main-lane request of the current
5828
+ * prompt, queue the session's one heavy-request warning when an earlier upload passed the
5829
+ * size and no other notice waits. It is the lowest priority (otherNoticePending): any
5830
+ * other notice may replace it, which leaves it due for a later turn.
5831
+ */
5832
+ function noteLaneActivity(state, rec, sessionId) {
5833
+ state.laneActivity.seen(sessionId, rec.routeLane);
5834
+ const heavy = state.laneActivity.heavyDue(sessionId);
5835
+ const prompt = noticeContexts.get(rec);
5836
+ if (!heavy || sessionId === undefined || rec.routeLane !== "main" || prompt === undefined ||
5837
+ prompt.generation !== state.mainPromptGeneration || state.notices.hasPending())
5838
+ return;
5839
+ state.heavyNotice = state.notices.queueSuffix(heavyRequestNoticeText(heavy), () => state.laneActivity.markWarned(sessionId), prompt.promptId);
5840
+ }
5841
+ /** Whether a notice other than the heavy-request one (the lowest priority) waits. */
5842
+ function otherNoticePending(state, promptId) {
5843
+ return state.notices.hasPending(promptId) && !state.notices.pendingIs(state.heavyNotice);
5844
+ }
5704
5845
  /**
5705
5846
  * A repeated retryable refusal on the main thread: say once per streak that
5706
5847
  * MemTree is slow and Claude Code is retrying, unless another notice waits.
@@ -5713,7 +5854,7 @@ function noteSlowRetry(args, failure) {
5713
5854
  const canShow = failure.status === 503 && rec.routeLane === "main" &&
5714
5855
  rec.indexBehind !== true && prompt !== undefined &&
5715
5856
  prompt.generation === state.mainPromptGeneration && !args.res.destroyed &&
5716
- !state.notices.hasPending();
5857
+ !otherNoticePending(state);
5717
5858
  if (!state.refusalStreaks.refused(args.routeKey, Date.now(), canShow))
5718
5859
  return;
5719
5860
  state.notices.queueSuffix(SLOW_RETRY_NOTICE, undefined, prompt.promptId);
@@ -5764,7 +5905,7 @@ function warnPausedNearWindow(state, rec, sessionId) {
5764
5905
  // Never replace another queued notice (or this one again): a later request tries.
5765
5906
  if (detail === null || rec.routeLane !== "main" || sessionId === undefined ||
5766
5907
  pausedNotices.has(rec) || nearWindowWarned(state, sessionId) ||
5767
- state.notices.hasPending(state.mainPromptId))
5908
+ otherNoticePending(state, state.mainPromptId))
5768
5909
  return;
5769
5910
  state.nearWindowNotice = state.notices.queueSuffix(paymentNoticeText(detail, undefined, { overBudget: true }), () => {
5770
5911
  state.paymentNotice.markShown();