claude-code-infinite 1.0.22 → 1.0.25

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 (47) hide show
  1. package/README.md +58 -23
  2. package/dist/cli-args.d.ts +15 -0
  3. package/dist/cli-args.d.ts.map +1 -1
  4. package/dist/cli-args.js +17 -0
  5. package/dist/cli-args.js.map +1 -1
  6. package/dist/cli-main.js +56 -33
  7. package/dist/cli-main.js.map +1 -1
  8. package/dist/cli-output.d.ts +15 -0
  9. package/dist/cli-output.d.ts.map +1 -0
  10. package/dist/cli-output.js +26 -0
  11. package/dist/cli-output.js.map +1 -0
  12. package/dist/hooks.d.ts +6 -1
  13. package/dist/hooks.d.ts.map +1 -1
  14. package/dist/hooks.js +12 -0
  15. package/dist/hooks.js.map +1 -1
  16. package/dist/link-command.d.ts +50 -0
  17. package/dist/link-command.d.ts.map +1 -0
  18. package/dist/link-command.js +168 -0
  19. package/dist/link-command.js.map +1 -0
  20. package/dist/memtree-mcp-config.d.ts +28 -5
  21. package/dist/memtree-mcp-config.d.ts.map +1 -1
  22. package/dist/memtree-mcp-config.js +116 -47
  23. package/dist/memtree-mcp-config.js.map +1 -1
  24. package/dist/memtree-mcp.js +1 -1
  25. package/dist/memtree-newest.d.ts +1 -0
  26. package/dist/memtree-newest.d.ts.map +1 -1
  27. package/dist/memtree-newest.js +1 -1
  28. package/dist/memtree-newest.js.map +1 -1
  29. package/dist/memtree.d.ts +9 -0
  30. package/dist/memtree.d.ts.map +1 -1
  31. package/dist/memtree.js +18 -0
  32. package/dist/memtree.js.map +1 -1
  33. package/dist/payment-notice.d.ts +19 -1
  34. package/dist/payment-notice.d.ts.map +1 -1
  35. package/dist/payment-notice.js +25 -2
  36. package/dist/payment-notice.js.map +1 -1
  37. package/dist/proxy.d.ts.map +1 -1
  38. package/dist/proxy.js +134 -10
  39. package/dist/proxy.js.map +1 -1
  40. package/dist/reqlog.d.ts +2 -0
  41. package/dist/reqlog.d.ts.map +1 -1
  42. package/dist/reqlog.js.map +1 -1
  43. package/dist/route-fallback.d.ts +6 -0
  44. package/dist/route-fallback.d.ts.map +1 -1
  45. package/dist/route-fallback.js +9 -0
  46. package/dist/route-fallback.js.map +1 -1
  47. package/package.json +1 -1
package/dist/proxy.js CHANGED
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { capCacheBreakpoints } from "./route-cache.js";
42
42
  import { PromptAccounting } from "./prompt-accounting.js";
43
- import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures } from "./route-fallback.js";
43
+ import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures, pausedRefusal } from "./route-fallback.js";
44
44
  import { createCountTokensSession, CountTokensCooldowns, plausibleSample, } from "./count-tokens.js";
45
45
  import { RequestSizing, estimateRequestTokens, fitsMeasured, fitsNative, outputReservation } from "./request-sizing.js";
46
46
  import { routeMessageHash, stablePrefixMessageHash } from "./route-identity.js";
@@ -56,7 +56,7 @@ 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 { PaymentNoticeThrottle, paymentNoticeText } from "./payment-notice.js";
59
+ import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } 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
62
  import { isResumableSessionId } from "./resume-hint.js";
