claude-code-infinite 1.0.27 → 1.0.29

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 (102) hide show
  1. package/README.md +63 -29
  2. package/dist/auto-update.d.ts +2 -0
  3. package/dist/auto-update.d.ts.map +1 -1
  4. package/dist/auto-update.js +4 -0
  5. package/dist/auto-update.js.map +1 -1
  6. package/dist/claude-auth.d.ts +27 -2
  7. package/dist/claude-auth.d.ts.map +1 -1
  8. package/dist/claude-auth.js +76 -4
  9. package/dist/claude-auth.js.map +1 -1
  10. package/dist/claude-install.d.ts +73 -0
  11. package/dist/claude-install.d.ts.map +1 -0
  12. package/dist/claude-install.js +142 -0
  13. package/dist/claude-install.js.map +1 -0
  14. package/dist/claude-plan-warning.d.ts +70 -7
  15. package/dist/claude-plan-warning.d.ts.map +1 -1
  16. package/dist/claude-plan-warning.js +152 -8
  17. package/dist/claude-plan-warning.js.map +1 -1
  18. package/dist/cli-args.d.ts +5 -0
  19. package/dist/cli-args.d.ts.map +1 -1
  20. package/dist/cli-args.js +9 -0
  21. package/dist/cli-args.js.map +1 -1
  22. package/dist/cli-main.js +222 -84
  23. package/dist/cli-main.js.map +1 -1
  24. package/dist/diagnostics-schema.d.ts +4 -0
  25. package/dist/diagnostics-schema.d.ts.map +1 -1
  26. package/dist/diagnostics-schema.js +20 -0
  27. package/dist/diagnostics-schema.js.map +1 -1
  28. package/dist/diagnostics.d.ts +24 -1
  29. package/dist/diagnostics.d.ts.map +1 -1
  30. package/dist/diagnostics.js +68 -4
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/hooks.d.ts +8 -0
  33. package/dist/hooks.d.ts.map +1 -1
  34. package/dist/hooks.js +57 -25
  35. package/dist/hooks.js.map +1 -1
  36. package/dist/install-ping.d.ts +54 -0
  37. package/dist/install-ping.d.ts.map +1 -0
  38. package/dist/install-ping.js +177 -0
  39. package/dist/install-ping.js.map +1 -0
  40. package/dist/key-page.d.ts +17 -0
  41. package/dist/key-page.d.ts.map +1 -0
  42. package/dist/key-page.js +32 -0
  43. package/dist/key-page.js.map +1 -0
  44. package/dist/key-status.d.ts +79 -0
  45. package/dist/key-status.d.ts.map +1 -0
  46. package/dist/key-status.js +143 -0
  47. package/dist/key-status.js.map +1 -0
  48. package/dist/memtree-finder.d.ts +2 -1
  49. package/dist/memtree-finder.d.ts.map +1 -1
  50. package/dist/memtree-finder.js +4 -5
  51. package/dist/memtree-finder.js.map +1 -1
  52. package/dist/memtree-mcp.d.ts.map +1 -1
  53. package/dist/memtree-mcp.js +17 -7
  54. package/dist/memtree-mcp.js.map +1 -1
  55. package/dist/memtree-times.d.ts +35 -0
  56. package/dist/memtree-times.d.ts.map +1 -0
  57. package/dist/memtree-times.js +166 -0
  58. package/dist/memtree-times.js.map +1 -0
  59. package/dist/memtree-tools.d.ts +12 -0
  60. package/dist/memtree-tools.d.ts.map +1 -1
  61. package/dist/memtree-tools.js +63 -7
  62. package/dist/memtree-tools.js.map +1 -1
  63. package/dist/memtree.d.ts +23 -0
  64. package/dist/memtree.d.ts.map +1 -1
  65. package/dist/memtree.js +39 -3
  66. package/dist/memtree.js.map +1 -1
  67. package/dist/notices.d.ts +5 -0
  68. package/dist/notices.d.ts.map +1 -1
  69. package/dist/notices.js +13 -0
  70. package/dist/notices.js.map +1 -1
  71. package/dist/paused-session.d.ts +29 -0
  72. package/dist/paused-session.d.ts.map +1 -0
  73. package/dist/paused-session.js +148 -0
  74. package/dist/paused-session.js.map +1 -0
  75. package/dist/payment-gate.d.ts +44 -13
  76. package/dist/payment-gate.d.ts.map +1 -1
  77. package/dist/payment-gate.js +82 -23
  78. package/dist/payment-gate.js.map +1 -1
  79. package/dist/payment-link.d.ts +68 -0
  80. package/dist/payment-link.d.ts.map +1 -0
  81. package/dist/payment-link.js +123 -0
  82. package/dist/payment-link.js.map +1 -0
  83. package/dist/postinstall.d.ts +2 -0
  84. package/dist/postinstall.d.ts.map +1 -0
  85. package/dist/postinstall.js +4 -0
  86. package/dist/postinstall.js.map +1 -0
  87. package/dist/project-trees.d.ts +100 -0
  88. package/dist/project-trees.d.ts.map +1 -0
  89. package/dist/project-trees.js +196 -0
  90. package/dist/project-trees.js.map +1 -0
  91. package/dist/proxy.d.ts +29 -8
  92. package/dist/proxy.d.ts.map +1 -1
  93. package/dist/proxy.js +167 -25
  94. package/dist/proxy.js.map +1 -1
  95. package/dist/rate-limit.d.ts +64 -0
  96. package/dist/rate-limit.d.ts.map +1 -0
  97. package/dist/rate-limit.js +126 -0
  98. package/dist/rate-limit.js.map +1 -0
  99. package/dist/reqlog.d.ts +13 -2
  100. package/dist/reqlog.d.ts.map +1 -1
  101. package/dist/reqlog.js.map +1 -1
  102. package/package.json +2 -1
