claude-code-infinite 1.0.29 → 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 +57 -7
- 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 +49 -15
- 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.map +1 -1
- package/dist/diagnostics-schema.js +5 -0
- package/dist/diagnostics-schema.js.map +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +18 -1
- 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/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.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/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 +363 -14
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +708 -90
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +55 -0
- 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 +1 -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,6 +64,7 @@ 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";
|
|
66
69
|
import { rateLimitFromResponse } from "./rate-limit.js";
|
|
67
70
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
@@ -78,6 +81,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
|
|
|
78
81
|
const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
|
|
79
82
|
/** Safety cap on a long-budget compress, should a transport ignore its abort. */
|
|
80
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;
|
|
81
86
|
const SKIP_REQUEST_HEADERS = new Set([
|
|
82
87
|
"host",
|
|
83
88
|
"connection",
|
|
@@ -96,20 +101,18 @@ const SKIP_RESPONSE_HEADERS = new Set([
|
|
|
96
101
|
]);
|
|
97
102
|
/**
|
|
98
103
|
* Budget fallback until the server reports `model_budget_tokens`, mirroring the
|
|
99
|
-
* server's ccc policy (polychat index_budget.py):
|
|
100
|
-
* near its window,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts
|
|
105
|
-
* window
|
|
106
|
-
*
|
|
107
|
-
* jumps past the budget reaches the proxy and is compressed here
|
|
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
|
|
111
|
+
* lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
|
|
112
|
+
* jumps past the budget reaches the proxy and is compressed here, on a
|
|
113
|
+
* prepared prefix when there is one (prepared-compression.ts).
|
|
108
114
|
*/
|
|
109
115
|
export const FALLBACK_BUDGET_WINDOW_RATIO = 0.8;
|
|
110
|
-
export const NEAR_WINDOW_FALLBACK_RATIO = 0.9;
|
|
111
|
-
/** Claude Code 2.1.302 auto-compacts at window - min(max_tokens, 20k) - 13k. */
|
|
112
|
-
const CLAUDE_CODE_AUTOCOMPACT_RESERVE_TOKENS = 33_000;
|
|
113
116
|
/** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
|
|
114
117
|
const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
|
|
115
118
|
/**
|
|
@@ -125,10 +128,14 @@ export function fallbackBudgetTokens(model, modelContextLimit) {
|
|
|
125
128
|
if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
|
|
126
129
|
return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
|
|
127
130
|
}
|
|
128
|
-
const nearWindow =
|
|
131
|
+
const nearWindow = nearWindowBudgetTokens(modelContextLimit);
|
|
129
132
|
const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
|
|
130
133
|
return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
|
|
131
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);
|
|
138
|
+
}
|
|
132
139
|
/**
|
|
133
140
|
* An explicit target is capped at this share of the budget: a compaction to
|
|
134
141
|
* the budget itself would be over budget again on the next turn and
|
|
@@ -320,6 +327,7 @@ export function startProxy(opts) {
|
|
|
320
327
|
const activeRequests = new Set();
|
|
321
328
|
const acceptedRequests = new Set();
|
|
322
329
|
const shutdownAbort = new AbortController();
|
|
330
|
+
const backgroundAbort = new AbortController();
|
|
323
331
|
const state = {
|
|
324
332
|
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
325
333
|
keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
@@ -347,6 +355,7 @@ export function startProxy(opts) {
|
|
|
347
355
|
toolRecoveryAttemptedLanes: new Map(),
|
|
348
356
|
toolRecoveryCooldownUntil: 0,
|
|
349
357
|
shutdownSignal: shutdownAbort.signal,
|
|
358
|
+
backgroundSignal: backgroundAbort.signal,
|
|
350
359
|
routeInstallFault: opts.routeInstallFault,
|
|
351
360
|
memtreePages: new Map(),
|
|
352
361
|
memorySourcePages: new Map(),
|
|
@@ -362,6 +371,9 @@ export function startProxy(opts) {
|
|
|
362
371
|
defaultCompactOff: opts.defaultCompactTarget === null,
|
|
363
372
|
compactNow: new Set(),
|
|
364
373
|
stablePrefixes: new Map(),
|
|
374
|
+
prepared: new PreparedCompressions(),
|
|
375
|
+
preparing: new Map(),
|
|
376
|
+
backgroundCompress: new Map(),
|
|
365
377
|
serverBudgets: new Map(),
|
|
366
378
|
serverReportsBudget: false,
|
|
367
379
|
countTokensCooldowns: new CountTokensCooldowns(),
|
|
@@ -412,6 +424,7 @@ export function startProxy(opts) {
|
|
|
412
424
|
const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
413
425
|
? Math.floor(timeoutMs)
|
|
414
426
|
: 5_000;
|
|
427
|
+
backgroundAbort.abort();
|
|
415
428
|
const quiesced = (async () => {
|
|
416
429
|
// Once close completes, every accepted request has entered the tracked
|
|
417
430
|
// set.
|
|
@@ -1089,7 +1102,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1089
1102
|
if (!rides) {
|
|
1090
1103
|
const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
|
|
1091
1104
|
if (measured.source === "exact") {
|
|
1092
|
-
sizeSlot.set(
|
|
1105
|
+
sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
|
|
1093
1106
|
}
|
|
1094
1107
|
}
|
|
1095
1108
|
rec.routeLane = requestRouteLane;
|
|
@@ -1150,12 +1163,18 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1150
1163
|
},
|
|
1151
1164
|
// The ride's bytes are only what failed; recovery always compresses the
|
|
1152
1165
|
// ORIGINAL conversation (the one MemTree indexed), never the ride itself.
|
|
1153
|
-
recoverLate: async (_ride, rideTokens) => {
|
|
1154
|
-
|
|
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) ||
|
|
1155
1171
|
(rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
|
|
1156
1172
|
return undefined;
|
|
1157
1173
|
return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
|
|
1158
|
-
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 } : {}) });
|
|
1159
1178
|
},
|
|
1160
1179
|
delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
|
|
1161
1180
|
});
|
|
@@ -1302,6 +1321,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1302
1321
|
ride: toolRide,
|
|
1303
1322
|
canCompress,
|
|
1304
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
|
+
}
|
|
1305
1329
|
if (plan.kind === "ride")
|
|
1306
1330
|
toolRide = plan.ride;
|
|
1307
1331
|
if (plan.kind === "pass" && plan.original) {
|
|
@@ -1341,9 +1365,13 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1341
1365
|
// for growth instead of paying a blocking call on every tool turn.
|
|
1342
1366
|
rec.routeRecovery = { outcome: "backoff" };
|
|
1343
1367
|
}
|
|
1344
|
-
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())))) {
|
|
1345
1371
|
// A recent attempt burned the full compress budget and still
|
|
1346
|
-
// 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
|
|
1347
1375
|
// compress() dedup can never absorb the repeat: without this
|
|
1348
1376
|
// cooldown a MemTree outage would add the whole blocking budget to
|
|
1349
1377
|
// every tool turn over the budget. A cooldown skip does not start
|
|
@@ -1424,9 +1452,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1424
1452
|
return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1425
1453
|
toolForwarding.settle(delivered);
|
|
1426
1454
|
if (toolRide)
|
|
1427
|
-
noteRideSize(toolRide.sizeHolder, rec, sendBody
|
|
1455
|
+
noteRideSize(toolRide.sizeHolder, rec, sendBody);
|
|
1428
1456
|
else {
|
|
1429
|
-
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody
|
|
1457
|
+
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
|
|
1430
1458
|
}
|
|
1431
1459
|
}));
|
|
1432
1460
|
}
|
|
@@ -1469,19 +1497,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1469
1497
|
const sessionId = requestSessionId(req);
|
|
1470
1498
|
// Stable-prefix (edge) compaction: the main thread's human turns only. The
|
|
1471
1499
|
// recap rides the main route above; subagents keep per-turn compression.
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
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
|
+
}
|
|
1485
1523
|
// An active subagent can repeat/embed the human prompt in its own request;
|
|
1486
1524
|
// producer suppression must also preserve the arm for the later main call.
|
|
1487
1525
|
if (displayForThisTurn)
|
|
@@ -1496,6 +1534,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1496
1534
|
// A lagging index left history verbatim: a refusal waits for indexing.
|
|
1497
1535
|
if (result)
|
|
1498
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
|
+
}
|
|
1499
1541
|
releaseUncommittedRouteDecision();
|
|
1500
1542
|
recordTurn(rec, "followup-degraded", rawBody);
|
|
1501
1543
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
|
|
@@ -1596,7 +1638,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1596
1638
|
: stable.installed
|
|
1597
1639
|
: undefined;
|
|
1598
1640
|
if (sizePrefix)
|
|
1599
|
-
notePrefixSize(sizePrefix, rec, compressedRaw
|
|
1641
|
+
notePrefixSize(sizePrefix, rec, compressedRaw);
|
|
1600
1642
|
// A route already installed at protocol-complete deliberately survives
|
|
1601
1643
|
// delivered=false: that settle may be the fast-tool abort (client
|
|
1602
1644
|
// consumed message_stop, closed the SSE response, and its immediate
|
|
@@ -1633,7 +1675,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1633
1675
|
}
|
|
1634
1676
|
}));
|
|
1635
1677
|
};
|
|
1636
|
-
if (existingRide && (!edge || edge.kind === "off")) {
|
|
1678
|
+
if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
|
|
1637
1679
|
const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
|
|
1638
1680
|
const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
|
|
1639
1681
|
candidateSizeOwners.set(candidate, owner);
|
|
@@ -1665,8 +1707,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1665
1707
|
const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
|
|
1666
1708
|
const noteWholeSize = () => {
|
|
1667
1709
|
if (isMainRequest)
|
|
1668
|
-
notePassthroughSize(state, sessionId, body.model, rec, forwardBody
|
|
1710
|
+
notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
|
|
1669
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
|
+
}
|
|
1670
1733
|
// The complete request upload can outlive its downstream subscriber while
|
|
1671
1734
|
// MemTree compression is in flight. Track that subscriber locally without
|
|
1672
1735
|
// feeding its lifetime into MemtreeClient.compress(): compression promises
|
|
@@ -1716,7 +1779,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1716
1779
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
|
|
1717
1780
|
.then((delivered) => {
|
|
1718
1781
|
if (isMainRequest)
|
|
1719
|
-
notePassthroughSize(state, sessionId, body.model, rec, rawBody
|
|
1782
|
+
notePassthroughSize(state, sessionId, body.model, rec, rawBody);
|
|
1720
1783
|
if (delivered)
|
|
1721
1784
|
markMainPromptDelivered();
|
|
1722
1785
|
}));
|
|
@@ -2315,7 +2378,8 @@ function rememberPage(pages, sessionId, page) {
|
|
|
2315
2378
|
*/
|
|
2316
2379
|
function noteMemtreeHealth(state, compression) {
|
|
2317
2380
|
if (compression.liveHealth === "failure") {
|
|
2318
|
-
|
|
2381
|
+
// Background calls the drain stopped (backgroundSignal) are no outage either.
|
|
2382
|
+
if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
|
|
2319
2383
|
return;
|
|
2320
2384
|
state.toolRecoveryCooldownUntil =
|
|
2321
2385
|
Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
|
|
@@ -2398,7 +2462,7 @@ stable) {
|
|
|
2398
2462
|
// Include trailing ambient role=system blocks: MemTree consolidated them
|
|
2399
2463
|
// into compressedBody.system, so treating them as suffix would duplicate
|
|
2400
2464
|
// those instructions on every tool request.
|
|
2401
|
-
|
|
2465
|
+
...prefixIdentity(originalMessages, "route"),
|
|
2402
2466
|
compressedMessages: cloneJson(compressedBody.messages),
|
|
2403
2467
|
compressedSystem: cloneJson(compressedBody.system),
|
|
2404
2468
|
hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
|
|
@@ -2437,7 +2501,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2437
2501
|
const prefix = {
|
|
2438
2502
|
sessionId,
|
|
2439
2503
|
originalSystemHash: compressed.originalSystemHash,
|
|
2440
|
-
|
|
2504
|
+
...prefixIdentity(originalMessages, "stable"),
|
|
2441
2505
|
compressedMessages: compressed.compressedMessages,
|
|
2442
2506
|
compressedSystem: compressed.compressedSystem,
|
|
2443
2507
|
hasCompressedSystem: compressed.hasCompressedSystem,
|
|
@@ -2448,6 +2512,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2448
2512
|
stable.installed = prefix;
|
|
2449
2513
|
boundedSet(state.stablePrefixes, sessionId, prefix);
|
|
2450
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);
|
|
2451
2518
|
return prefix;
|
|
2452
2519
|
}
|
|
2453
2520
|
/**
|
|
@@ -2468,6 +2535,7 @@ function persistStablePrefix(state, sessionId, prefix) {
|
|
|
2468
2535
|
store.save(sessionId, owner, {
|
|
2469
2536
|
originalSystemHash: prefix.originalSystemHash,
|
|
2470
2537
|
originalPrefixHashes: prefix.originalPrefixHashes,
|
|
2538
|
+
originalMediaFreeHashes: prefix.originalMediaFreeHashes,
|
|
2471
2539
|
compressedMessages: prefix.compressedMessages,
|
|
2472
2540
|
compressedSystem: prefix.compressedSystem,
|
|
2473
2541
|
hasCompressedSystem: prefix.hasCompressedSystem,
|
|
@@ -2520,7 +2588,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
|
|
|
2520
2588
|
function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
|
|
2521
2589
|
if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
|
|
2522
2590
|
return "defer";
|
|
2523
|
-
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId,
|
|
2591
|
+
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
|
|
2524
2592
|
if (miss === null)
|
|
2525
2593
|
return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
|
|
2526
2594
|
const authoritative = miss === "prefix" &&
|
|
@@ -2550,7 +2618,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
|
|
|
2550
2618
|
* at least one message after them. The suffix may be anything, human turns
|
|
2551
2619
|
* included.
|
|
2552
2620
|
*/
|
|
2553
|
-
function prefixMismatch(body, messages, prefix, sessionId,
|
|
2621
|
+
function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
|
|
2554
2622
|
if (!sessionId || prefix.sessionId !== sessionId)
|
|
2555
2623
|
return "session";
|
|
2556
2624
|
if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
|
|
@@ -2560,7 +2628,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
|
|
|
2560
2628
|
if (messages.length < prefixLength)
|
|
2561
2629
|
return "prefix";
|
|
2562
2630
|
for (let i = 0; i < prefixLength; i++) {
|
|
2563
|
-
if (
|
|
2631
|
+
if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
|
|
2564
2632
|
return "prefix";
|
|
2565
2633
|
}
|
|
2566
2634
|
}
|
|
@@ -2604,20 +2672,24 @@ function serverBudgetKey(model, modelContextLimit) {
|
|
|
2604
2672
|
return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
|
|
2605
2673
|
}
|
|
2606
2674
|
/**
|
|
2607
|
-
* The session's whole-request budget
|
|
2608
|
-
*
|
|
2609
|
-
*
|
|
2610
|
-
* 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.
|
|
2611
2682
|
*/
|
|
2612
2683
|
function resolveBudget(opts, state, model, modelContextLimit) {
|
|
2684
|
+
const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
|
|
2613
2685
|
if (opts.budgetTokensOverride !== undefined) {
|
|
2614
|
-
return { tokens: opts.budgetTokensOverride, source: "override" };
|
|
2686
|
+
return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
|
|
2615
2687
|
}
|
|
2616
2688
|
const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
|
|
2617
2689
|
if (reported !== undefined)
|
|
2618
|
-
return { tokens: reported, source: "server" };
|
|
2690
|
+
return { tokens: trigger(reported), source: "server" };
|
|
2619
2691
|
return {
|
|
2620
|
-
tokens: fallbackBudgetTokens(model, modelContextLimit),
|
|
2692
|
+
tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
|
|
2621
2693
|
source: "window-ratio",
|
|
2622
2694
|
};
|
|
2623
2695
|
}
|
|
@@ -2700,17 +2772,17 @@ function reportedInputTokens(rec) {
|
|
|
2700
2772
|
(usage.cache_creation_input_tokens ?? 0));
|
|
2701
2773
|
}
|
|
2702
2774
|
/** Keep the newest (largest-body) reported size of a request on this prefix. */
|
|
2703
|
-
function notePrefixSize(prefix, rec,
|
|
2704
|
-
const sample = reportedSample(rec,
|
|
2775
|
+
function notePrefixSize(prefix, rec, sent) {
|
|
2776
|
+
const sample = reportedSample(rec, sent);
|
|
2705
2777
|
if (sample === undefined)
|
|
2706
2778
|
return;
|
|
2707
|
-
if (prefix.lastSize &&
|
|
2779
|
+
if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
|
|
2708
2780
|
return;
|
|
2709
2781
|
prefix.lastSize = sample;
|
|
2710
2782
|
}
|
|
2711
2783
|
/** Record the reported size of a main-thread request forwarded whole. */
|
|
2712
|
-
function notePassthroughSize(state, sessionId, model, rec,
|
|
2713
|
-
const sample = reportedSample(rec,
|
|
2784
|
+
function notePassthroughSize(state, sessionId, model, rec, sent) {
|
|
2785
|
+
const sample = reportedSample(rec, sent);
|
|
2714
2786
|
if (sessionId === undefined || sample === undefined)
|
|
2715
2787
|
return;
|
|
2716
2788
|
boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
|
|
@@ -2740,13 +2812,18 @@ function usedServerTools(rec) {
|
|
|
2740
2812
|
* an implausible ratio would mis-size every later request it anchors, so it
|
|
2741
2813
|
* is dropped and the previous sample kept.
|
|
2742
2814
|
*/
|
|
2743
|
-
function reportedSample(rec,
|
|
2815
|
+
function reportedSample(rec, sent) {
|
|
2744
2816
|
const tokens = reportedBodyTokens(rec);
|
|
2745
2817
|
if (tokens === undefined)
|
|
2746
2818
|
return undefined;
|
|
2747
|
-
const sample =
|
|
2819
|
+
const sample = sizeSampleOf(sent, tokens, rec.model);
|
|
2748
2820
|
return plausibleSample(sample) ? sample : undefined;
|
|
2749
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
|
+
}
|
|
2750
2827
|
/**
|
|
2751
2828
|
* Anthropic's input tokens for the body this request forwarded, when its usage
|
|
2752
2829
|
* describes that body (not a changed delivery, no server tools). Not filtered
|
|
@@ -2793,12 +2870,12 @@ const noteRideSize = notePrefixSize;
|
|
|
2793
2870
|
* Record the reported size of a request forwarded whole: the session's
|
|
2794
2871
|
* passthrough size on the main thread, else its lane's.
|
|
2795
2872
|
*/
|
|
2796
|
-
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec,
|
|
2873
|
+
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
|
|
2797
2874
|
if (isMainRequest && sessionId !== undefined) {
|
|
2798
|
-
notePassthroughSize(state, sessionId, model, rec,
|
|
2875
|
+
notePassthroughSize(state, sessionId, model, rec, sent);
|
|
2799
2876
|
return;
|
|
2800
2877
|
}
|
|
2801
|
-
const sample = reportedSample(rec,
|
|
2878
|
+
const sample = reportedSample(rec, sent);
|
|
2802
2879
|
if (sample === undefined)
|
|
2803
2880
|
return;
|
|
2804
2881
|
boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
|
|
@@ -2859,7 +2936,7 @@ async function planEdgeCompaction(args) {
|
|
|
2859
2936
|
};
|
|
2860
2937
|
let prefix = state.stablePrefixes.get(sessionId);
|
|
2861
2938
|
if (prefix) {
|
|
2862
|
-
const miss = prefixMismatch(body, messages, prefix, sessionId,
|
|
2939
|
+
const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
|
|
2863
2940
|
if (miss) {
|
|
2864
2941
|
// Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
|
|
2865
2942
|
// longer stands for this conversation. Start over from passthrough.
|
|
@@ -2895,8 +2972,13 @@ async function planEdgeCompaction(args) {
|
|
|
2895
2972
|
(explicitTarget && prefix.targetTokens !== targetTokens)) {
|
|
2896
2973
|
return forced("target-change", fallback);
|
|
2897
2974
|
}
|
|
2898
|
-
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 };
|
|
2899
2980
|
return forced("budget", fallback);
|
|
2981
|
+
}
|
|
2900
2982
|
return { kind: "ride", prefix, routed };
|
|
2901
2983
|
}
|
|
2902
2984
|
if (state.compactNow.has(sessionId))
|
|
@@ -2909,12 +2991,18 @@ async function planEdgeCompaction(args) {
|
|
|
2909
2991
|
compaction.sizeSource = size.source;
|
|
2910
2992
|
noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
|
|
2911
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 };
|
|
2912
2998
|
return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
|
|
2913
2999
|
}
|
|
2914
3000
|
if (state.serverReportsBudget) {
|
|
2915
3001
|
// The server measures the request and compresses only past the budget.
|
|
2916
3002
|
const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
|
|
2917
3003
|
compaction.thresholdTokens = threshold;
|
|
3004
|
+
if (clearlyUnderBudget(opts, state, budget, size, compaction))
|
|
3005
|
+
return { kind: "pass" };
|
|
2918
3006
|
return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
|
|
2919
3007
|
}
|
|
2920
3008
|
// Without calibrated usage or threshold support, let the server decide
|
|
@@ -2955,7 +3043,7 @@ async function planToolCompaction(args) {
|
|
|
2955
3043
|
const stableLane = args.isMainRequest && sessionId !== undefined;
|
|
2956
3044
|
const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
|
|
2957
3045
|
let ride = args.ride;
|
|
2958
|
-
if (!ride && current && !prefixMismatch(body, messages, current, sessionId,
|
|
3046
|
+
if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
|
|
2959
3047
|
const routed = prefixRoutedBody(body, messages, current);
|
|
2960
3048
|
if (routed) {
|
|
2961
3049
|
const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
|
|
@@ -2993,12 +3081,27 @@ async function planToolCompaction(args) {
|
|
|
2993
3081
|
if (size.tokens < budget.tokens && !overWindow) {
|
|
2994
3082
|
return ride ? { kind: "ride", ride } : { kind: "pass" };
|
|
2995
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;
|
|
2996
3099
|
compaction.reason = "budget";
|
|
2997
3100
|
return {
|
|
2998
3101
|
kind: "compress",
|
|
2999
|
-
...budgetCompaction(state, compaction,
|
|
3000
|
-
targetTokens,
|
|
3001
|
-
explicitTarget:
|
|
3102
|
+
...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
|
|
3103
|
+
targetTokens: targetNow,
|
|
3104
|
+
explicitTarget: modeNow.mode === "explicit",
|
|
3002
3105
|
estimateTokens: size.tokens,
|
|
3003
3106
|
budgetTokens: budget.tokens,
|
|
3004
3107
|
overWindow,
|
|
@@ -3007,6 +3110,224 @@ async function planToolCompaction(args) {
|
|
|
3007
3110
|
...(stableLane ? { replaces: current ?? null } : {}),
|
|
3008
3111
|
};
|
|
3009
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
|
+
}
|
|
3010
3331
|
function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
3011
3332
|
// Model is deliberately not route identity: Claude Code switches models
|
|
3012
3333
|
// mid-loop (overload fallback, /model, /fast), and the compressed prefix is
|
|
@@ -3024,7 +3345,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
|
3024
3345
|
if (messages.length < prefixLength)
|
|
3025
3346
|
return null;
|
|
3026
3347
|
for (let i = 0; i < prefixLength; i++) {
|
|
3027
|
-
if (
|
|
3348
|
+
if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
|
|
3028
3349
|
return null;
|
|
3029
3350
|
}
|
|
3030
3351
|
}
|
|
@@ -3407,8 +3728,9 @@ async function runBlockingCompression(args) {
|
|
|
3407
3728
|
: undefined;
|
|
3408
3729
|
const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
|
|
3409
3730
|
const compressStarted = Date.now();
|
|
3410
|
-
|
|
3411
|
-
|
|
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));
|
|
3412
3734
|
const compressMeta = {
|
|
3413
3735
|
// Model + tools drive the server's model-based memory budget
|
|
3414
3736
|
// (e.g. 800k whole-request target for Fable / Opus 5). Omitting
|
|
@@ -3436,7 +3758,9 @@ async function runBlockingCompression(args) {
|
|
|
3436
3758
|
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
3437
3759
|
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
3438
3760
|
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3439
|
-
|
|
3761
|
+
// The deadline this wait uses (the upload-scaled budget included), so the
|
|
3762
|
+
// call and its waiters end together.
|
|
3763
|
+
timeoutMs: budgetMs,
|
|
3440
3764
|
};
|
|
3441
3765
|
// Sampled BEFORE the call, while it still describes this call: after the
|
|
3442
3766
|
// await the hash is in the cache regardless of who put it there.
|
|
@@ -3444,9 +3768,9 @@ async function runBlockingCompression(args) {
|
|
|
3444
3768
|
// compress() maps every failure to a resolved null (it never rejects).
|
|
3445
3769
|
// Keep foreground work finite even if a transport ignores its abort signal.
|
|
3446
3770
|
let deadline;
|
|
3447
|
-
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));
|
|
3448
3772
|
const result = await Promise.race([
|
|
3449
|
-
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
|
|
3773
|
+
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
|
|
3450
3774
|
new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
|
|
3451
3775
|
]).finally(() => { if (deadline)
|
|
3452
3776
|
clearTimeout(deadline); });
|
|
@@ -3456,19 +3780,27 @@ async function runBlockingCompression(args) {
|
|
|
3456
3780
|
// another class can overwrite the complete-key entry.
|
|
3457
3781
|
const failureArming = result === null &&
|
|
3458
3782
|
opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
|
|
3783
|
+
const failureStatus = result === null
|
|
3784
|
+
? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
|
|
3459
3785
|
const hadLiveSuccess = !cached && result !== null;
|
|
3460
3786
|
// Only arming-class live failures (network error, timeout, 5xx, 402) are
|
|
3461
3787
|
// fuse evidence. A responsive server's other 4xx failed THIS call but says
|
|
3462
3788
|
// nothing about MemTree's health, so it contributes neither failure nor
|
|
3463
3789
|
// success — the fuse is untouched.
|
|
3464
|
-
|
|
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;
|
|
3465
3799
|
rec.compress = {
|
|
3466
3800
|
ms: compressMs,
|
|
3467
3801
|
ok: result !== null,
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
// the AbortSignal timeout, not a fast server error.
|
|
3471
|
-
timedOut: result === null && compressMs >= budgetMs,
|
|
3802
|
+
timedOut,
|
|
3803
|
+
...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
|
|
3472
3804
|
};
|
|
3473
3805
|
return {
|
|
3474
3806
|
result,
|
|
@@ -3480,6 +3812,61 @@ async function runBlockingCompression(args) {
|
|
|
3480
3812
|
: "none",
|
|
3481
3813
|
};
|
|
3482
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
|
+
}
|
|
3483
3870
|
/**
|
|
3484
3871
|
* Shared final request shape for a validated compression result: lift any
|
|
3485
3872
|
* returned system message to `body.system`, and forward the SERVER's flatten
|
|
@@ -3579,14 +3966,14 @@ async function recoverToolRouteMiss(args) {
|
|
|
3579
3966
|
capture(opts, "anthropic-request-memory-tool", fallback.raw);
|
|
3580
3967
|
return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3581
3968
|
toolForwarding.settle(delivered);
|
|
3582
|
-
noteRideSize(fallback.sizeHolder, rec, fallback.raw
|
|
3969
|
+
noteRideSize(fallback.sizeHolder, rec, fallback.raw);
|
|
3583
3970
|
});
|
|
3584
3971
|
}
|
|
3585
3972
|
recordTurn(rec, "tool", args.originalBody);
|
|
3586
3973
|
capture(opts, "anthropic-request", args.originalBody);
|
|
3587
3974
|
return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3588
3975
|
toolForwarding.settle(delivered);
|
|
3589
|
-
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody
|
|
3976
|
+
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
|
|
3590
3977
|
});
|
|
3591
3978
|
};
|
|
3592
3979
|
// Same downstream-close tracking as the followup path: compression
|
|
@@ -3727,6 +4114,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3727
4114
|
const { compressedBody, compressedRaw } = built;
|
|
3728
4115
|
if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
|
|
3729
4116
|
rec.routeRecovery = { conversationBytes, outcome: "unusable" };
|
|
4117
|
+
noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
|
|
3730
4118
|
releaseOwnReservation();
|
|
3731
4119
|
if (!noteIndexLag(opts, rec, result))
|
|
3732
4120
|
return forwardOriginal();
|
|
@@ -3873,7 +4261,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3873
4261
|
// The compacted request's reported size anchors the next budget check.
|
|
3874
4262
|
const sizeHolder = stable?.installed ?? installedRoute;
|
|
3875
4263
|
if (sizeHolder)
|
|
3876
|
-
notePrefixSize(sizeHolder, rec, compressedRaw
|
|
4264
|
+
notePrefixSize(sizeHolder, rec, compressedRaw);
|
|
3877
4265
|
// Nothing to ride next turn, or a result that still does not fit the
|
|
3878
4266
|
// window (the next turn would reject it and compress again): wait for
|
|
3879
4267
|
// growth before the lane tries again.
|
|
@@ -3972,18 +4360,29 @@ const recapLinkAppenders = new WeakMap();
|
|
|
3972
4360
|
* miss the tree, index a divergent conversation and bill for it.
|
|
3973
4361
|
*/
|
|
3974
4362
|
async function recoverLateCount(args) {
|
|
4363
|
+
let generation;
|
|
3975
4364
|
try {
|
|
3976
4365
|
const { body, msgsForMemtree: messages } = args;
|
|
3977
4366
|
const budget = resolveBudget(args.opts, args.state, body.model, args.modelContextLimit).tokens;
|
|
3978
|
-
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);
|
|
3979
4376
|
const outcome = await runBlockingCompression({
|
|
3980
4377
|
opts: args.opts, state: args.state, body, msgsForMemtree: messages,
|
|
3981
4378
|
hash: args.hash, modelContextLimit: args.modelContextLimit,
|
|
3982
|
-
rec: args.rec, sessionId
|
|
4379
|
+
rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
|
|
3983
4380
|
clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
|
|
3984
4381
|
compaction: { target },
|
|
3985
4382
|
});
|
|
3986
4383
|
noteMemtreeHealth(args.state, outcome);
|
|
4384
|
+
if (linkSeq !== undefined)
|
|
4385
|
+
noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
|
|
3987
4386
|
// Like every compress path: a refusal after a lagging answer waits for
|
|
3988
4387
|
// the index, and the cached answer expires so a retry asks again.
|
|
3989
4388
|
if (outcome.result)
|
|
@@ -3994,11 +4393,63 @@ async function recoverLateCount(args) {
|
|
|
3994
4393
|
const built = buildCompressedBody(body, outcome.result);
|
|
3995
4394
|
if (!built)
|
|
3996
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
|
|
3997
4401
|
return built.compressedRaw;
|
|
3998
4402
|
}
|
|
3999
4403
|
catch {
|
|
4000
4404
|
return undefined;
|
|
4001
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
|
+
};
|
|
4002
4453
|
}
|
|
4003
4454
|
/**
|
|
4004
4455
|
* The ride compressed to `target` still counted `rideTokens` (over the
|
|
@@ -4018,16 +4469,25 @@ async function selectForwardCandidate(args) {
|
|
|
4018
4469
|
const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
|
|
4019
4470
|
const measured = await policy.sizing.measure(preferred, args.limits);
|
|
4020
4471
|
let target = preferred;
|
|
4021
|
-
|
|
4022
|
-
|
|
4472
|
+
const late = lateRecoveryPlan(args.state, args.rec, preferred, measured, args.limits, args.routeKey);
|
|
4473
|
+
if (late) {
|
|
4023
4474
|
args.rec.lateCountRecovery = { outcome: "forwarded" };
|
|
4024
|
-
const replacement = await policy.recoverLate(preferred.body,
|
|
4475
|
+
const replacement = await holdingLane(args.state, args.routeKey, () => policy.recoverLate(preferred.body, late.tokens, late.target));
|
|
4025
4476
|
if (replacement)
|
|
4026
4477
|
target = policy.sizing.register(replacement, "replacement");
|
|
4027
4478
|
}
|
|
4028
|
-
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);
|
|
4029
4484
|
const quietDegrade = quietDegrades.get(args.rec);
|
|
4030
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
|
+
}
|
|
4031
4491
|
if (!selection) {
|
|
4032
4492
|
if (args.rec.lateCountRecovery)
|
|
4033
4493
|
args.rec.lateCountRecovery.outcome = "refused";
|
|
@@ -4035,6 +4495,10 @@ async function selectForwardCandidate(args) {
|
|
|
4035
4495
|
return undefined;
|
|
4036
4496
|
}
|
|
4037
4497
|
const { candidate, measurement } = selection;
|
|
4498
|
+
const preparedBase = preparedTruncationBases.get(policy.sizing);
|
|
4499
|
+
if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
|
|
4500
|
+
preparedBase.install();
|
|
4501
|
+
}
|
|
4038
4502
|
const changed = candidate !== preferred || intended !== args.preferredBody;
|
|
4039
4503
|
if (changed) {
|
|
4040
4504
|
policy.allowed.delete(args.preferredBody);
|
|
@@ -4057,6 +4521,154 @@ async function selectForwardCandidate(args) {
|
|
|
4057
4521
|
}
|
|
4058
4522
|
return changed ? candidate.body : intended;
|
|
4059
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
|
+
}
|
|
4060
4672
|
/**
|
|
4061
4673
|
* Whether delivery would refuse `candidate` sent whole, by delivery's own
|
|
4062
4674
|
* admission: it is sendable only when its plan fits and either an exact count
|
|
@@ -4207,7 +4819,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
|
|
|
4207
4819
|
const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
|
|
4208
4820
|
if (!selected)
|
|
4209
4821
|
return false;
|
|
4210
|
-
|
|
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);
|
|
4211
4828
|
if (delivered && rec)
|
|
4212
4829
|
noteSelectedCandidateSize(rec);
|
|
4213
4830
|
return delivered;
|
|
@@ -4222,7 +4839,7 @@ function noteSelectedCandidateSize(rec) {
|
|
|
4222
4839
|
// Server tools' injected results inflate the reported input (reportedSample).
|
|
4223
4840
|
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
4224
4841
|
return;
|
|
4225
|
-
const sample =
|
|
4842
|
+
const sample = sizeSampleOf(candidate.body, tokens, rec.model);
|
|
4226
4843
|
if (!plausibleSample(sample))
|
|
4227
4844
|
return;
|
|
4228
4845
|
if ("set" in owner)
|
|
@@ -4948,4 +5565,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
|
|
|
4948
5565
|
return undefined;
|
|
4949
5566
|
return (messages) => source.timesFor(sessionId, messages, agentId);
|
|
4950
5567
|
}
|
|
5568
|
+
export const __testHoldingLane = holdingLane;
|
|
4951
5569
|
//# sourceMappingURL=proxy.js.map
|