@@ -134,9 +134,9 @@ function checkAwaitedIndex(opts, attempt, shutdownSignal) {
134
134
  // Bound both the individual body read and repeated 202 responses. A page
135
135
  // that never completes must eventually return the lane to growth backoff.
136
136
  const timeoutMs = Math.max(1, Math.min(opts.awaitedIndexProbeTimeoutMs ?? 5_000, waiting.deadline - Date.now()));
137
- const signal = AbortSignal.any([shutdownSignal, AbortSignal.timeout(timeoutMs)]);
137
+ const probe = abortOnTimeoutOrShutdown(timeoutMs, shutdownSignal);
138
138
  opts.memtree
139
- .fetchMemTree(`/usage/memtree/${waiting.pageId}.json`, "application/json", signal)
139
+ .fetchMemTree(`/usage/memtree/${waiting.pageId}.json`, "application/json", probe.signal)
140
140
  .then((page) => {
141
141
  if (page.status === 202 && Date.now() < waiting.deadline)
142
142
  return;
@@ -151,9 +151,32 @@ function checkAwaitedIndex(opts, attempt, shutdownSignal) {
151
151
  attempt.awaitingIndex = undefined;
152
152
  })
153
153
  .finally(() => {
154
+ probe.dispose();
154
155
  waiting.checking = false;
155
156
  });
156
157
  }
158
+ /**
159
+ * A signal that aborts after `timeoutMs` or when `shutdownSignal` aborts.
160
+ * Not AbortSignal.any([shutdown, AbortSignal.timeout(ms)]): on Node 20 the
161
+ * composite holds its sources weakly, so a GC can collect the timeout signal
162
+ * and its abort never fires, leaving a hung probe open for good. Here the
163
+ * timer and the shutdown listener hold the controller until dispose().
164
+ */
165
+ function abortOnTimeoutOrShutdown(timeoutMs, shutdownSignal) {
166
+ const controller = new AbortController();
167
+ const abort = () => controller.abort();
168
+ // Unreferenced like AbortSignal.timeout: a probe never holds the process open.
169
+ const timer = setTimeout(abort, timeoutMs);
170
+ timer.unref();
171
+ shutdownSignal.addEventListener("abort", abort, { once: true });
172
+ return {
173
+ signal: controller.signal,
174
+ dispose: () => {
175
+ clearTimeout(timer);
176
+ shutdownSignal.removeEventListener("abort", abort);
177
+ },
178
+ };
179
+ }
157
180
  /**
158
181
  * Record lag evidence from `result` (indexLagEvidence): a refusal of this
159
182
  * request then waits for the index, and the cached answer expires after the
@@ -280,7 +303,9 @@ export function startProxy(opts) {
280
303
  const shutdownAbort = new AbortController();
281
304
  const state = {
282
305
  paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
306
+ pausedNearWindowWarned: new Set(),
283
307
  unpaidDetail: () => opts.memtree.paymentRequiredDetail,
308
+ pausedDetail: () => opts.memtree.pausedDetail,
284
309
  usageUrl: opts.memtree.usageDashboardUrl,
285
310
  sessionStartNotice: opts.sessionStartNotice,
286
311
  diagnostics: opts.diagnostics,
@@ -1694,11 +1719,20 @@ async function handleMessages(req, res, opts, upstream, state) {
1694
1719
  capture(opts, "anthropic-request", forwardBody);
1695
1720
  const paymentDetail = opts.memtree.paymentRequiredDetail;
1696
1721
  const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
1697
- if (mayQueueNotice && paymentDetail !== null && state.paymentNotice.due()) {
1698
- state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () => {
1722
+ const overBudget = reachedBudget(rec.compaction);
1723
+ if (mayQueueNotice && paymentDetail !== null &&
1724
+ (state.paymentNotice.due() || (overBudget && !nearWindowWarned(state, sessionId)))) {
1725
+ // A paused refusal of this very request withdraws it (refuseWholeRequest),
1726
+ // and any later one withdraws its near-window line.
1727
+ const handle = state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link, { overBudget }), () => {
1699
1728
  state.paymentNotice.markShown();
1729
+ if (overBudget && sessionId !== undefined)
1730
+ addNearWindowWarned(state, sessionId);
1700
1731
  state.diagnostics?.record("paused_notice", { trigger: "turn" });
1701
1732
  }, noticePromptId);
1733
+ pausedNotices.set(rec, handle);
1734
+ if (overBudget)
1735
+ state.nearWindowNotice = handle;
1702
1736
  }
1703
1737
  else if (mayQueueNotice && paymentDetail === null) {
1704
1738
  state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
@@ -1953,10 +1987,15 @@ function joinNoticeLines(...parts) {
1953
1987
  export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
1954
1988
  /** The reply to a ccc slash command, or undefined for an ordinary prompt. */
1955
1989
  function sessionCommandReply(state, sessionId, prompt) {
1990
+ const unpaid = state.unpaidDetail();
1991
+ // Paused, compaction needs the MemTree it cannot reach: say how to resume instead.
1992
+ const paused = unpaid === null ? undefined
1993
+ : pausedCompactionText(viewUrl(subscribeUrlFromDetail(unpaid)));
1956
1994
  if (sessionCommandArgs(prompt, MEMTREE_HELP_COMMAND) !== undefined) {
1957
1995
  return [
1958
1996
  "• /memtree-view · show this session's MemTree page link",
1959
- `• /memtree-compact [tokens | off] · compact this session on your next message (default half the budget, at least ${MEMTREE_COMPACT_MIN_TOKENS / 1000}k)`,
1997
+ paused !== undefined ? `• ${paused}`
1998
+ : `• /memtree-compact [tokens | off] · compact this session on your next message (default half the budget, at least ${MEMTREE_COMPACT_MIN_TOKENS / 1000}k)`,
1960
1999
  usageLines(state),
1961
2000
  ].filter((line) => line !== undefined).join("\n");
1962
2001
  }
@@ -1977,6 +2016,8 @@ function sessionCommandReply(state, sessionId, prompt) {
1977
2016
  state.storedPrefixLookups.set(sessionId, null);
1978
2017
  return `${TRAILER_LABEL} compaction off: the conversation is sent whole, and MemTree compresses only when it outgrows the model's budget.`;
1979
2018
  }
2019
+ if (paused !== undefined)
2020
+ return `${TRAILER_LABEL} ${paused}`;
1980
2021
  if (args === "") {
1981
2022
  // Back to the automatic target (half the budget, or CCC_COMPACT_TARGET).
1982
2023
  // Under CCC_COMPACT_TARGET=off the default is off, so pin the automatic
@@ -2059,7 +2100,9 @@ function viewUrl(url) {
2059
2100
  */
2060
2101
  function sessionLink(state, sessionId, source) {
2061
2102
  const newest = state.newestTrees?.peek(sessionId);
2062
- const note = source && !source.compressed ? { note: NOT_COMPRESSED_NOTE } : {};
2103
+ // The note suggests /memtree-compact, which cannot run while MemTree is paused.
2104
+ const note = source && !source.compressed && state.unpaidDetail() === null
2105
+ ? { note: NOT_COMPRESSED_NOTE } : {};
2063
2106
  if (newest)
2064
2107
  return { key: newest.key, link: newest.url, ...note };
2065
2108
  return source ? { key: source.index, link: source.url, ...note } : undefined;
@@ -2170,6 +2213,9 @@ function noteMemtreeHealth(state, compression) {
2170
2213
  }
2171
2214
  if (compression.liveHealth === "success") {
2172
2215
  state.toolRecoveryCooldownUntil = 0;
2216
+ // Paid again: a later pause (next month's limit) warns near the window afresh.
2217
+ if (state.unpaidDetail() === null)
2218
+ state.pausedNearWindowWarned.clear();
2173
2219
  }
2174
2220
  }
2175
2221
  /**
@@ -3892,6 +3938,7 @@ async function selectForwardCandidate(args) {
3892
3938
  args.rec.overBudgetForward = { reason: measurement.source === "exact"
3893
3939
  ? "exact-count-fits-window" : measurement.source === "bytes"
3894
3940
  ? "bytes-fallback-fits-window" : "estimate-fits-window" };
3941
+ warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
3895
3942
  }
3896
3943
  return changed ? candidate.body : intended;
3897
3944
  }
@@ -3911,7 +3958,9 @@ async function wholeRequestRefused(sizing, candidate, limits) {
3911
3958
  }
3912
3959
  /** Write the bounded fallback refusal (route-fallback.ts); always false. */
3913
3960
  function refuseWholeRequest(args, inputTokens, budgetTokens) {
3914
- const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
3961
+ const unpaid = args.state.pausedDetail();
3962
+ const failure = unpaid !== null ? pausedRefusal(pausedRefusalMessage(unpaid))
3963
+ : args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
3915
3964
  args.rec.forwardedBytes = 0;
3916
3965
  args.rec.approxInputTokens = 0;
3917
3966
  args.state.diagnostics?.record("refusal", {
@@ -3923,14 +3972,89 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
3923
3972
  lane: args.routeKey, attempt: failure.attempt, status: failure.status,
3924
3973
  inputTokens, outputTokens: args.body.max_tokens ?? 0,
3925
3974
  outputReservation: bodyOutputReservation(args.body), indexBehind: args.rec.indexBehind === true,
3926
- budgetTokens, modelContextLimit: args.modelContextLimit,
3975
+ budgetTokens, modelContextLimit: args.modelContextLimit, ...(unpaid !== null ? { paused: true } : {}),
3927
3976
  };
3977
+ if (unpaid !== null)
3978
+ cancelPausedNotice(args.state, args.rec);
3928
3979
  if (args.res.headersSent || args.res.destroyed)
3929
3980
  return false;
3930
3981
  args.res.writeHead(failure.status, { "content-type": "application/json", ...failure.headers });
3931
3982
  args.res.end(JSON.stringify(failure.body));
3983
+ if (unpaid !== null) {
3984
+ notePausedRefusal(args.state, args.rec, args.routeKey, requestSessionId(args.req));
3985
+ }
3932
3986
  return false;
3933
3987
  }
3988
+ /**
3989
+ * The refusal text already says MemTree is paused and how to resume, so it
3990
+ * counts as the paused notice: the throttle restarts, and this request's
3991
+ * queued notice is withdrawn rather than shown a second time after it.
3992
+ */
3993
+ function notePausedRefusal(state, rec, lane, sessionId) {
3994
+ // A subagent's or hidden away-summary's refusal is not on the user's screen.
3995
+ if (rec.routeLane === "main") {
3996
+ state.paymentNotice.markShown();
3997
+ // Past the window /compact no longer fits: withdraw an undelivered
3998
+ // near-window line and show none later.
3999
+ state.notices.cancel(state.nearWindowNotice);
4000
+ if (sessionId !== undefined)
4001
+ addNearWindowWarned(state, sessionId);
4002
+ }
4003
+ // A tool turn in the outage cooldown or its lane's growth backoff refuses on
4004
+ // the last 402 without asking. End both so the next request asks MemTree
4005
+ // again (a 402 answers fast) and paying takes effect with no restart. A
4006
+ // concurrent attempt still compressing (no backoff yet) keeps its slot.
4007
+ state.toolRecoveryCooldownUntil = 0;
4008
+ const attempt = state.toolRecoveryAttemptedLanes.get(lane);
4009
+ if (attempt && (!attempt.inFlight || attempt.retryAtTokens !== undefined)) {
4010
+ state.toolRecoveryAttemptedLanes.delete(lane);
4011
+ }
4012
+ }
4013
+ /** Withdraw the paused notice this request queued: its refusal says the same. */
4014
+ function cancelPausedNotice(state, rec) {
4015
+ state.notices.cancel(pausedNotices.get(rec));
4016
+ pausedNotices.delete(rec);
4017
+ }
4018
+ /**
4019
+ * A paused main-lane request over the budget, still fitting the window (often a
4020
+ * tool round): once per session, say /compact still works now. A turn that
4021
+ * already queued the paused notice carries the line itself.
4022
+ */
4023
+ function warnPausedNearWindow(state, rec, sessionId) {
4024
+ const detail = state.unpaidDetail();
4025
+ // Paid again: a later pause (next month's limit) warns afresh.
4026
+ if (detail === null)
4027
+ state.pausedNearWindowWarned.clear();
4028
+ // Never replace another queued notice (or this one again): a later request tries.
4029
+ if (detail === null || rec.routeLane !== "main" || sessionId === undefined ||
4030
+ pausedNotices.has(rec) || nearWindowWarned(state, sessionId) ||
4031
+ state.notices.hasPending(state.mainPromptId))
4032
+ return;
4033
+ state.nearWindowNotice = state.notices.queueSuffix(paymentNoticeText(detail, undefined, { overBudget: true }), () => {
4034
+ state.paymentNotice.markShown();
4035
+ addNearWindowWarned(state, sessionId);
4036
+ state.diagnostics?.record("paused_notice", { trigger: "turn" });
4037
+ }, state.mainPromptId);
4038
+ }
4039
+ /** Remember a warned session, keeping the set bounded (oldest first out). */
4040
+ function addNearWindowWarned(state, sessionId) {
4041
+ state.pausedNearWindowWarned.delete(sessionId);
4042
+ state.pausedNearWindowWarned.add(sessionId);
4043
+ if (state.pausedNearWindowWarned.size > MEMTREE_LINKS_MAX_SESSIONS) {
4044
+ const oldest = state.pausedNearWindowWarned.values().next().value;
4045
+ if (oldest !== undefined)
4046
+ state.pausedNearWindowWarned.delete(oldest);
4047
+ }
4048
+ }
4049
+ function nearWindowWarned(state, sessionId) {
4050
+ return sessionId !== undefined && state.pausedNearWindowWarned.has(sessionId);
4051
+ }
4052
+ /** Whether this turn's own size estimate reached its whole-request budget. */
4053
+ function reachedBudget(compaction) {
4054
+ return compaction !== undefined && (compaction.estimatedTokens ?? 0) >= compaction.budgetTokens;
4055
+ }
4056
+ /** The paused notice a degraded request queued, until it is refused or settles. */
4057
+ const pausedNotices = new WeakMap();
3934
4058
  const changedDeliveryRecords = new WeakSet();
3935
4059
  const selectedCandidates = new WeakMap();
3936
4060
  const candidateSizeOwners = new WeakMap();