package/dist/proxy.js CHANGED
@@ -56,12 +56,14 @@ import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSE
56
56
  import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
57
57
  import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
58
58
  import { hyperlink } from "./payment-gate.js";
59
+ import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
59
60
  import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
60
61
  import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
61
62
  import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
62
63
  import { isResumableSessionId } from "./resume-hint.js";
63
64
  import { conservativeBodyTokens } from "./token-estimate.js";
64
65
  import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
66
+ import { rateLimitFromResponse } from "./rate-limit.js";
65
67
  import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
66
68
  const DEFAULT_UPSTREAM = "https://api.anthropic.com";
67
69
  const HOOK_BODY_LIMIT = 64 * 1024;
@@ -93,23 +95,39 @@ const SKIP_RESPONSE_HEADERS = new Set([
93
95
  "transfer-encoding",
94
96
  ]);
95
97
  /**
96
- * Budget fallback until the server reports `model_budget_tokens`: this share
97
- * of the model's context window. 0.8 by default; NEAR_WINDOW_FALLBACK_RATIO
98
- * (900k of 1M) for the 1M-window models the server budgets at 900k for ccc
99
- * (NEAR_WINDOW_MODEL_RE). Plain Claude Code 2.1.302 auto-compacts a 1M window
100
- * at 967k, so MemTree compresses no earlier than ~70k before it would; ccc
101
- * lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
98
+ * Budget fallback until the server reports `model_budget_tokens`, mirroring the
99
+ * server's ccc policy (polychat index_budget.py): every Claude model compresses
100
+ * near its window, NEAR_WINDOW_FALLBACK_RATIO of it but no later than Claude
101
+ * Code's own auto-compact point (window - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS)
102
+ * and never below FALLBACK_BUDGET_WINDOW_RATIO: 900k of 1M, 167k of 200k; at
103
+ * most a Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
104
+ * FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts a 1M
105
+ * window at 967k, so MemTree compresses no earlier than ~70k before it would;
106
+ * ccc lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
102
107
  * jumps past the budget reaches the proxy and is compressed here.
103
108
  */
104
109
  export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
105
110
  export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
