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.
- package/README.md +58 -23
- package/dist/cli-args.d.ts +15 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +17 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-main.js +56 -33
- package/dist/cli-main.js.map +1 -1
- package/dist/cli-output.d.ts +15 -0
- package/dist/cli-output.d.ts.map +1 -0
- package/dist/cli-output.js +26 -0
- package/dist/cli-output.js.map +1 -0
- package/dist/hooks.d.ts +6 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +12 -0
- package/dist/hooks.js.map +1 -1
- package/dist/link-command.d.ts +50 -0
- package/dist/link-command.d.ts.map +1 -0
- package/dist/link-command.js +168 -0
- package/dist/link-command.js.map +1 -0
- package/dist/memtree-mcp-config.d.ts +28 -5
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +116 -47
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.js +1 -1
- package/dist/memtree-newest.d.ts +1 -0
- package/dist/memtree-newest.d.ts.map +1 -1
- package/dist/memtree-newest.js +1 -1
- package/dist/memtree-newest.js.map +1 -1
- package/dist/memtree.d.ts +9 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +18 -0
- package/dist/memtree.js.map +1 -1
- package/dist/payment-notice.d.ts +19 -1
- package/dist/payment-notice.d.ts.map +1 -1
- package/dist/payment-notice.js +25 -2
- package/dist/payment-notice.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +134 -10
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +2 -0
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/route-fallback.d.ts +6 -0
- package/dist/route-fallback.d.ts.map +1 -1
- package/dist/route-fallback.js +9 -0
- package/dist/route-fallback.js.map +1 -1
- 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
|
|
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
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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();
|