claude-code-infinite 1.0.29 → 1.0.31
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 +65 -7
- package/dist/cli-args.d.ts +10 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +30 -0
- package/dist/cli-args.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 +61 -16
- 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/hooks.d.ts +24 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -2
- package/dist/hooks.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-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +4 -0
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree.d.ts +86 -3
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +283 -53
- package/dist/memtree.js.map +1 -1
- package/dist/model-context.d.ts +80 -0
- package/dist/model-context.d.ts.map +1 -0
- package/dist/model-context.js +192 -0
- package/dist/model-context.js.map +1 -0
- 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/payment-gate.d.ts +2 -0
- package/dist/payment-gate.d.ts.map +1 -1
- package/dist/payment-gate.js +1 -1
- package/dist/payment-gate.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 +370 -14
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +797 -94
- 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";
|
|
@@ -53,15 +55,17 @@ import { join } from "node:path";
|
|
|
53
55
|
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
54
56
|
import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
|
|
55
57
|
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
|
|
56
|
-
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
58
|
+
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH, } from "./hooks.js";
|
|
57
59
|
import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
|
|
58
60
|
import { hyperlink } from "./payment-gate.js";
|
|
59
61
|
import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
|
|
62
|
+
import { KEY_INVALID_KIND, ModelContextNotices, PAUSED_KIND, keyInvalidModelContext, lastToldKindInFile, pausedModelContext } from "./model-context.js";
|
|
60
63
|
import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
|
|
61
64
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
62
65
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
63
66
|
import { isResumableSessionId } from "./resume-hint.js";
|
|
64
67
|
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
68
|
+
import { bodyImages } from "./image-tokens.js";
|
|
65
69
|
import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
|
|
66
70
|
import { rateLimitFromResponse } from "./rate-limit.js";
|
|
67
71
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
@@ -78,6 +82,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
|
|
|
78
82
|
const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
|
|
79
83
|
/** Safety cap on a long-budget compress, should a transport ignore its abort. */
|
|
80
84
|
const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
|
|
85
|
+
/** The most a blocking compress with a fallback may wait (its budget grows with its upload). */
|
|
86
|
+
const MAX_COMPRESS_WAIT_MS = 60_000;
|
|
81
87
|
const SKIP_REQUEST_HEADERS = new Set([
|
|
82
88
|
"host",
|
|
83
89
|
"connection",
|
|
@@ -96,20 +102,18 @@ const SKIP_RESPONSE_HEADERS = new Set([
|
|
|
96
102
|
]);
|
|
97
103
|
/**
|
|
98
104
|
* 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
|
|
105
|
+
* server's ccc policy (polychat index_budget.py near_window_budget_tokens):
|
|
106
|
+
* every Claude model compresses near its window, at the window less the
|
|
107
|
+
* reply's room (COMPRESSION_RESERVE_TOKENS, 40k) and never below
|
|
108
|
+
* FALLBACK_BUDGET_WINDOW_RATIO of it: 960k of 1M, 160k of 200k; at most a
|
|
109
|
+
* Sonnet's usable-context ceiling (SONNET_CEILINGS). Other models get
|
|
110
|
+
* FALLBACK_BUDGET_WINDOW_RATIO. Plain Claude Code 2.1.302 auto-compacts at
|
|
111
|
+
* window - 33k (967k of 1M), so MemTree compresses just before it would; ccc
|
|
112
|
+
* lifts Claude Code's own 977k refusal (claude-env.ts) so a tool turn that
|
|
113
|
+
* jumps past the budget reaches the proxy and is compressed here, on a
|
|
114
|
+
* prepared prefix when there is one (prepared-compression.ts).
|
|
108
115
|
*/
|
|
109
116
|
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
117
|
/** The ids the server resolves to a Claude model (polychat ANTHROPIC_MODEL_FAMILIES). */
|
|
114
118
|
const CLAUDE_FAMILY_RE = /opus|sonnet|haiku|fable|mythos/i;
|
|
115
119
|
/**
|
|
@@ -125,10 +129,14 @@ export function fallbackBudgetTokens(model, modelContextLimit) {
|
|
|
125
129
|
if (typeof model !== "string" || !CLAUDE_FAMILY_RE.test(model)) {
|
|
126
130
|
return Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO);
|
|
127
131
|
}
|
|
128
|
-
const nearWindow =
|
|
132
|
+
const nearWindow = nearWindowBudgetTokens(modelContextLimit);
|
|
129
133
|
const ceiling = SONNET_CEILINGS.find(([re]) => re.test(model))?.[1];
|
|
130
134
|
return ceiling === undefined ? nearWindow : Math.min(nearWindow, ceiling);
|
|
131
135
|
}
|
|
136
|
+
/** A Claude model's near-window budget before any ceiling (fallbackBudgetTokens). */
|
|
137
|
+
function nearWindowBudgetTokens(modelContextLimit) {
|
|
138
|
+
return Math.max(Math.floor(modelContextLimit * FALLBACK_BUDGET_WINDOW_RATIO), modelContextLimit - COMPRESSION_RESERVE_TOKENS);
|
|
139
|
+
}
|
|
132
140
|
/**
|
|
133
141
|
* An explicit target is capped at this share of the budget: a compaction to
|
|
134
142
|
* the budget itself would be over budget again on the next turn and
|
|
@@ -320,6 +328,7 @@ export function startProxy(opts) {
|
|
|
320
328
|
const activeRequests = new Set();
|
|
321
329
|
const acceptedRequests = new Set();
|
|
322
330
|
const shutdownAbort = new AbortController();
|
|
331
|
+
const backgroundAbort = new AbortController();
|
|
323
332
|
const state = {
|
|
324
333
|
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
325
334
|
keyNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
@@ -328,6 +337,9 @@ export function startProxy(opts) {
|
|
|
328
337
|
pausedNearWindowWarned: new Set(),
|
|
329
338
|
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
330
339
|
pausedDetail: () => opts.memtree.pausedDetail,
|
|
340
|
+
pausedReason: () => opts.memtree.pausedReason,
|
|
341
|
+
memtreeAnswering: () => opts.memtree.answering,
|
|
342
|
+
modelNotices: new ModelContextNotices(),
|
|
331
343
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
332
344
|
sessionStartNotice: opts.sessionStartNotice,
|
|
333
345
|
promptSubmitNotice: opts.promptSubmitNotice,
|
|
@@ -347,6 +359,7 @@ export function startProxy(opts) {
|
|
|
347
359
|
toolRecoveryAttemptedLanes: new Map(),
|
|
348
360
|
toolRecoveryCooldownUntil: 0,
|
|
349
361
|
shutdownSignal: shutdownAbort.signal,
|
|
362
|
+
backgroundSignal: backgroundAbort.signal,
|
|
350
363
|
routeInstallFault: opts.routeInstallFault,
|
|
351
364
|
memtreePages: new Map(),
|
|
352
365
|
memorySourcePages: new Map(),
|
|
@@ -362,6 +375,9 @@ export function startProxy(opts) {
|
|
|
362
375
|
defaultCompactOff: opts.defaultCompactTarget === null,
|
|
363
376
|
compactNow: new Set(),
|
|
364
377
|
stablePrefixes: new Map(),
|
|
378
|
+
prepared: new PreparedCompressions(),
|
|
379
|
+
preparing: new Map(),
|
|
380
|
+
backgroundCompress: new Map(),
|
|
365
381
|
serverBudgets: new Map(),
|
|
366
382
|
serverReportsBudget: false,
|
|
367
383
|
countTokensCooldowns: new CountTokensCooldowns(),
|
|
@@ -412,6 +428,7 @@ export function startProxy(opts) {
|
|
|
412
428
|
const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
413
429
|
? Math.floor(timeoutMs)
|
|
414
430
|
: 5_000;
|
|
431
|
+
backgroundAbort.abort();
|
|
415
432
|
const quiesced = (async () => {
|
|
416
433
|
// Once close completes, every accepted request has entered the tracked
|
|
417
434
|
// set.
|
|
@@ -476,6 +493,9 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
|
|
|
476
493
|
res.end(JSON.stringify({ detail: "The ccc proxy only serves programs on this machine" }));
|
|
477
494
|
return;
|
|
478
495
|
}
|
|
496
|
+
if (url.pathname === PRECOMPACT_HOOK_PATH) {
|
|
497
|
+
return handleNoticeHook(req, res, state, opts.reqlog, undefined, "PreCompact");
|
|
498
|
+
}
|
|
479
499
|
if (url.pathname === hookPath) {
|
|
480
500
|
return handleNoticeHook(req, res, state, opts.reqlog, (sessionId) => opts.memtree.scheduleFinalIndex(sessionId, (signal) => opts.transcriptUsage?.finalReplyAsync
|
|
481
501
|
? opts.transcriptUsage.finalReplyAsync(sessionId, signal)
|
|
@@ -710,7 +730,9 @@ function noteMainSessionStart(state, source, sessionId) {
|
|
|
710
730
|
/** Serve only validated Claude hook POSTs on the randomized localhost path. */
|
|
711
731
|
async function handleNoticeHook(req, res, state, reqlog,
|
|
712
732
|
/** A main-lane turn ended: the session may be over (its final index). */
|
|
713
|
-
onMainStop
|
|
733
|
+
onMainStop,
|
|
734
|
+
/** The only event this path takes (the token-free PreCompact path). */
|
|
735
|
+
onlyEvent) {
|
|
714
736
|
if (req.method !== "POST") {
|
|
715
737
|
res.writeHead(405, { allow: "POST" });
|
|
716
738
|
res.end();
|
|
@@ -744,19 +766,32 @@ onMainStop) {
|
|
|
744
766
|
res.end();
|
|
745
767
|
return;
|
|
746
768
|
}
|
|
769
|
+
if (onlyEvent !== undefined && parsed.hook_event_name !== onlyEvent) {
|
|
770
|
+
res.writeHead(403);
|
|
771
|
+
res.end();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
747
774
|
if (parsed.hook_event_name === "SessionStart") {
|
|
748
775
|
if (parsed.agent_id === undefined) {
|
|
749
776
|
recordSessionStart(state, parsed.source);
|
|
750
777
|
noteMainSessionStart(state, parsed.source, parsed.session_id);
|
|
778
|
+
// Claude Code's compaction dropped any MemTree notice from the transcript.
|
|
779
|
+
if (parsed.source === "compact")
|
|
780
|
+
state.modelNotices.forget(parsed.session_id);
|
|
751
781
|
}
|
|
752
782
|
// Earlier trees in this project: asked now, so its wait overlaps the resume's.
|
|
753
783
|
const contextPromise = projectTreesContext(state.projectTrees, parsed);
|
|
784
|
+
// A resumed or forked history may carry notices an earlier process gave.
|
|
785
|
+
const restored = parsed.agent_id === undefined &&
|
|
786
|
+
(parsed.source === "resume" || parsed.source === "fork")
|
|
787
|
+
? restoreModelNotices(state, parsed) : Promise.resolve();
|
|
754
788
|
// A resume waits briefly for the newest tree; a fresh session has none to wait for.
|
|
755
789
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
756
790
|
await state.newestTrees?.settle(parsed.session_id);
|
|
757
791
|
}
|
|
758
792
|
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
|
|
759
793
|
const context = await contextPromise;
|
|
794
|
+
await restored;
|
|
760
795
|
if (!line && !context) {
|
|
761
796
|
res.writeHead(204);
|
|
762
797
|
res.end();
|
|
@@ -771,6 +806,25 @@ onMainStop) {
|
|
|
771
806
|
res.end(body);
|
|
772
807
|
return;
|
|
773
808
|
}
|
|
809
|
+
if (parsed.hook_event_name === "PreCompact") {
|
|
810
|
+
// A typed /compact while MemTree compresses: say why it isn't needed, once.
|
|
811
|
+
const reason = parsed.agent_id === undefined && parsed.trigger === "manual"
|
|
812
|
+
? state.modelNotices.blockCompact(parsed.session_id, memtreeWorking(state))
|
|
813
|
+
: undefined;
|
|
814
|
+
if (reason === undefined) {
|
|
815
|
+
res.writeHead(204);
|
|
816
|
+
res.end();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const body = Buffer.from(JSON.stringify({ decision: "block", reason }), "utf-8");
|
|
820
|
+
res.writeHead(200, {
|
|
821
|
+
"content-type": "application/json",
|
|
822
|
+
"content-length": String(body.length),
|
|
823
|
+
"cache-control": "no-store",
|
|
824
|
+
});
|
|
825
|
+
res.end(body);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
774
828
|
if (parsed.hook_event_name === "UserPromptSubmit") {
|
|
775
829
|
// `/memtree-view`: answered here and blocked, so no model turn runs and
|
|
776
830
|
// the prompt never enters the conversation. Not a human turn either, so
|
|
@@ -809,8 +863,11 @@ onMainStop) {
|
|
|
809
863
|
recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
|
|
810
864
|
}
|
|
811
865
|
const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
|
|
812
|
-
|
|
813
|
-
|
|
866
|
+
const context = parsed.agent_id === undefined
|
|
867
|
+
? state.modelNotices.next(parsed.session_id, currentModelNotice(state))
|
|
868
|
+
: undefined;
|
|
869
|
+
if (line || context) {
|
|
870
|
+
const body = Buffer.from(JSON.stringify(promptSubmitOutput(line, context)), "utf-8");
|
|
814
871
|
res.writeHead(200, {
|
|
815
872
|
"content-type": "application/json",
|
|
816
873
|
"content-length": String(body.length),
|
|
@@ -1089,7 +1146,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1089
1146
|
if (!rides) {
|
|
1090
1147
|
const measured = await requestSizing.measure(forwardCandidate, sizingLimits(), "plan");
|
|
1091
1148
|
if (measured.source === "exact") {
|
|
1092
|
-
sizeSlot.set(
|
|
1149
|
+
sizeSlot.set(sizeSampleOf(forwardCandidate.body, measured.tokens, rec.model ?? body.model));
|
|
1093
1150
|
}
|
|
1094
1151
|
}
|
|
1095
1152
|
rec.routeLane = requestRouteLane;
|
|
@@ -1150,12 +1207,18 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1150
1207
|
},
|
|
1151
1208
|
// The ride's bytes are only what failed; recovery always compresses the
|
|
1152
1209
|
// ORIGINAL conversation (the one MemTree indexed), never the ride itself.
|
|
1153
|
-
recoverLate: async (_ride, rideTokens) => {
|
|
1154
|
-
|
|
1210
|
+
recoverLate: async (_ride, rideTokens, target) => {
|
|
1211
|
+
// A compression already made may be redone only to a smaller target.
|
|
1212
|
+
if ((rec.compress && target === undefined) ||
|
|
1213
|
+
compactionMode(opts, state, requestSessionKey).mode === "off" ||
|
|
1214
|
+
!hasEarlierNonToolUserMessage(messages) ||
|
|
1155
1215
|
(rec.turnType.startsWith("tool") && opts.toolRouteRecovery === false))
|
|
1156
1216
|
return undefined;
|
|
1157
1217
|
return recoverLateCount({ opts, state, req, rec, body, msgsForMemtree, hash, rideTokens,
|
|
1158
|
-
modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate
|
|
1218
|
+
modelContextLimit, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
|
|
1219
|
+
route: { key: requestRouteKey, messages, epoch: routeEpoch,
|
|
1220
|
+
lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
|
|
1221
|
+
...(target !== undefined ? { target } : {}) });
|
|
1159
1222
|
},
|
|
1160
1223
|
delivered: () => { state.fallbackFailures.succeeded(requestRouteKey); toolForwarding.settle(true); },
|
|
1161
1224
|
});
|
|
@@ -1302,6 +1365,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1302
1365
|
ride: toolRide,
|
|
1303
1366
|
canCompress,
|
|
1304
1367
|
});
|
|
1368
|
+
if (isMainRequest && toolSessionId !== undefined && canCompress &&
|
|
1369
|
+
(plan.kind === "ride" || plan.kind === "pass")) {
|
|
1370
|
+
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
|
|
1371
|
+
sessionId: toolSessionId, modelContextLimit, rec, clientMeta });
|
|
1372
|
+
}
|
|
1305
1373
|
if (plan.kind === "ride")
|
|
1306
1374
|
toolRide = plan.ride;
|
|
1307
1375
|
if (plan.kind === "pass" && plan.original) {
|
|
@@ -1341,9 +1409,13 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1341
1409
|
// for growth instead of paying a blocking call on every tool turn.
|
|
1342
1410
|
rec.routeRecovery = { outcome: "backoff" };
|
|
1343
1411
|
}
|
|
1344
|
-
else if (Date.now() < state.toolRecoveryCooldownUntil
|
|
1412
|
+
else if (Date.now() < state.toolRecoveryCooldownUntil &&
|
|
1413
|
+
!(plan.overWindow && !plan.fallback && (!plan.uncountedOverWindowOnly ||
|
|
1414
|
+
await wholeRequestRefused(requestSizing, originalCandidate, sizingLimits())))) {
|
|
1345
1415
|
// A recent attempt burned the full compress budget and still
|
|
1346
|
-
// failed.
|
|
1416
|
+
// failed. (Not when this turn is over the window with no prefix to
|
|
1417
|
+
// ride and delivery would refuse it: skipping could only end in a
|
|
1418
|
+
// refusal, so it compresses anyway, on the long budget.) Every tool turn appends a tool_result and rehashes, so
|
|
1347
1419
|
// compress() dedup can never absorb the repeat: without this
|
|
1348
1420
|
// cooldown a MemTree outage would add the whole blocking budget to
|
|
1349
1421
|
// every tool turn over the budget. A cooldown skip does not start
|
|
@@ -1424,9 +1496,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1424
1496
|
return logged(forwardRaw(req, res, sendBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1425
1497
|
toolForwarding.settle(delivered);
|
|
1426
1498
|
if (toolRide)
|
|
1427
|
-
noteRideSize(toolRide.sizeHolder, rec, sendBody
|
|
1499
|
+
noteRideSize(toolRide.sizeHolder, rec, sendBody);
|
|
1428
1500
|
else {
|
|
1429
|
-
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody
|
|
1501
|
+
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody);
|
|
1430
1502
|
}
|
|
1431
1503
|
}));
|
|
1432
1504
|
}
|
|
@@ -1469,19 +1541,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1469
1541
|
const sessionId = requestSessionId(req);
|
|
1470
1542
|
// Stable-prefix (edge) compaction: the main thread's human turns only. The
|
|
1471
1543
|
// 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
|
-
|
|
1544
|
+
const planEdge = () => planEdgeCompaction({ sizing: requestSizing, opts, state, body, messages,
|
|
1545
|
+
sessionId: sessionId, modelContextLimit, forwardBody, rec });
|
|
1546
|
+
let edge = isMainRequest && sessionId !== undefined ? await planEdge() : undefined;
|
|
1547
|
+
if (edge?.kind === "compress" && (state.preparing.has(sessionId) ||
|
|
1548
|
+
rec.compaction?.preparedWaitMs !== undefined || rec.compaction?.prepared !== undefined)) {
|
|
1549
|
+
// Never beside the session's preparation (both would be billed): wait for
|
|
1550
|
+
// it, then plan again with the settings now current (a /memtree-compact
|
|
1551
|
+
// that arrived while it waited or counted) and the preparation it left.
|
|
1552
|
+
const waited = rec.compaction?.preparedWaitMs ?? 0;
|
|
1553
|
+
const prepared = rec.compaction?.prepared;
|
|
1554
|
+
await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
|
|
1555
|
+
const total = waited + (rec.compaction?.preparedWaitMs ?? 0);
|
|
1556
|
+
edge = await planEdge();
|
|
1557
|
+
if (rec.compaction) {
|
|
1558
|
+
rec.compaction.preparedWaitMs = total;
|
|
1559
|
+
rec.compaction.prepared ??= prepared;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
// Not beside a blocking compress call: the two would overlap on the server.
|
|
1563
|
+
if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
|
|
1564
|
+
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
|
|
1565
|
+
modelContextLimit, rec, clientMeta });
|
|
1566
|
+
}
|
|
1485
1567
|
// An active subagent can repeat/embed the human prompt in its own request;
|
|
1486
1568
|
// producer suppression must also preserve the arm for the later main call.
|
|
1487
1569
|
if (displayForThisTurn)
|
|
@@ -1496,6 +1578,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1496
1578
|
// A lagging index left history verbatim: a refusal waits for indexing.
|
|
1497
1579
|
if (result)
|
|
1498
1580
|
noteIndexLag(opts, rec, result);
|
|
1581
|
+
// Too large for the window: delivery recompresses it once, smaller.
|
|
1582
|
+
if (result && !rec.indexBehind) {
|
|
1583
|
+
noteOversizedCompression(rec, requestSizing, body, compressedRaw, rec.compaction?.targetTokens);
|
|
1584
|
+
}
|
|
1499
1585
|
releaseUncommittedRouteDecision();
|
|
1500
1586
|
recordTurn(rec, "followup-degraded", rawBody);
|
|
1501
1587
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
|
|
@@ -1596,7 +1682,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1596
1682
|
: stable.installed
|
|
1597
1683
|
: undefined;
|
|
1598
1684
|
if (sizePrefix)
|
|
1599
|
-
notePrefixSize(sizePrefix, rec, compressedRaw
|
|
1685
|
+
notePrefixSize(sizePrefix, rec, compressedRaw);
|
|
1600
1686
|
// A route already installed at protocol-complete deliberately survives
|
|
1601
1687
|
// delivered=false: that settle may be the fast-tool abort (client
|
|
1602
1688
|
// consumed message_stop, closed the SSE response, and its immediate
|
|
@@ -1633,7 +1719,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1633
1719
|
}
|
|
1634
1720
|
}));
|
|
1635
1721
|
};
|
|
1636
|
-
if (existingRide && (!edge || edge.kind === "off")) {
|
|
1722
|
+
if (existingRide && (!edge || edge.kind === "off" || edge.kind === "pass")) {
|
|
1637
1723
|
const owner = rideableCandidate.stablePrefix ?? rideableCandidate;
|
|
1638
1724
|
const candidate = requestSizing.register(existingRide, "ride", owner.lastSize);
|
|
1639
1725
|
candidateSizeOwners.set(candidate, owner);
|
|
@@ -1665,8 +1751,29 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1665
1751
|
const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
|
|
1666
1752
|
const noteWholeSize = () => {
|
|
1667
1753
|
if (isMainRequest)
|
|
1668
|
-
notePassthroughSize(state, sessionId, body.model, rec, forwardBody
|
|
1754
|
+
notePassthroughSize(state, sessionId, body.model, rec, forwardBody);
|
|
1669
1755
|
};
|
|
1756
|
+
if (edge?.kind === "pass") {
|
|
1757
|
+
// Clearly under the budget by its own count: the compress call could only
|
|
1758
|
+
// pass it through, so the turn goes out now and the call (which also
|
|
1759
|
+
// indexes it and reports the page and budget) runs in the background.
|
|
1760
|
+
compressInBackground({ opts, state, body, msgsForMemtree, hash, modelContextLimit, rec,
|
|
1761
|
+
sessionId, clientMeta, sizing: requestSizing, inputCandidate: forwardCandidate,
|
|
1762
|
+
// Never compress: the turn went out whole, so a compression the server's
|
|
1763
|
+
// own (more conservative) sizing chose could not be used, only billed.
|
|
1764
|
+
compaction: { target: NEVER_COMPRESS_TARGET_TOKENS },
|
|
1765
|
+
linkSeq: isAwaySummary ? undefined : nextMemtreeCallSeq(state) });
|
|
1766
|
+
releaseForOriginalFallback();
|
|
1767
|
+
sendingWhole();
|
|
1768
|
+
recordTurn(rec, "followup-under-budget", forwardBody);
|
|
1769
|
+
capture(opts, "anthropic-request", forwardBody);
|
|
1770
|
+
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec)
|
|
1771
|
+
.then((delivered) => {
|
|
1772
|
+
noteWholeSize();
|
|
1773
|
+
if (delivered)
|
|
1774
|
+
markMainPromptDelivered();
|
|
1775
|
+
}));
|
|
1776
|
+
}
|
|
1670
1777
|
// The complete request upload can outlive its downstream subscriber while
|
|
1671
1778
|
// MemTree compression is in flight. Track that subscriber locally without
|
|
1672
1779
|
// feeding its lifetime into MemtreeClient.compress(): compression promises
|
|
@@ -1716,7 +1823,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1716
1823
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
|
|
1717
1824
|
.then((delivered) => {
|
|
1718
1825
|
if (isMainRequest)
|
|
1719
|
-
notePassthroughSize(state, sessionId, body.model, rec, rawBody
|
|
1826
|
+
notePassthroughSize(state, sessionId, body.model, rec, rawBody);
|
|
1720
1827
|
if (delivered)
|
|
1721
1828
|
markMainPromptDelivered();
|
|
1722
1829
|
}));
|
|
@@ -2051,6 +2158,47 @@ function promptSubmitLine(state) {
|
|
|
2051
2158
|
return undefined;
|
|
2052
2159
|
}
|
|
2053
2160
|
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Whether MemTree is known to be compressing: its latest answer a success and
|
|
2163
|
+
* no pause (402) remembered. Before any answer, or after an outage, it is not.
|
|
2164
|
+
*/
|
|
2165
|
+
function memtreeWorking(state) {
|
|
2166
|
+
return state.memtreeAnswering() === true && state.unpaidDetail() === null;
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* What the model should know about MemTree right now: a notice, undefined
|
|
2170
|
+
* when it is known to work, null when neither is known (no answer, an outage).
|
|
2171
|
+
*/
|
|
2172
|
+
function currentModelNotice(state) {
|
|
2173
|
+
if (state.keyInvalid()) {
|
|
2174
|
+
return { kind: KEY_INVALID_KIND, text: keyInvalidModelContext(state.keyPageUrl) };
|
|
2175
|
+
}
|
|
2176
|
+
const detail = state.unpaidDetail();
|
|
2177
|
+
if (detail !== null) {
|
|
2178
|
+
return { kind: PAUSED_KIND, text: pausedModelContext(detail, state.pausedReason()) };
|
|
2179
|
+
}
|
|
2180
|
+
return memtreeWorking(state) ? undefined : null;
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* A resumed session's transcript may hold a notice from an earlier ccc process
|
|
2184
|
+
* (paused, then paid in between): start from it, so the change is announced.
|
|
2185
|
+
* A missing or unreadable transcript, or a scan past its time box, starts from
|
|
2186
|
+
* nothing. Never throws.
|
|
2187
|
+
*/
|
|
2188
|
+
async function restoreModelNotices(state, input) {
|
|
2189
|
+
if (!input.transcript_path)
|
|
2190
|
+
return;
|
|
2191
|
+
state.modelNotices.restore(input.session_id, await lastToldKindInFile(input.transcript_path));
|
|
2192
|
+
}
|
|
2193
|
+
/** UserPromptSubmit's answer: a line for the user, context for the model, or both. */
|
|
2194
|
+
function promptSubmitOutput(line, context) {
|
|
2195
|
+
return {
|
|
2196
|
+
...(line ? { systemMessage: line } : {}),
|
|
2197
|
+
...(context
|
|
2198
|
+
? { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }
|
|
2199
|
+
: {}),
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2054
2202
|
/** The caller's extra SessionStart line; a throwing provider shows nothing. */
|
|
2055
2203
|
function extraSessionStartLine(provider, input) {
|
|
2056
2204
|
if (!provider || input.agent_id !== undefined || input.source === "compact")
|
|
@@ -2315,7 +2463,8 @@ function rememberPage(pages, sessionId, page) {
|
|
|
2315
2463
|
*/
|
|
2316
2464
|
function noteMemtreeHealth(state, compression) {
|
|
2317
2465
|
if (compression.liveHealth === "failure") {
|
|
2318
|
-
|
|
2466
|
+
// Background calls the drain stopped (backgroundSignal) are no outage either.
|
|
2467
|
+
if (state.shutdownSignal.aborted || state.backgroundSignal.aborted)
|
|
2319
2468
|
return;
|
|
2320
2469
|
state.toolRecoveryCooldownUntil =
|
|
2321
2470
|
Date.now() + TOOL_RECOVERY_FAILURE_COOLDOWN_MS;
|
|
@@ -2398,7 +2547,7 @@ stable) {
|
|
|
2398
2547
|
// Include trailing ambient role=system blocks: MemTree consolidated them
|
|
2399
2548
|
// into compressedBody.system, so treating them as suffix would duplicate
|
|
2400
2549
|
// those instructions on every tool request.
|
|
2401
|
-
|
|
2550
|
+
...prefixIdentity(originalMessages, "route"),
|
|
2402
2551
|
compressedMessages: cloneJson(compressedBody.messages),
|
|
2403
2552
|
compressedSystem: cloneJson(compressedBody.system),
|
|
2404
2553
|
hasCompressedSystem: Object.prototype.hasOwnProperty.call(compressedBody, "system"),
|
|
@@ -2437,7 +2586,7 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2437
2586
|
const prefix = {
|
|
2438
2587
|
sessionId,
|
|
2439
2588
|
originalSystemHash: compressed.originalSystemHash,
|
|
2440
|
-
|
|
2589
|
+
...prefixIdentity(originalMessages, "stable"),
|
|
2441
2590
|
compressedMessages: compressed.compressedMessages,
|
|
2442
2591
|
compressedSystem: compressed.compressedSystem,
|
|
2443
2592
|
hasCompressedSystem: compressed.hasCompressedSystem,
|
|
@@ -2448,6 +2597,9 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2448
2597
|
stable.installed = prefix;
|
|
2449
2598
|
boundedSet(state.stablePrefixes, sessionId, prefix);
|
|
2450
2599
|
state.compactNow.delete(sessionId);
|
|
2600
|
+
// Prepared against the old prefix's ride sizes: a fresh one is made when
|
|
2601
|
+
// the new prefix's rides near the budget again.
|
|
2602
|
+
state.prepared.discard(sessionId);
|
|
2451
2603
|
return prefix;
|
|
2452
2604
|
}
|
|
2453
2605
|
/**
|
|
@@ -2468,6 +2620,7 @@ function persistStablePrefix(state, sessionId, prefix) {
|
|
|
2468
2620
|
store.save(sessionId, owner, {
|
|
2469
2621
|
originalSystemHash: prefix.originalSystemHash,
|
|
2470
2622
|
originalPrefixHashes: prefix.originalPrefixHashes,
|
|
2623
|
+
originalMediaFreeHashes: prefix.originalMediaFreeHashes,
|
|
2471
2624
|
compressedMessages: prefix.compressedMessages,
|
|
2472
2625
|
compressedSystem: prefix.compressedSystem,
|
|
2473
2626
|
hasCompressedSystem: prefix.hasCompressedSystem,
|
|
@@ -2520,7 +2673,7 @@ function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelCon
|
|
|
2520
2673
|
function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
|
|
2521
2674
|
if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
|
|
2522
2675
|
return "defer";
|
|
2523
|
-
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId,
|
|
2676
|
+
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, "stable");
|
|
2524
2677
|
if (miss === null)
|
|
2525
2678
|
return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
|
|
2526
2679
|
const authoritative = miss === "prefix" &&
|
|
@@ -2550,7 +2703,7 @@ function forkRoutedBody(body, messages, route, sessionId) {
|
|
|
2550
2703
|
* at least one message after them. The suffix may be anything, human turns
|
|
2551
2704
|
* included.
|
|
2552
2705
|
*/
|
|
2553
|
-
function prefixMismatch(body, messages, prefix, sessionId,
|
|
2706
|
+
function prefixMismatch(body, messages, prefix, sessionId, kind = "route") {
|
|
2554
2707
|
if (!sessionId || prefix.sessionId !== sessionId)
|
|
2555
2708
|
return "session";
|
|
2556
2709
|
if (routeValueHash(normalizeRouteSystem(body.system)) !== prefix.originalSystemHash) {
|
|
@@ -2560,7 +2713,7 @@ function prefixMismatch(body, messages, prefix, sessionId, hashMessage = routeMe
|
|
|
2560
2713
|
if (messages.length < prefixLength)
|
|
2561
2714
|
return "prefix";
|
|
2562
2715
|
for (let i = 0; i < prefixLength; i++) {
|
|
2563
|
-
if (
|
|
2716
|
+
if (!prefixMessageMatches(messages[i], kind, prefix.originalPrefixHashes[i], prefix.originalMediaFreeHashes?.[i])) {
|
|
2564
2717
|
return "prefix";
|
|
2565
2718
|
}
|
|
2566
2719
|
}
|
|
@@ -2604,20 +2757,24 @@ function serverBudgetKey(model, modelContextLimit) {
|
|
|
2604
2757
|
return JSON.stringify([typeof model === "string" ? model : "", modelContextLimit]);
|
|
2605
2758
|
}
|
|
2606
2759
|
/**
|
|
2607
|
-
* The session's whole-request budget
|
|
2608
|
-
*
|
|
2609
|
-
*
|
|
2610
|
-
* the first request before
|
|
2760
|
+
* The session's whole-request budget, the size at which a request is
|
|
2761
|
+
* compressed: CCC_BUDGET_TOKENS when set (tests), else the model budget the
|
|
2762
|
+
* server last reported for this model and window, else fallbackBudgetTokens
|
|
2763
|
+
* (servers that predate `model_budget_tokens`, and the first request before
|
|
2764
|
+
* any response). Never above the window less the reply's room
|
|
2765
|
+
* (compressionTrigger): a request is compressed when its projected size plus
|
|
2766
|
+
* that reserve would exceed the window, or the budget.
|
|
2611
2767
|
*/
|
|
2612
2768
|
function resolveBudget(opts, state, model, modelContextLimit) {
|
|
2769
|
+
const trigger = (tokens) => compressionTrigger(tokens, modelContextLimit);
|
|
2613
2770
|
if (opts.budgetTokensOverride !== undefined) {
|
|
2614
|
-
return { tokens: opts.budgetTokensOverride, source: "override" };
|
|
2771
|
+
return { tokens: trigger(opts.budgetTokensOverride), source: "override" };
|
|
2615
2772
|
}
|
|
2616
2773
|
const reported = state.serverBudgets.get(serverBudgetKey(model, modelContextLimit));
|
|
2617
2774
|
if (reported !== undefined)
|
|
2618
|
-
return { tokens: reported, source: "server" };
|
|
2775
|
+
return { tokens: trigger(reported), source: "server" };
|
|
2619
2776
|
return {
|
|
2620
|
-
tokens: fallbackBudgetTokens(model, modelContextLimit),
|
|
2777
|
+
tokens: trigger(fallbackBudgetTokens(model, modelContextLimit)),
|
|
2621
2778
|
source: "window-ratio",
|
|
2622
2779
|
};
|
|
2623
2780
|
}
|
|
@@ -2700,17 +2857,17 @@ function reportedInputTokens(rec) {
|
|
|
2700
2857
|
(usage.cache_creation_input_tokens ?? 0));
|
|
2701
2858
|
}
|
|
2702
2859
|
/** Keep the newest (largest-body) reported size of a request on this prefix. */
|
|
2703
|
-
function notePrefixSize(prefix, rec,
|
|
2704
|
-
const sample = reportedSample(rec,
|
|
2860
|
+
function notePrefixSize(prefix, rec, sent) {
|
|
2861
|
+
const sample = reportedSample(rec, sent);
|
|
2705
2862
|
if (sample === undefined)
|
|
2706
2863
|
return;
|
|
2707
|
-
if (prefix.lastSize &&
|
|
2864
|
+
if (prefix.lastSize && sent.length < prefix.lastSize.forwardedBytes)
|
|
2708
2865
|
return;
|
|
2709
2866
|
prefix.lastSize = sample;
|
|
2710
2867
|
}
|
|
2711
2868
|
/** Record the reported size of a main-thread request forwarded whole. */
|
|
2712
|
-
function notePassthroughSize(state, sessionId, model, rec,
|
|
2713
|
-
const sample = reportedSample(rec,
|
|
2869
|
+
function notePassthroughSize(state, sessionId, model, rec, sent) {
|
|
2870
|
+
const sample = reportedSample(rec, sent);
|
|
2714
2871
|
if (sessionId === undefined || sample === undefined)
|
|
2715
2872
|
return;
|
|
2716
2873
|
boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
|
|
@@ -2740,13 +2897,18 @@ function usedServerTools(rec) {
|
|
|
2740
2897
|
* an implausible ratio would mis-size every later request it anchors, so it
|
|
2741
2898
|
* is dropped and the previous sample kept.
|
|
2742
2899
|
*/
|
|
2743
|
-
function reportedSample(rec,
|
|
2900
|
+
function reportedSample(rec, sent) {
|
|
2744
2901
|
const tokens = reportedBodyTokens(rec);
|
|
2745
2902
|
if (tokens === undefined)
|
|
2746
2903
|
return undefined;
|
|
2747
|
-
const sample =
|
|
2904
|
+
const sample = sizeSampleOf(sent, tokens, rec.model);
|
|
2748
2905
|
return plausibleSample(sample) ? sample : undefined;
|
|
2749
2906
|
}
|
|
2907
|
+
/** A size sample of `sent` carrying its inline images (estimateRequestTokens). */
|
|
2908
|
+
function sizeSampleOf(sent, tokens, model) {
|
|
2909
|
+
const { imageBytes, imageTokens } = bodyImages(sent, typeof model === "string" ? model : undefined);
|
|
2910
|
+
return { tokens, forwardedBytes: sent.length, imageBytes, imageTokens };
|
|
2911
|
+
}
|
|
2750
2912
|
/**
|
|
2751
2913
|
* Anthropic's input tokens for the body this request forwarded, when its usage
|
|
2752
2914
|
* describes that body (not a changed delivery, no server tools). Not filtered
|
|
@@ -2793,12 +2955,12 @@ const noteRideSize = notePrefixSize;
|
|
|
2793
2955
|
* Record the reported size of a request forwarded whole: the session's
|
|
2794
2956
|
* passthrough size on the main thread, else its lane's.
|
|
2795
2957
|
*/
|
|
2796
|
-
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec,
|
|
2958
|
+
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, sent) {
|
|
2797
2959
|
if (isMainRequest && sessionId !== undefined) {
|
|
2798
|
-
notePassthroughSize(state, sessionId, model, rec,
|
|
2960
|
+
notePassthroughSize(state, sessionId, model, rec, sent);
|
|
2799
2961
|
return;
|
|
2800
2962
|
}
|
|
2801
|
-
const sample = reportedSample(rec,
|
|
2963
|
+
const sample = reportedSample(rec, sent);
|
|
2802
2964
|
if (sample === undefined)
|
|
2803
2965
|
return;
|
|
2804
2966
|
boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
|
|
@@ -2859,7 +3021,7 @@ async function planEdgeCompaction(args) {
|
|
|
2859
3021
|
};
|
|
2860
3022
|
let prefix = state.stablePrefixes.get(sessionId);
|
|
2861
3023
|
if (prefix) {
|
|
2862
|
-
const miss = prefixMismatch(body, messages, prefix, sessionId,
|
|
3024
|
+
const miss = prefixMismatch(body, messages, prefix, sessionId, "stable");
|
|
2863
3025
|
if (miss) {
|
|
2864
3026
|
// Rewind, edit, fork, /clear, or a changed system prompt: the prefix no
|
|
2865
3027
|
// longer stands for this conversation. Start over from passthrough.
|
|
@@ -2895,8 +3057,13 @@ async function planEdgeCompaction(args) {
|
|
|
2895
3057
|
(explicitTarget && prefix.targetTokens !== targetTokens)) {
|
|
2896
3058
|
return forced("target-change", fallback);
|
|
2897
3059
|
}
|
|
2898
|
-
if (size.tokens >= budget.tokens || overWindow)
|
|
3060
|
+
if (size.tokens >= budget.tokens || overWindow) {
|
|
3061
|
+
const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
|
|
3062
|
+
waitMs: opts.memtree.longCompressBudgetMs });
|
|
3063
|
+
if (adopted)
|
|
3064
|
+
return { kind: "ride", ...adopted };
|
|
2899
3065
|
return forced("budget", fallback);
|
|
3066
|
+
}
|
|
2900
3067
|
return { kind: "ride", prefix, routed };
|
|
2901
3068
|
}
|
|
2902
3069
|
if (state.compactNow.has(sessionId))
|
|
@@ -2909,12 +3076,18 @@ async function planEdgeCompaction(args) {
|
|
|
2909
3076
|
compaction.sizeSource = size.source;
|
|
2910
3077
|
noteUncountedBound(compaction, args.sizing, originalCandidate, body, modelContextLimit);
|
|
2911
3078
|
if (calibratedSizeExceedsLimits(compaction, body, modelContextLimit)) {
|
|
3079
|
+
const adopted = await adoptPreparedPrefix({ ...args, budgetTokens: budget.tokens, compaction,
|
|
3080
|
+
waitMs: opts.memtree.longCompressBudgetMs });
|
|
3081
|
+
if (adopted)
|
|
3082
|
+
return { kind: "ride", ...adopted };
|
|
2912
3083
|
return forced(compaction.prefixMiss ? "prefix-mismatch" : "budget");
|
|
2913
3084
|
}
|
|
2914
3085
|
if (state.serverReportsBudget) {
|
|
2915
3086
|
// The server measures the request and compresses only past the budget.
|
|
2916
3087
|
const threshold = Math.max(SERVER_MIN_TARGET_TOKENS, budget.tokens);
|
|
2917
3088
|
compaction.thresholdTokens = threshold;
|
|
3089
|
+
if (clearlyUnderBudget(opts, state, budget, size, compaction))
|
|
3090
|
+
return { kind: "pass" };
|
|
2918
3091
|
return { kind: "compress", target: targetTokens, threshold, targetTokens, explicitTarget };
|
|
2919
3092
|
}
|
|
2920
3093
|
// Without calibrated usage or threshold support, let the server decide
|
|
@@ -2955,7 +3128,7 @@ async function planToolCompaction(args) {
|
|
|
2955
3128
|
const stableLane = args.isMainRequest && sessionId !== undefined;
|
|
2956
3129
|
const current = stableLane ? state.stablePrefixes.get(sessionId) : undefined;
|
|
2957
3130
|
let ride = args.ride;
|
|
2958
|
-
if (!ride && current && !prefixMismatch(body, messages, current, sessionId,
|
|
3131
|
+
if (!ride && current && !prefixMismatch(body, messages, current, sessionId, "stable")) {
|
|
2959
3132
|
const routed = prefixRoutedBody(body, messages, current);
|
|
2960
3133
|
if (routed) {
|
|
2961
3134
|
const prefixCandidate = args.sizing.register(routed.raw, "ride", current.lastSize);
|
|
@@ -2993,12 +3166,27 @@ async function planToolCompaction(args) {
|
|
|
2993
3166
|
if (size.tokens < budget.tokens && !overWindow) {
|
|
2994
3167
|
return ride ? { kind: "ride", ride } : { kind: "pass" };
|
|
2995
3168
|
}
|
|
3169
|
+
const adopted = stableLane
|
|
3170
|
+
? await adoptPreparedPrefix({ ...args, sessionId: sessionId, budgetTokens: budget.tokens,
|
|
3171
|
+
compaction, waitMs: opts.memtree.longCompressBudgetMs })
|
|
3172
|
+
: undefined;
|
|
3173
|
+
if (adopted) {
|
|
3174
|
+
return { kind: "ride",
|
|
3175
|
+
ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
|
|
3176
|
+
}
|
|
3177
|
+
// Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
|
|
3178
|
+
const modeNow = compactionMode(opts, state, sessionId);
|
|
3179
|
+
if (modeNow.mode === "off")
|
|
3180
|
+
return { kind: "off" };
|
|
3181
|
+
const targetNow = compactionTarget(modeNow, budget.tokens);
|
|
3182
|
+
compaction.mode = modeNow.mode;
|
|
3183
|
+
compaction.targetTokens = targetNow;
|
|
2996
3184
|
compaction.reason = "budget";
|
|
2997
3185
|
return {
|
|
2998
3186
|
kind: "compress",
|
|
2999
|
-
...budgetCompaction(state, compaction,
|
|
3000
|
-
targetTokens,
|
|
3001
|
-
explicitTarget:
|
|
3187
|
+
...budgetCompaction(state, compaction, targetNow, body, modelContextLimit),
|
|
3188
|
+
targetTokens: targetNow,
|
|
3189
|
+
explicitTarget: modeNow.mode === "explicit",
|
|
3002
3190
|
estimateTokens: size.tokens,
|
|
3003
3191
|
budgetTokens: budget.tokens,
|
|
3004
3192
|
overWindow,
|
|
@@ -3007,6 +3195,224 @@ async function planToolCompaction(args) {
|
|
|
3007
3195
|
...(stableLane ? { replaces: current ?? null } : {}),
|
|
3008
3196
|
};
|
|
3009
3197
|
}
|
|
3198
|
+
/** Server `compression_target_tokens` sentinel: never compress, always pass through. */
|
|
3199
|
+
const NEVER_COMPRESS_TARGET_TOKENS = -1;
|
|
3200
|
+
/** Share of the budget under which a counted human turn skips its blocking call. */
|
|
3201
|
+
const CLEARLY_UNDER_BUDGET_RATIO = 0.85;
|
|
3202
|
+
/**
|
|
3203
|
+
* Whether a main human turn with no prefix is clearly under the budget by its
|
|
3204
|
+
* token count (an exact count, or a projection from Anthropic's reported
|
|
3205
|
+
* usage; never a byte guess) against a budget the server reported. The
|
|
3206
|
+
* compress call could then only pass it through, so it need not wait for it.
|
|
3207
|
+
*/
|
|
3208
|
+
function clearlyUnderBudget(opts, state, budget, size, compaction) {
|
|
3209
|
+
// A known payment or key failure keeps the blocking path: it gives the notice.
|
|
3210
|
+
if (opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
|
|
3211
|
+
return false;
|
|
3212
|
+
return budget.source !== "window-ratio" && size.source === "reported" &&
|
|
3213
|
+
compaction.uncountedOverWindow !== true &&
|
|
3214
|
+
size.tokens <= Math.floor(budget.tokens * CLEARLY_UNDER_BUDGET_RATIO);
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Whether this model and window prepare compressions: a Claude model on a 1M
|
|
3218
|
+
* window whose budget is the near-window one (not a Sonnet ceiling).
|
|
3219
|
+
*/
|
|
3220
|
+
function preparesCompression(model, modelContextLimit) {
|
|
3221
|
+
return modelContextLimit >= ONE_MILLION_TOKENS &&
|
|
3222
|
+
fallbackBudgetTokens(model, modelContextLimit) === nearWindowBudgetTokens(modelContextLimit) &&
|
|
3223
|
+
typeof model === "string" && CLAUDE_FAMILY_RE.test(model);
|
|
3224
|
+
}
|
|
3225
|
+
const ONE_MILLION_TOKENS = 1_000_000;
|
|
3226
|
+
/**
|
|
3227
|
+
* The session's prepared prefix (prepared-compression.ts) as this turn's
|
|
3228
|
+
* ride, now the session's stable prefix, when it still covers this history
|
|
3229
|
+
* and prefix + the turns after it fit under the budget. Undefined otherwise,
|
|
3230
|
+
* and the prepared prefix is dropped: the turn compresses as before.
|
|
3231
|
+
*/
|
|
3232
|
+
async function adoptPreparedPrefix(args) {
|
|
3233
|
+
const { state, sessionId } = args;
|
|
3234
|
+
const base = state.stablePrefixes.get(sessionId);
|
|
3235
|
+
await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
|
|
3236
|
+
const entry = state.prepared.get(sessionId);
|
|
3237
|
+
if (!entry)
|
|
3238
|
+
return undefined;
|
|
3239
|
+
state.prepared.discard(sessionId);
|
|
3240
|
+
const fit = await preparedFit(args, entry);
|
|
3241
|
+
// Another request installed a prefix, or `/memtree-compact` asked for its
|
|
3242
|
+
// own target, while this one waited or counted: that wins.
|
|
3243
|
+
const kind = fit.kind === "used" && (state.stablePrefixes.get(sessionId) !== base ||
|
|
3244
|
+
state.compactNow.has(sessionId) || compactionMode(args.opts, state, sessionId).mode !== "auto")
|
|
3245
|
+
? "stale" : fit.kind;
|
|
3246
|
+
args.compaction.prepared = kind;
|
|
3247
|
+
if (kind !== "used" || fit.kind !== "used") {
|
|
3248
|
+
// A preparation not adopted is never sent, not even as a fallback.
|
|
3249
|
+
if (fit.candidate)
|
|
3250
|
+
args.sizing.excludeFallback(fit.candidate);
|
|
3251
|
+
if (kind === "too-large" && fit.candidate) {
|
|
3252
|
+
notePreparedTruncationBase(args, entry, base, fit.candidate);
|
|
3253
|
+
}
|
|
3254
|
+
return undefined;
|
|
3255
|
+
}
|
|
3256
|
+
args.compaction.estimatedTokens = fit.tokens;
|
|
3257
|
+
args.compaction.estimatedBytes = fit.routed.raw.length;
|
|
3258
|
+
installPreparedPrefix(state, sessionId, entry);
|
|
3259
|
+
return { prefix: entry.prefix, routed: fit.routed };
|
|
3260
|
+
}
|
|
3261
|
+
/** An adopted preparation becomes the session's stable prefix (and its page, persisted). */
|
|
3262
|
+
function installPreparedPrefix(state, sessionId, entry) {
|
|
3263
|
+
boundedSet(state.stablePrefixes, sessionId, entry.prefix);
|
|
3264
|
+
state.compactNow.delete(sessionId);
|
|
3265
|
+
if (entry.source)
|
|
3266
|
+
notePreparedSource(state, sessionId, entry.source);
|
|
3267
|
+
persistStablePrefix(state, sessionId, entry.prefix);
|
|
3268
|
+
}
|
|
3269
|
+
const preparedTruncationBases = new WeakMap();
|
|
3270
|
+
function notePreparedTruncationBase(args, entry, base, candidate) {
|
|
3271
|
+
const { state, sessionId } = args;
|
|
3272
|
+
preparedTruncationBases.set(args.sizing, { candidate, install: () => {
|
|
3273
|
+
if (state.stablePrefixes.get(sessionId) !== base || state.compactNow.has(sessionId) ||
|
|
3274
|
+
compactionMode(args.opts, state, sessionId).mode !== "auto")
|
|
3275
|
+
return;
|
|
3276
|
+
args.compaction.prepared = "truncated";
|
|
3277
|
+
installPreparedPrefix(state, sessionId, entry);
|
|
3278
|
+
} });
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* The turn that crosses the budget while its session's preparation is still
|
|
3282
|
+
* in flight waits for it (it started earlier than a fresh compression could),
|
|
3283
|
+
* for at most `waitMs`, rather than making a second, concurrent compress call.
|
|
3284
|
+
*/
|
|
3285
|
+
async function awaitPreparation(state, sessionId, waitMs, compaction) {
|
|
3286
|
+
const pending = state.preparing.get(sessionId);
|
|
3287
|
+
if (!pending)
|
|
3288
|
+
return;
|
|
3289
|
+
const started = Date.now();
|
|
3290
|
+
let timer;
|
|
3291
|
+
await Promise.race([pending, new Promise((resolve) => {
|
|
3292
|
+
timer = setTimeout(resolve, Math.max(0, waitMs));
|
|
3293
|
+
})]).finally(() => clearTimeout(timer));
|
|
3294
|
+
compaction.preparedWaitMs = Date.now() - started;
|
|
3295
|
+
}
|
|
3296
|
+
/** An adopted preparation's MemTree page becomes the session's memory source and page. */
|
|
3297
|
+
function notePreparedSource(state, sessionId, source) {
|
|
3298
|
+
const result = { messages: [], compressed: true, memtreeUrl: source.url,
|
|
3299
|
+
memtreeIndex: source.index };
|
|
3300
|
+
noteMemtreePage(state, source.seq, sessionId, result);
|
|
3301
|
+
noteMemorySource(state, source.seq, sessionId, result);
|
|
3302
|
+
}
|
|
3303
|
+
/** Whether a prepared prefix can carry this request (adoptPreparedPrefix). */
|
|
3304
|
+
async function preparedFit(args, entry) {
|
|
3305
|
+
const { sizing, body, messages, modelContextLimit, budgetTokens, compaction } = args;
|
|
3306
|
+
if (entry.prefix.modelContextLimit !== modelContextLimit || entry.triggerTokens !== budgetTokens ||
|
|
3307
|
+
entry.prefix.targetTokens !== compaction.targetTokens ||
|
|
3308
|
+
entry.prefix.explicitTarget !== (compaction.mode === "explicit")) {
|
|
3309
|
+
return { kind: "stale" };
|
|
3310
|
+
}
|
|
3311
|
+
if (prefixMismatch(body, messages, entry.prefix, args.sessionId, "stable")) {
|
|
3312
|
+
return { kind: "mismatch" };
|
|
3313
|
+
}
|
|
3314
|
+
const routed = prefixRoutedBody(body, messages, entry.prefix);
|
|
3315
|
+
if (!routed)
|
|
3316
|
+
return { kind: "mismatch" };
|
|
3317
|
+
const candidate = sizing.register(routed.raw, "ride", entry.prefix.lastSize);
|
|
3318
|
+
candidateSizeOwners.set(candidate, entry.prefix);
|
|
3319
|
+
await countIfOverBound(sizing, candidate, body, modelContextLimit);
|
|
3320
|
+
const tokens = sizing.plan(candidate).tokens;
|
|
3321
|
+
const fits = tokens < budgetTokens && fitsNative(tokens, { budget: budgetTokens,
|
|
3322
|
+
context: modelContextLimit, output: bodyOutputReservation(body, sizing.isExact(candidate)) }) &&
|
|
3323
|
+
(sizing.isExact(candidate) || sizing.boundFits(candidate, { budget: budgetTokens, context: modelContextLimit, output: bodyOutputReservation(body, false) }));
|
|
3324
|
+
// Too large, the turn compresses as before; the ride stays registered, so
|
|
3325
|
+
// if that cannot fit either, delivery's safety net may shorten its newest
|
|
3326
|
+
// tool result instead (truncatedToFit).
|
|
3327
|
+
return fits ? { kind: "used", routed, tokens, candidate } : { kind: "too-large", candidate };
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* After a main-thread turn's plan: once its size nears the budget, ask
|
|
3331
|
+
* MemTree in the background for the compressed prefix the turn that crosses
|
|
3332
|
+
* it will ride (prepared-compression.ts). Fire and forget, one call per
|
|
3333
|
+
* session at a time; never delays this turn.
|
|
3334
|
+
*/
|
|
3335
|
+
function maybePrepareCompression(args) {
|
|
3336
|
+
const { state, sessionId, rec } = args;
|
|
3337
|
+
const compaction = rec.compaction;
|
|
3338
|
+
// Automatic compaction only: an explicit target (/memtree-compact N) is the
|
|
3339
|
+
// user's own schedule, and a high one would leave nothing to prepare for.
|
|
3340
|
+
if (!compaction || compaction.mode !== "auto" || compaction.estimatedTokens === undefined ||
|
|
3341
|
+
compaction.targetTokens === undefined || state.keyInvalid() ||
|
|
3342
|
+
state.backgroundSignal.aborted ||
|
|
3343
|
+
state.pausedDetail() !== null ||
|
|
3344
|
+
!preparesCompression(args.body.model, args.modelContextLimit))
|
|
3345
|
+
return;
|
|
3346
|
+
const tokens = compaction.estimatedTokens;
|
|
3347
|
+
if (tokens < prepareStartTokens(compaction.budgetTokens))
|
|
3348
|
+
return;
|
|
3349
|
+
const decision = state.prepared.decide(sessionId, tokens, compaction.budgetTokens);
|
|
3350
|
+
rec.prepare = decision;
|
|
3351
|
+
if (decision !== "prepare" ||
|
|
3352
|
+
!state.prepared.begin(sessionId, tokens, compaction.budgetTokens))
|
|
3353
|
+
return;
|
|
3354
|
+
const base = state.stablePrefixes.get(sessionId);
|
|
3355
|
+
const pending = prepareInBackground({ ...args, tokens, targetTokens: compaction.targetTokens,
|
|
3356
|
+
explicitTarget: false, triggerTokens: compaction.budgetTokens,
|
|
3357
|
+
linkSeq: nextMemtreeCallSeq(state) })
|
|
3358
|
+
.catch(() => undefined)
|
|
3359
|
+
.then((prepared) => {
|
|
3360
|
+
// A prefix installed meanwhile (a blocking compaction) supersedes it.
|
|
3361
|
+
const current = state.stablePrefixes.get(sessionId) === base;
|
|
3362
|
+
state.prepared.finish(sessionId, current ? prepared : undefined);
|
|
3363
|
+
if (state.preparing.get(sessionId) === pending)
|
|
3364
|
+
state.preparing.delete(sessionId);
|
|
3365
|
+
});
|
|
3366
|
+
boundedSet(state.preparing, sessionId, pending);
|
|
3367
|
+
}
|
|
3368
|
+
/** The background prepare call and the prefix it yields (maybePrepareCompression). */
|
|
3369
|
+
async function prepareInBackground(args) {
|
|
3370
|
+
const { opts, state, body, msgsForMemtree, modelContextLimit, sessionId } = args;
|
|
3371
|
+
const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
|
|
3372
|
+
noteServerBudget(state, body.model, modelContextLimit, result);
|
|
3373
|
+
if (!result)
|
|
3374
|
+
return undefined;
|
|
3375
|
+
state.prepared.noteServerAnswer(result.prepared === true);
|
|
3376
|
+
if (result.prepared !== true || !didMemtreeCompress(result) ||
|
|
3377
|
+
!checkCompressedHistory(result, msgsForMemtree).usable)
|
|
3378
|
+
return undefined;
|
|
3379
|
+
const built = buildCompressedBody(body, result);
|
|
3380
|
+
if (!built)
|
|
3381
|
+
return undefined;
|
|
3382
|
+
const prefix = {
|
|
3383
|
+
sessionId,
|
|
3384
|
+
originalSystemHash: routeValueHash(normalizeRouteSystem(body.system)),
|
|
3385
|
+
...prefixIdentity(args.messages, "stable"),
|
|
3386
|
+
compressedMessages: cloneJson(built.compressedBody.messages),
|
|
3387
|
+
compressedSystem: cloneJson(built.compressedBody.system),
|
|
3388
|
+
hasCompressedSystem: Object.prototype.hasOwnProperty.call(built.compressedBody, "system"),
|
|
3389
|
+
targetTokens: args.targetTokens,
|
|
3390
|
+
explicitTarget: args.explicitTarget,
|
|
3391
|
+
modelContextLimit,
|
|
3392
|
+
};
|
|
3393
|
+
const source = result.memtreeUrl && result.memtreeIndex
|
|
3394
|
+
? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
|
|
3395
|
+
return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
|
|
3396
|
+
...(source ? { source } : {}) };
|
|
3397
|
+
}
|
|
3398
|
+
/** What a prepare call sends: a forced compression to the target, no threshold. */
|
|
3399
|
+
function preparedCompressMeta(args) {
|
|
3400
|
+
const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
|
|
3401
|
+
const messageUsage = opts.transcriptUsage?.usageFor(sessionId, msgsForMemtree, undefined);
|
|
3402
|
+
const messageTimes = transcriptTimesFor(opts, sessionId, undefined)?.(msgsForMemtree);
|
|
3403
|
+
return {
|
|
3404
|
+
model: modelForMemtree(typeof body.model === "string" ? body.model : undefined, modelContextLimit),
|
|
3405
|
+
tools: Array.isArray(body.tools) ? body.tools : undefined,
|
|
3406
|
+
compressionTargetTokens: args.targetTokens,
|
|
3407
|
+
prepare: true,
|
|
3408
|
+
...(messageUsage && Object.keys(messageUsage).length ? { messageUsage } : {}),
|
|
3409
|
+
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
3410
|
+
sessionId,
|
|
3411
|
+
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3412
|
+
// Nothing waits on it: the long budget, so a slow server still prepares.
|
|
3413
|
+
timeoutMs: opts.memtree.longCompressBudgetMs,
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3010
3416
|
function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
3011
3417
|
// Model is deliberately not route identity: Claude Code switches models
|
|
3012
3418
|
// mid-loop (overload fallback, /model, /fast), and the compressed prefix is
|
|
@@ -3024,7 +3430,7 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
|
3024
3430
|
if (messages.length < prefixLength)
|
|
3025
3431
|
return null;
|
|
3026
3432
|
for (let i = 0; i < prefixLength; i++) {
|
|
3027
|
-
if (
|
|
3433
|
+
if (!prefixMessageMatches(messages[i], "route", route.originalPrefixHashes[i], route.originalMediaFreeHashes?.[i])) {
|
|
3028
3434
|
return null;
|
|
3029
3435
|
}
|
|
3030
3436
|
}
|
|
@@ -3407,8 +3813,9 @@ async function runBlockingCompression(args) {
|
|
|
3407
3813
|
: undefined;
|
|
3408
3814
|
const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
|
|
3409
3815
|
const compressStarted = Date.now();
|
|
3410
|
-
|
|
3411
|
-
|
|
3816
|
+
// The upload is about the request's size: the budget grows with it.
|
|
3817
|
+
const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? opts.memtree.longCompressBudgetMs
|
|
3818
|
+
: opts.memtree.compressBudgetForBytes(rec.requestBytes));
|
|
3412
3819
|
const compressMeta = {
|
|
3413
3820
|
// Model + tools drive the server's model-based memory budget
|
|
3414
3821
|
// (e.g. 800k whole-request target for Fable / Opus 5). Omitting
|
|
@@ -3436,7 +3843,9 @@ async function runBlockingCompression(args) {
|
|
|
3436
3843
|
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
3437
3844
|
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
3438
3845
|
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3439
|
-
|
|
3846
|
+
// The deadline this wait uses (the upload-scaled budget included), so the
|
|
3847
|
+
// call and its waiters end together.
|
|
3848
|
+
timeoutMs: budgetMs,
|
|
3440
3849
|
};
|
|
3441
3850
|
// Sampled BEFORE the call, while it still describes this call: after the
|
|
3442
3851
|
// await the hash is in the cache regardless of who put it there.
|
|
@@ -3444,9 +3853,9 @@ async function runBlockingCompression(args) {
|
|
|
3444
3853
|
// compress() maps every failure to a resolved null (it never rejects).
|
|
3445
3854
|
// Keep foreground work finite even if a transport ignores its abort signal.
|
|
3446
3855
|
let deadline;
|
|
3447
|
-
const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS :
|
|
3856
|
+
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
3857
|
const result = await Promise.race([
|
|
3449
|
-
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
|
|
3858
|
+
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta),
|
|
3450
3859
|
new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
|
|
3451
3860
|
]).finally(() => { if (deadline)
|
|
3452
3861
|
clearTimeout(deadline); });
|
|
@@ -3456,19 +3865,27 @@ async function runBlockingCompression(args) {
|
|
|
3456
3865
|
// another class can overwrite the complete-key entry.
|
|
3457
3866
|
const failureArming = result === null &&
|
|
3458
3867
|
opts.memtree.lastCompressFailureArming(hash, modelContextLimit, compressMeta);
|
|
3868
|
+
const failureStatus = result === null
|
|
3869
|
+
? opts.memtree.lastCompressFailureStatus?.(hash, modelContextLimit, compressMeta) : undefined;
|
|
3459
3870
|
const hadLiveSuccess = !cached && result !== null;
|
|
3460
3871
|
// Only arming-class live failures (network error, timeout, 5xx, 402) are
|
|
3461
3872
|
// fuse evidence. A responsive server's other 4xx failed THIS call but says
|
|
3462
3873
|
// nothing about MemTree's health, so it contributes neither failure nor
|
|
3463
3874
|
// success — the fuse is untouched.
|
|
3464
|
-
|
|
3875
|
+
// Budget-consumed heuristic: the client maps every failure to null, so a
|
|
3876
|
+
// null that took (roughly) the whole abort budget was almost certainly
|
|
3877
|
+
// the AbortSignal timeout, not a fast server error.
|
|
3878
|
+
const timedOut = result === null && compressMs >= Math.min(budgetMs, maximumWait);
|
|
3879
|
+
// A timeout while other compress calls answered (one succeeded after this
|
|
3880
|
+
// one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
|
|
3881
|
+
// healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
|
|
3882
|
+
const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
|
|
3883
|
+
const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered;
|
|
3465
3884
|
rec.compress = {
|
|
3466
3885
|
ms: compressMs,
|
|
3467
3886
|
ok: result !== null,
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
// the AbortSignal timeout, not a fast server error.
|
|
3471
|
-
timedOut: result === null && compressMs >= budgetMs,
|
|
3887
|
+
timedOut,
|
|
3888
|
+
...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
|
|
3472
3889
|
};
|
|
3473
3890
|
return {
|
|
3474
3891
|
result,
|
|
@@ -3480,6 +3897,61 @@ async function runBlockingCompression(args) {
|
|
|
3480
3897
|
: "none",
|
|
3481
3898
|
};
|
|
3482
3899
|
}
|
|
3900
|
+
/**
|
|
3901
|
+
* A clearly under-budget human turn's compress call, not awaited: its answer
|
|
3902
|
+
* (a passthrough) only updates the server budget, MemTree health and the
|
|
3903
|
+
* session's page link. Its telemetry goes to a copy of the turn's record.
|
|
3904
|
+
*/
|
|
3905
|
+
function compressInBackground(args) {
|
|
3906
|
+
if (args.state.backgroundSignal.aborted)
|
|
3907
|
+
return;
|
|
3908
|
+
const lanes = args.state.backgroundCompress;
|
|
3909
|
+
const lane = backgroundCompressLane(args);
|
|
3910
|
+
if (lane === undefined) {
|
|
3911
|
+
void runBackgroundCompress(args);
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3914
|
+
const busy = lanes.get(lane);
|
|
3915
|
+
if (busy) {
|
|
3916
|
+
// One whole-history call at a time per conversation: under the budget each turn
|
|
3917
|
+
// started its own (16 to 22 s each on staging in the long-session test, about 12 at
|
|
3918
|
+
// once beside the index uploads). A waiting call is replaced only by one whose
|
|
3919
|
+
// history contains all of it; otherwise it goes now.
|
|
3920
|
+
if (busy.waiting && !extendsHistory(args.msgsForMemtree, busy.waiting.msgsForMemtree)) {
|
|
3921
|
+
void runBackgroundCompress(busy.waiting);
|
|
3922
|
+
}
|
|
3923
|
+
busy.waiting = args;
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
const owner = {};
|
|
3927
|
+
lanes.set(lane, owner);
|
|
3928
|
+
void runBackgroundCompress(args).finally(() => {
|
|
3929
|
+
if (lanes.get(lane) === owner)
|
|
3930
|
+
lanes.delete(lane);
|
|
3931
|
+
if (owner.waiting)
|
|
3932
|
+
compressInBackground(owner.waiting);
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
function runBackgroundCompress(args) {
|
|
3936
|
+
const { state, sessionId, linkSeq } = args;
|
|
3937
|
+
return runBlockingCompression({ ...args, rec: { ...args.rec }, signal: state.backgroundSignal })
|
|
3938
|
+
.then((compression) => {
|
|
3939
|
+
noteMemtreeHealth(state, compression);
|
|
3940
|
+
if (linkSeq !== undefined)
|
|
3941
|
+
noteMemtreePage(state, linkSeq, sessionId, compression.result);
|
|
3942
|
+
})
|
|
3943
|
+
.catch(() => undefined);
|
|
3944
|
+
}
|
|
3945
|
+
/**
|
|
3946
|
+
* A conversation for compressInBackground: session and first message, media left out (Claude
|
|
3947
|
+
* Code's media eviction keeps it, as indexLaneKey does); none without a session.
|
|
3948
|
+
*/
|
|
3949
|
+
function backgroundCompressLane(args) {
|
|
3950
|
+
if (args.sessionId === undefined)
|
|
3951
|
+
return undefined;
|
|
3952
|
+
const first = args.msgsForMemtree.find((message) => message.role !== "system");
|
|
3953
|
+
return JSON.stringify([args.sessionId, first ? mediaFreeMessageHash(first) : ""]);
|
|
3954
|
+
}
|
|
3483
3955
|
/**
|
|
3484
3956
|
* Shared final request shape for a validated compression result: lift any
|
|
3485
3957
|
* returned system message to `body.system`, and forward the SERVER's flatten
|
|
@@ -3579,14 +4051,14 @@ async function recoverToolRouteMiss(args) {
|
|
|
3579
4051
|
capture(opts, "anthropic-request-memory-tool", fallback.raw);
|
|
3580
4052
|
return forwardRaw(req, res, fallback.raw, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3581
4053
|
toolForwarding.settle(delivered);
|
|
3582
|
-
noteRideSize(fallback.sizeHolder, rec, fallback.raw
|
|
4054
|
+
noteRideSize(fallback.sizeHolder, rec, fallback.raw);
|
|
3583
4055
|
});
|
|
3584
4056
|
}
|
|
3585
4057
|
recordTurn(rec, "tool", args.originalBody);
|
|
3586
4058
|
capture(opts, "anthropic-request", args.originalBody);
|
|
3587
4059
|
return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3588
4060
|
toolForwarding.settle(delivered);
|
|
3589
|
-
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody
|
|
4061
|
+
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody);
|
|
3590
4062
|
});
|
|
3591
4063
|
};
|
|
3592
4064
|
// Same downstream-close tracking as the followup path: compression
|
|
@@ -3727,6 +4199,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3727
4199
|
const { compressedBody, compressedRaw } = built;
|
|
3728
4200
|
if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
|
|
3729
4201
|
rec.routeRecovery = { conversationBytes, outcome: "unusable" };
|
|
4202
|
+
noteOversizedCompression(rec, args.sizing, body, compressedRaw, args.compaction?.target ?? rec.compaction?.targetTokens);
|
|
3730
4203
|
releaseOwnReservation();
|
|
3731
4204
|
if (!noteIndexLag(opts, rec, result))
|
|
3732
4205
|
return forwardOriginal();
|
|
@@ -3873,7 +4346,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3873
4346
|
// The compacted request's reported size anchors the next budget check.
|
|
3874
4347
|
const sizeHolder = stable?.installed ?? installedRoute;
|
|
3875
4348
|
if (sizeHolder)
|
|
3876
|
-
notePrefixSize(sizeHolder, rec, compressedRaw
|
|
4349
|
+
notePrefixSize(sizeHolder, rec, compressedRaw);
|
|
3877
4350
|
// Nothing to ride next turn, or a result that still does not fit the
|
|
3878
4351
|
// window (the next turn would reject it and compress again): wait for
|
|
3879
4352
|
// growth before the lane tries again.
|
|
@@ -3972,18 +4445,29 @@ const recapLinkAppenders = new WeakMap();
|
|
|
3972
4445
|
* miss the tree, index a divergent conversation and bill for it.
|
|
3973
4446
|
*/
|
|
3974
4447
|
async function recoverLateCount(args) {
|
|
4448
|
+
let generation;
|
|
3975
4449
|
try {
|
|
3976
4450
|
const { body, msgsForMemtree: messages } = args;
|
|
3977
4451
|
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);
|
|
4452
|
+
const target = args.target ?? lateRecoveryTarget(compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget), budget, args.rideTokens);
|
|
4453
|
+
const sessionId = requestSessionId(args.req);
|
|
4454
|
+
const linkSeq = args.route.lane === "main" ? nextMemtreeCallSeq(args.state) : undefined;
|
|
4455
|
+
const replaces = sessionId !== undefined
|
|
4456
|
+
? args.state.stablePrefixes.get(sessionId) ?? null : null;
|
|
4457
|
+
// Reserved before the compress call: a newer same-lane decision made
|
|
4458
|
+
// while it runs outranks this one, so this never overwrites its route.
|
|
4459
|
+
generation = ++args.state.mainRouteDecisionGeneration;
|
|
4460
|
+
reserveRouteDecision(args.state, args.route.key, generation);
|
|
3979
4461
|
const outcome = await runBlockingCompression({
|
|
3980
4462
|
opts: args.opts, state: args.state, body, msgsForMemtree: messages,
|
|
3981
4463
|
hash: args.hash, modelContextLimit: args.modelContextLimit,
|
|
3982
|
-
rec: args.rec, sessionId
|
|
4464
|
+
rec: args.rec, sessionId, agentId: agentAttributionId(args.req),
|
|
3983
4465
|
clientMeta: args.clientMeta, sizing: args.sizing, inputCandidate: args.inputCandidate,
|
|
3984
4466
|
compaction: { target },
|
|
3985
4467
|
});
|
|
3986
4468
|
noteMemtreeHealth(args.state, outcome);
|
|
4469
|
+
if (linkSeq !== undefined)
|
|
4470
|
+
noteMemtreePage(args.state, linkSeq, sessionId, outcome.result);
|
|
3987
4471
|
// Like every compress path: a refusal after a lagging answer waits for
|
|
3988
4472
|
// the index, and the cached answer expires so a retry asks again.
|
|
3989
4473
|
if (outcome.result)
|
|
@@ -3994,11 +4478,63 @@ async function recoverLateCount(args) {
|
|
|
3994
4478
|
const built = buildCompressedBody(body, outcome.result);
|
|
3995
4479
|
if (!built)
|
|
3996
4480
|
return undefined;
|
|
4481
|
+
lateRouteInstalls.set(args.rec, lateRouteInstall(args, built.compressedBody, outcome.result, {
|
|
4482
|
+
targetTokens: target, explicitTarget: true, modelContextLimit: args.modelContextLimit,
|
|
4483
|
+
replaces
|
|
4484
|
+
}, linkSeq, generation));
|
|
4485
|
+
generation = undefined; // the install owns the reservation now
|
|
3997
4486
|
return built.compressedRaw;
|
|
3998
4487
|
}
|
|
3999
4488
|
catch {
|
|
4000
4489
|
return undefined;
|
|
4001
4490
|
}
|
|
4491
|
+
finally {
|
|
4492
|
+
if (generation !== undefined)
|
|
4493
|
+
releaseRouteDecision(args.state, args.route.key, generation);
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
/** Late recoveries' installs, per request, until delivery settles. */
|
|
4497
|
+
const lateRouteInstalls = new WeakMap();
|
|
4498
|
+
function lateRouteInstall(args, compressedBody, result, stable, linkSeq,
|
|
4499
|
+
/** Reserved (reserveRouteDecision) before the compress call; released here if unused. */
|
|
4500
|
+
generation) {
|
|
4501
|
+
const { state, route } = args;
|
|
4502
|
+
const sessionId = requestSessionId(args.req);
|
|
4503
|
+
let done = false;
|
|
4504
|
+
let installed;
|
|
4505
|
+
return {
|
|
4506
|
+
activate: () => {
|
|
4507
|
+
if (done)
|
|
4508
|
+
return;
|
|
4509
|
+
done = true;
|
|
4510
|
+
if (sessionId !== undefined && route.lane !== "away") {
|
|
4511
|
+
installed = installMemoryRoute(state, route.key, args.req, args.body, route.messages, compressedBody, route.epoch, generation, route.lane === "main" ? stable : undefined);
|
|
4512
|
+
}
|
|
4513
|
+
if (!installed)
|
|
4514
|
+
releaseRouteDecision(state, route.key, generation);
|
|
4515
|
+
else if (linkSeq !== undefined)
|
|
4516
|
+
noteMemorySource(state, linkSeq, sessionId, result);
|
|
4517
|
+
},
|
|
4518
|
+
settle(delivered, sent) {
|
|
4519
|
+
if (delivered)
|
|
4520
|
+
this.activate();
|
|
4521
|
+
// The replacement is a changed delivery (reportedSample skips those):
|
|
4522
|
+
// its reported size is this very body's, so it is noted directly.
|
|
4523
|
+
const holder = stable.installed ?? installed;
|
|
4524
|
+
const tokens = reportedInputTokens(args.rec);
|
|
4525
|
+
if (holder && sent && delivered && tokens !== undefined &&
|
|
4526
|
+
!serverToolRecords.has(args.rec) && !usedServerTools(args.rec)) {
|
|
4527
|
+
const sample = sizeSampleOf(sent, tokens, args.rec.model);
|
|
4528
|
+
if (plausibleSample(sample) && !(holder.lastSize && sent.length < holder.lastSize.forwardedBytes)) {
|
|
4529
|
+
holder.lastSize = sample;
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
if (!installed) {
|
|
4533
|
+
done = true;
|
|
4534
|
+
releaseRouteDecision(state, route.key, generation);
|
|
4535
|
+
}
|
|
4536
|
+
},
|
|
4537
|
+
};
|
|
4002
4538
|
}
|
|
4003
4539
|
/**
|
|
4004
4540
|
* The ride compressed to `target` still counted `rideTokens` (over the
|
|
@@ -4018,16 +4554,25 @@ async function selectForwardCandidate(args) {
|
|
|
4018
4554
|
const preferred = policy.sizing.register(intended, policy.allowed.has(intended) ? args.rec.compress ? "replacement" : "ride" : "original");
|
|
4019
4555
|
const measured = await policy.sizing.measure(preferred, args.limits);
|
|
4020
4556
|
let target = preferred;
|
|
4021
|
-
|
|
4022
|
-
|
|
4557
|
+
const late = lateRecoveryPlan(args.state, args.rec, preferred, measured, args.limits, args.routeKey);
|
|
4558
|
+
if (late) {
|
|
4023
4559
|
args.rec.lateCountRecovery = { outcome: "forwarded" };
|
|
4024
|
-
const replacement = await policy.recoverLate(preferred.body,
|
|
4560
|
+
const replacement = await holdingLane(args.state, args.routeKey, () => policy.recoverLate(preferred.body, late.tokens, late.target));
|
|
4025
4561
|
if (replacement)
|
|
4026
4562
|
target = policy.sizing.register(replacement, "replacement");
|
|
4027
4563
|
}
|
|
4028
|
-
const selection = await policy.sizing.select(target, args.limits)
|
|
4564
|
+
const selection = await policy.sizing.select(target, args.limits) ??
|
|
4565
|
+
await policy.sizing.retryTimedOutCount(args.limits) ??
|
|
4566
|
+
(args.rec.routeLane === "main" && args.opts.toolOutputs
|
|
4567
|
+
? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
|
|
4568
|
+
: undefined);
|
|
4029
4569
|
const quietDegrade = quietDegrades.get(args.rec);
|
|
4030
4570
|
quietDegrades.delete(args.rec);
|
|
4571
|
+
const lateInstall = lateRouteInstalls.get(args.rec);
|
|
4572
|
+
if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
|
|
4573
|
+
lateRouteInstalls.delete(args.rec);
|
|
4574
|
+
lateInstall.settle(false);
|
|
4575
|
+
}
|
|
4031
4576
|
if (!selection) {
|
|
4032
4577
|
if (args.rec.lateCountRecovery)
|
|
4033
4578
|
args.rec.lateCountRecovery.outcome = "refused";
|
|
@@ -4035,6 +4580,10 @@ async function selectForwardCandidate(args) {
|
|
|
4035
4580
|
return undefined;
|
|
4036
4581
|
}
|
|
4037
4582
|
const { candidate, measurement } = selection;
|
|
4583
|
+
const preparedBase = preparedTruncationBases.get(policy.sizing);
|
|
4584
|
+
if (preparedBase?.derived !== undefined && preparedBase.derived === candidate) {
|
|
4585
|
+
preparedBase.install();
|
|
4586
|
+
}
|
|
4038
4587
|
const changed = candidate !== preferred || intended !== args.preferredBody;
|
|
4039
4588
|
if (changed) {
|
|
4040
4589
|
policy.allowed.delete(args.preferredBody);
|
|
@@ -4057,6 +4606,154 @@ async function selectForwardCandidate(args) {
|
|
|
4057
4606
|
}
|
|
4058
4607
|
return changed ? candidate.body : intended;
|
|
4059
4608
|
}
|
|
4609
|
+
/** MemTree's answer when the conversation cannot fit the window even compressed. */
|
|
4610
|
+
const OVER_CONTEXT_STATUS = 413;
|
|
4611
|
+
/** Truncation attempts, each cutting more, before the request is refused. */
|
|
4612
|
+
const TRUNCATION_ATTEMPTS = 3;
|
|
4613
|
+
/** Cut this much more than the measured excess (tokens per character vary). */
|
|
4614
|
+
const TRUNCATION_MARGIN = 1.25;
|
|
4615
|
+
/**
|
|
4616
|
+
* The safety net for a main-thread request that does not fit even after
|
|
4617
|
+
* compression (tool-result-truncation.ts): its newest tool result saved whole
|
|
4618
|
+
* (tool-output-store.ts) and sent with the middle replaced by a pointer to
|
|
4619
|
+
* that copy, cut until the request leaves the reply's room below the window.
|
|
4620
|
+
* A compressed candidate first (a ride on a prefix, or a replacement whose
|
|
4621
|
+
* suffix is structured), else the whole history.
|
|
4622
|
+
*/
|
|
4623
|
+
async function truncatedToFit(rec, sizing, preferred, limits, store) {
|
|
4624
|
+
if (!compressionSettled(rec))
|
|
4625
|
+
return undefined;
|
|
4626
|
+
const prepared = preparedTruncationBases.get(sizing);
|
|
4627
|
+
const rank = (item) => item.kind === "original" ? 1 : 0;
|
|
4628
|
+
const bases = [preferred, ...sizing.fallbacks().filter((item) => item !== preferred),
|
|
4629
|
+
...(prepared && prepared.candidate !== preferred ? [prepared.candidate] : [])]
|
|
4630
|
+
.sort((a, b) => rank(a) - rank(b) || a.body.length - b.body.length);
|
|
4631
|
+
for (const base of bases) {
|
|
4632
|
+
const fitted = await truncateCandidate(rec, sizing, base, limits, store);
|
|
4633
|
+
if (fitted && prepared && base === prepared.candidate)
|
|
4634
|
+
prepared.derived = fitted.candidate;
|
|
4635
|
+
if (fitted)
|
|
4636
|
+
return fitted;
|
|
4637
|
+
}
|
|
4638
|
+
return undefined;
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Whether compression had its chance and the request still cannot fit: a ride
|
|
4642
|
+
* on a prepared prefix, a compress answer that compressed (sent or found too
|
|
4643
|
+
* large), or MemTree's 413 (it cannot fit the window even compressed). Not
|
|
4644
|
+
* after a failed or skipped compress call (an outage, an exception, an auth
|
|
4645
|
+
* or payment refusal, a backoff: the retryable refusal stands), a passthrough
|
|
4646
|
+
* of a warming index, or while the index is behind: waiting for it lets
|
|
4647
|
+
* compression summarize instead.
|
|
4648
|
+
*/
|
|
4649
|
+
function compressionSettled(rec) {
|
|
4650
|
+
if (rec.indexBehind === true)
|
|
4651
|
+
return false;
|
|
4652
|
+
if (rec.compress?.refused === true || rec.compaction?.prepared === "used")
|
|
4653
|
+
return true;
|
|
4654
|
+
return rec.compress?.ok === true && rec.routeRecovery?.outcome !== "noop" &&
|
|
4655
|
+
rec.turnType !== "followup-noop";
|
|
4656
|
+
}
|
|
4657
|
+
/** truncatedToFit for one candidate; undefined when it has no tool result to
|
|
4658
|
+
* shorten, the copy could not be saved, or it cannot be made to fit. */
|
|
4659
|
+
async function truncateCandidate(rec, sizing, base, limits, store) {
|
|
4660
|
+
const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
|
|
4661
|
+
const parsed = JSON.parse(base.body.toString("utf-8"));
|
|
4662
|
+
const first = await sizing.measure(base, limits);
|
|
4663
|
+
const bytesPerToken = base.body.length / Math.max(1, first.tokens);
|
|
4664
|
+
for (let attempt = 1; attempt <= TRUNCATION_ATTEMPTS; attempt++) {
|
|
4665
|
+
const excess = Math.max(1_000, first.tokens - goal);
|
|
4666
|
+
const cut = Math.ceil(excess * bytesPerToken * TRUNCATION_MARGIN * attempt);
|
|
4667
|
+
// The store pins each copy it saves, so none this request points at is
|
|
4668
|
+
// evicted while it is counted and forwarded.
|
|
4669
|
+
const plan = truncateToolResults(parsed, cut, (text) => store.save(text));
|
|
4670
|
+
if (!plan)
|
|
4671
|
+
return undefined;
|
|
4672
|
+
const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
|
|
4673
|
+
const measurement = await sizing.measure(candidate, limits);
|
|
4674
|
+
if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
|
|
4675
|
+
rec.truncatedToolResult = { originalChars: plan.originalChars,
|
|
4676
|
+
omittedChars: plan.omittedChars };
|
|
4677
|
+
return { candidate, measurement };
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return undefined;
|
|
4681
|
+
}
|
|
4682
|
+
/**
|
|
4683
|
+
* Whether delivery recompresses before sending, and to what: a ride counted
|
|
4684
|
+
* over the budget (no compression yet); the whole history counted over the
|
|
4685
|
+
* window with no compression yet (outside an outage cooldown: a refusal is
|
|
4686
|
+
* all that is left otherwise); or a compression that came back too large for
|
|
4687
|
+
* the window (oversizedCompressions), redone once to a target smaller by the
|
|
4688
|
+
* excess, so the turn is compressed and sent in the same request instead of
|
|
4689
|
+
* being refused and retried.
|
|
4690
|
+
*/
|
|
4691
|
+
function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
|
|
4692
|
+
if (rec.lateCountRecovery)
|
|
4693
|
+
return undefined;
|
|
4694
|
+
const oversized = oversizedCompressions.get(rec);
|
|
4695
|
+
// A lagging index kept history verbatim: the refusal waits for it instead.
|
|
4696
|
+
if (oversized && rec.indexBehind !== true) {
|
|
4697
|
+
const goal = limits.context - Math.max(limits.output, COMPRESSION_RESERVE_TOKENS);
|
|
4698
|
+
const excess = oversized.tokens - goal;
|
|
4699
|
+
const target = Math.floor(oversized.target - excess - oversized.target * SMALLER_TARGET_MARGIN);
|
|
4700
|
+
return target >= SERVER_MIN_TARGET_TOKENS ? { tokens: oversized.tokens, target } : undefined;
|
|
4701
|
+
}
|
|
4702
|
+
if (rec.compress || measured.source !== "exact" || !(measured.tokens > limits.budget)) {
|
|
4703
|
+
return undefined;
|
|
4704
|
+
}
|
|
4705
|
+
// Another request of this lane is compressing (rec.routeRecovery "in-flight"):
|
|
4706
|
+
// a second compression of the same history at another target would miss its
|
|
4707
|
+
// dedupe and bill twice. This one waits for neither and goes as it is.
|
|
4708
|
+
if (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight)
|
|
4709
|
+
return undefined;
|
|
4710
|
+
if (preferred.kind === "ride")
|
|
4711
|
+
return { tokens: measured.tokens };
|
|
4712
|
+
// Over the window with nothing compressed, a refusal is all that is left:
|
|
4713
|
+
// an outage cooldown does not skip this one compress.
|
|
4714
|
+
return preferred.kind === "original" && !fitsMeasured(measured, limits)
|
|
4715
|
+
? { tokens: measured.tokens } : undefined;
|
|
4716
|
+
}
|
|
4717
|
+
/**
|
|
4718
|
+
* Run a delivery-time compression as the lane's in-flight operation, so a
|
|
4719
|
+
* concurrent request of the lane neither starts its own (planning sees
|
|
4720
|
+
* "in-flight") nor races it; the lane's own attempt state is kept.
|
|
4721
|
+
*/
|
|
4722
|
+
async function holdingLane(state, routeKey, run) {
|
|
4723
|
+
const lanes = state.toolRecoveryAttemptedLanes;
|
|
4724
|
+
const prior = lanes.get(routeKey);
|
|
4725
|
+
const attempt = prior ?? { modelContextLimit: 0, inFlight: true };
|
|
4726
|
+
// Clears only what it set: an attempt another request holds in flight
|
|
4727
|
+
// stays that request's to end.
|
|
4728
|
+
const setHere = !attempt.inFlight;
|
|
4729
|
+
attempt.inFlight = true;
|
|
4730
|
+
if (!prior)
|
|
4731
|
+
lanes.set(routeKey, attempt);
|
|
4732
|
+
try {
|
|
4733
|
+
return await run();
|
|
4734
|
+
}
|
|
4735
|
+
finally {
|
|
4736
|
+
if (setHere)
|
|
4737
|
+
attempt.inFlight = false;
|
|
4738
|
+
if (!prior && lanes.get(routeKey) === attempt)
|
|
4739
|
+
lanes.delete(routeKey);
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
/** A recompression aims this share of its old target lower than the excess alone. */
|
|
4743
|
+
const SMALLER_TARGET_MARGIN = 0.1;
|
|
4744
|
+
/**
|
|
4745
|
+
* Compressions of this request that came back too large for the window: their
|
|
4746
|
+
* size (an exact count, else the conservative bound) and the target they had.
|
|
4747
|
+
*/
|
|
4748
|
+
const oversizedCompressions = new WeakMap();
|
|
4749
|
+
/** Note a compression of `rec` that does not fit the window (lateRecoveryPlan). */
|
|
4750
|
+
function noteOversizedCompression(rec, sizing, body, compressedRaw, target) {
|
|
4751
|
+
if (target === undefined)
|
|
4752
|
+
return;
|
|
4753
|
+
const tokens = sizing.exactTokensFor(compressedRaw) ??
|
|
4754
|
+
conservativeBodyTokens(compressedRaw, typeof body.model === "string" ? body.model : undefined);
|
|
4755
|
+
oversizedCompressions.set(rec, { tokens, target });
|
|
4756
|
+
}
|
|
4060
4757
|
/**
|
|
4061
4758
|
* Whether delivery would refuse `candidate` sent whole, by delivery's own
|
|
4062
4759
|
* admission: it is sendable only when its plan fits and either an exact count
|
|
@@ -4207,7 +4904,12 @@ async function forwardRaw(req, res, bodyBuffer, opts, upstream, shutdownSignal,
|
|
|
4207
4904
|
const selected = policy ? await policy.select(bodyBuffer) : bodyBuffer;
|
|
4208
4905
|
if (!selected)
|
|
4209
4906
|
return false;
|
|
4210
|
-
|
|
4907
|
+
// A late recovery's replacement went out instead: it installs as its route.
|
|
4908
|
+
const lateInstall = rec && selected !== bodyBuffer ? lateRouteInstalls.get(rec) : undefined;
|
|
4909
|
+
if (rec)
|
|
4910
|
+
lateRouteInstalls.delete(rec);
|
|
4911
|
+
const delivered = await forwardAccepted(req, res, selected, opts, upstream, shutdownSignal, rec, selected === bodyBuffer ? onProtocolComplete : lateInstall?.activate);
|
|
4912
|
+
lateInstall?.settle(delivered, selected);
|
|
4211
4913
|
if (delivered && rec)
|
|
4212
4914
|
noteSelectedCandidateSize(rec);
|
|
4213
4915
|
return delivered;
|
|
@@ -4222,7 +4924,7 @@ function noteSelectedCandidateSize(rec) {
|
|
|
4222
4924
|
// Server tools' injected results inflate the reported input (reportedSample).
|
|
4223
4925
|
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
4224
4926
|
return;
|
|
4225
|
-
const sample =
|
|
4927
|
+
const sample = sizeSampleOf(candidate.body, tokens, rec.model);
|
|
4226
4928
|
if (!plausibleSample(sample))
|
|
4227
4929
|
return;
|
|
4228
4930
|
if ("set" in owner)
|
|
@@ -4948,4 +5650,5 @@ function transcriptTimesFor(opts, sessionId, agentId) {
|
|
|
4948
5650
|
return undefined;
|
|
4949
5651
|
return (messages) => source.timesFor(sessionId, messages, agentId);
|
|
4950
5652
|
}
|
|
5653
|
+
export const __testHoldingLane = holdingLane;
|
|
4951
5654
|
//# sourceMappingURL=proxy.js.map
|