106
- /** Opus 5.5, Opus 5, Opus 4.8 and Fable 5 / 5.1 (polychat LARGE_CONTEXT_MODELS). */
107
- const NEAR_WINDOW_MODEL_RE = /claude-(?:opus-(?:4-8|5-5|5)|fable-5(?:-1)?)(?![0-9])/i;
108
- const ONE_MILLION_TOKENS = 1_000_000;
109
- /** The fallback share of the window for `model` (see FALLBACK_BUDGET_WINDOW_RATIO). */
110
- export function fallbackBudgetRatio(model, modelContextLimit) {
111
- return typeof model === "string" && modelContextLimit >= ONE_MILLION_TOKENS &&
112
- NEAR_WINDOW_MODEL_RE.test(model) ? NEAR_WINDOW_FALLBACK_RATIO : FALLBACK_BUDGET_WINDOW_RATIO;
111
+ /** Claude Code 2.1.302 auto-compacts at window - min(max_tokens, 20k) - 13k. */
112
+ const CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS = 33_000;
113
+ /** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
114
+ const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
115
+ /**
116
+ * polychat USABLE_CONTEXT_CEILING_TOKENS_BY_MODEL: the Sonnets degrade well
117
+ * before their window. Sonnet 5 / 5.5 550k; every other Sonnet 400k. First match wins.
118
+ */
119
+ const SONNET_CEILINGS = [
120
+ [/sonnet-5(?![0-9])/i, 550_000],
121
+ [/sonnet/i, 400_000],
122
+ ];
123
+ /** The fallback whole-request budget for `model` on this window (see above). */
124
+ export function fallbackBudgetTokens(model, modelContextLimit) {
125
+ if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
126
+ return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
127
+ }
128
+ const nearWindow = Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), Math.min(Math.floor(modelContextLimit * NEAR_WINDOW_FALLBACK_RATIO), modelContextLimit - CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS));
129
+ const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
130
+ return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
113
131
  }
114
132
  /**
115
133
  * An explicit target is capped at this share of the budget: a compaction to
@@ -304,11 +322,16 @@ export function startProxy(opts) {
304
322
  const shutdownAbort = new AbortController();
305
323
  const state = {
306
324
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
325
+ keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
326
+ keyInvalid: () => opts.memtree.keyInvalid,
327
+ keyPageUrl: opts.memtree.keyPageUrl,
307
328
  pausedNearWindowWarned: new Set(),
308
329
  unpaidDetail: () => opts.memtree.paymentRequiredDetail,
309
330
  pausedDetail: () => opts.memtree.pausedDetail,
310
331
  usageUrl: opts.memtree.usageDashboardUrl,
311
332
  sessionStartNotice: opts.sessionStartNotice,
333
+ promptSubmitNotice: opts.promptSubmitNotice,
334
+ projectTrees: opts.projectTrees,
312
335
  diagnostics: opts.diagnostics,
313
336
  notices: new NoticeDeliveryQueue(),
314
337
  mainNoticePending: false,
@@ -726,17 +749,20 @@ onMainStop) {
726
749
  recordSessionStart(state, parsed.source);
727
750
  noteMainSessionStart(state, parsed.source, parsed.session_id);
728
751
  }
752
+ // Earlier trees in this project: asked now, so its wait overlaps the resume's.
753
+ const contextPromise = projectTreesContext(state.projectTrees, parsed);
729
754
  // A resume waits briefly for the newest tree; a fresh session has none to wait for.
730
755
  if (parsed.agent_id === undefined && parsed.source === "resume") {
731
756
  await state.newestTrees?.settle(parsed.session_id);
732
757
  }
733
758
  const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
734
- if (!line) {
759
+ const context = await contextPromise;
760
+ if (!line && !context) {
735
761
  res.writeHead(204);
736
762
  res.end();
737
763
  return;
738
764
  }
739
- const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
765
+ const body = Buffer.from(JSON.stringify(sessionStartOutput(line, context)), "utf-8");
740
766
  res.writeHead(200, {
741
767
  "content-type": "application/json",
742
768
  "content-length": String(body.length),
@@ -782,6 +808,17 @@ onMainStop) {
782
808
  state.promptAccounting.add(parsed.session_id, parsed.prompt_id, parsed.prompt);
783
809
  recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
784
810
  }
811
+ const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
812
+ if (line) {
813
+ const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
814
+ res.writeHead(200, {
815
+ "content-type": "application/json",
816
+ "content-length": String(body.length),
817
+ "cache-control": "no-store",
818
+ });
819
+ res.end(body);
820
+ return;
821
+ }
785
822
  res.writeHead(204);
786
823
  res.end();
787
824
  return;
@@ -1697,6 +1734,13 @@ async function handleMessages(req, res, opts, upstream, state) {
1697
1734
  // it pending for the next turn.
1698
1735
  if (result && sessionId !== undefined)
1699
1736
  state.compactNow.delete(sessionId);
1737
+ // A first 402 starts fetching the short checkout link (memtree.ts): give the
1738
+ // paused notice below a moment to show it instead of the long one. Only a turn
1739
+ // that can show the notice waits, and only while that fetch is in flight.
1740
+ if (!result && displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration &&
1741
+ state.paymentNotice.due() && opts.memtree.paymentRequiredDetail !== null) {
1742
+ await opts.memtree.paymentLinkSettled?.(PAYMENT_LINK_NOTICE_WAIT_MS);
1743
+ }
1700
1744
  if (downstreamClosedDuringCompression ||
1701
1745
  (res.destroyed && !res.writableFinished)) {
1702
1746
  releaseUncommittedRouteDecision();
@@ -1724,7 +1768,15 @@ async function handleMessages(req, res, opts, upstream, state) {
1724
1768
  const paymentDetail = opts.memtree.paymentRequiredDetail;
1725
1769
  const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
1726
1770
  const overBudget = reachedBudget(rec.compaction);
1727
- if (mayQueueNotice && paymentDetail !== null &&
1771
+ if (state.keyInvalid()) {
1772
+ // A refused key (401/403) says why MemTree is off and how to fix it;
1773
+ // throttled like the payment notice, and quiet in between.
1774
+ if (mayQueueNotice && state.keyNotice.due()) {
1775
+ // A refusal of this very request says the same and withdraws it.
1776
+ pausedNotices.set(rec, state.notices.queueSuffix(keyInvalidNoticeText(state.keyPageUrl), () => state.keyNotice.markShown(), noticePromptId));
1777
+ }
1778
+ }
1779
+ else if (mayQueueNotice && paymentDetail !== null &&
1728
1780
  (state.paymentNotice.due() || (overBudget && !nearWindowWarned(state, sessionId)))) {
1729
1781
  // A paused refusal of this very request withdraws it (refuseWholeRequest),
1730
1782
  // and any later one withdraws its near-window line.
@@ -1739,7 +1791,24 @@ async function handleMessages(req, res, opts, upstream, state) {
1739
1791
  state.nearWindowNotice = handle;
1740
1792
  }
1741
1793
  else if (mayQueueNotice && paymentDetail === null) {
1742
- state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
1794
+ // Under the budget the whole request is what compression would have
1795
+ // sent: the turn is not degraded, so the failure stays in the logs.
1796
+ const warn = () => {
1797
+ state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
1798
+ rec.degradedNotice = "shown";
1799
+ };
1800
+ if (compressionNeeded(rec.compaction)) {
1801
+ warn();
1802
+ }
1803
+ else {
1804
+ // Delivery sizes the request again (an exact count, else a
1805
+ // conservative bound): a turn it finds over the budget warns then.
1806
+ rec.degradedNotice = "quiet-under-budget";
1807
+ quietDegrades.set(rec, () => {
1808
+ if (state.mainPromptGeneration === noticePromptGeneration)
1809
+ warn();
1810
+ });
1811
+ }
1743
1812
  }
1744
1813
  return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
1745
1814
  noteWholeSize();
@@ -1973,6 +2042,15 @@ function resumePaymentNotice(state, input) {
1973
2042
  state.diagnostics?.record("paused_notice", { trigger: "resume" });
1974
2043
  return state.notices.warningText(paymentNoticeText(detail));
1975
2044
  }
2045
+ /** ProxyOptions.promptSubmitNotice's line for this prompt; never throws. */
2046
+ function promptSubmitLine(state) {
2047
+ try {
2048
+ return state.promptSubmitNotice?.();
2049
+ }
2050
+ catch {
2051
+ return undefined;
2052
+ }
2053
+ }
1976
2054
  /** The caller's extra SessionStart line; a throwing provider shows nothing. */
