claude-code-infinite 1.0.20 → 1.0.22
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 +165 -30
- package/dist/auto-update-runner.d.ts +52 -0
- package/dist/auto-update-runner.d.ts.map +1 -0
- package/dist/auto-update-runner.js +307 -0
- package/dist/auto-update-runner.js.map +1 -0
- package/dist/auto-update-setup.d.ts +38 -0
- package/dist/auto-update-setup.d.ts.map +1 -0
- package/dist/auto-update-setup.js +72 -0
- package/dist/auto-update-setup.js.map +1 -0
- package/dist/auto-update.d.ts +170 -0
- package/dist/auto-update.d.ts.map +1 -0
- package/dist/auto-update.js +401 -0
- package/dist/auto-update.js.map +1 -0
- package/dist/claude-env.d.ts +14 -1
- package/dist/claude-env.d.ts.map +1 -1
- package/dist/claude-env.js +17 -1
- package/dist/claude-env.js.map +1 -1
- package/dist/cli-lifecycle.d.ts +4 -0
- package/dist/cli-lifecycle.d.ts.map +1 -1
- package/dist/cli-lifecycle.js +18 -0
- package/dist/cli-lifecycle.js.map +1 -1
- package/dist/cli-main.js +139 -19
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/count-tokens.d.ts +38 -3
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +94 -19
- package/dist/count-tokens.js.map +1 -1
- package/dist/diagnostics-schema.d.ts +44 -0
- package/dist/diagnostics-schema.d.ts.map +1 -0
- package/dist/diagnostics-schema.js +162 -0
- package/dist/diagnostics-schema.js.map +1 -0
- package/dist/diagnostics.d.ts +116 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +316 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/first-tree.d.ts +11 -0
- package/dist/first-tree.d.ts.map +1 -0
- package/dist/first-tree.js +45 -0
- package/dist/first-tree.js.map +1 -0
- package/dist/hooks.d.ts +37 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +69 -9
- package/dist/hooks.js.map +1 -1
- package/dist/memtree-finder.d.ts +11 -0
- package/dist/memtree-finder.d.ts.map +1 -1
- package/dist/memtree-finder.js +18 -0
- package/dist/memtree-finder.js.map +1 -1
- package/dist/memtree-links.d.ts +2 -0
- package/dist/memtree-links.d.ts.map +1 -1
- package/dist/memtree-links.js +4 -0
- package/dist/memtree-links.js.map +1 -1
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +4 -2
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.d.ts +24 -4
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +47 -14
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree-tools.d.ts +17 -1
- package/dist/memtree-tools.d.ts.map +1 -1
- package/dist/memtree-tools.js +50 -2
- package/dist/memtree-tools.js.map +1 -1
- package/dist/memtree.d.ts +17 -1
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +62 -19
- package/dist/memtree.js.map +1 -1
- package/dist/notices.d.ts +6 -1
- package/dist/notices.d.ts.map +1 -1
- package/dist/notices.js +13 -1
- package/dist/notices.js.map +1 -1
- package/dist/prefix-store.js +3 -0
- package/dist/prefix-store.js.map +1 -1
- package/dist/proxy.d.ts +45 -3
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +193 -54
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +27 -1
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +21 -2
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +60 -7
- package/dist/request-sizing.js.map +1 -1
- package/dist/resume-hint.d.ts +25 -0
- package/dist/resume-hint.d.ts.map +1 -0
- package/dist/resume-hint.js +36 -0
- package/dist/resume-hint.js.map +1 -0
- package/dist/token-estimate.d.ts +35 -0
- package/dist/token-estimate.d.ts.map +1 -0
- package/dist/token-estimate.js +97 -0
- package/dist/token-estimate.js.map +1 -0
- package/dist/update-check.d.ts +4 -3
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +4 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-lock.d.ts +68 -0
- package/dist/update-lock.d.ts.map +1 -0
- package/dist/update-lock.js +176 -0
- package/dist/update-lock.js.map +1 -0
- package/package.json +3 -3
package/dist/proxy.js
CHANGED
|
@@ -59,6 +59,8 @@ 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 { isResumableSessionId } from "./resume-hint.js";
|
|
63
|
+
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
62
64
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
63
65
|
const DEFAULT_UPSTREAM = "https://api.anthropic.com";
|
|
64
66
|
const HOOK_BODY_LIMIT = 64 * 1024;
|
|
@@ -91,10 +93,23 @@ const SKIP_RESPONSE_HEADERS = new Set([
|
|
|
91
93
|
]);
|
|
92
94
|
/**
|
|
93
95
|
* Budget fallback until the server reports `model_budget_tokens`: this share
|
|
94
|
-
* of the model's context window
|
|
95
|
-
*
|
|
96
|
+
* of the model's context window. 0.8 by default; NEAR_WINDOW_FALLBACK_RATIO
|
|
97
|
+
* (900k of 1M) for the 1M-window models the server budgets at 900k for ccc
|
|
98
|
+
* (NEAR_WINDOW_MODEL_RE). Plain Claude Code 2.1.302 auto-compacts a 1M window
|
|
99
|
+
* at 967k, so MemTree compresses no earlier than ~70k before it would; ccc
|
|
100
|
+
* lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
|
|
101
|
+
* jumps past the budget reaches the proxy and is compressed here.
|
|
96
102
|
*/
|
|
97
103
|
export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
|
|
104
|
+
export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
|
|
105
|
+
/** Opus 5.5, Opus 5, Opus 4.8 and Fable 5 / 5.1 (polychat LARGE_CONTEXT_MODELS). */
|
|
106
|
+
const NEAR_WINDOW_MODEL_RE = /claude-(?:opus-(?:4-8|5-5|5)|fable-5(?:-1)?)(?![0-9])/i;
|
|
107
|
+
const ONE_MILLION_TOKENS = 1_000_000;
|
|
108
|
+
/** The fallback share of the window for `model` (see FALLBACK_BUDGET_WINDOW_RATIO). */
|
|
109
|
+
export function fallbackBudgetRatio(model, modelContextLimit) {
|
|
110
|
+
return typeof model === "string" && modelContextLimit >= ONE_MILLION_TOKENS &&
|
|
111
|
+
NEAR_WINDOW_MODEL_RE.test(model) ? NEAR_WINDOW_FALLBACK_RATIO : FALLBACK_BUDGET_WINDOW_RATIO;
|
|
112
|
+
}
|
|
98
113
|
/**
|
|
99
114
|
* An explicit target is capped at this share of the budget: a compaction to
|
|
100
115
|
* the budget itself would be over budget again on the next turn and
|
|
@@ -267,6 +282,8 @@ export function startProxy(opts) {
|
|
|
267
282
|
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
268
283
|
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
269
284
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
285
|
+
sessionStartNotice: opts.sessionStartNotice,
|
|
286
|
+
diagnostics: opts.diagnostics,
|
|
270
287
|
notices: new NoticeDeliveryQueue(),
|
|
271
288
|
mainNoticePending: false,
|
|
272
289
|
mainPromptGeneration: 0,
|
|
@@ -303,6 +320,7 @@ export function startProxy(opts) {
|
|
|
303
320
|
laneSizes: new Map(),
|
|
304
321
|
};
|
|
305
322
|
installMemtreeLink(state, state.memtreeLinkPlacement);
|
|
323
|
+
state.notices.setFirstTreeIntro(opts.firstTreeIntro ?? null);
|
|
306
324
|
const server = http.createServer((req, res) => {
|
|
307
325
|
const accepted = { req, res };
|
|
308
326
|
acceptedRequests.add(accepted);
|
|
@@ -385,6 +403,7 @@ export function startProxy(opts) {
|
|
|
385
403
|
void beginClose();
|
|
386
404
|
},
|
|
387
405
|
drain,
|
|
406
|
+
mainSessionId: () => state.resumableMainSession,
|
|
388
407
|
});
|
|
389
408
|
});
|
|
390
409
|
});
|
|
@@ -615,6 +634,30 @@ export function memtreePageId(pageUrl) {
|
|
|
615
634
|
return undefined;
|
|
616
635
|
}
|
|
617
636
|
}
|
|
637
|
+
const SESSION_START_SOURCES = new Set(["startup", "resume", "clear", "compact"]);
|
|
638
|
+
/** A main-agent SessionStart, for diagnostics; never fails the hook. */
|
|
639
|
+
function recordSessionStart(state, source) {
|
|
640
|
+
try {
|
|
641
|
+
const known = source !== undefined && SESSION_START_SOURCES.has(source);
|
|
642
|
+
state.diagnostics?.record("session_start", { source: known ? source : "other" });
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Diagnostics are best effort.
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* A resumed or forked session can be resumed again at once; a fresh or
|
|
650
|
+
* cleared one has nothing to resume until its first turn. Compaction keeps
|
|
651
|
+
* the same session.
|
|
652
|
+
*/
|
|
653
|
+
function noteMainSessionStart(state, source, sessionId) {
|
|
654
|
+
if (source === "resume" || source === "fork") {
|
|
655
|
+
state.resumableMainSession = isResumableSessionId(sessionId) ? sessionId : undefined;
|
|
656
|
+
}
|
|
657
|
+
else if (source === "startup" || source === "clear") {
|
|
658
|
+
state.resumableMainSession = undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
618
661
|
/** Serve only validated Claude hook POSTs on the randomized localhost path. */
|
|
619
662
|
async function handleNoticeHook(req, res, state, reqlog,
|
|
620
663
|
/** A main-lane turn ended: the session may be over (its final index). */
|
|
@@ -653,11 +696,15 @@ onMainStop) {
|
|
|
653
696
|
return;
|
|
654
697
|
}
|
|
655
698
|
if (parsed.hook_event_name === "SessionStart") {
|
|
699
|
+
if (parsed.agent_id === undefined) {
|
|
700
|
+
recordSessionStart(state, parsed.source);
|
|
701
|
+
noteMainSessionStart(state, parsed.source, parsed.session_id);
|
|
702
|
+
}
|
|
656
703
|
// A resume waits briefly for the newest tree; a fresh session has none to wait for.
|
|
657
704
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
658
705
|
await state.newestTrees?.settle(parsed.session_id);
|
|
659
706
|
}
|
|
660
|
-
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed));
|
|
707
|
+
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
|
|
661
708
|
if (!line) {
|
|
662
709
|
res.writeHead(204);
|
|
663
710
|
res.end();
|
|
@@ -950,10 +997,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
950
997
|
upstream, headers: forwardableRequestHeaders(req), requestUrl: req.url,
|
|
951
998
|
signal: state.shutdownSignal,
|
|
952
999
|
onAttempt: (result) => {
|
|
953
|
-
(rec.countTokens ??= []).push({ statusClass: result.outcome, ms: result.ms
|
|
1000
|
+
(rec.countTokens ??= []).push({ statusClass: result.outcome, ms: result.ms,
|
|
1001
|
+
bytes: result.bytes ?? 0, timeoutMs: result.timeoutMs ?? 0 });
|
|
954
1002
|
},
|
|
955
1003
|
}, state.countTokensCooldowns);
|
|
956
|
-
const requestSizing = new RequestSizing(countSession, opts.countTokens === true);
|
|
1004
|
+
const requestSizing = new RequestSizing(countSession, opts.countTokens === true, typeof body.model === "string" ? body.model : undefined);
|
|
957
1005
|
if (carriesServerTools(body))
|
|
958
1006
|
serverToolRecords.add(rec);
|
|
959
1007
|
const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey, body.model);
|
|
@@ -1052,6 +1100,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1052
1100
|
// reply the session ends with (scheduleFinalIndex, on Stop).
|
|
1053
1101
|
const mainSessionId = isMainRequest ? requestSessionId(req) : undefined;
|
|
1054
1102
|
if (mainSessionId !== undefined) {
|
|
1103
|
+
if (isResumableSessionId(mainSessionId))
|
|
1104
|
+
state.resumableMainSession = mainSessionId;
|
|
1055
1105
|
opts.memtree.noteMainConversation(mainSessionId, {
|
|
1056
1106
|
messages: msgsForMemtree,
|
|
1057
1107
|
modelContextLimit,
|
|
@@ -1170,7 +1220,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1170
1220
|
// prefix, else whole) with no compress call; at it, compress once to
|
|
1171
1221
|
// the target (planToolCompaction). A "replay" is a client retry of the
|
|
1172
1222
|
// request that installed the route, forwarded verbatim.
|
|
1173
|
-
const plan = planToolCompaction({
|
|
1223
|
+
const plan = await planToolCompaction({
|
|
1174
1224
|
opts,
|
|
1175
1225
|
state,
|
|
1176
1226
|
body,
|
|
@@ -1207,11 +1257,15 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1207
1257
|
// A concurrent request of this lane is already compressing.
|
|
1208
1258
|
rec.routeRecovery = { outcome: "in-flight" };
|
|
1209
1259
|
}
|
|
1210
|
-
else if (prior?.awaitingIndex && !plan.overWindow) {
|
|
1260
|
+
else if (prior?.awaitingIndex && (!plan.overWindow || plan.uncountedOverWindowOnly)) {
|
|
1211
1261
|
// The last attempt found no tree for this conversation, so a
|
|
1212
1262
|
// compress call could only pass everything through again. Forward
|
|
1213
1263
|
// now; the page check lets the next tool turn try once it exists.
|
|
1264
|
+
// Over the window only by its uncounted bound, delivery may still
|
|
1265
|
+
// count it; if it refuses, that is an index wait, not a strike.
|
|
1214
1266
|
rec.routeRecovery = { outcome: "awaiting-index" };
|
|
1267
|
+
if (plan.uncountedOverWindowOnly)
|
|
1268
|
+
rec.indexBehind = true;
|
|
1215
1269
|
checkAwaitedIndex(opts, prior, state.shutdownSignal);
|
|
1216
1270
|
}
|
|
1217
1271
|
else if (prior?.retryAtTokens !== undefined &&
|
|
@@ -1350,7 +1404,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1350
1404
|
// Stable-prefix (edge) compaction: the main thread's human turns only. The
|
|
1351
1405
|
// recap rides the main route above; subagents keep per-turn compression.
|
|
1352
1406
|
const edge = isMainRequest && sessionId !== undefined
|
|
1353
|
-
? planEdgeCompaction({
|
|
1407
|
+
? await planEdgeCompaction({
|
|
1354
1408
|
sizing: requestSizing,
|
|
1355
1409
|
opts,
|
|
1356
1410
|
state,
|
|
@@ -1641,7 +1695,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1641
1695
|
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1642
1696
|
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1643
1697
|
if (mayQueueNotice && paymentDetail !== null && state.paymentNotice.due()) {
|
|
1644
|
-
state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () =>
|
|
1698
|
+
state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () => {
|
|
1699
|
+
state.paymentNotice.markShown();
|
|
1700
|
+
state.diagnostics?.record("paused_notice", { trigger: "turn" });
|
|
1701
|
+
}, noticePromptId);
|
|
1645
1702
|
}
|
|
1646
1703
|
else if (mayQueueNotice && paymentDetail === null) {
|
|
1647
1704
|
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
@@ -1875,8 +1932,20 @@ function resumePaymentNotice(state, input) {
|
|
|
1875
1932
|
if (detail === null)
|
|
1876
1933
|
return undefined;
|
|
1877
1934
|
state.paymentNotice.markShown();
|
|
1935
|
+
state.diagnostics?.record("paused_notice", { trigger: "resume" });
|
|
1878
1936
|
return state.notices.warningText(paymentNoticeText(detail));
|
|
1879
1937
|
}
|
|
1938
|
+
/** The caller's extra SessionStart line; a throwing provider shows nothing. */
|
|
1939
|
+
function extraSessionStartLine(provider, input) {
|
|
1940
|
+
if (!provider || input.agent_id !== undefined || input.source === "compact")
|
|
1941
|
+
return undefined;
|
|
1942
|
+
try {
|
|
1943
|
+
return provider();
|
|
1944
|
+
}
|
|
1945
|
+
catch {
|
|
1946
|
+
return undefined;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1880
1949
|
function joinNoticeLines(...parts) {
|
|
1881
1950
|
const present = parts.filter((part) => !!part);
|
|
1882
1951
|
return present.length ? present.join("\n") : undefined;
|
|
@@ -2381,7 +2450,7 @@ function serverBudgetKey(model, modelContextLimit) {
|
|
|
2381
2450
|
/**
|
|
2382
2451
|
* The session's whole-request budget: CCC_BUDGET_TOKENS when set (tests),
|
|
2383
2452
|
* else the model budget the server last reported for this model and window,
|
|
2384
|
-
* else the window times
|
|
2453
|
+
* else the window times fallbackBudgetRatio (servers that predate
|
|
2385
2454
|
* `model_budget_tokens`, and the first request before any response).
|
|
2386
2455
|
*/
|
|
2387
2456
|
function resolveBudget(opts, state, model, modelContextLimit) {
|
|
@@ -2392,7 +2461,7 @@ function resolveBudget(opts, state, model, modelContextLimit) {
|
|
|
2392
2461
|
if (reported !== undefined)
|
|
2393
2462
|
return { tokens: reported, source: "server" };
|
|
2394
2463
|
return {
|
|
2395
|
-
tokens: Math.floor(modelContextLimit *
|
|
2464
|
+
tokens: Math.floor(modelContextLimit * fallbackBudgetRatio(model, modelContextLimit)),
|
|
2396
2465
|
source: "window-ratio",
|
|
2397
2466
|
};
|
|
2398
2467
|
}
|
|
@@ -2442,10 +2511,11 @@ function laneCompaction(opts, state, sessionId, model, modelContextLimit) {
|
|
|
2442
2511
|
}
|
|
2443
2512
|
/** Calibrated input must leave room for the requested output as well. */
|
|
2444
2513
|
function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
|
|
2445
|
-
//
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2514
|
+
// An exact count reserves the model's output floor, as delivery does; an
|
|
2515
|
+
// estimate reserves the full max_tokens.
|
|
2516
|
+
const outputTokens = bodyOutputReservation(body, exactSizedRecords.has(record));
|
|
2517
|
+
return record.uncountedOverWindow === true || (record.sizeSource === "reported" &&
|
|
2518
|
+
(record.estimatedTokens ?? 0) + outputTokens > modelContextLimit);
|
|
2449
2519
|
}
|
|
2450
2520
|
function calibratedSizeExceedsLimits(record, body, modelContextLimit) {
|
|
2451
2521
|
return (record.sizeSource === "reported" &&
|
|
@@ -2515,16 +2585,24 @@ function usedServerTools(rec) {
|
|
|
2515
2585
|
* is dropped and the previous sample kept.
|
|
2516
2586
|
*/
|
|
2517
2587
|
function reportedSample(rec, bytes) {
|
|
2518
|
-
|
|
2519
|
-
return undefined;
|
|
2520
|
-
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
2521
|
-
return undefined;
|
|
2522
|
-
const tokens = reportedInputTokens(rec);
|
|
2588
|
+
const tokens = reportedBodyTokens(rec);
|
|
2523
2589
|
if (tokens === undefined)
|
|
2524
2590
|
return undefined;
|
|
2525
2591
|
const sample = { tokens, forwardedBytes: bytes };
|
|
2526
2592
|
return plausibleSample(sample) ? sample : undefined;
|
|
2527
2593
|
}
|
|
2594
|
+
/**
|
|
2595
|
+
* Anthropic's input tokens for the body this request forwarded, when its usage
|
|
2596
|
+
* describes that body (not a changed delivery, no server tools). Not filtered
|
|
2597
|
+
* by plausibility: a body that is mostly images is truly cheap per byte.
|
|
2598
|
+
*/
|
|
2599
|
+
function reportedBodyTokens(rec) {
|
|
2600
|
+
if (changedDeliveryRecords.has(rec))
|
|
2601
|
+
return undefined;
|
|
2602
|
+
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
2603
|
+
return undefined;
|
|
2604
|
+
return reportedInputTokens(rec);
|
|
2605
|
+
}
|
|
2528
2606
|
/**
|
|
2529
2607
|
* Key of a non-main request's size sample (ProxyState.laneSizes): its lane,
|
|
2530
2608
|
* its own agent id (helpers that carry only a parent id stay apart from the
|
|
@@ -2592,7 +2670,7 @@ function boundedSet(map, key, value, limit = STABLE_PREFIX_MAX_SESSIONS) {
|
|
|
2592
2670
|
*
|
|
2593
2671
|
* Fills rec.compaction with the budget, estimate and reason.
|
|
2594
2672
|
*/
|
|
2595
|
-
function planEdgeCompaction(args) {
|
|
2673
|
+
async function planEdgeCompaction(args) {
|
|
2596
2674
|
const { opts, state, body, messages, sessionId, modelContextLimit, forwardBody, rec } = args;
|
|
2597
2675
|
const mode = compactionMode(opts, state, sessionId);
|
|
2598
2676
|
const budget = resolveBudget(opts, state, body.model, modelContextLimit);
|
|
@@ -2644,12 +2722,16 @@ function planEdgeCompaction(args) {
|
|
|
2644
2722
|
}
|
|
2645
2723
|
const prefixCandidate = args.sizing.register(routed.raw, "ride", prefix.lastSize);
|
|
2646
2724
|
candidateSizeOwners.set(prefixCandidate, prefix);
|
|
2725
|
+
await countIfOverBound(args.sizing, prefixCandidate, body, modelContextLimit);
|
|
2647
2726
|
const size = args.sizing.plan(prefixCandidate);
|
|
2648
2727
|
compaction.estimatedTokens = size.tokens;
|
|
2649
2728
|
compaction.estimatedBytes = routed.raw.length;
|
|
2650
2729
|
compaction.sizeSource = size.source;
|
|
2651
|
-
|
|
2652
|
-
|
|
2730
|
+
noteUncountedBound(compaction, args.sizing, prefixCandidate, body, modelContextLimit);
|
|
2731
|
+
// Request-local sizing (an exact count, a calibrated sample, or the
|
|
2732
|
+
// image-aware bound), not bytes: a flatten that kept images is mostly
|
|
2733
|
+
// base64 that costs a fixed amount per image.
|
|
2734
|
+
const overWindow = calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
|
|
2653
2735
|
const fallback = overWindow ? undefined : { prefix, routed };
|
|
2654
2736
|
if (state.compactNow.has(sessionId))
|
|
2655
2737
|
return forced("manual", fallback);
|
|
@@ -2663,10 +2745,13 @@ function planEdgeCompaction(args) {
|
|
|
2663
2745
|
}
|
|
2664
2746
|
if (state.compactNow.has(sessionId))
|
|
2665
2747
|
return forced("manual");
|
|
2666
|
-
const
|
|
2748
|
+
const originalCandidate = args.sizing.register(forwardBody, "original");
|
|
2749
|
+
await countIfOverBound(args.sizing, originalCandidate, body, modelContextLimit);
|
|
2750
|
+
const size = args.sizing.plan(originalCandidate);
|
|
2667
2751
|
compaction.estimatedTokens = size.tokens;
|
|
2668
2752
|
compaction.estimatedBytes = forwardBody.length;
|
|
2669
2753
|
compaction.sizeSource = size.source;
|
|
2754
|
+
noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
|
|
2670
2755
|
if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
|
|
2671
2756
|
return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
|
|
2672
2757
|
}
|
|
@@ -2697,7 +2782,7 @@ function planEdgeCompaction(args) {
|
|
|
2697
2782
|
*
|
|
2698
2783
|
* Fills rec.compaction.
|
|
2699
2784
|
*/
|
|
2700
|
-
function planToolCompaction(args) {
|
|
2785
|
+
async function planToolCompaction(args) {
|
|
2701
2786
|
const { opts, state, body, messages, sessionId, modelContextLimit, forwardBody, rec } = args;
|
|
2702
2787
|
const mode = compactionMode(opts, state, sessionId);
|
|
2703
2788
|
const budget = resolveBudget(opts, state, body.model, modelContextLimit);
|
|
@@ -2714,23 +2799,16 @@ function planToolCompaction(args) {
|
|
|
2714
2799
|
const stableLane = args.isMainRequest && sessionId !== undefined;
|
|
2715
2800
|
const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
|
|
2716
2801
|
let ride = args.ride;
|
|
2717
|
-
let overWindow = false;
|
|
2718
2802
|
if (!ride && current && !prefixMismatch(body, messages, current, sessionId, stablePrefixMessageHash)) {
|
|
2719
2803
|
const routed = prefixRoutedBody(body, messages, current);
|
|
2720
2804
|
if (routed) {
|
|
2721
2805
|
const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
|
|
2722
2806
|
candidateSizeOwners.set(prefixCandidate, current);
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
ride = { raw: routed.raw, turnType: "tool-prefix", sizeHolder: current };
|
|
2807
|
+
// Sized below with the request-local sizing; an over-window ride is not
|
|
2808
|
+
// kept as a fallback.
|
|
2809
|
+
ride = { raw: routed.raw, turnType: "tool-prefix", sizeHolder: current };
|
|
2727
2810
|
}
|
|
2728
2811
|
}
|
|
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
2812
|
const sample = ride
|
|
2735
2813
|
? ride.sizeHolder.lastSize
|
|
2736
2814
|
: stableLane
|
|
@@ -2739,12 +2817,18 @@ function planToolCompaction(args) {
|
|
|
2739
2817
|
const candidate = args.sizing.register(ride?.raw ?? forwardBody, ride ? "ride" : "original", sample);
|
|
2740
2818
|
if (ride)
|
|
2741
2819
|
candidateSizeOwners.set(candidate, ride.sizeHolder);
|
|
2820
|
+
await countIfOverBound(args.sizing, candidate, body, modelContextLimit);
|
|
2742
2821
|
const size = args.sizing.plan(candidate);
|
|
2743
2822
|
compaction.estimatedTokens = size.tokens;
|
|
2744
2823
|
compaction.estimatedBytes = (ride?.raw ?? forwardBody).length;
|
|
2745
2824
|
compaction.sizeSource = size.source;
|
|
2746
|
-
|
|
2825
|
+
noteUncountedBound(compaction, args.sizing, candidate, body, modelContextLimit);
|
|
2826
|
+
// Judged after any planning count, so a fitting exact count is not
|
|
2827
|
+
// overridden by the estimate it replaced.
|
|
2828
|
+
let overWindow = !fitsNative(size.tokens, { budget: budget.tokens, context: modelContextLimit,
|
|
2747
2829
|
output: bodyOutputReservation(body, args.sizing.isExact(candidate)) });
|
|
2830
|
+
const uncountedOnly = !overWindow && compaction.uncountedOverWindow === true;
|
|
2831
|
+
overWindow ||= uncountedOnly;
|
|
2748
2832
|
if (!args.canCompress) {
|
|
2749
2833
|
if (overWindow)
|
|
2750
2834
|
return { kind: "pass", original: true };
|
|
@@ -2762,6 +2846,7 @@ function planToolCompaction(args) {
|
|
|
2762
2846
|
estimateTokens: size.tokens,
|
|
2763
2847
|
budgetTokens: budget.tokens,
|
|
2764
2848
|
overWindow,
|
|
2849
|
+
...(uncountedOnly ? { uncountedOverWindowOnly: true } : {}),
|
|
2765
2850
|
...(ride && !overWindow ? { fallback: ride } : {}),
|
|
2766
2851
|
...(stableLane ? { replaces: current ?? null } : {}),
|
|
2767
2852
|
};
|
|
@@ -2806,12 +2891,46 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
|
2806
2891
|
return null;
|
|
2807
2892
|
return Buffer.from(JSON.stringify(routed), "utf-8");
|
|
2808
2893
|
}
|
|
2809
|
-
/**
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2894
|
+
/**
|
|
2895
|
+
* Count the candidate a planner is about to judge (ride or original, sampled
|
|
2896
|
+
* or not) when only a count could admit it: no exact count, and its per-model
|
|
2897
|
+
* conservative bound plus max_tokens exceeds the window. Delivery reuses the
|
|
2898
|
+
* result.
|
|
2899
|
+
*/
|
|
2900
|
+
async function countIfOverBound(sizing, candidate, body, modelContextLimit) {
|
|
2901
|
+
await sizing.countIfOverBound(candidate, { budget: modelContextLimit, context: modelContextLimit, output: bodyOutputReservation(body, false) });
|
|
2902
|
+
}
|
|
2903
|
+
/** Compaction records whose estimatedTokens is an exact count (noteUncountedBound). */
|
|
2904
|
+
const exactSizedRecords = new WeakSet();
|
|
2905
|
+
/**
|
|
2906
|
+
* Note an exactly counted candidate (exactSizedRecords), or mark a planned
|
|
2907
|
+
* candidate still without an exact count (counting off, or its
|
|
2908
|
+
* planning count failed: countIfOverBound ran first) whose conservative bound
|
|
2909
|
+
* plus max_tokens exceeds the window: delivery would refuse it uncounted, so
|
|
2910
|
+
* it is over the window for planning and takes the normal gated compression.
|
|
2911
|
+
*/
|
|
2912
|
+
function noteUncountedBound(record, sizing, candidate, body, modelContextLimit) {
|
|
2913
|
+
if (sizing.isExact(candidate)) {
|
|
2914
|
+
exactSizedRecords.add(record);
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
if (sizing.exceedsRequestLimit(candidate) ||
|
|
2918
|
+
candidate.conservativeTokens + bodyOutputReservation(body, false) > modelContextLimit) {
|
|
2919
|
+
record.uncountedOverWindow = true;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Whether a forwarded compressed body still overflows the window: its exact
|
|
2924
|
+
* count (`exactTokens`, made for delivery), else its reported input tokens,
|
|
2925
|
+
* both Anthropic's own count, so the output floor applies; else its
|
|
2926
|
+
* conservative image-aware bound (token-estimate.ts) with full max_tokens,
|
|
2927
|
+
* never bytes/4, which reads base64 images as text.
|
|
2928
|
+
*/
|
|
2929
|
+
function compressedResultExceedsWindow(body, rec, compressedRaw, modelContextLimit, exactTokens) {
|
|
2930
|
+
const counted = exactTokens ?? reportedBodyTokens(rec);
|
|
2931
|
+
const tokens = counted ??
|
|
2932
|
+
conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
|
|
2933
|
+
return tokens + bodyOutputReservation(body, counted !== undefined) > modelContextLimit;
|
|
2815
2934
|
}
|
|
2816
2935
|
/** Output room a native-fit check reserves: the model's floor for an exact
|
|
2817
2936
|
* count, else (an estimate) the full max_tokens. */
|
|
@@ -2998,12 +3117,18 @@ function withFlattenCacheBreakpoint(body, ttl) {
|
|
|
2998
3117
|
if (messages.length !== 1)
|
|
2999
3118
|
return messages;
|
|
3000
3119
|
const only = messages[0];
|
|
3001
|
-
const text = typeof only.content === "string" ? only.content : undefined;
|
|
3002
|
-
if (text === undefined)
|
|
3003
|
-
return messages;
|
|
3004
3120
|
const cacheControl = { type: "ephemeral" };
|
|
3005
3121
|
if (ttl !== undefined)
|
|
3006
3122
|
cacheControl.ttl = ttl;
|
|
3123
|
+
if (Array.isArray(only.content) && only.content.length > 0) {
|
|
3124
|
+
// A flatten that kept images: the breakpoint goes on its last block.
|
|
3125
|
+
const blocks = only.content.map((block) => ({ ...block }));
|
|
3126
|
+
blocks[blocks.length - 1].cache_control = cacheControl;
|
|
3127
|
+
return [{ ...only, content: blocks }];
|
|
3128
|
+
}
|
|
3129
|
+
const text = typeof only.content === "string" ? only.content : undefined;
|
|
3130
|
+
if (text === undefined)
|
|
3131
|
+
return messages;
|
|
3007
3132
|
return [{ ...only, content: [{ type: "text", text, cache_control: cacheControl }] }];
|
|
3008
3133
|
}
|
|
3009
3134
|
/**
|
|
@@ -3147,7 +3272,9 @@ async function runBlockingCompression(args) {
|
|
|
3147
3272
|
// whatever the fallback gate would refuse to send uncompressed; a bytes/4
|
|
3148
3273
|
// guess is not sent, as it would force needless compression.
|
|
3149
3274
|
...(args.inputCandidate && args.sizing.clientInputTokens(args.inputCandidate) !== undefined
|
|
3150
|
-
? { clientInputTokens: args.sizing.clientInputTokens(args.inputCandidate)
|
|
3275
|
+
? { clientInputTokens: args.sizing.clientInputTokens(args.inputCandidate),
|
|
3276
|
+
// Only an exact count of these bytes may loosen the server's sizing.
|
|
3277
|
+
clientInputTokensExact: args.sizing.isExact(args.inputCandidate) }
|
|
3151
3278
|
: {}),
|
|
3152
3279
|
...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
|
|
3153
3280
|
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
@@ -3592,8 +3719,11 @@ async function recoverToolRouteMiss(args) {
|
|
|
3592
3719
|
// Nothing to ride next turn, or a result that still does not fit the
|
|
3593
3720
|
// window (the next turn would reject it and compress again): wait for
|
|
3594
3721
|
// growth before the lane tries again.
|
|
3595
|
-
|
|
3722
|
+
const exactTokens = args.sizing.exactTokensFor(compressedRaw);
|
|
3723
|
+
if (!sizeHolder ||
|
|
3724
|
+
compressedResultExceedsWindow(body, rec, compressedRaw, modelContextLimit, exactTokens)) {
|
|
3596
3725
|
backOff();
|
|
3726
|
+
}
|
|
3597
3727
|
// A reservation that lost its race (stale) or never reached
|
|
3598
3728
|
// protocol-complete (upstream 5xx, truncated stream) installed nothing, so
|
|
3599
3729
|
// it must stop suppressing whoever is still trying to install.
|
|
@@ -3766,13 +3896,16 @@ async function selectForwardCandidate(args) {
|
|
|
3766
3896
|
return changed ? candidate.body : intended;
|
|
3767
3897
|
}
|
|
3768
3898
|
/**
|
|
3769
|
-
* Whether delivery would refuse `candidate` sent whole
|
|
3770
|
-
*
|
|
3771
|
-
*
|
|
3772
|
-
*
|
|
3899
|
+
* Whether delivery would refuse `candidate` sent whole, by delivery's own
|
|
3900
|
+
* admission: it is sendable only when its plan fits and either an exact count
|
|
3901
|
+
* or its conservative per-model bound (with the full max_tokens) fits too;
|
|
3902
|
+
* otherwise the measurement delivery would use (a count, else that bound)
|
|
3903
|
+
* decides. Only when it is refused is there no fallback, and only then may a
|
|
3904
|
+
* compress take the long budget.
|
|
3773
3905
|
*/
|
|
3774
3906
|
async function wholeRequestRefused(sizing, candidate, limits) {
|
|
3775
|
-
if (fitsNative(sizing.plan(candidate).tokens, limits)
|
|
3907
|
+
if (fitsNative(sizing.plan(candidate).tokens, limits) &&
|
|
3908
|
+
(sizing.isExact(candidate) || sizing.boundFits(candidate, limits)))
|
|
3776
3909
|
return false;
|
|
3777
3910
|
return !fitsMeasured(await sizing.measure(candidate, limits), limits);
|
|
3778
3911
|
}
|
|
@@ -3781,12 +3914,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
3781
3914
|
const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
3782
3915
|
args.rec.forwardedBytes = 0;
|
|
3783
3916
|
args.rec.approxInputTokens = 0;
|
|
3784
|
-
|
|
3917
|
+
args.state.diagnostics?.record("refusal", {
|
|
3918
|
+
status: failure.status, attempt: failure.attempt,
|
|
3919
|
+
input_ktokens: Math.round(inputTokens / 1000), budget_ktokens: Math.round(budgetTokens / 1000),
|
|
3920
|
+
});
|
|
3921
|
+
// To requests.jsonl, never the console: Claude Code's TUI shows stderr inline.
|
|
3922
|
+
args.rec.refusal = {
|
|
3785
3923
|
lane: args.routeKey, attempt: failure.attempt, status: failure.status,
|
|
3786
3924
|
inputTokens, outputTokens: args.body.max_tokens ?? 0,
|
|
3787
3925
|
outputReservation: bodyOutputReservation(args.body), indexBehind: args.rec.indexBehind === true,
|
|
3788
3926
|
budgetTokens, modelContextLimit: args.modelContextLimit,
|
|
3789
|
-
}
|
|
3927
|
+
};
|
|
3790
3928
|
if (args.res.headersSent || args.res.destroyed)
|
|
3791
3929
|
return false;
|
|
3792
3930
|
args.res.writeHead(failure.status, { "content-type": "application/json", ...failure.headers });
|
|
@@ -4529,6 +4667,7 @@ function readAll(stream) {
|
|
|
4529
4667
|
/** Test seam. */
|
|
4530
4668
|
export const __testCapCacheBreakpoints = capCacheBreakpoints;
|
|
4531
4669
|
export const __testEstimateRequestTokens = estimateRequestTokens;
|
|
4670
|
+
export const __testCompressedResultExceedsWindow = compressedResultExceedsWindow;
|
|
4532
4671
|
/** Reads message times from the session's transcript, when there is one. */
|
|
4533
4672
|
function transcriptTimesFor(opts, sessionId, agentId) {
|
|
4534
4673
|
const source = opts.transcriptUsage;
|