claude-code-infinite 1.0.28 → 1.0.30
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 +96 -19
- package/dist/auto-update.d.ts +2 -0
- package/dist/auto-update.d.ts.map +1 -1
- package/dist/auto-update.js +4 -0
- package/dist/auto-update.js.map +1 -1
- package/dist/claude-auth.d.ts +27 -2
- package/dist/claude-auth.d.ts.map +1 -1
- package/dist/claude-auth.js +76 -4
- package/dist/claude-auth.js.map +1 -1
- package/dist/claude-install.d.ts +73 -0
- package/dist/claude-install.d.ts.map +1 -0
- package/dist/claude-install.js +142 -0
- package/dist/claude-install.js.map +1 -0
- package/dist/claude-plan-warning.d.ts +70 -7
- package/dist/claude-plan-warning.d.ts.map +1 -1
- package/dist/claude-plan-warning.js +152 -8
- package/dist/claude-plan-warning.js.map +1 -1
- package/dist/cli-lifecycle.d.ts +45 -1
- package/dist/cli-lifecycle.d.ts.map +1 -1
- package/dist/cli-lifecycle.js +59 -0
- package/dist/cli-lifecycle.js.map +1 -1
- package/dist/cli-main.js +136 -29
- package/dist/cli-main.js.map +1 -1
- package/dist/count-tokens.d.ts +15 -0
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +23 -1
- package/dist/count-tokens.js.map +1 -1
- package/dist/diagnostics-schema.d.ts +4 -0
- package/dist/diagnostics-schema.d.ts.map +1 -1
- package/dist/diagnostics-schema.js +25 -0
- package/dist/diagnostics-schema.js.map +1 -1
- package/dist/diagnostics.d.ts +24 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +86 -5
- package/dist/diagnostics.js.map +1 -1
- package/dist/final-index.d.ts +7 -1
- package/dist/final-index.d.ts.map +1 -1
- package/dist/final-index.js +10 -3
- package/dist/final-index.js.map +1 -1
- package/dist/image-refs.d.ts +51 -0
- package/dist/image-refs.d.ts.map +1 -0
- package/dist/image-refs.js +169 -0
- package/dist/image-refs.js.map +1 -0
- package/dist/image-tokens.d.ts +37 -0
- package/dist/image-tokens.d.ts.map +1 -0
- package/dist/image-tokens.js +158 -0
- package/dist/image-tokens.js.map +1 -0
- package/dist/install-ping.d.ts +54 -0
- package/dist/install-ping.d.ts.map +1 -0
- package/dist/install-ping.js +177 -0
- package/dist/install-ping.js.map +1 -0
- package/dist/memtree-body.d.ts +52 -0
- package/dist/memtree-body.d.ts.map +1 -0
- package/dist/memtree-body.js +99 -0
- package/dist/memtree-body.js.map +1 -0
- package/dist/memtree-finder.d.ts +2 -1
- package/dist/memtree-finder.d.ts.map +1 -1
- package/dist/memtree-finder.js +4 -5
- package/dist/memtree-finder.js.map +1 -1
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +17 -7
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree-times.d.ts +35 -0
- package/dist/memtree-times.d.ts.map +1 -0
- package/dist/memtree-times.js +166 -0
- package/dist/memtree-times.js.map +1 -0
- package/dist/memtree-tools.d.ts +12 -0
- package/dist/memtree-tools.d.ts.map +1 -1
- package/dist/memtree-tools.js +63 -7
- package/dist/memtree-tools.js.map +1 -1
- package/dist/memtree.d.ts +77 -3
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +267 -53
- package/dist/memtree.js.map +1 -1
- package/dist/paused-session.d.ts.map +1 -1
- package/dist/paused-session.js +11 -8
- package/dist/paused-session.js.map +1 -1
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +4 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/prefix-store.d.ts +2 -0
- package/dist/prefix-store.d.ts.map +1 -1
- package/dist/prefix-store.js +4 -0
- package/dist/prefix-store.js.map +1 -1
- package/dist/prepared-compression.d.ts +78 -0
- package/dist/prepared-compression.d.ts.map +1 -0
- package/dist/prepared-compression.js +127 -0
- package/dist/prepared-compression.js.map +1 -0
- package/dist/proxy.d.ts +370 -13
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +790 -92
- package/dist/proxy.js.map +1 -1
- package/dist/rate-limit.d.ts +64 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +126 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/reqlog.d.ts +66 -2
- 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 +57 -11
- package/dist/request-sizing.js.map +1 -1
- package/dist/route-identity.d.ts +27 -0
- package/dist/route-identity.d.ts.map +1 -1
- package/dist/route-identity.js +88 -8
- package/dist/route-identity.js.map +1 -1
- package/dist/token-estimate.d.ts +3 -20
- package/dist/token-estimate.d.ts.map +1 -1
- package/dist/token-estimate.js +9 -50
- package/dist/token-estimate.js.map +1 -1
- package/dist/tool-output-store.d.ts +12 -0
- package/dist/tool-output-store.d.ts.map +1 -0
- package/dist/tool-output-store.js +146 -0
- package/dist/tool-output-store.js.map +1 -0
- package/dist/tool-result-truncation.d.ts +32 -0
- package/dist/tool-result-truncation.d.ts.map +1 -0
- package/dist/tool-result-truncation.js +135 -0
- package/dist/tool-result-truncation.js.map +1 -0
- package/package.json +2 -1
package/dist/proxy.js
CHANGED
|
@@ -43,7 +43,9 @@ import { PromptAccounting } from "./prompt-accounting.js";
|
|
|
43
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
|
-
import {
|
|
46
|
+
import { extendsHistory, mediaFreeMessageHash, prefixIdentity, prefixMessageMatches, } from "./route-identity.js";
|
|
47
|
+
import { COMPRESSION_RESERVE_TOKENS, compressionTrigger, PreparedCompressions, prepareStartTokens } from "./prepared-compression.js";
|
|
48
|
+
import { truncateToolResults } from "./tool-result-truncation.js";
|
|
47
49
|
import http from "node:http";
|
|
48
50
|
import https from "node:https";
|
|
49
51
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -62,7 +64,9 @@ import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
|
62
64
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
63
65
|
import { isResumableSessionId } from "./resume-hint.js";
|
|
64
66
|
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
67
|
+
import { bodyImages } from "./image-tokens.js";
|
|
65
68
|
import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
|
|
69
|
+
import { rateLimitFromResponse } from "./rate-limit.js";
|
|
66
70
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
67
71
|
const DEFAULT_UPSTREAM = "https://api.anthropic.com";
|
|
68
72
|
const HOOK_BODY_LIMIT = 64 * 1024;
|
|
@@ -77,6 +81,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
|
|
|
77
81
|
const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
|
|
78
82
|
/** Safety cap on a long-budget compress, should a transport ignore its abort. */
|
|
79
83
|
const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
|
|
84
|
+
/** The most a blocking compress with a fallback may wait (its budget grows with its upload). */
|
|
85
|
+
const MAX_COMPRESS_WAIT_MS = 60_000;
|
|
80
86
|
const SKIP_REQUEST_HEADERS = new Set([
|
|
81
87
|
"host",
|
|
82
88
|
"connection",
|
|
@@ -94,23 +100,41 @@ const SKIP_RESPONSE_HEADERS = new Set([
|
|
|
94
100
|
"transfer-encoding",
|
|
95
101
|
]);
|
|
96
102
|
/**
|
|
97
|
-
* Budget fallback until the server reports `model_budget_tokens
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
103
|
+
* Budget fallback until the server reports `model_budget_tokens`, mirroring the
|
|
104
|
+
* server's ccc policy (polychat index_budget.py near_window_budget_tokens):
|
|
105
|
+
* every Claude model compresses near its window, at the window less the
|
|
106
|
+
* reply's room (COMPRESSION_RESERVE_TOKENS, 40k) and never below
|
|
107
|
+
* FALLBACK_BUDGET_WINDOW_RATIO of it: 960k of 1M, 160k of 200k; at most a
|
|
108
|
+
* Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
|
|
109
|
+
* FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts at
|
|
110
|
+
* window - 33k (967k of 1M), so MemTree compresses just before it would; ccc
|
|
102
111
|
* lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
|
|
103
|
-
* jumps past the budget reaches the proxy and is compressed here
|
|
112
|
+
* jumps past the budget reaches the proxy and is compressed here, on a
|
|
113
|
+
* prepared prefix when there is one (prepared-compression.ts).
|
|
104
114
|
*/
|
|
105
115
|
export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
/** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
|
|
117
|
+
const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
|
|
118
|
+
/**
|
|
119
|
+
* polychat USABLE_CONTEXT_CEILING_TOKENS_BY_MODEL: the Sonnets degrade well
|
|
120
|
+
* before their window. Sonnet 5 / 5.5 550k; every other Sonnet 400k. First match wins.
|
|
121
|
+
*/
|
|
122
|
+
const SONNET_CEILINGS = [
|
|
123
|
+
[/sonnet-5(?![0-9])/i, 550_000],
|
|
124
|
+
[/sonnet/i, 400_000],
|
|
125
|
+
];
|
|
126
|
+
/** The fallback whole-request budget for `model` on this window (see above). */
|
|
127
|
+
export function fallbackBudgetTokens(model, modelContextLimit) {
|
|
128
|
+
if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
|
|
129
|
+
return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
|
|
130
|
+
}
|
|
131
|
+
const nearWindow = nearWindowBudgetTokens(modelContextLimit);
|
|
132
|
+
const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
|
|
133
|
+
return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
|
|
134
|
+
}
|
|
135
|
+
/** A Claude model's near-window budget before any ceiling (fallbackBudgetTokens). */
|
|
136
|
+
function nearWindowBudgetTokens(modelContextLimit) {
|
|
137
|
+
return Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), modelContextLimit - COMPRESSION_RESERVE_TOKENS);
|
|
114
138
|
}
|
|
115
139
|
/**
|
|
116
140
|
* An explicit target is capped at this share of the budget: a compaction to
|
|
@@ -303,6 +327,7 @@ export function startProxy(opts) {
|
|
|
303
327
|
const activeRequests = new Set();
|
|
304
328
|
const acceptedRequests = new Set();
|
|
305
329
|
const shutdownAbort = new AbortController();
|
|
330
|
+
const backgroundAbort = new AbortController();
|
|
306
331
|
const state = {
|
|
307
332
|
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
308
333
|
keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
@@ -313,6 +338,7 @@ export function startProxy(opts) {
|
|
|
313
338
|
pausedDetail: () => opts.memtree.pausedDetail,
|
|
314
339
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
315
340
|
sessionStartNotice: opts.sessionStartNotice,
|
|
341
|
+
promptSubmitNotice: opts.promptSubmitNotice,
|
|
316
342
|
projectTrees: opts.projectTrees,
|
|
317
343
|
diagnostics: opts.diagnostics,
|
|
318
344
|
notices: new NoticeDeliveryQueue(),
|
|
@@ -329,6 +355,7 @@ export function startProxy(opts) {
|
|
|
329
355
|
toolRecoveryAttemptedLanes: new Map(),
|
|
330
356
|
toolRecoveryCooldownUntil: 0,
|
|
331
357
|
shutdownSignal: shutdownAbort.signal,
|
|
358
|
+
backgroundSignal: backgroundAbort.signal,
|
|
332
359
|
routeInstallFault: opts.routeInstallFault,
|
|
333
360
|
memtreePages: new Map(),
|
|
334
361
|
memorySourcePages: new Map(),
|
|
@@ -344,6 +371,9 @@ export function startProxy(opts) {
|
|
|
344
371
|
defaultCompactOff: opts.defaultCompactTarget === null,
|
|
345
372
|
compactNow: new Set(),
|
|
346
373
|
stablePrefixes: new Map(),
|
|
374
|
+
prepared: new PreparedCompressions(),
|
|
375
|
+
preparing: new Map(),
|
|
376
|
+
backgroundCompress: new Map(),
|
|
347
377
|
serverBudgets: new Map(),
|
|
348
378
|
serverReportsBudget: false,
|
|
349
379
|
countTokensCooldowns: new CountTokensCooldowns(),
|
|
@@ -394,6 +424,7 @@ export function startProxy(opts) {
|
|
|
394
424
|
const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
395
425
|
? Math.floor(timeoutMs)
|
|
396
426
|
: 5_000;
|
|
427
|
+
backgroundAbort.abort();
|
|
397
428
|
const quiesced = (async () => {
|
|
398
429
|
// Once close completes, every accepted request has entered the tracked
|
|
399
430
|
// set.
|
|
@@ -790,6 +821,17 @@ onMainStop) {
|
|
|
790
821
|
state.promptAccounting.add(parsed.session_id, parsed.prompt_id, parsed.prompt);
|
|
791
822
|
recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
|
|
792
823
|
}
|
|
824
|
+
const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
|
|
825
|
+
if (line) {
|
|
826
|
+
const body = Buffer.from(JSON.stringify({ systemMessage: line }), "utf-8");
|
|
827
|
+
res.writeHead(200, {
|
|
828
|
+
"content-type": "application/json",
|
|
829
|
+
"content-length": String(body.length),
|
|
830
|
+
"cache-control": "no-store",
|
|
831
|
+
});
|
|
832
|
+
res.end(body);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
793
835
|
res.writeHead(204);
|
|
794
836
|
res.end();
|
|
795
837
|
return;
|
|
@@ -1060,7 +1102,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1060
1102
|
if (!rides) {
|
|
1061
1103
|
const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
|
|
1062
1104
|
if (measured.source === "exact") {
|
|
1063
|
-
sizeSlot.set(
|
|
1105
|
+
sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
|
|
1064
1106
|
}
|
|
1065
1107
|
}
|
|
1066
1108
|
rec.routeLane = requestRouteLane;
|
|
@@ -1121,12 +1163,18 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1121
1163
|
},
|
|
1122
1164
|
// The ride's bytes are only what failed; recovery always compresses the
|
|
1123
1165
|
// ORIGINAL conversation (the one MemTree indexed), never the ride itself.
|
|
1124
|
-
recoverLate: async (_ride, rideTokens) => {
|
|
1125
|
-
|
|
1166
|
+
recoverLate: async (_ride, rideTokens, target) => {
|
|
1167
|
+
// A compression already made may be redone only to a smaller target.
|
|
1168
|
+
if ((rec.compress && target === undefined) ||
|
|
1169
|
+
compactionMode(opts, state, requestSessionKey).mode === "off" ||
|
|
1170
|
+
!hasEarlierNonToolUserMessage(messages) ||
|
|
1126
1171
|
(rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
|
|
1127
1172
|
return undefined;
|
|
1128
1173
|
return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
|
|
1129
|
-
modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate
|
|
1174
|
+
modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
|
|
1175
|
+
route: { key: requestRouteKey, messages, epoch: routeEpoch,
|
|
1176
|
+
lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
|
|
1177
|
+
...(target !== undefined ? { target } : {}) });
|
|
1130
1178
|
},
|
|
1131
1179
|
delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
|
|
1132
1180
|
});
|
|
@@ -1273,6 +1321,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1273
1321
|
ride: toolRide,
|
|
1274
1322
|
canCompress,
|
|
1275
1323
|
});
|
|
1324
|
+
if (isMainRequest && toolSessionId !== undefined && canCompress &&
|
|
1325
|
+
(plan.kind === "ride" || plan.kind === "pass")) {
|
|
1326
|
+
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
|
|
1327
|
+
sessionId: toolSessionId, modelContextLimit, rec, clientMeta });
|
|
1328
|
+
}
|
|
1276
1329
|
if (plan.kind === "ride")
|
|
1277
1330
|
toolRide = plan.ride;
|
|
1278
1331
|
if (plan.kind === "pass" && plan.original) {
|
|
@@ -1312,9 +1365,13 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1312
1365
|
// for growth instead of paying a blocking call on every tool turn.
|
|
1313
1366
|
rec.routeRecovery = { outcome: "backoff" };
|
|
1314
1367
|
}
|
|
1315
|
-
else if (Date.now() < state.toolRecoveryCooldownUntil
|
|
1368
|
+
else if (Date.now() < state.toolRecoveryCooldownUntil &&
|
|
1369
|
+
!(plan.overWindow && !plan.fallback && (!plan.uncountedOverWindowOnly ||
|
|
1370
|
+
await wholeRequestRefused(requestSizing, originalCandidate, sizingLimits())))) {
|
|
1316
1371
|
// A recent attempt burned the full compress budget and still
|
|
1317
|
-
// failed.
|
|
1372
|
+
// failed. (Not when this turn is over the window with no prefix to
|
|
1373
|
+
// ride and delivery would refuse it: skipping could only end in a
|
|
1374
|
+
// refusal, so it compresses anyway, on the long budget.) Every tool turn appends a tool_result and rehashes, so
|
|
1318
1375
|
// compress() dedup can never absorb the repeat: without this
|
|
1319
1376
|
// cooldown a MemTree outage would add the whole blocking budget to
|
|
1320
1377
|
// every tool turn over the budget. A cooldown skip does not start
|
|
@@ -1395,9 +1452,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1395
1452
|
return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1396
1453
|
toolForwarding.settle(delivered);
|
|
1397
1454
|
if (toolRide)
|
|
1398
|
-
noteRideSize(toolRide.sizeHolder, rec, sendBody
|
|
1455
|
+
noteRideSize(toolRide.sizeHolder, rec, sendBody);
|
|
1399
1456
|
else {
|
|
1400
|
-
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody
|
|
1457
|
+
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
|
|
1401
1458
|
}
|
|
1402
1459
|
}));
|
|
1403
1460
|
}
|
|
@@ -1440,19 +1497,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1440
1497
|
const sessionId = requestSessionId(req);
|
|
1441
1498
|
// Stable-prefix (edge) compaction: the main thread's human turns only. The
|
|
1442
1499
|
// recap rides the main route above; subagents keep per-turn compression.
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1500
|
+
const planEdge = () => planEdgeCompaction({ sizing: requestSizing, opts, state, body, messages,
|
|
1501
|
+
sessionId: sessionId, modelContextLimit, forwardBody, rec });
|
|
1502
|
+
let edge = isMainRequest && sessionId !== undefined ? await planEdge() : undefined;
|
|
1503
|
+
if (edge?.kind === "compress" && (state.preparing.has(sessionId) ||
|
|
1504
|
+
rec.compaction?.preparedWaitMs !== undefined || rec.compaction?.prepared !== undefined)) {
|
|
1505
|
+
// Never beside the session's preparation (both would be billed): wait for
|
|
1506
|
+
// it, then plan again with the settings now current (a /memtree-compact
|
|
1507
|
+
// that arrived while it waited or counted) and the preparation it left.
|
|
1508
|
+
const waited = rec.compaction?.preparedWaitMs ?? 0;
|
|
1509
|
+
const prepared = rec.compaction?.prepared;
|
|
1510
|
+
await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
|
|
1511
|
+
const total = waited + (rec.compaction?.preparedWaitMs ?? 0);
|
|
1512
|
+
edge = await planEdge();
|
|
1513
|
+
if (rec.compaction) {
|
|
1514
|
+
rec.compaction.preparedWaitMs = total;
|
|
1515
|
+
rec.compaction.prepared ??= prepared;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
// Not beside a blocking compress call: the two would overlap on the server.
|
|
1519
|
+
if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
|
|
1520
|
+
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
|
|
1521
|
+
modelContextLimit, rec, clientMeta });
|
|
1522
|
+
}
|
|
1456
1523
|
// An active subagent can repeat/embed the human prompt in its own request;
|
|
1457
1524
|
// producer suppression must also preserve the arm for the later main call.
|
|
1458
1525
|
if (displayForThisTurn)
|
|
@@ -1467,6 +1534,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1467
1534
|
// A lagging index left history verbatim: a refusal waits for indexing.
|
|
1468
1535
|
if (result)
|
|
1469
1536
|
noteIndexLag(opts, rec, result);
|
|
1537
|
+
// Too large for the window: delivery recompresses it once, smaller.
|
|
1538
|
+
if (result && !rec.indexBehind) {
|
|
1539
|
+
noteOversizedCompression(rec, requestSizing, body, compressedRaw, rec.compaction?.targetTokens);
|
|
1540
|
+
}
|
|
1470
1541
|
releaseUncommittedRouteDecision();
|
|
1471
1542
|
recordTurn(rec, "followup-degraded", rawBody);
|
|
1472
1543
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
|
|
@@ -1567,7 +1638,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1567
1638
|
: stable.installed
|
|
1568
1639
|
: undefined;
|
|
1569
1640
|
if (sizePrefix)
|
|
1570
|
-
notePrefixSize(sizePrefix, rec, compressedRaw
|
|
1641
|
+
notePrefixSize(sizePrefix, rec, compressedRaw);
|
|
1571
1642
|
// A route already installed at protocol-complete deliberately survives
|
|
1572
1643
|
// delivered=false: that settle may be the fast-tool abort (client
|
|
1573
1644
|
// consumed message_stop, closed the SSE response, and its immediate
|
|
@@ -1604,7 +1675,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1604
1675
|
}
|
|
1605
1676
|
}));
|
|
1606
1677
|
};
|
|
1607
|
-
if (existingRide && (!edge || edge.kind === "off")) {
|
|
1678
|
+
if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
|
|
1608
1679
|
const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
|
|
1609
1680
|
const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
|
|
1610
1681
|
candidateSizeOwners.set(candidate, owner);
|
|
@@ -1636,8 +1707,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1636
1707
|
const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
|
|
1637
1708
|
const noteWholeSize = () => {
|
|
1638
1709
|
if (isMainRequest)
|
|
1639
|
-
notePassthroughSize(state, sessionId, body.model, rec, forwardBody
|
|
1710
|
+
notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
|
|
1640
1711
|
};
|
|
1712
|
+
if (edge?.kind === "pass") {
|
|
1713
|
+
// Clearly under the budget by its own count: the compress call could only
|
|
1714
|
+
// pass it through, so the turn goes out now and the call (which also
|
|
1715
|
+
// indexes it and reports the page and budget) runs in the background.
|
|
1716
|
+
compressInBackground({ opts, state, body, msgsForMemtree, hash, modelContextLimit, rec,
|
|
1717
|
+
sessionId, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
|
|
1718
|
+
// Never compress: the turn went out whole, so a compression the server's
|
|
1719
|
+
// own (more conservative) sizing chose could not be used, only billed.
|
|
1720
|
+
compaction: { target: NEVER_COMPRESS_TARGET_TOKENS },
|
|
1721
|
+
linkSeq: isAwaySummary ? undefined : nextMemtreeCallSeq(state) });
|
|
1722
|
+
releaseForOriginalFallback();
|
|
1723
|
+
sendingWhole();
|
|
1724
|
+
recordTurn(rec, "followup-under-budget", forwardBody);
|
|
1725
|
+
capture(opts, "anthropic-request", forwardBody);
|
|
1726
|
+
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec)
|
|
1727
|
+
.then((delivered) => {
|
|
1728
|
+
noteWholeSize();
|
|
1729
|
+
if (delivered)
|
|
1730
|
+
markMainPromptDelivered();
|
|
1731
|
+
}));
|
|
1732
|
+
}
|
|
1641
1733
|
// The complete request upload can outlive its downstream subscriber while
|
|
1642
1734
|
// MemTree compression is in flight. Track that subscriber locally without
|
|
1643
1735
|
// feeding its lifetime into MemtreeClient.compress(): compression promises
|
|
@@ -1687,7 +1779,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1687
1779
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
|
|
1688
1780
|
.then((delivered) => {
|
|
1689
1781
|
if (isMainRequest)
|
|
1690
|
-
notePassthroughSize(state, sessionId, body.model, rec, rawBody
|
|
1782
|
+
notePassthroughSize(state, sessionId, body.model, rec, rawBody);
|
|
1691
1783
|
if (delivered)
|
|
1692
1784
|
markMainPromptDelivered();
|
|
1693
1785
|
}));
|
|
@@ -1762,7 +1854,24 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1762
1854
|
state.nearWindowNotice = handle;
|
|
1763
1855
|
}
|
|
1764
1856
|
else if (mayQueueNotice && paymentDetail === null) {
|
|
1765
|
-
|
|
1857
|
+
// Under the budget the whole request is what compression would have
|
|
1858
|
+
// sent: the turn is not degraded, so the failure stays in the logs.
|
|
1859
|
+
const warn = () => {
|
|
1860
|
+
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
1861
|
+
rec.degradedNotice = "shown";
|
|
1862
|
+
};
|
|
1863
|
+
if (compressionNeeded(rec.compaction)) {
|
|
1864
|
+
warn();
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
// Delivery sizes the request again (an exact count, else a
|
|
1868
|
+
// conservative bound): a turn it finds over the budget warns then.
|
|
1869
|
+
rec.degradedNotice = "quiet-under-budget";
|
|
1870
|
+
quietDegrades.set(rec, () => {
|
|
1871
|
+
if (state.mainPromptGeneration === noticePromptGeneration)
|
|
1872
|
+
warn();
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1766
1875
|
}
|
|
1767
1876
|
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1768
1877
|
noteWholeSize();
|
|
@@ -1996,6 +2105,15 @@ function resumePaymentNotice(state, input) {
|
|
|
1996
2105
|
state.diagnostics?.record("paused_notice", { trigger: "resume" });
|
|
1997
2106
|
return state.notices.warningText(paymentNoticeText(detail));
|
|
1998
2107
|
}
|
|
2108
|
+
/** ProxyOptions.promptSubmitNotice's line for this prompt; never throws. */
|
|
2109
|
+
function promptSubmitLine(state) {
|
|
2110
|
+
try {
|
|
2111
|
+
return state.promptSubmitNotice?.();
|
|
2112
|
+
}
|
|
2113
|
+
catch {
|
|
2114
|
+
return undefined;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
1999
2117
|
/** The caller's extra SessionStart line; a throwing provider shows nothing. */
|
|
2000
2118
|
function extraSessionStartLine(provider, input) {
|
|
2001
2119
|
if (!provider || input.agent_id !== undefined || input.source === "compact")
|
|
@@ -2260,7 +2378,8 @@ function rememberPage(pages, sessionId, page) {
|
|
|
2260
2378
|
*/
|
|
2261
2379
|
function noteMemtreeHealth(state, compression) {
|
|
2262
2380
|
if (compression.liveHealth === "failure") {
|
|
2263
|
-
|
|
2381
|
+
// Background calls the drain stopped (backgroundSignal) are no outage either.
|
|
2382
|
+
if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
|
|
2264
2383
|
return;
|
|
2265
2384
|
state.toolRecoveryCooldownUntil =
|
|
2266
2385
|
Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
|
|
@@ -2343,7 +2462,7 @@ stable) {
|
|
|
2343
2462
|
// Include trailing ambient role=system blocks: MemTree consolidated them
|
|
2344
2463
|
// into compressedBody.system, so treating them as suffix would duplicate
|
|
2345
2464
|
// those instructions on every tool request.
|
|
2346
|
-
|
|
2465
|
+
...prefixIdentity(originalMessages, "route"),
|
|
2347
2466
|
compressedMessages: cloneJson(compressedBody.messages),
|
|
2348
2467
|
compressedSystem: cloneJson(compressedBody.system),
|
|
2349
2468
|
hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
|
|
@@ -2382,7 +2501,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2382
2501
|
const prefix = {
|
|
2383
2502
|
sessionId,
|
|
2384
2503
|
originalSystemHash: compressed.originalSystemHash,
|
|
2385
|
-
|
|
2504
|
+
...prefixIdentity(originalMessages, "stable"),
|
|
2386
2505
|
compressedMessages: compressed.compressedMessages,
|
|
2387
2506
|
compressedSystem: compressed.compressedSystem,
|
|
2388
2507
|
hasCompressedSystem: compressed.hasCompressedSystem,
|
|
@@ -2393,6 +2512,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2393
2512
|
stable.installed = prefix;
|
|
2394
2513
|
boundedSet(state.stablePrefixes, sessionId, prefix);
|
|
2395
2514
|
state.compactNow.delete(sessionId);
|
|
2515
|
+
// Prepared against the old prefix's ride sizes: a fresh one is made when
|
|
2516
|
+
// the new prefix's rides near the budget again.
|
|
2517
|
+
state.prepared.discard(sessionId);
|
|
2396
2518
|
return prefix;
|
|
2397
2519
|
}
|
|
2398
2520
|
/**
|
|
@@ -2413,6 +2535,7 @@ function persistStablePrefix(state, sessionId, prefix) {
|
|
|
2413
2535
|
store.save(sessionId, owner, {
|
|
2414
2536
|
originalSystemHash: prefix.originalSystemHash,
|
|
2415
2537
|
originalPrefixHashes: prefix.originalPrefixHashes,
|
|
2538
|
+
originalMediaFreeHashes: prefix.originalMediaFreeHashes,
|
|
2416
2539
|
compressedMessages: prefix.compressedMessages,
|
|
2417
2540
|
compressedSystem: prefix.compressedSystem,
|
|
2418
2541
|
hasCompressedSystem: prefix.hasCompressedSystem,
|
|
@@ -2465,7 +2588,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
|
|
|
2465
2588
|
function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
|
|
2466
2589
|
if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
|
|
2467
2590
|
return "defer";
|
|
2468
|
-
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId,
|
|
2591
|
+
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
|
|
2469
2592
|
if (miss === null)
|
|
2470
2593
|
return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
|
|
2471
2594
|
const authoritative = miss === "prefix" &&
|
|
@@ -2495,7 +2618,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
|
|
|
2495
2618
|
* at least one message after them. The suffix may be anything, human turns
|
|
2496
2619
|
* included.
|
|
2497
2620
|
*/
|
|
2498
|
-
function prefixMismatch(body, messages, prefix, sessionId,
|
|
2621
|
+
function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
|
|
2499
2622
|
if (!sessionId || prefix.sessionId !== sessionId)
|
|
2500
2623
|
return "session";
|
|
2501
2624
|
if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
|
|
@@ -2505,7 +2628,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
|
|
|
2505
2628
|
if (messages.length < prefixLength)
|
|
2506
2629
|
return "prefix";
|
|
2507
2630
|
for (let i = 0; i < prefixLength; i++) {
|
|
2508
|
-
if (
|
|
2631
|
+
if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
|
|
2509
2632
|
return "prefix";
|
|
2510
2633
|
}
|
|
2511
2634
|
}
|
|
@@ -2549,20 +2672,24 @@ function serverBudgetKey(model, modelContextLimit) {
|
|
|
2549
2672
|
return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
|
|
2550
2673
|
}
|
|
2551
2674
|
/**
|
|
2552
|
-
* The session's whole-request budget
|
|
2553
|
-
*
|
|
2554
|
-
*
|
|
2555
|
-
* `model_budget_tokens`, and the first request before
|
|
2675
|
+
* The session's whole-request budget, the size at which a request is
|
|
2676
|
+
* compressed: CCC_BUDGET_TOKENS when set (tests), else the model budget the
|
|
2677
|
+
* server last reported for this model and window, else fallbackBudgetTokens
|
|
2678
|
+
* (servers that predate `model_budget_tokens`, and the first request before
|
|
2679
|
+
* any response). Never above the window less the reply's room
|
|
2680
|
+
* (compressionTrigger): a request is compressed when its projected size plus
|
|
2681
|
+
* that reserve would exceed the window, or the budget.
|
|
2556
2682
|
*/
|
|
2557
2683
|
function resolveBudget(opts, state, model, modelContextLimit) {
|
|
2684
|
+
const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
|
|
2558
2685
|
if (opts.budgetTokensOverride !== undefined) {
|
|
2559
|
-
return { tokens: opts.budgetTokensOverride, source: "override" };
|
|
2686
|
+
return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
|
|
2560
2687
|
}
|
|
2561
2688
|
const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
|
|
2562
2689
|
if (reported !== undefined)
|
|
2563
|
-
return { tokens: reported, source: "server" };
|
|
2690
|
+
return { tokens: trigger(reported), source: "server" };
|
|
2564
2691
|
return {
|
|
2565
|
-
tokens:
|
|
2692
|
+
tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
|
|
2566
2693
|
source: "window-ratio",
|
|
2567
2694
|
};
|
|
2568
2695
|
}
|
|
@@ -2645,17 +2772,17 @@ function reportedInputTokens(rec) {
|
|
|
2645
2772
|
(usage.cache_creation_input_tokens ?? 0));
|
|
2646
2773
|
}
|
|
2647
2774
|
/** Keep the newest (largest-body) reported size of a request on this prefix. */
|
|
2648
|
-
function notePrefixSize(prefix, rec,
|
|
2649
|
-
const sample = reportedSample(rec,
|
|
2775
|
+
function notePrefixSize(prefix, rec, sent) {
|
|
2776
|
+
const sample = reportedSample(rec, sent);
|
|
2650
2777
|
if (sample === undefined)
|
|
2651
2778
|
return;
|
|
2652
|
-
if (prefix.lastSize &&
|
|
2779
|
+
if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
|
|
2653
2780
|
return;
|
|
2654
2781
|
prefix.lastSize = sample;
|
|
2655
2782
|
}
|
|
2656
2783
|
/** Record the reported size of a main-thread request forwarded whole. */
|
|
2657
|
-
function notePassthroughSize(state, sessionId, model, rec,
|
|
2658
|
-
const sample = reportedSample(rec,
|
|
2784
|
+
function notePassthroughSize(state, sessionId, model, rec, sent) {
|
|
2785
|
+
const sample = reportedSample(rec, sent);
|
|
2659
2786
|
if (sessionId === undefined || sample === undefined)
|
|
2660
2787
|
return;
|
|
2661
2788
|
boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
|
|
@@ -2685,13 +2812,18 @@ function usedServerTools(rec) {
|
|
|
2685
2812
|
* an implausible ratio would mis-size every later request it anchors, so it
|
|
2686
2813
|
* is dropped and the previous sample kept.
|
|
2687
2814
|
*/
|
|
2688
|
-
function reportedSample(rec,
|
|
2815
|
+
function reportedSample(rec, sent) {
|
|
2689
2816
|
const tokens = reportedBodyTokens(rec);
|
|
2690
2817
|
if (tokens === undefined)
|
|
2691
2818
|
return undefined;
|
|
2692
|
-
const sample =
|
|
2819
|
+
const sample = sizeSampleOf(sent, tokens, rec.model);
|
|
2693
2820
|
return plausibleSample(sample) ? sample : undefined;
|
|
2694
2821
|
}
|
|
2822
|
+
/** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
|
|
2823
|
+
function sizeSampleOf(sent, tokens, model) {
|
|
2824
|
+
const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
|
|
2825
|
+
return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
|
|
2826
|
+
}
|
|
2695
2827
|
/**
|
|
2696
2828
|
* Anthropic's input tokens for the body this request forwarded, when its usage
|
|
2697
2829
|
* describes that body (not a changed delivery, no server tools). Not filtered
|
|
@@ -2738,12 +2870,12 @@ const noteRideSize = notePrefixSize;
|
|
|
2738
2870
|
* Record the reported size of a request forwarded whole: the session's
|
|
2739
2871
|
* passthrough size on the main thread, else its lane's.
|
|
2740
2872
|
*/
|
|
2741
|
-
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec,
|
|
2873
|
+
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
|
|
2742
2874
|
if (isMainRequest && sessionId !== undefined) {
|
|
2743
|
-
notePassthroughSize(state, sessionId, model, rec,
|
|
2875
|
+
notePassthroughSize(state, sessionId, model, rec, sent);
|
|
2744
2876
|
return;
|
|
2745
2877
|
}
|
|
2746
|
-
const sample = reportedSample(rec,
|
|
2878
|
+
const sample = reportedSample(rec, sent);
|
|
2747
2879
|
if (sample === undefined)
|
|
2748
2880
|
return;
|
|
2749
2881
|
boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
|
|
@@ -2804,7 +2936,7 @@ async function planEdgeCompaction(args) {
|
|
|
2804
2936
|
};
|
|
2805
2937
|
let prefix = state.stablePrefixes.get(sessionId);
|
|
2806
2938
|
if (prefix) {
|
|
2807
|
-
const miss = prefixMismatch(body, messages, prefix, sessionId,
|
|
2939
|
+
const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
|
|
2808
2940
|
if (miss) {
|
|
2809
2941
|
// Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
|
|
2810
2942
|
// longer stands for this conversation. Start over from passthrough.
|
|
@@ -2840,8 +2972,13 @@ async function planEdgeCompaction(args) {
|
|
|
2840
2972
|
(explicitTarget && prefix.targetTokens !== targetTokens)) {
|
|
2841
2973
|
return forced("target-change", fallback);
|
|
2842
2974
|
}
|
|
2843
|
-
if (size.tokens >= budget.tokens || overWindow)
|
|
2975
|
+
if (size.tokens >= budget.tokens || overWindow) {
|
|
2976
|
+
const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
|
|
2977
|
+
waitMs: opts.memtree.longCompressBudgetMs });
|
|
2978
|
+
if (adopted)
|
|
2979
|
+
return { kind: "ride", ...adopted };
|
|
2844
2980
|
return forced("budget", fallback);
|
|
2981
|
+
}
|
|
2845
2982
|
return { kind: "ride", prefix, routed };
|
|
2846
2983
|
}
|
|
2847
2984
|
if (state.compactNow.has(sessionId))
|
|
@@ -2854,12 +2991,18 @@ async function planEdgeCompaction(args) {
|
|
|
2854
2991
|
compaction.sizeSource = size.source;
|
|
2855
2992
|
noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
|
|
2856
2993
|
if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
|
|
2994
|
+
const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
|
|
2995
|
+
waitMs: opts.memtree.longCompressBudgetMs });
|
|
2996
|
+
if (adopted)
|
|
2997
|
+
return { kind: "ride", ...adopted };
|
|
2857
2998
|
return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
|
|
2858
2999
|
}
|
|
2859
3000
|
if (state.serverReportsBudget) {
|
|
2860
3001
|
// The server measures the request and compresses only past the budget.
|
|
2861
3002
|
const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
|
|
2862
3003
|
compaction.thresholdTokens = threshold;
|
|
3004
|
+
if (clearlyUnderBudget(opts, state, budget, size, compaction))
|
|
3005
|
+
return { kind: "pass" };
|
|
2863
3006
|
return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
|
|
2864
3007
|
}
|
|
2865
3008
|
// Without calibrated usage or threshold support, let the server decide
|
|
@@ -2900,7 +3043,7 @@ async function planToolCompaction(args) {
|
|
|
2900
3043
|
const stableLane = args.isMainRequest && sessionId !== undefined;
|
|
2901
3044
|
const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
|
|
2902
3045
|
let ride = args.ride;
|
|
2903
|
-
if (!ride && current && !prefixMismatch(body, messages, current, sessionId,
|
|
3046
|
+
if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
|
|
2904
3047
|
const routed = prefixRoutedBody(body, messages, current);
|
|
2905
3048
|
if (routed) {
|
|
2906
3049
|
const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
|
|
@@ -2938,12 +3081,27 @@ async function planToolCompaction(args) {
|
|
|
2938
3081
|
if (size.tokens < budget.tokens && !overWindow) {
|
|
2939
3082
|
return ride ? { kind: "ride", ride } : { kind: "pass" };
|
|
2940
3083
|
}
|
|
3084
|
+
const adopted = stableLane
|
|
3085
|
+
? await adoptPreparedPrefix({ ...args, sessionId: sessionId, budgetTokens: budget.tokens,
|
|
3086
|
+
compaction, waitMs: opts.memtree.longCompressBudgetMs })
|
|
3087
|
+
: undefined;
|
|
3088
|
+
if (adopted) {
|
|
3089
|
+
return { kind: "ride",
|
|
3090
|
+
ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
|
|
3091
|
+
}
|
|
3092
|
+
// Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
|
|
3093
|
+
const modeNow = compactionMode(opts, state, sessionId);
|
|
3094
|
+
if (modeNow.mode === "off")
|
|
3095
|
+
return { kind: "off" };
|
|
3096
|
+
const targetNow = compactionTarget(modeNow, budget.tokens);
|
|
3097
|
+
compaction.mode = modeNow.mode;
|
|
3098
|
+
compaction.targetTokens = targetNow;
|
|
2941
3099
|
compaction.reason = "budget";
|
|
2942
3100
|
return {
|
|
2943
3101
|
kind: "compress",
|
|
2944
|
-
...budgetCompaction(state, compaction,
|
|
2945
|
-
targetTokens,
|
|
2946
|
-
explicitTarget:
|
|
3102
|
+
...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
|
|
3103
|
+
targetTokens: targetNow,
|
|
3104
|
+
explicitTarget: modeNow.mode === "explicit",
|
|
2947
3105
|
estimateTokens: size.tokens,
|
|
2948
3106
|
budgetTokens: budget.tokens,
|
|
2949
3107
|
overWindow,
|
|
@@ -2952,6 +3110,224 @@ async function planToolCompaction(args) {
|
|
|
2952
3110
|
...(stableLane ? { replaces: current ?? null } : {}),
|
|
2953
3111
|
};
|
|
2954
3112
|
}
|
|
3113
|
+
/** Server `compression_target_tokens` sentinel: never compress, always pass through. */
|
|
3114
|
+
const NEVER_COMPRESS_TARGET_TOKENS = -1;
|
|
3115
|
+
/** Share of the budget under which a counted human turn skips its blocking call. */
|
|
3116
|
+
const CLEARLY_UNDER_BUDGET_RATIO = 0.85;
|
|
3117
|
+
/**
|
|
3118
|
+
* Whether a main human turn with no prefix is clearly under the budget by its
|
|
3119
|
+
* token count (an exact count, or a projection from Anthropic's reported
|
|
3120
|
+
* usage; never a byte guess) against a budget the server reported. The
|
|
3121
|
+
* compress call could then only pass it through, so it need not wait for it.
|
|
3122
|
+
*/
|
|
3123
|
+
function clearlyUnderBudget(opts, state, budget, size, compaction) {
|
|
3124
|
+
// A known payment or key failure keeps the blocking path: it gives the notice.
|
|
3125
|
+
if (opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
|
|
3126
|
+
return false;
|
|
3127
|
+
return budget.source !== "window-ratio" && size.source === "reported" &&
|
|
3128
|
+
compaction.uncountedOverWindow !== true &&
|
|
3129
|
+
size.tokens <= Math.floor(budget.tokens * CLEARLY_UNDER_BUDGET_RATIO);
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Whether this model and window prepare compressions: a Claude model on a 1M
|
|
3133
|
+
* window whose budget is the near-window one (not a Sonnet ceiling).
|
|
3134
|
+
*/
|
|
3135
|
+
function preparesCompression(model, modelContextLimit) {
|
|
3136
|
+
return modelContextLimit >= ONE_MILLION_TOKENS &&
|
|
3137
|
+
fallbackBudgetTokens(model, modelContextLimit) === nearWindowBudgetTokens(modelContextLimit) &&
|
|
3138
|
+
typeof model === "string" && CLAUDE_FAMILY_RE.test(model);
|
|
3139
|
+
}
|
|
3140
|
+
const ONE_MILLION_TOKENS = 1_000_000;
|
|
3141
|
+
/**
|
|
3142
|
+
* The session's prepared prefix (prepared-compression.ts) as this turn's
|
|
3143
|
+
* ride, now the session's stable prefix, when it still covers this history
|
|
3144
|
+
* and prefix + the turns after it fit under the budget. Undefined otherwise,
|
|
3145
|
+
* and the prepared prefix is dropped: the turn compresses as before.
|
|
3146
|
+
*/
|
|
3147
|
+
async function adoptPreparedPrefix(args) {
|
|
3148
|
+
const { state, sessionId } = args;
|
|
3149
|
+
const base = state.stablePrefixes.get(sessionId);
|
|
3150
|
+
await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
|
|
3151
|
+
const entry = state.prepared.get(sessionId);
|
|
3152
|
+
if (!entry)
|
|
3153
|
+
return undefined;
|
|
3154
|
+
state.prepared.discard(sessionId);
|
|
3155
|
+
const fit = await preparedFit(args, entry);
|
|
3156
|
+
// Another request installed a prefix, or `/memtree-compact` asked for its
|
|
3157
|
+
// own target, while this one waited or counted: that wins.
|
|
3158
|
+
const kind = fit.kind === "used" && (state.stablePrefixes.get(sessionId) !== base ||
|
|
3159
|
+
state.compactNow.has(sessionId) || compactionMode(args.opts, state, sessionId).mode !== "auto")
|
|
3160
|
+
? "stale" : fit.kind;
|
|
3161
|
+
args.compaction.prepared = kind;
|
|
3162
|
+
if (kind !== "used" || fit.kind !== "used") {
|
|
3163
|
+
// A preparation not adopted is never sent, not even as a fallback.
|
|
3164
|
+
if (fit.candidate)
|
|
3165
|
+
args.sizing.excludeFallback(fit.candidate);
|
|
3166
|
+
if (kind === "too-large" && fit.candidate) {
|
|
3167
|
+
notePreparedTruncationBase(args, entry, base, fit.candidate);
|
|
3168
|
+
}
|
|
3169
|
+
return undefined;
|
|
3170
|
+
}
|
|
3171
|
+
args.compaction.estimatedTokens = fit.tokens;
|
|
3172
|
+
args.compaction.estimatedBytes = fit.routed.raw.length;
|
|
3173
|
+
installPreparedPrefix(state, sessionId, entry);
|
|
3174
|
+
return { prefix: entry.prefix, routed: fit.routed };
|
|
3175
|
+
}
|
|
3176
|
+
/** An adopted preparation becomes the session's stable prefix (and its page, persisted). */
|
|
3177
|
+
function installPreparedPrefix(state, sessionId, entry) {
|
|
3178
|
+
boundedSet(state.stablePrefixes, sessionId, entry.prefix);
|
|
3179
|
+
state.compactNow.delete(sessionId);
|
|
3180
|
+
if (entry.source)
|
|
3181
|
+
notePreparedSource(state, sessionId, entry.source);
|
|
3182
|
+
persistStablePrefix(state, sessionId, entry.prefix);
|
|
3183
|
+
}
|
|
3184
|
+
const preparedTruncationBases = new WeakMap();
|
|
3185
|
+
function notePreparedTruncationBase(args, entry, base, candidate) {
|
|
3186
|
+
const { state, sessionId } = args;
|
|
3187
|
+
preparedTruncationBases.set(args.sizing, { candidate, install: () => {
|
|
3188
|
+
if (state.stablePrefixes.get(sessionId) !== base || state.compactNow.has(sessionId) ||
|
|
3189
|
+
compactionMode(args.opts, state, sessionId).mode !== "auto")
|
|
3190
|
+
return;
|
|
3191
|
+
args.compaction.prepared = "truncated";
|
|
3192
|
+
installPreparedPrefix(state, sessionId, entry);
|
|
3193
|
+
} });
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* The turn that crosses the budget while its session's preparation is still
|
|
3197
|
+
* in flight waits for it (it started earlier than a fresh compression could),
|
|
3198
|
+
* for at most `waitMs`, rather than making a second, concurrent compress call.
|
|
3199
|
+
*/
|
|
3200
|
+
async function awaitPreparation(state, sessionId, waitMs, compaction) {
|
|
3201
|
+
const pending = state.preparing.get(sessionId);
|
|
3202
|
+
if (!pending)
|
|
3203
|
+
return;
|
|
3204
|
+
const started = Date.now();
|
|
3205
|
+
let timer;
|
|
3206
|
+
await Promise.race([pending, new Promise((resolve) => {
|
|
3207
|
+
timer = setTimeout(resolve, Math.max(0, waitMs));
|
|
3208
|
+
})]).finally(() => clearTimeout(timer));
|
|
3209
|
+
compaction.preparedWaitMs = Date.now() - started;
|
|
3210
|
+
}
|
|
3211
|
+
/** An adopted preparation's MemTree page becomes the session's memory source and page. */
|
|
3212
|
+
function notePreparedSource(state, sessionId, source) {
|
|
3213
|
+
const result = { messages: [], compressed: true, memtreeUrl: source.url,
|
|
3214
|
+
memtreeIndex: source.index };
|
|
3215
|
+
noteMemtreePage(state, source.seq, sessionId, result);
|
|
3216
|
+
noteMemorySource(state, source.seq, sessionId, result);
|
|
3217
|
+
}
|
|
3218
|
+
/** Whether a prepared prefix can carry this request (adoptPreparedPrefix). */
|
|
3219
|
+
async function preparedFit(args, entry) {
|
|
3220
|
+
const { sizing, body, messages, modelContextLimit, budgetTokens, compaction } = args;
|
|
3221
|
+
if (entry.prefix.modelContextLimit !== modelContextLimit || entry.triggerTokens !== budgetTokens ||
|
|
3222
|
+
entry.prefix.targetTokens !== compaction.targetTokens ||
|
|
3223
|
+
entry.prefix.explicitTarget !== (compaction.mode === "explicit")) {
|
|
3224
|
+
return { kind: "stale" };
|
|
3225
|
+
}
|
|
3226
|
+
if (prefixMismatch(body, messages, entry.prefix, args.sessionId, "stable")) {
|
|
3227
|
+
return { kind: "mismatch" };
|
|
3228
|
+
}
|
|
3229
|
+
const routed = prefixRoutedBody(body, messages, entry.prefix);
|
|
3230
|
+
if (!routed)
|
|
3231
|
+
return { kind: "mismatch" };
|
|
3232
|
+
const candidate = sizing.register(routed.raw, "ride", entry.prefix.lastSize);
|
|
3233
|
+
candidateSizeOwners.set(candidate, entry.prefix);
|
|
3234
|
+
await countIfOverBound(sizing, candidate, body, modelContextLimit);
|
|
3235
|
+
const tokens = sizing.plan(candidate).tokens;
|
|
3236
|
+
const fits = tokens < budgetTokens && fitsNative(tokens, { budget: budgetTokens,
|
|
3237
|
+
context: modelContextLimit, output: bodyOutputReservation(body, sizing.isExact(candidate)) }) &&
|
|
3238
|
+
(sizing.isExact(candidate) || sizing.boundFits(candidate, { budget: budgetTokens, context: modelContextLimit, output: bodyOutputReservation(body, false) }));
|
|
3239
|
+
// Too large, the turn compresses as before; the ride stays registered, so
|
|
3240
|
+
// if that cannot fit either, delivery's safety net may shorten its newest
|
|
3241
|
+
// tool result instead (truncatedToFit).
|
|
3242
|
+
return fits ? { kind: "used", routed, tokens, candidate } : { kind: "too-large", candidate };
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* After a main-thread turn's plan: once its size nears the budget, ask
|
|
3246
|
+
* MemTree in the background for the compressed prefix the turn that crosses
|
|
3247
|
+
* it will ride (prepared-compression.ts). Fire and forget, one call per
|
|
3248
|
+
* session at a time; never delays this turn.
|
|
3249
|
+
*/
|
|
3250
|
+
function maybePrepareCompression(args) {
|
|
3251
|
+
const { state, sessionId, rec } = args;
|
|
3252
|
+
const compaction = rec.compaction;
|
|
3253
|
+
// Automatic compaction only: an explicit target (/memtree-compact N) is the
|
|
3254
|
+
// user's own schedule, and a high one would leave nothing to prepare for.
|
|
3255
|
+
if (!compaction || compaction.mode !== "auto" || compaction.estimatedTokens === undefined ||
|
|
3256
|
+
compaction.targetTokens === undefined || state.keyInvalid() ||
|
|
3257
|
+
state.backgroundSignal.aborted ||
|
|
3258
|
+
state.pausedDetail() !== null ||
|
|
3259
|
+
!preparesCompression(args.body.model, args.modelContextLimit))
|
|
3260
|
+
return;
|
|
3261
|
+
const tokens = compaction.estimatedTokens;
|
|
3262
|
+
if (tokens < prepareStartTokens(compaction.budgetTokens))
|
|
3263
|
+
return;
|
|
3264
|
+
const decision = state.prepared.decide(sessionId, tokens, compaction.budgetTokens);
|
|
3265
|
+
rec.prepare = decision;
|
|
3266
|
+
if (decision !== "prepare" ||
|
|
3267
|
+
!state.prepared.begin(sessionId, tokens, compaction.budgetTokens))
|
|
3268
|
+
return;
|
|
3269
|
+
const base = state.stablePrefixes.get(sessionId);
|
|
3270
|
+
const pending = prepareInBackground({ ...args, tokens, targetTokens: compaction.targetTokens,
|
|
3271
|
+
explicitTarget: false, triggerTokens: compaction.budgetTokens,
|
|
3272
|
+
linkSeq: nextMemtreeCallSeq(state) })
|
|
3273
|
+
.catch(() => undefined)
|
|
3274
|
+
.then((prepared) => {
|
|
3275
|
+
// A prefix installed meanwhile (a blocking compaction) supersedes it.
|
|
3276
|
+
const current = state.stablePrefixes.get(sessionId) === base;
|
|
3277
|
+
state.prepared.finish(sessionId, current ? prepared : undefined);
|
|
3278
|
+
if (state.preparing.get(sessionId) === pending)
|
|
3279
|
+
state.preparing.delete(sessionId);
|
|
3280
|
+
});
|
|
3281
|
+
boundedSet(state.preparing, sessionId, pending);
|
|
3282
|
+
}
|
|
3283
|
+
/** The background prepare call and the prefix it yields (maybePrepareCompression). */
|
|
3284
|
+
async function prepareInBackground(args) {
|
|
3285
|
+
const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
|
|
3286
|
+
const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
|
|
3287
|
+
noteServerBudget(state, body.model, modelContextLimit, result);
|
|
3288
|
+
if (!result)
|
|
3289
|
+
return undefined;
|
|
3290
|
+
state.prepared.noteServerAnswer(result.prepared === true);
|
|
3291
|
+
if (result.prepared !== true || !didMemtreeCompress(result) ||
|
|
3292
|
+
!checkCompressedHistory(result, msgsForMemtree).usable)
|
|
3293
|
+
return undefined;
|
|
3294
|
+
const built = buildCompressedBody(body, result);
|
|
3295
|
+
if (!built)
|
|
3296
|
+
return undefined;
|
|
3297
|
+
const prefix = {
|
|
3298
|
+
sessionId,
|
|
3299
|
+
originalSystemHash: routeValueHash(normalizeRouteSystem(body.system)),
|
|
3300
|
+
...prefixIdentity(args.messages, "stable"),
|
|
3301
|
+
compressedMessages: cloneJson(built.compressedBody.messages),
|
|
3302
|
+
compressedSystem: cloneJson(built.compressedBody.system),
|
|
3303
|
+
hasCompressedSystem: Object.prototype.hasOwnProperty.call(built.compressedBody, "system"),
|
|
3304
|
+
targetTokens: args.targetTokens,
|
|
3305
|
+
explicitTarget: args.explicitTarget,
|
|
3306
|
+
modelContextLimit,
|
|
3307
|
+
};
|
|
3308
|
+
const source = result.memtreeUrl && result.memtreeIndex
|
|
3309
|
+
? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
|
|
3310
|
+
return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
|
|
3311
|
+
...(source ? { source } : {}) };
|
|
3312
|
+
}
|
|
3313
|
+
/** What a prepare call sends: a forced compression to the target, no threshold. */
|
|
3314
|
+
function preparedCompressMeta(args) {
|
|
3315
|
+
const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
|
|
3316
|
+
const messageUsage = opts.transcriptUsage?.usageFor(sessionId, msgsForMemtree, undefined);
|
|
3317
|
+
const messageTimes = transcriptTimesFor(opts, sessionId, undefined)?.(msgsForMemtree);
|
|
3318
|
+
return {
|
|
3319
|
+
model: modelForMemtree(typeof body.model === "string" ? body.model : undefined, modelContextLimit),
|
|
3320
|
+
tools: Array.isArray(body.tools) ? body.tools : undefined,
|
|
3321
|
+
compressionTargetTokens: args.targetTokens,
|
|
3322
|
+
prepare: true,
|
|
3323
|
+
...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
|
|
3324
|
+
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
3325
|
+
sessionId,
|
|
3326
|
+
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3327
|
+
// Nothing waits on it: the long budget, so a slow server still prepares.
|
|
3328
|
+
timeoutMs: opts.memtree.longCompressBudgetMs,
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
2955
3331
|
function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
2956
3332
|
// Model is deliberately not route identity: Claude Code switches models
|
|
2957
3333
|
// mid-loop (overload fallback, /model, /fast), and the compressed prefix is
|
|
@@ -2969,7 +3345,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
|
2969
3345
|
if (messages.length < prefixLength)
|
|
2970
3346
|
return null;
|
|
2971
3347
|
for (let i = 0; i < prefixLength; i++) {
|
|
2972
|
-
if (
|
|
3348
|
+
if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
|
|
2973
3349
|
return null;
|
|
2974
3350
|
}
|
|
2975
3351
|
}
|
|
@@ -3352,8 +3728,9 @@ async function runBlockingCompression(args) {
|
|
|
3352
3728
|
: undefined;
|
|
3353
3729
|
const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
|
|
3354
3730
|
const compressStarted = Date.now();
|
|
3355
|
-
|
|
3356
|
-
|
|
3731
|
+
// The upload is about the request's size: the budget grows with it.
|
|
3732
|
+
const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? opts.memtree.longCompressBudgetMs
|
|
3733
|
+
: opts.memtree.compressBudgetForBytes(rec.requestBytes));
|
|
3357
3734
|
const compressMeta = {
|
|
3358
3735
|
// Model + tools drive the server's model-based memory budget
|
|
3359
3736
|
// (e.g. 800k whole-request target for Fable / Opus 5). Omitting
|
|
@@ -3381,7 +3758,9 @@ async function runBlockingCompression(args) {
|
|
|
3381
3758
|
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
3382
3759
|
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
3383
3760
|
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3384
|
-
|
|
3761
|
+
// The deadline this wait uses (the upload-scaled budget included), so the
|
|
3762
|
+
// call and its waiters end together.
|
|
3763
|
+
timeoutMs: budgetMs,
|
|
3385
3764
|
};
|
|
3386
3765
|
// Sampled BEFORE the call, while it still describes this call: after the
|
|
3387
3766
|
// await the hash is in the cache regardless of who put it there.
|
|
@@ -3389,9 +3768,9 @@ async function runBlockingCompression(args) {
|
|
|
3389
3768
|
// compress() maps every failure to a resolved null (it never rejects).
|
|
3390
3769
|
// Keep foreground work finite even if a transport ignores its abort signal.
|
|
3391
3770
|
let deadline;
|
|
3392
|
-
const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS :
|
|
3771
|
+
const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
|
|
3393
3772
|
const result = await Promise.race([
|
|
3394
|
-
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
|
|
3773
|
+
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
|
|
3395
3774
|
new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
|
|
3396
3775
|
]).finally(() => { if (deadline)
|
|
3397
3776
|
clearTimeout(deadline); });
|
|
@@ -3401,19 +3780,27 @@ async function runBlockingCompression(args) {
|
|
|
3401
3780
|
// another class can overwrite the complete-key entry.
|
|
3402
3781
|
const failureArming = result === null &&
|
|
3403
3782
|
opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
|
|
3783
|
+
const failureStatus = result === null
|
|
3784
|
+
? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
|
|
3404
3785
|
const hadLiveSuccess = !cached && result !== null;
|
|
3405
3786
|
// Only arming-class live failures (network error, timeout, 5xx, 402) are
|
|
3406
3787
|
// fuse evidence. A responsive server's other 4xx failed THIS call but says
|
|
3407
3788
|
// nothing about MemTree's health, so it contributes neither failure nor
|
|
3408
3789
|
// success — the fuse is untouched.
|
|
3409
|
-
|
|
3790
|
+
// Budget-consumed heuristic: the client maps every failure to null, so a
|
|
3791
|
+
// null that took (roughly) the whole abort budget was almost certainly
|
|
3792
|
+
// the AbortSignal timeout, not a fast server error.
|
|
3793
|
+
const timedOut = result === null && compressMs >= Math.min(budgetMs, maximumWait);
|
|
3794
|
+
// A timeout while other compress calls answered (one succeeded after this
|
|
3795
|
+
// one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
|
|
3796
|
+
// healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
|
|
3797
|
+
const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
|
|
3798
|
+
const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered;
|
|
3410
3799
|
rec.compress = {
|
|
3411
3800
|
ms: compressMs,
|
|
3412
3801
|
ok: result !== null,
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
// the AbortSignal timeout, not a fast server error.
|
|
3416
|
-
timedOut: result === null && compressMs >= budgetMs,
|
|
3802
|
+
timedOut,
|
|
3803
|
+
...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
|
|
3417
3804
|
};
|
|
3418
3805
|
return {
|
|
3419
3806
|
result,
|
|
@@ -3425,6 +3812,61 @@ async function runBlockingCompression(args) {
|
|
|
3425
3812
|
: "none",
|
|
3426
3813
|
};
|
|
3427
3814
|
}
|
|
3815
|
+
/**
|
|
3816
|
+
* A clearly under-budget human turn's compress call, not awaited: its answer
|
|
3817
|
+
* (a passthrough) only updates the server budget, MemTree health and the
|
|
3818
|
+
* session's page link. Its telemetry goes to a copy of the turn's record.
|
|
3819
|
+
*/
|
|
3820
|
+
function compressInBackground(args) {
|
|
3821
|
+
if (args.state.backgroundSignal.aborted)
|
|
3822
|
+
return;
|
|
3823
|
+
const lanes = args.state.backgroundCompress;
|
|
3824
|
+
const lane = backgroundCompressLane(args);
|
|
3825
|
+
if (lane === undefined) {
|
|
3826
|
+
void runBackgroundCompress(args);
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
const busy = lanes.get(lane);
|
|
3830
|
+
if (busy) {
|
|
3831
|
+
// One whole-history call at a time per conversation: under the budget each turn
|
|
3832
|
+
// started its own (16 to 22 s each on staging in the long-session test, about 12 at
|
|
3833
|
+
// once beside the index uploads). A waiting call is replaced only by one whose
|
|
3834
|
+
// history contains all of it; otherwise it goes now.
|
|
3835
|
+
if (busy.waiting && !extendsHistory(args.msgsForMemtree, busy.waiting.msgsForMemtree)) {
|
|
3836
|
+
void runBackgroundCompress(busy.waiting);
|
|
3837
|
+
}
|
|
3838
|
+
busy.waiting = args;
|
|
3839
|
+
return;
|
|
3840
|
+
}
|
|
3841
|
+
const owner = {};
|
|
3842
|
+
lanes.set(lane, owner);
|
|
3843
|
+
void runBackgroundCompress(args).finally(() => {
|
|
3844
|
+
if (lanes.get(lane) === owner)
|
|
3845
|
+
lanes.delete(lane);
|
|
3846
|
+
if (owner.waiting)
|
|
3847
|
+
compressInBackground(owner.waiting);
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
function runBackgroundCompress(args) {
|
|
3851
|
+
const { state, sessionId, linkSeq } = args;
|
|
3852
|
+
return runBlockingCompression({ ...args, rec: { ...args.rec }, signal: state.backgroundSignal })
|
|
3853
|
+
.then((compression) => {
|
|
3854
|
+
noteMemtreeHealth(state, compression);
|
|
3855
|
+
if (linkSeq !== undefined)
|
|
3856
|
+
noteMemtreePage(state, linkSeq, sessionId, compression.result);
|
|
3857
|
+
})
|
|
3858
|
+
.catch(() => undefined);
|
|
3859
|
+
}
|
|
3860
|
+
/**
|
|
3861
|
+
* A conversation for compressInBackground: session and first message, media left out (Claude
|
|
3862
|
+
* Code's media eviction keeps it, as indexLaneKey does); none without a session.
|
|
3863
|
+
*/
|
|
3864
|
+
function backgroundCompressLane(args) {
|
|
3865
|
+
if (args.sessionId === undefined)
|
|
3866
|
+
return undefined;
|
|
3867
|
+
const first = args.msgsForMemtree.find((message) => message.role !== "system");
|
|
3868
|
+
return JSON.stringify([args.sessionId, first ? mediaFreeMessageHash(first) : ""]);
|
|
3869
|
+
}
|
|
3428
3870
|
/**
|
|
3429
3871
|
* Shared final request shape for a validated compression result: lift any
|
|
3430
3872
|
* returned system message to `body.system`, and forward the SERVER's flatten
|
|
@@ -3524,14 +3966,14 @@ async function recoverToolRouteMiss(args) {
|
|
|
3524
3966
|
capture(opts, "anthropic-request-memory-tool", fallback.raw);
|
|
3525
3967
|
return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3526
3968
|
toolForwarding.settle(delivered);
|
|
3527
|
-
noteRideSize(fallback.sizeHolder, rec, fallback.raw
|
|
3969
|
+
noteRideSize(fallback.sizeHolder, rec, fallback.raw);
|
|
3528
3970
|
});
|
|
3529
3971
|
}
|
|
3530
3972
|
recordTurn(rec, "tool", args.originalBody);
|
|
3531
3973
|
capture(opts, "anthropic-request", args.originalBody);
|
|
3532
3974
|
return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3533
3975
|
toolForwarding.settle(delivered);
|
|
3534
|
-
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody
|
|
3976
|
+
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
|
|
3535
3977
|
});
|
|
3536
3978
|
};
|
|
3537
3979
|
// Same downstream-close tracking as the followup path: compression
|
|
@@ -3672,6 +4114,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3672
4114
|
const { compressedBody, compressedRaw } = built;
|
|
3673
4115
|
if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
|
|
3674
4116
|
rec.routeRecovery = { conversationBytes, outcome: "unusable" };
|
|
4117
|
+
noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
|
|
3675
4118
|
releaseOwnReservation();
|
|
3676
4119
|
if (!noteIndexLag(opts, rec, result))
|
|
3677
4120
|
return forwardOriginal();
|
|
@@ -3818,7 +4261,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3818
4261
|
// The compacted request's reported size anchors the next budget check.
|
|
3819
4262
|
const sizeHolder = stable?.installed ?? installedRoute;
|
|
3820
4263
|
if (sizeHolder)
|
|
3821
|
-
notePrefixSize(sizeHolder, rec, compressedRaw
|
|
4264
|
+
notePrefixSize(sizeHolder, rec, compressedRaw);
|
|
3822
4265
|
// Nothing to ride next turn, or a result that still does not fit the
|
|
3823
4266
|
// window (the next turn would reject it and compress again): wait for
|
|
3824
4267
|
// growth before the lane tries again.
|
|
@@ -3917,18 +4360,29 @@ const recapLinkAppenders = new WeakMap();
|
|
|
3917
4360
|
* miss the tree, index a divergent conversation and bill for it.
|
|
3918
4361
|
*/
|
|
3919
4362
|
async function recoverLateCount(args) {
|
|
4363
|
+
let generation;
|
|
3920
4364
|
try {
|
|
3921
4365
|
const { body, msgsForMemtree: messages } = args;
|
|
3922
4366
|
const budget = resolveBudget(args.opts, args.state, body.model, args.modelContextLimit).tokens;
|
|
3923
|
-
const target = lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
|
|
4367
|
+
const target = args.target ?? lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
|
|
4368
|
+
const sessionId = requestSessionId(args.req);
|
|
4369
|
+
const linkSeq = args.route.lane === "main" ? nextMemtreeCallSeq(args.state) : undefined;
|
|
4370
|
+
const replaces = sessionId !== undefined
|
|
4371
|
+
? args.state.stablePrefixes.get(sessionId) ?? null : null;
|
|
4372
|
+
// Reserved before the compress call: a newer same-lane decision made
|
|
4373
|
+
// while it runs outranks this one, so this never overwrites its route.
|
|
4374
|
+
generation = ++args.state.mainRouteDecisionGeneration;
|
|
4375
|
+
reserveRouteDecision(args.state, args.route.key, generation);
|
|
3924
4376
|
const outcome = await runBlockingCompression({
|
|
3925
4377
|
opts: args.opts, state: args.state, body, msgsForMemtree: messages,
|
|
3926
4378
|
hash: args.hash, modelContextLimit: args.modelContextLimit,
|
|
3927
|
-
rec: args.rec, sessionId
|
|
4379
|
+
rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
|
|
3928
4380
|
clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
|
|
3929
4381
|
compaction: { target },
|
|
3930
4382
|
});
|
|
3931
4383
|
noteMemtreeHealth(args.state, outcome);
|
|
4384
|
+
if (linkSeq !== undefined)
|
|
4385
|
+
noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
|
|
3932
4386
|
// Like every compress path: a refusal after a lagging answer waits for
|
|
3933
4387
|
// the index, and the cached answer expires so a retry asks again.
|
|
3934
4388
|
if (outcome.result)
|
|
@@ -3939,11 +4393,63 @@ async function recoverLateCount(args) {
|
|
|
3939
4393
|
const built = buildCompressedBody(body, outcome.result);
|
|
3940
4394
|
if (!built)
|
|
3941
4395
|
return undefined;
|
|
4396
|
+
lateRouteInstalls.set(args.rec, lateRouteInstall(args, built.compressedBody, outcome.result, {
|
|
4397
|
+
targetTokens: target, explicitTarget: true, modelContextLimit: args.modelContextLimit,
|
|
4398
|
+
replaces
|
|
4399
|
+
}, linkSeq, generation));
|
|
4400
|
+
generation = undefined; // the install owns the reservation now
|
|
3942
4401
|
return built.compressedRaw;
|
|
3943
4402
|
}
|
|
3944
4403
|
catch {
|
|
3945
4404
|
return undefined;
|
|
3946
4405
|
}
|
|
4406
|
+
finally {
|
|
4407
|
+
if (generation !== undefined)
|
|
4408
|
+
releaseRouteDecision(args.state, args.route.key, generation);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
/** Late recoveries' installs, per request, until delivery settles. */
|
|
4412
|
+
const lateRouteInstalls = new WeakMap();
|
|
4413
|
+
function lateRouteInstall(args, compressedBody, result, stable, linkSeq,
|
|
4414
|
+
/** Reserved (reserveRouteDecision) before the compress call; released here if unused. */
|
|
4415
|
+
generation) {
|
|
4416
|
+
const { state, route } = args;
|
|
4417
|
+
const sessionId = requestSessionId(args.req);
|
|
4418
|
+
let done = false;
|
|
4419
|
+
let installed;
|
|
4420
|
+
return {
|
|
4421
|
+
activate: () => {
|
|
4422
|
+
if (done)
|
|
4423
|
+
return;
|
|
4424
|
+
done = true;
|
|
4425
|
+
if (sessionId !== undefined && route.lane !== "away") {
|
|
4426
|
+
installed = installMemoryRoute(state, route.key, args.req, args.body, route.messages, compressedBody, route.epoch, generation, route.lane === "main" ? stable : undefined);
|
|
4427
|
+
}
|
|
4428
|
+
if (!installed)
|
|
4429
|
+
releaseRouteDecision(state, route.key, generation);
|
|
4430
|
+
else if (linkSeq !== undefined)
|
|
4431
|
+
noteMemorySource(state, linkSeq, sessionId, result);
|
|
4432
|
+
},
|
|
4433
|
+
settle(delivered, sent) {
|
|
4434
|
+
if (delivered)
|
|
4435
|
+
this.activate();
|
|
4436
|
+
// The replacement is a changed delivery (reportedSample skips those):
|
|
4437
|
+
// its reported size is this very body's, so it is noted directly.
|
|
4438
|
+
const holder = stable.installed ?? installed;
|
|
4439
|
+
const tokens = reportedInputTokens(args.rec);
|
|
4440
|
+
if (holder && sent && delivered && tokens !== undefined &&
|
|
4441
|
+
!serverToolRecords.has(args.rec) && !usedServerTools(args.rec)) {
|
|
4442
|
+
const sample = sizeSampleOf(sent, tokens, args.rec.model);
|
|
4443
|
+
if (plausibleSample(sample) && !(holder.lastSize && sent.length < holder.lastSize.forwardedBytes)) {
|
|
4444
|
+
holder.lastSize = sample;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
if (!installed) {
|
|
4448
|
+
done = true;
|
|
4449
|
+
releaseRouteDecision(state, route.key, generation);
|
|
4450
|
+
}
|
|
4451
|
+
},
|
|
4452
|
+
};
|
|
3947
4453
|
}
|
|
3948
4454
|
/**
|
|
3949
4455
|
* The ride compressed to `target` still counted `rideTokens` (over the
|
|
@@ -3963,14 +4469,25 @@ async function selectForwardCandidate(args) {
|
|
|
3963
4469
|
const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
|
|
3964
4470
|
const measured = await policy.sizing.measure(preferred, args.limits);
|
|
3965
4471
|
let target = preferred;
|
|
3966
|
-
|
|
3967
|
-
|
|
4472
|
+
const late = lateRecoveryPlan(args.state, args.rec, preferred, measured, args.limits, args.routeKey);
|
|
4473
|
+
if (late) {
|
|
3968
4474
|
args.rec.lateCountRecovery = { outcome: "forwarded" };
|
|
3969
|
-
const replacement = await policy.recoverLate(preferred.body,
|
|
4475
|
+
const replacement = await holdingLane(args.state, args.routeKey, () => policy.recoverLate(preferred.body, late.tokens, late.target));
|
|
3970
4476
|
if (replacement)
|
|
3971
4477
|
target = policy.sizing.register(replacement, "replacement");
|
|
3972
4478
|
}
|
|
3973
|
-
const selection = await policy.sizing.select(target, args.limits)
|
|
4479
|
+
const selection = await policy.sizing.select(target, args.limits) ??
|
|
4480
|
+
await policy.sizing.retryTimedOutCount(args.limits) ??
|
|
4481
|
+
(args.rec.routeLane === "main" && args.opts.toolOutputs
|
|
4482
|
+
? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
|
|
4483
|
+
: undefined);
|
|
4484
|
+
const quietDegrade = quietDegrades.get(args.rec);
|
|
4485
|
+
quietDegrades.delete(args.rec);
|
|
4486
|
+
const lateInstall = lateRouteInstalls.get(args.rec);
|
|
4487
|
+
if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
|
|
4488
|
+
lateRouteInstalls.delete(args.rec);
|
|
4489
|
+
lateInstall.settle(false);
|
|
4490
|
+
}
|
|
3974
4491
|
if (!selection) {
|
|
3975
4492
|
if (args.rec.lateCountRecovery)
|
|
3976
4493
|
args.rec.lateCountRecovery.outcome = "refused";
|
|
@@ -3978,6 +4495,10 @@ async function selectForwardCandidate(args) {
|
|
|
3978
4495
|
return undefined;
|
|
3979
4496
|
}
|
|
3980
4497
|
const { candidate, measurement } = selection;
|
|
4498
|
+
const preparedBase = preparedTruncationBases.get(policy.sizing);
|
|
4499
|
+
if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
|
|
4500
|
+
preparedBase.install();
|
|
4501
|
+
}
|
|
3981
4502
|
const changed = candidate !== preferred || intended !== args.preferredBody;
|
|
3982
4503
|
if (changed) {
|
|
3983
4504
|
policy.allowed.delete(args.preferredBody);
|
|
@@ -3996,9 +4517,158 @@ async function selectForwardCandidate(args) {
|
|
|
3996
4517
|
? "exact-count-fits-window" : measurement.source === "bytes"
|
|
3997
4518
|
? "bytes-fallback-fits-window" : "estimate-fits-window" };
|
|
3998
4519
|
warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
|
|
4520
|
+
quietDegrade?.();
|
|
3999
4521
|
}
|
|
4000
4522
|
return changed ? candidate.body : intended;
|
|
4001
4523
|
}
|
|
4524
|
+
/** MemTree's answer when the conversation cannot fit the window even compressed. */
|
|
4525
|
+
const OVER_CONTEXT_STATUS = 413;
|
|
4526
|
+
/** Truncation attempts, each cutting more, before the request is refused. */
|
|
4527
|
+
const TRUNCATION_ATTEMPTS = 3;
|
|
4528
|
+
/** Cut this much more than the measured excess (tokens per character vary). */
|
|
4529
|
+
const TRUNCATION_MARGIN = 1.25;
|
|
4530
|
+
/**
|
|
4531
|
+
* The safety net for a main-thread request that does not fit even after
|
|
4532
|
+
* compression (tool-result-truncation.ts): its newest tool result saved whole
|
|
4533
|
+
* (tool-output-store.ts) and sent with the middle replaced by a pointer to
|
|
4534
|
+
* that copy, cut until the request leaves the reply's room below the window.
|
|
4535
|
+
* A compressed candidate first (a ride on a prefix, or a replacement whose
|
|
4536
|
+
* suffix is structured), else the whole history.
|
|
4537
|
+
*/
|
|
4538
|
+
async function truncatedToFit(rec, sizing, preferred, limits, store) {
|
|
4539
|
+
if (!compressionSettled(rec))
|
|
4540
|
+
return undefined;
|
|
4541
|
+
const prepared = preparedTruncationBases.get(sizing);
|
|
4542
|
+
const rank = (item) => item.kind === "original" ? 1 : 0;
|
|
4543
|
+
const bases = [preferred, ...sizing.fallbacks().filter((item) => item !== preferred),
|
|
4544
|
+
...(prepared && prepared.candidate !== preferred ? [prepared.candidate] : [])]
|
|
4545
|
+
.sort((a, b) => rank(a) - rank(b) || a.body.length - b.body.length);
|
|
4546
|
+
for (const base of bases) {
|
|
4547
|
+
const fitted = await truncateCandidate(rec, sizing, base, limits, store);
|
|
4548
|
+
if (fitted && prepared && base === prepared.candidate)
|
|
4549
|
+
prepared.derived = fitted.candidate;
|
|
4550
|
+
if (fitted)
|
|
4551
|
+
return fitted;
|
|
4552
|
+
}
|
|
4553
|
+
return undefined;
|
|
4554
|
+
}
|
|
4555
|
+
/**
|
|
4556
|
+
* Whether compression had its chance and the request still cannot fit: a ride
|
|
4557
|
+
* on a prepared prefix, a compress answer that compressed (sent or found too
|
|
4558
|
+
* large), or MemTree's 413 (it cannot fit the window even compressed). Not
|
|
4559
|
+
* after a failed or skipped compress call (an outage, an exception, an auth
|
|
4560
|
+
* or payment refusal, a backoff: the retryable refusal stands), a passthrough
|
|
4561
|
+
* of a warming index, or while the index is behind: waiting for it lets
|
|
4562
|
+
* compression summarize instead.
|
|
4563
|
+
*/
|
|
4564
|
+
function compressionSettled(rec) {
|
|
4565
|
+
if (rec.indexBehind === true)
|
|
4566
|
+
return false;
|
|
4567
|
+
if (rec.compress?.refused === true || rec.compaction?.prepared === "used")
|
|
4568
|
+
return true;
|
|
4569
|
+
return rec.compress?.ok === true && rec.routeRecovery?.outcome !== "noop" &&
|
|
4570
|
+
rec.turnType !== "followup-noop";
|
|
4571
|
+
}
|
|
4572
|
+
/** truncatedToFit for one candidate; undefined when it has no tool result to
|
|
4573
|
+
* shorten, the copy could not be saved, or it cannot be made to fit. */
|
|
4574
|
+
async function truncateCandidate(rec, sizing, base, limits, store) {
|
|
4575
|
+
const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
|
|
4576
|
+
const parsed = JSON.parse(base.body.toString("utf-8"));
|
|
4577
|
+
const first = await sizing.measure(base, limits);
|
|
4578
|
+
const bytesPerToken = base.body.length / Math.max(1, first.tokens);
|
|
4579
|
+
for (let attempt = 1; attempt <= TRUNCATION_ATTEMPTS; attempt++) {
|
|
4580
|
+
const excess = Math.max(1_000, first.tokens - goal);
|
|
4581
|
+
const cut = Math.ceil(excess * bytesPerToken * TRUNCATION_MARGIN * attempt);
|
|
4582
|
+
// The store pins each copy it saves, so none this request points at is
|
|
4583
|
+
// evicted while it is counted and forwarded.
|
|
4584
|
+
const plan = truncateToolResults(parsed, cut, (text) => store.save(text));
|
|
4585
|
+
if (!plan)
|
|
4586
|
+
return undefined;
|
|
4587
|
+
const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
|
|
4588
|
+
const measurement = await sizing.measure(candidate, limits);
|
|
4589
|
+
if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
|
|
4590
|
+
rec.truncatedToolResult = { originalChars: plan.originalChars,
|
|
4591
|
+
omittedChars: plan.omittedChars };
|
|
4592
|
+
return { candidate, measurement };
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
return undefined;
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Whether delivery recompresses before sending, and to what: a ride counted
|
|
4599
|
+
* over the budget (no compression yet); the whole history counted over the
|
|
4600
|
+
* window with no compression yet (outside an outage cooldown: a refusal is
|
|
4601
|
+
* all that is left otherwise); or a compression that came back too large for
|
|
4602
|
+
* the window (oversizedCompressions), redone once to a target smaller by the
|
|
4603
|
+
* excess, so the turn is compressed and sent in the same request instead of
|
|
4604
|
+
* being refused and retried.
|
|
4605
|
+
*/
|
|
4606
|
+
function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
|
|
4607
|
+
if (rec.lateCountRecovery)
|
|
4608
|
+
return undefined;
|
|
4609
|
+
const oversized = oversizedCompressions.get(rec);
|
|
4610
|
+
// A lagging index kept history verbatim: the refusal waits for it instead.
|
|
4611
|
+
if (oversized && rec.indexBehind !== true) {
|
|
4612
|
+
const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
|
|
4613
|
+
const excess = oversized.tokens - goal;
|
|
4614
|
+
const target = Math.floor(oversized.target - excess - oversized.target * SMALLER_TARGET_MARGIN);
|
|
4615
|
+
return target >= SERVER_MIN_TARGET_TOKENS ? { tokens: oversized.tokens, target } : undefined;
|
|
4616
|
+
}
|
|
4617
|
+
if (rec.compress || measured.source !== "exact" || !(measured.tokens > limits.budget)) {
|
|
4618
|
+
return undefined;
|
|
4619
|
+
}
|
|
4620
|
+
// Another request of this lane is compressing (rec.routeRecovery "in-flight"):
|
|
4621
|
+
// a second compression of the same history at another target would miss its
|
|
4622
|
+
// dedupe and bill twice. This one waits for neither and goes as it is.
|
|
4623
|
+
if (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight)
|
|
4624
|
+
return undefined;
|
|
4625
|
+
if (preferred.kind === "ride")
|
|
4626
|
+
return { tokens: measured.tokens };
|
|
4627
|
+
// Over the window with nothing compressed, a refusal is all that is left:
|
|
4628
|
+
// an outage cooldown does not skip this one compress.
|
|
4629
|
+
return preferred.kind === "original" && !fitsMeasured(measured, limits)
|
|
4630
|
+
? { tokens: measured.tokens } : undefined;
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Run a delivery-time compression as the lane's in-flight operation, so a
|
|
4634
|
+
* concurrent request of the lane neither starts its own (planning sees
|
|
4635
|
+
* "in-flight") nor races it; the lane's own attempt state is kept.
|
|
4636
|
+
*/
|
|
4637
|
+
async function holdingLane(state, routeKey, run) {
|
|
4638
|
+
const lanes = state.toolRecoveryAttemptedLanes;
|
|
4639
|
+
const prior = lanes.get(routeKey);
|
|
4640
|
+
const attempt = prior ?? { modelContextLimit: 0, inFlight: true };
|
|
4641
|
+
// Clears only what it set: an attempt another request holds in flight
|
|
4642
|
+
// stays that request's to end.
|
|
4643
|
+
const setHere = !attempt.inFlight;
|
|
4644
|
+
attempt.inFlight = true;
|
|
4645
|
+
if (!prior)
|
|
4646
|
+
lanes.set(routeKey, attempt);
|
|
4647
|
+
try {
|
|
4648
|
+
return await run();
|
|
4649
|
+
}
|
|
4650
|
+
finally {
|
|
4651
|
+
if (setHere)
|
|
4652
|
+
attempt.inFlight = false;
|
|
4653
|
+
if (!prior && lanes.get(routeKey) === attempt)
|
|
4654
|
+
lanes.delete(routeKey);
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
/** A recompression aims this share of its old target lower than the excess alone. */
|
|
4658
|
+
const SMALLER_TARGET_MARGIN = 0.1;
|
|
4659
|
+
/**
|
|
4660
|
+
* Compressions of this request that came back too large for the window: their
|
|
4661
|
+
* size (an exact count, else the conservative bound) and the target they had.
|
|
4662
|
+
*/
|
|
4663
|
+
const oversizedCompressions = new WeakMap();
|
|
4664
|
+
/** Note a compression of `rec` that does not fit the window (lateRecoveryPlan). */
|
|
4665
|
+
function noteOversizedCompression(rec, sizing, body, compressedRaw, target) {
|
|
4666
|
+
if (target === undefined)
|
|
4667
|
+
return;
|
|
4668
|
+
const tokens = sizing.exactTokensFor(compressedRaw) ??
|
|
4669
|
+
conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
|
|
4670
|
+
oversizedCompressions.set(rec, { tokens, target });
|
|
4671
|
+
}
|
|
4002
4672
|
/**
|
|
4003
4673
|
* Whether delivery would refuse `candidate` sent whole, by delivery's own
|
|
4004
4674
|
* admission: it is sendable only when its plan fits and either an exact count
|
|
@@ -4119,6 +4789,25 @@ function nearWindowWarned(state, sessionId) {
|
|
|
4119
4789
|
function reachedBudget(compaction) {
|
|
4120
4790
|
return compaction !== undefined && (compaction.estimatedTokens ?? 0) >= compaction.budgetTokens;
|
|
4121
4791
|
}
|
|
4792
|
+
/**
|
|
4793
|
+
* Whether the planned size says a turn needed MemTree to compress it: a
|
|
4794
|
+
* forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
|
|
4795
|
+
* size at or over the budget, or a size nobody measured. A planned fit is
|
|
4796
|
+
* provisional: delivery's own sizing can still find the turn over the budget
|
|
4797
|
+
* (quietDegrades), since a plan can undercount.
|
|
4798
|
+
*/
|
|
4799
|
+
function compressionNeeded(compaction) {
|
|
4800
|
+
if (compaction === undefined || compaction.reason !== undefined)
|
|
4801
|
+
return true;
|
|
4802
|
+
if (compaction.estimatedTokens === undefined)
|
|
4803
|
+
return true;
|
|
4804
|
+
return compaction.estimatedTokens >= compaction.budgetTokens;
|
|
4805
|
+
}
|
|
4806
|
+
/**
|
|
4807
|
+
* A degraded turn whose plan fit the budget, so it queued no notice: the
|
|
4808
|
+
* notice to queue if delivery's sizing finds it over the budget after all.
|
|
4809
|
+
*/
|
|
4810
|
+
const quietDegrades = new WeakMap();
|
|
4122
4811
|
/** The paused notice a degraded request queued, until it is refused or settles. */
|
|
4123
4812
|
const pausedNotices = new WeakMap();
|
|
4124
4813
|
const changedDeliveryRecords = new WeakSet();
|
|
@@ -4130,7 +4819,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
|
|
|
4130
4819
|
const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
|
|
4131
4820
|
if (!selected)
|
|
4132
4821
|
return false;
|
|
4133
|
-
|
|
4822
|
+
// A late recovery's replacement went out instead: it installs as its route.
|
|
4823
|
+
const lateInstall = rec && selected !== bodyBuffer ? lateRouteInstalls.get(rec) : undefined;
|
|
4824
|
+
if (rec)
|
|
4825
|
+
lateRouteInstalls.delete(rec);
|
|
4826
|
+
const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : lateInstall?.activate);
|
|
4827
|
+
lateInstall?.settle(delivered, selected);
|
|
4134
4828
|
if (delivered && rec)
|
|
4135
4829
|
noteSelectedCandidateSize(rec);
|
|
4136
4830
|
return delivered;
|
|
@@ -4145,7 +4839,7 @@ function noteSelectedCandidateSize(rec) {
|
|
|
4145
4839
|
// Server tools' injected results inflate the reported input (reportedSample).
|
|
4146
4840
|
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
4147
4841
|
return;
|
|
4148
|
-
const sample =
|
|
4842
|
+
const sample = sizeSampleOf(candidate.body, tokens, rec.model);
|
|
4149
4843
|
if (!plausibleSample(sample))
|
|
4150
4844
|
return;
|
|
4151
4845
|
if ("set" in owner)
|
|
@@ -4329,6 +5023,9 @@ function forwardAccepted(req, res, bodyBuffer, opts, upstream, shutdownSignal, r
|
|
|
4329
5023
|
};
|
|
4330
5024
|
if (rec) {
|
|
4331
5025
|
rec.upstreamStatus = upstreamRes.statusCode ?? 502;
|
|
5026
|
+
const rateLimit = rateLimitFromResponse(rec.upstreamStatus, upstreamRes.headers);
|
|
5027
|
+
if (rateLimit)
|
|
5028
|
+
rec.rateLimit = rateLimit;
|
|
4332
5029
|
}
|
|
4333
5030
|
successfulStatus =
|
|
4334
5031
|
typeof upstreamRes.statusCode === "number" &&
|
|
@@ -4868,4 +5565,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
|
|
|
4868
5565
|
return undefined;
|
|
4869
5566
|
return (messages) => source.timesFor(sessionId, messages, agentId);
|
|
4870
5567
|
}
|
|
5568
|
+
export const __testHoldingLane = holdingLane;
|
|
4871
5569
|
//# sourceMappingURL=proxy.js.map
|