1977
2055
  function extraSessionStartLine(provider, input) {
1978
2056
  if (!provider || input.agent_id !== undefined || input.source === "compact")
@@ -1984,11 +2062,39 @@ function extraSessionStartLine(provider, input) {
1984
2062
  return undefined;
1985
2063
  }
1986
2064
  }
2065
+ /**
2066
+ * Earlier sessions in this project (project-trees.ts) for a main session's
2067
+ * start: fresh, resumed, cleared or forked, never a compaction. Never throws.
2068
+ */
2069
+ async function projectTreesContext(hint, input) {
2070
+ if (!hint || input.agent_id !== undefined || input.source === "compact")
2071
+ return undefined;
2072
+ try {
2073
+ return await hint.contextFor(input.session_id);
2074
+ }
2075
+ catch {
2076
+ return undefined;
2077
+ }
2078
+ }
2079
+ /**
2080
+ * The SessionStart hook's answer: the banner line for the user
2081
+ * (`systemMessage`), model context (`additionalContext`), or both.
2082
+ */
2083
+ export function sessionStartOutput(line, context) {
2084
+ return {
2085
+ ...(line ? { systemMessage: line } : {}),
2086
+ ...(context
2087
+ ? { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } }
2088
+ : {}),
2089
+ };
2090
+ }
1987
2091
  function joinNoticeLines(...parts) {
1988
2092
  const present = parts.filter((part) => !!part);
1989
2093
  return present.length ? present.join("\n") : undefined;
1990
2094
  }
1991
2095
  export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
2096
+ /** How long a paused turn waits for the short checkout link before its notice (once). */
2097
+ export const PAYMENT_LINK_NOTICE_WAIT_MS = 1_000;
1992
2098
  /** The reply to a ccc slash command, or undefined for an ordinary prompt. */
1993
2099
  function sessionCommandReply(state, sessionId, prompt) {
1994
2100
  const unpaid = state.unpaidDetail();
@@ -2500,8 +2606,8 @@ function serverBudgetKey(model, modelContextLimit) {
2500
2606
  /**
2501
2607
  * The session's whole-request budget: CCC_BUDGET_TOKENS when set (tests),
2502
2608
  * else the model budget the server last reported for this model and window,
2503
- * else the window times fallbackBudgetRatio (servers that predate
2504
- * `model_budget_tokens`, and the first request before any response).
2609
+ * else fallbackBudgetTokens (servers that predate `model_budget_tokens`, and
2610
+ * the first request before any response).
2505
2611
  */
2506
2612
  function resolveBudget(opts, state, model, modelContextLimit) {
2507
2613
  if (opts.budgetTokensOverride !== undefined) {
@@ -2511,7 +2617,7 @@ function resolveBudget(opts, state, model, modelContextLimit) {
2511
2617
  if (reported !== undefined)
2512
2618
  return { tokens: reported, source: "server" };
2513
2619
  return {
2514
- tokens: Math.floor(modelContextLimit * fallbackBudgetRatio(model, modelContextLimit)),
2620
+ tokens: fallbackBudgetTokens(model, modelContextLimit),
2515
2621
  source: "window-ratio",
2516
2622
  };
2517
2623
  }
@@ -3554,7 +3660,9 @@ async function recoverToolRouteMiss(args) {
3554
3660
  // a later turn. An unpaid key (402, possibly set by this very compress)
3555
3661
  // or a shutting-down proxy gets no retry.
3556
3662
  if (!state.shutdownSignal.aborted &&
3557
- opts.memtree.paymentRequiredDetail === null) {
3663
+ opts.memtree.paymentRequiredDetail === null &&
3664
+ !opts.memtree.keyInvalid // a refused key refuses the index too
3665
+ ) {
3558
3666
  opts.memtree.indexInBackground(hash, msgsForMemtree, modelContextLimit, requestSessionId(req), args.clientMeta, transcriptTimesFor(opts, requestSessionId(req), agentAttributionId(req)));
3559
3667
  }
3560
3668
  return forwardOriginal();
@@ -3918,6 +4026,8 @@ async function selectForwardCandidate(args) {
3918
4026
  target = policy.sizing.register(replacement, "replacement");
3919
4027
  }
3920
4028
  const selection = await policy.sizing.select(target, args.limits);
4029
+ const quietDegrade = quietDegrades.get(args.rec);
4030
+ quietDegrades.delete(args.rec);
3921
4031
  if (!selection) {
3922
4032
  if (args.rec.lateCountRecovery)
3923
4033
  args.rec.lateCountRecovery.outcome = "refused";
@@ -3943,6 +4053,7 @@ async function selectForwardCandidate(args) {
3943
4053
  ? "exact-count-fits-window" : measurement.source === "bytes"
3944
4054
  ? "bytes-fallback-fits-window" : "estimate-fits-window" };
3945
4055
  warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
4056
+ quietDegrade?.();
3946
4057
  }
3947
4058
  return changed ? candidate.body : intended;
3948
4059
  }
@@ -3962,9 +4073,12 @@ async function wholeRequestRefused(sizing, candidate, limits) {
3962
4073
  }
3963
4074
  /** Write the bounded fallback refusal (route-fallback.ts); always false. */
3964
4075
  function refuseWholeRequest(args, inputTokens, budgetTokens) {
3965
- const unpaid = args.state.pausedDetail();
3966
- const failure = unpaid !== null ? pausedRefusal(pausedRefusalMessage(unpaid))
3967
- : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
4076
+ // A refused key or a paused one: a retry cannot fix either, so it is final at once.
4077
+ const keyInvalid = args.state.keyInvalid();
4078
+ const unpaid = keyInvalid ? null : args.state.pausedDetail();
4079
+ const failure = keyInvalid ? pausedRefusal(keyInvalidRefusalMessage(args.state.keyPageUrl))
4080
+ : unpaid !== null ? pausedRefusal(pausedRefusalMessage(unpaid))
4081
+ : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
3968
4082
  args.rec.forwardedBytes = 0;
3969
4083
  args.rec.approxInputTokens = 0;
3970
4084
  args.state.diagnostics?.record("refusal", {
@@ -3977,8 +4091,9 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
3977
4091
  inputTokens, outputTokens: args.body.max_tokens ?? 0,
3978
4092
  outputReservation: bodyOutputReservation(args.body), indexBehind: args.rec.indexBehind === true,
3979
4093
  budgetTokens, modelContextLimit: args.modelContextLimit, ...(unpaid !== null ? { paused: true } : {}),
4094
+ ...(keyInvalid ? { keyInvalid: true } : {}),
3980
4095
  };
3981
- if (unpaid !== null)
4096
+ if (unpaid !== null || keyInvalid)
3982
4097
  cancelPausedNotice(args.state, args.rec);
3983
4098
  if (args.res.headersSent || args.res.destroyed)
3984
4099
  return false;
@@ -3987,6 +4102,9 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
3987
4102
  if (unpaid !== null) {
3988
4103
  notePausedRefusal(args.state, args.rec, args.routeKey, requestSessionId(args.req));
3989
4104
  }
4105
+ // The refusal named the key: it counts as the key notice on the user's screen.
4106
+ if (keyInvalid && args.rec.routeLane === "main")
4107
+ args.state.keyNotice.markShown();
3990
4108
  return false;
3991
4109
  }
3992
4110
  /**
@@ -4025,6 +4143,8 @@ function cancelPausedNotice(state, rec) {
4025
4143
  * already queued the paused notice carries the line itself.
4026
4144
  */
4027
4145
  function warnPausedNearWindow(state, rec, sessionId) {
4146
+ if (state.keyInvalid())
4147
+ return; // a refused key, not a pause: the key notice says why
4028
4148
  const detail = state.unpaidDetail();
4029
4149
  // Paid again: a later pause (next month's limit) warns afresh.
4030
4150
  if (detail === null)
@@ -4057,6 +4177,25 @@ function nearWindowWarned(state, sessionId) {
4057
4177
  function reachedBudget(compaction) {
4058
4178
  return compaction !== undefined && (compaction.estimatedTokens ?? 0) >= compaction.budgetTokens;
4059
4179
  }
4180
+ /**
4181
+ * Whether the planned size says a turn needed MemTree to compress it: a
4182
+ * forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
4183
+ * size at or over the budget, or a size nobody measured. A planned fit is
4184
+ * provisional: delivery's own sizing can still find the turn over the budget
4185
+ * (quietDegrades), since a plan can undercount.
4186
+ */
4187
+ function compressionNeeded(compaction) {
4188
+ if (compaction === undefined || compaction.reason !== undefined)
4189
+ return true;
4190
+ if (compaction.estimatedTokens === undefined)
4191
+ return true;
4192
+ return compaction.estimatedTokens >= compaction.budgetTokens;
4193
+ }
4194
+ /**
4195
+ * A degraded turn whose plan fit the budget, so it queued no notice: the
4196
+ * notice to queue if delivery's sizing finds it over the budget after all.
4197
+ */
4198
+ const quietDegrades = new WeakMap();
4060
4199
  /** The paused notice a degraded request queued, until it is refused or settles. */
4061
4200
  const pausedNotices = new WeakMap();
4062
4201
  const changedDeliveryRecords = new WeakSet();
@@ -4267,6 +4406,9 @@ function forwardAccepted(req, res, bodyBuffer, opts, upstream, shutdownSignal, r
4267
4406
  };
4268
4407
  if (rec) {
4269
4408
  rec.upstreamStatus = upstreamRes.statusCode ?? 502;
4409
+ const rateLimit = rateLimitFromResponse(rec.upstreamStatus, upstreamRes.headers);
4410
+ if (rateLimit)
4411
+ rec.rateLimit = rateLimit;
4270
4412
  }
4271
4413
  successfulStatus =
4272
4414
  typeof upstreamRes.statusCode === "number" &&