claude-code-infinite 1.0.16 → 1.0.18
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 +9 -1
- package/dist/cli-main.js +7 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/hooks.d.ts +20 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +35 -8
- package/dist/hooks.js.map +1 -1
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +7 -2
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.d.ts +21 -0
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +8 -1
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree.d.ts +45 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +96 -11
- package/dist/memtree.js.map +1 -1
- package/dist/notices.js +2 -2
- package/dist/notices.js.map +1 -1
- package/dist/payment-notice.d.ts +47 -0
- package/dist/payment-notice.d.ts.map +1 -0
- package/dist/payment-notice.js +72 -0
- package/dist/payment-notice.js.map +1 -0
- package/dist/prefix-store.d.ts +59 -0
- package/dist/prefix-store.d.ts.map +1 -0
- package/dist/prefix-store.js +330 -0
- package/dist/prefix-store.js.map +1 -0
- package/dist/proxy.d.ts +14 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +255 -45
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +8 -0
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js +11 -0
- package/dist/reqlog.js.map +1 -1
- package/package.json +1 -1
package/dist/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AASvD,OAAO,EAAiB,qBAAqB,EACD,MAAM,qBAAqB,CAAC;AAiBxE,OAAO,EAKL,aAAa,EAMd,MAAM,cAAc,CAAC;AAyCtB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAS5D,OAAO,KAAK,EAAgB,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,cAAc,EAGpB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AASvD,OAAO,EAAiB,qBAAqB,EACD,MAAM,qBAAqB,CAAC;AAiBxE,OAAO,EAKL,aAAa,EAMd,MAAM,cAAc,CAAC;AAyCtB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACvF,OAAO,KAAK,EAAkB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAS5D,OAAO,KAAK,EAAgB,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,cAAc,EAGpB,MAAM,aAAa,CAAC;AAkCrB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;OASG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,8EAA8E;IAC9E,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,oFAAoF;IACpF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;;OAIG;IACH,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACjD;AAkOD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAC;AA0OhD,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwIpE;AAgRD,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOjE;AAq8CD,eAAO,MAAM,0BAA0B,QAAS,CAAC;AA8CjD,eAAO,MAAM,kBAAkB,4DAA4D,CAAC;AAC5F,eAAO,MAAM,sBAAsB,sDAAsD,CAAC;AAqB1F,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMhE;AAwiGD,iBAAiB;AACjB,eAAO,MAAM,yBAAyB,4BAAsB,CAAC;AAC7D,eAAO,MAAM,2BAA2B,8BAAwB,CAAC"}
|
package/dist/proxy.js
CHANGED
|
@@ -52,9 +52,10 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
52
52
|
import { join } from "node:path";
|
|
53
53
|
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
54
54
|
import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
|
|
55
|
-
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText,
|
|
56
|
-
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
55
|
+
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";
|
|
57
57
|
import { hyperlink } from "./payment-gate.js";
|
|
58
|
+
import { PaymentNoticeThrottle, paymentNoticeText } from "./payment-notice.js";
|
|
58
59
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
59
60
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
60
61
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
@@ -69,6 +70,8 @@ const HOOK_BODY_LIMIT = 64 * 1024;
|
|
|
69
70
|
// Long enough that a real outage costs one stall, short enough that a
|
|
70
71
|
// transient blip does not disable recovery for a working session.
|
|
71
72
|
const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
|
|
73
|
+
/** Safety cap on a long-budget compress, should a transport ignore its abort. */
|
|
74
|
+
const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
|
|
72
75
|
const SKIP_REQUEST_HEADERS = new Set([
|
|
73
76
|
"host",
|
|
74
77
|
"connection",
|
|
@@ -260,7 +263,8 @@ export function startProxy(opts) {
|
|
|
260
263
|
const acceptedRequests = new Set();
|
|
261
264
|
const shutdownAbort = new AbortController();
|
|
262
265
|
const state = {
|
|
263
|
-
|
|
266
|
+
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
267
|
+
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
264
268
|
notices: new NoticeDeliveryQueue(),
|
|
265
269
|
mainNoticePending: false,
|
|
266
270
|
mainPromptGeneration: 0,
|
|
@@ -277,8 +281,13 @@ export function startProxy(opts) {
|
|
|
277
281
|
shutdownSignal: shutdownAbort.signal,
|
|
278
282
|
routeInstallFault: opts.routeInstallFault,
|
|
279
283
|
memtreePages: new Map(),
|
|
284
|
+
memorySourcePages: new Map(),
|
|
280
285
|
memtreeCallSeq: 0,
|
|
281
286
|
memtreeLinkStore: opts.memtreeLinkStore,
|
|
287
|
+
stablePrefixStore: opts.stablePrefixStore,
|
|
288
|
+
storedPrefixLookups: new Map(),
|
|
289
|
+
storageOwner: typeof opts.memtree.storageIdentity === "string"
|
|
290
|
+
? opts.memtree.storageIdentity : undefined,
|
|
282
291
|
newestTrees: opts.newestTrees,
|
|
283
292
|
memtreeLinkPlacement: opts.memtreeLinkPlacement ?? "turn",
|
|
284
293
|
compactTargets: new Map(),
|
|
@@ -547,7 +556,7 @@ async function handleMemTreeFinder(req, res, opts, upstreamPath, url) {
|
|
|
547
556
|
*/
|
|
548
557
|
async function handleMemTreeCurrent(req, res, opts, state, url) {
|
|
549
558
|
const sessionId = url.searchParams.get("session") || undefined;
|
|
550
|
-
const page =
|
|
559
|
+
const page = currentMemorySourcePage(state, sessionId);
|
|
551
560
|
const id = page ? memtreePageId(page.url) : undefined;
|
|
552
561
|
if (!page || !id) {
|
|
553
562
|
res.writeHead(404, { "content-type": "application/json" });
|
|
@@ -579,6 +588,12 @@ async function handleMemTreeCurrent(req, res, opts, state, url) {
|
|
|
579
588
|
}
|
|
580
589
|
}
|
|
581
590
|
const MEMTREE_CURRENT_PATH = `${MEMTREE_PASSTHROUGH_PREFIX}current`;
|
|
591
|
+
/** The page whose memory is in the prompt, else (none adopted yet) the newest page. */
|
|
592
|
+
function currentMemorySourcePage(state, sessionId) {
|
|
593
|
+
if (!sessionId)
|
|
594
|
+
return undefined;
|
|
595
|
+
return state.memorySourcePages.get(sessionId) ?? currentMemtreePage(state, sessionId);
|
|
596
|
+
}
|
|
582
597
|
function currentMemtreePage(state, sessionId) {
|
|
583
598
|
if (!sessionId)
|
|
584
599
|
return undefined;
|
|
@@ -640,7 +655,7 @@ onMainStop) {
|
|
|
640
655
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
641
656
|
await state.newestTrees?.settle(parsed.session_id);
|
|
642
657
|
}
|
|
643
|
-
const line = resumeLinkLine(state, parsed);
|
|
658
|
+
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed));
|
|
644
659
|
if (!line) {
|
|
645
660
|
res.writeHead(204);
|
|
646
661
|
res.end();
|
|
@@ -659,16 +674,16 @@ onMainStop) {
|
|
|
659
674
|
// `/memtree-view`: answered here and blocked, so no model turn runs and
|
|
660
675
|
// the prompt never enters the conversation. Not a human turn either, so
|
|
661
676
|
// none of the turn state below is touched.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
677
|
+
// `/memtree-view` waits briefly for the newest tree; a prompt only warms it.
|
|
678
|
+
// In a subagent's view the command is answered too (with the main
|
|
679
|
+
// session's page): unanswered, the plugin's fallback body would run.
|
|
680
|
+
if (isMemtreeViewCommand(parsed.prompt))
|
|
681
|
+
await state.newestTrees?.settle(parsed.session_id);
|
|
682
|
+
else if (parsed.agent_id === undefined)
|
|
683
|
+
state.newestTrees?.peek(parsed.session_id);
|
|
669
684
|
const commandReply = parsed.agent_id === undefined
|
|
670
685
|
? sessionCommandReply(state, parsed.session_id, parsed.prompt)
|
|
671
|
-
:
|
|
686
|
+
: subagentCommandReply(state, parsed.session_id, parsed.prompt);
|
|
672
687
|
if (commandReply !== undefined) {
|
|
673
688
|
const body = Buffer.from(JSON.stringify({
|
|
674
689
|
decision: "block",
|
|
@@ -936,7 +951,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
936
951
|
},
|
|
937
952
|
}, state.countTokensCooldowns);
|
|
938
953
|
const requestSizing = new RequestSizing(countSession, opts.countTokens === true);
|
|
939
|
-
|
|
954
|
+
if (carriesServerTools(body))
|
|
955
|
+
serverToolRecords.add(rec);
|
|
956
|
+
const sizeSlot = wholeRequestSizeSlot(state, isMainRequest, requestSessionKey, requestSizeKey, body.model);
|
|
940
957
|
const originalCandidate = requestSizing.register(rawBody, "original", sizeSlot.get());
|
|
941
958
|
const forwardCandidate = requestSizing.register(forwardBody, "original", sizeSlot.get());
|
|
942
959
|
candidateSizeOwners.set(originalCandidate, sizeSlot);
|
|
@@ -947,6 +964,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
947
964
|
output: bodyOutputReservation(body),
|
|
948
965
|
recountOutput: outputReservation(undefined, body.max_tokens), // full max_tokens
|
|
949
966
|
});
|
|
967
|
+
if (isMainRequest && requestSessionKey !== undefined) {
|
|
968
|
+
adoptStoredPrefix(opts, state, rec, requestSessionKey, body, messages, modelContextLimit);
|
|
969
|
+
}
|
|
950
970
|
const rides = state.memoryRoutes.has(requestRouteKey) ||
|
|
951
971
|
(isMainRequest && requestSessionKey !== undefined && state.stablePrefixes.has(requestSessionKey));
|
|
952
972
|
if (!rides) {
|
|
@@ -1254,6 +1274,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1254
1274
|
...(plan.fallback ? { fallback: plan.fallback } : {}),
|
|
1255
1275
|
retryAtTokens: plan.estimateTokens +
|
|
1256
1276
|
Math.floor(plan.budgetTokens * TOOL_COMPACTION_RETRY_BUDGET_RATIO),
|
|
1277
|
+
...(plan.fallback ? {} : { wholeRefused: () => wholeRequestRefused(requestSizing, originalCandidate, sizingLimits()) }),
|
|
1257
1278
|
}).finally(() => {
|
|
1258
1279
|
recoveryAttempt.inFlight = false;
|
|
1259
1280
|
}));
|
|
@@ -1280,7 +1301,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1280
1301
|
if (toolRide)
|
|
1281
1302
|
noteRideSize(toolRide.sizeHolder, rec, sendBody.length);
|
|
1282
1303
|
else {
|
|
1283
|
-
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, rec, sendBody.length);
|
|
1304
|
+
noteWholeRequestSize(state, isMainRequest, toolSessionId, requestSizeKey, body.model, rec, sendBody.length);
|
|
1284
1305
|
}
|
|
1285
1306
|
}));
|
|
1286
1307
|
}
|
|
@@ -1390,6 +1411,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1390
1411
|
return;
|
|
1391
1412
|
}
|
|
1392
1413
|
const installed = installMemoryRoute(state, requestRouteKey, req, body, messages, compressedBody, routeEpoch, routeDecisionGeneration, stable);
|
|
1414
|
+
// The answer's memory is now what future requests reuse; a ride keeps
|
|
1415
|
+
// the prior source, and a failed or stale forward adopts nothing.
|
|
1416
|
+
if (installed && result && linkSeq !== undefined) {
|
|
1417
|
+
noteMemorySource(state, linkSeq, sessionId, result);
|
|
1418
|
+
}
|
|
1393
1419
|
commitRouteDecision();
|
|
1394
1420
|
// Leave this false if installMemoryRoute unexpectedly throws: the
|
|
1395
1421
|
// delivery-complete fallback then gets one safe retry.
|
|
@@ -1514,7 +1540,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1514
1540
|
const sendingWhole = () => { requestSendPolicies.get(req).failedRebuild = true; };
|
|
1515
1541
|
const noteWholeSize = () => {
|
|
1516
1542
|
if (isMainRequest)
|
|
1517
|
-
notePassthroughSize(state, sessionId, rec, forwardBody.length);
|
|
1543
|
+
notePassthroughSize(state, sessionId, body.model, rec, forwardBody.length);
|
|
1518
1544
|
};
|
|
1519
1545
|
// The complete request upload can outlive its downstream subscriber while
|
|
1520
1546
|
// MemTree compression is in flight. Track that subscriber locally without
|
|
@@ -1549,6 +1575,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1549
1575
|
: edge?.kind === "off"
|
|
1550
1576
|
? { compaction: {} }
|
|
1551
1577
|
: {}),
|
|
1578
|
+
// Riding the old prefix, or sending a raw request that fits, keeps the
|
|
1579
|
+
// short budget; with neither, a timeout could only end in a refusal.
|
|
1580
|
+
longBudget: !(edge?.kind === "compress" && edge.fallback) &&
|
|
1581
|
+
await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
|
|
1552
1582
|
});
|
|
1553
1583
|
}
|
|
1554
1584
|
catch {
|
|
@@ -1561,7 +1591,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1561
1591
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec)
|
|
1562
1592
|
.then((delivered) => {
|
|
1563
1593
|
if (isMainRequest)
|
|
1564
|
-
notePassthroughSize(state, sessionId, rec, rawBody.length);
|
|
1594
|
+
notePassthroughSize(state, sessionId, body.model, rec, rawBody.length);
|
|
1565
1595
|
if (delivered)
|
|
1566
1596
|
markMainPromptDelivered();
|
|
1567
1597
|
}));
|
|
@@ -1596,21 +1626,17 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1596
1626
|
// it. Degrade to passthrough and queue a display-only hook notice for a
|
|
1597
1627
|
// visible turn. The hidden away-summary request deliberately stays quiet.
|
|
1598
1628
|
// Unpaid key (402, from this compress OR an earlier background index) gets
|
|
1599
|
-
// a payment-specific notice instead of the generic
|
|
1600
|
-
//
|
|
1629
|
+
// a payment-specific notice with the subscribe link instead of the generic
|
|
1630
|
+
// degraded one: on the first 402, then at most once per throttle interval
|
|
1631
|
+
// (counted from actual delivery). In between, unpaid turns stay quiet.
|
|
1601
1632
|
recordTurn(rec, "followup-degraded", forwardBody);
|
|
1602
1633
|
releaseForOriginalFallback();
|
|
1603
1634
|
sendingWhole();
|
|
1604
1635
|
capture(opts, "anthropic-request", forwardBody);
|
|
1605
1636
|
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1606
1637
|
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1607
|
-
if (mayQueueNotice && paymentDetail !== null &&
|
|
1608
|
-
|
|
1609
|
-
state.notices.queueSuffix(detailFirstLine
|
|
1610
|
-
? `${PAYMENT_REQUIRED_NOTICE}\n${detailFirstLine}`
|
|
1611
|
-
: PAYMENT_REQUIRED_NOTICE, () => {
|
|
1612
|
-
state.paymentNoticeShown = true;
|
|
1613
|
-
}, noticePromptId);
|
|
1638
|
+
if (mayQueueNotice && paymentDetail !== null && state.paymentNotice.due()) {
|
|
1639
|
+
state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () => state.paymentNotice.markShown(), noticePromptId);
|
|
1614
1640
|
}
|
|
1615
1641
|
else if (mayQueueNotice && paymentDetail === null) {
|
|
1616
1642
|
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
@@ -1831,6 +1857,25 @@ function resumeLinkLine(state, input) {
|
|
|
1831
1857
|
}
|
|
1832
1858
|
return state.notices.resumeLine(link, sessionId);
|
|
1833
1859
|
}
|
|
1860
|
+
/**
|
|
1861
|
+
* The unpaid-key notice for a resumed main session whose key the server has
|
|
1862
|
+
* already answered 402 for: a resume is a fresh look at the session, so it
|
|
1863
|
+
* says again why it runs uncompressed, regardless of the throttle. The link
|
|
1864
|
+
* line above it already names the session's MemTree page.
|
|
1865
|
+
*/
|
|
1866
|
+
function resumePaymentNotice(state, input) {
|
|
1867
|
+
if (input.agent_id !== undefined || input.source !== "resume")
|
|
1868
|
+
return undefined;
|
|
1869
|
+
const detail = state.unpaidDetail();
|
|
1870
|
+
if (detail === null)
|
|
1871
|
+
return undefined;
|
|
1872
|
+
state.paymentNotice.markShown();
|
|
1873
|
+
return state.notices.warningText(paymentNoticeText(detail));
|
|
1874
|
+
}
|
|
1875
|
+
function joinNoticeLines(...parts) {
|
|
1876
|
+
const present = parts.filter((part) => !!part);
|
|
1877
|
+
return present.length ? present.join("\n") : undefined;
|
|
1878
|
+
}
|
|
1834
1879
|
export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
|
|
1835
1880
|
/** The reply to a ccc slash command, or undefined for an ordinary prompt. */
|
|
1836
1881
|
function sessionCommandReply(state, sessionId, prompt) {
|
|
@@ -1851,6 +1896,9 @@ function sessionCommandReply(state, sessionId, prompt) {
|
|
|
1851
1896
|
state.memoryRoutes.delete(JSON.stringify([sessionId, "main"]));
|
|
1852
1897
|
state.compactNow.delete(sessionId);
|
|
1853
1898
|
state.stablePrefixes.delete(sessionId);
|
|
1899
|
+
if (state.storageOwner)
|
|
1900
|
+
state.stablePrefixStore?.delete(sessionId, state.storageOwner);
|
|
1901
|
+
state.storedPrefixLookups.set(sessionId, null);
|
|
1854
1902
|
return `${TRAILER_LABEL} compaction off: the conversation is sent whole, and MemTree compresses only when it outgrows the model's budget.`;
|
|
1855
1903
|
}
|
|
1856
1904
|
if (args === "") {
|
|
@@ -1872,6 +1920,22 @@ function sessionCommandReply(state, sessionId, prompt) {
|
|
|
1872
1920
|
state.compactNow.add(sessionId);
|
|
1873
1921
|
return `${TRAILER_LABEL} compacting: your next message is sent compressed to about ${Math.round(target / 1000)}k tokens, ${keep}`;
|
|
1874
1922
|
}
|
|
1923
|
+
export const SUBAGENT_VIEW_NOTE = "(main session's page; subagent pages aren't linked yet)";
|
|
1924
|
+
export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main session only.";
|
|
1925
|
+
/**
|
|
1926
|
+
* The reply to a ccc slash command typed in a subagent's view. `sessionId` is
|
|
1927
|
+
* the parent session's: the view shows its page, and compaction (per session)
|
|
1928
|
+
* is refused rather than applied to the main conversation from here.
|
|
1929
|
+
*/
|
|
1930
|
+
function subagentCommandReply(state, sessionId, prompt) {
|
|
1931
|
+
if (isMemtreeViewCommand(prompt)) {
|
|
1932
|
+
return `${memtreeViewLine(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
|
|
1933
|
+
}
|
|
1934
|
+
if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
|
|
1935
|
+
return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
|
|
1936
|
+
}
|
|
1937
|
+
return sessionCommandReply(state, sessionId, prompt);
|
|
1938
|
+
}
|
|
1875
1939
|
/** "50k", "50000", "1.5m" → tokens; undefined when not a positive count. */
|
|
1876
1940
|
export function parseTokenCount(text) {
|
|
1877
1941
|
const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(text.trim());
|
|
@@ -1892,13 +1956,16 @@ function memtreeViewLine(state, sessionId) {
|
|
|
1892
1956
|
return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
|
|
1893
1957
|
}
|
|
1894
1958
|
// Blocked-prompt reasons need an explicit hyperlink in styled terminals.
|
|
1895
|
-
const
|
|
1896
|
-
const lines = linkLines(LINK_LABEL, url, link.note);
|
|
1959
|
+
const lines = linkLines(LINK_LABEL, viewUrl(link.link), link.note);
|
|
1897
1960
|
// The memory the agent works from can be an older tree than the newest one.
|
|
1898
1961
|
return page?.compressed && page.url !== link.link
|
|
1899
|
-
? `${lines}\n
|
|
1962
|
+
? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
|
|
1900
1963
|
: lines;
|
|
1901
1964
|
}
|
|
1965
|
+
/** A `/memtree-view` URL: an explicit OSC 8 link where the terminal has them. */
|
|
1966
|
+
function viewUrl(url) {
|
|
1967
|
+
return terminalSupportsHyperlinks() ? hyperlink(url, url) : url;
|
|
1968
|
+
}
|
|
1902
1969
|
/**
|
|
1903
1970
|
* The link shown for a session: its newest completed tree when the server
|
|
1904
1971
|
* named one (newestTrees), else `source`, the page its memory came from. The
|
|
@@ -1947,12 +2014,30 @@ function noteMemtreePage(state, seq, sessionId, result) {
|
|
|
1947
2014
|
// A cached newest tree may predate this page; ask again.
|
|
1948
2015
|
state.newestTrees?.invalidate(sessionId);
|
|
1949
2016
|
}
|
|
1950
|
-
/**
|
|
2017
|
+
/**
|
|
2018
|
+
* Record a compress answer's page as the source of the memory now forwarded
|
|
2019
|
+
* (memorySourcePages), unless a later-submitted call's memory already is.
|
|
2020
|
+
*/
|
|
2021
|
+
function noteMemorySource(state, seq, sessionId, result) {
|
|
2022
|
+
const url = result.memtreeUrl;
|
|
2023
|
+
const index = result.memtreeIndex;
|
|
2024
|
+
if (!sessionId || !url || !index)
|
|
2025
|
+
return;
|
|
2026
|
+
const latest = state.memorySourcePages.get(sessionId);
|
|
2027
|
+
if (latest && latest.seq >= seq)
|
|
2028
|
+
return;
|
|
2029
|
+
const page = { sessionId, url, index, compressed: didMemtreeCompress(result), seq };
|
|
2030
|
+
rememberPage(state.memorySourcePages, sessionId, page);
|
|
2031
|
+
}
|
|
1951
2032
|
function rememberMemtreePage(state, sessionId, page) {
|
|
1952
|
-
state.memtreePages
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2033
|
+
rememberPage(state.memtreePages, sessionId, page);
|
|
2034
|
+
}
|
|
2035
|
+
/** Bound optional page state independently of how many sessions use the proxy. */
|
|
2036
|
+
function rememberPage(pages, sessionId, page) {
|
|
2037
|
+
pages.delete(sessionId);
|
|
2038
|
+
pages.set(sessionId, page);
|
|
2039
|
+
while (pages.size > MEMTREE_LINKS_MAX_SESSIONS) {
|
|
2040
|
+
pages.delete(pages.keys().next().value);
|
|
1956
2041
|
}
|
|
1957
2042
|
}
|
|
1958
2043
|
/**
|
|
@@ -2085,6 +2170,9 @@ stable) {
|
|
|
2085
2170
|
// request carried, and the compressed bytes sent for them. Later requests
|
|
2086
2171
|
// reuse these bytes unchanged, so the prefix stays a cache read.
|
|
2087
2172
|
route.stablePrefix = storeStablePrefix(state, sessionId, originalMessages, route, stable);
|
|
2173
|
+
if (route.stablePrefix && key === JSON.stringify([sessionId, "main"])) {
|
|
2174
|
+
persistStablePrefix(state, sessionId, route.stablePrefix);
|
|
2175
|
+
}
|
|
2088
2176
|
}
|
|
2089
2177
|
setMemoryRoute(state, key, route);
|
|
2090
2178
|
state.routeDecisionsLive.set(key, new Set([decisionGeneration]));
|
|
@@ -2120,6 +2208,83 @@ function storeStablePrefix(state, sessionId, originalMessages, compressed, stabl
|
|
|
2120
2208
|
state.compactNow.delete(sessionId);
|
|
2121
2209
|
return prefix;
|
|
2122
2210
|
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Persist the main lane's newly adopted prefix with its memory-source page.
|
|
2213
|
+
* Deferred one tick so the page noted right after the install is included,
|
|
2214
|
+
* and skipped when a newer prefix replaced this one meanwhile.
|
|
2215
|
+
*/
|
|
2216
|
+
function persistStablePrefix(state, sessionId, prefix) {
|
|
2217
|
+
const store = state.stablePrefixStore;
|
|
2218
|
+
const owner = state.storageOwner;
|
|
2219
|
+
if (!store || !owner)
|
|
2220
|
+
return;
|
|
2221
|
+
state.storedPrefixLookups.set(sessionId, null);
|
|
2222
|
+
setImmediate(() => {
|
|
2223
|
+
if (state.stablePrefixes.get(sessionId) !== prefix)
|
|
2224
|
+
return;
|
|
2225
|
+
const page = state.memorySourcePages.get(sessionId);
|
|
2226
|
+
store.save(sessionId, owner, {
|
|
2227
|
+
originalSystemHash: prefix.originalSystemHash,
|
|
2228
|
+
originalPrefixHashes: prefix.originalPrefixHashes,
|
|
2229
|
+
compressedMessages: prefix.compressedMessages,
|
|
2230
|
+
compressedSystem: prefix.compressedSystem,
|
|
2231
|
+
hasCompressedSystem: prefix.hasCompressedSystem,
|
|
2232
|
+
targetTokens: prefix.targetTokens,
|
|
2233
|
+
explicitTarget: prefix.explicitTarget,
|
|
2234
|
+
modelContextLimit: prefix.modelContextLimit,
|
|
2235
|
+
}, page && { url: page.url, index: page.index, compressed: page.compressed });
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* A fresh process's main-lane requests for a session: adopt the session's
|
|
2240
|
+
* persisted prefix when this history passes the checks the in-memory prefix
|
|
2241
|
+
* would (same session, system and covered messages; same window; a routable
|
|
2242
|
+
* body). Only an authoritative main history may discard it: one with the same
|
|
2243
|
+
* system and window, no server tools, and at least the covered length, whose
|
|
2244
|
+
* covered messages differ. Anything else (a side or helper request such as
|
|
2245
|
+
* the Haiku WebSearch call) leaves it for a later request. The disk is read
|
|
2246
|
+
* once per session; the store checks the backend/account owner.
|
|
2247
|
+
*/
|
|
2248
|
+
function adoptStoredPrefix(opts, state, rec, sessionId, body, messages, modelContextLimit) {
|
|
2249
|
+
const { stablePrefixStore: store, storageOwner: owner } = state;
|
|
2250
|
+
if (!store || !owner || state.stablePrefixes.has(sessionId))
|
|
2251
|
+
return;
|
|
2252
|
+
if (compactionMode(opts, state, sessionId).mode === "off")
|
|
2253
|
+
return;
|
|
2254
|
+
let entry = state.storedPrefixLookups.get(sessionId);
|
|
2255
|
+
if (entry === undefined) {
|
|
2256
|
+
entry = store.load(sessionId, owner) ?? null;
|
|
2257
|
+
boundedSet(state.storedPrefixLookups, sessionId, entry);
|
|
2258
|
+
}
|
|
2259
|
+
if (entry === null)
|
|
2260
|
+
return;
|
|
2261
|
+
const prefix = { sessionId, ...entry.prefix };
|
|
2262
|
+
const verdict = storedPrefixVerdict(body, messages, prefix, modelContextLimit);
|
|
2263
|
+
if (verdict === "defer")
|
|
2264
|
+
return;
|
|
2265
|
+
state.storedPrefixLookups.set(sessionId, null);
|
|
2266
|
+
if (verdict === "discard") {
|
|
2267
|
+
store.delete(sessionId, owner);
|
|
2268
|
+
rec.storedPrefix = "discarded";
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
boundedSet(state.stablePrefixes, sessionId, prefix);
|
|
2272
|
+
if (entry.source && !state.memorySourcePages.has(sessionId)) {
|
|
2273
|
+
rememberPage(state.memorySourcePages, sessionId, { sessionId, ...entry.source, seq: 0 });
|
|
2274
|
+
}
|
|
2275
|
+
rec.storedPrefix = "adopted";
|
|
2276
|
+
}
|
|
2277
|
+
/** What this request says about a persisted prefix (adoptStoredPrefix). */
|
|
2278
|
+
function storedPrefixVerdict(body, messages, prefix, modelContextLimit) {
|
|
2279
|
+
if (carriesServerTools(body) || prefix.modelContextLimit !== modelContextLimit)
|
|
2280
|
+
return "defer";
|
|
2281
|
+
const miss = prefixMismatch(body, messages, prefix, prefix.sessionId, stablePrefixMessageHash);
|
|
2282
|
+
if (miss === null)
|
|
2283
|
+
return prefixRoutedBody(body, messages, prefix) ? "adopt" : "defer";
|
|
2284
|
+
const authoritative = miss === "prefix" &&
|
|
2285
|
+
messages.length >= prefix.originalPrefixHashes.length;
|
|
2286
|
+
return authoritative ? "discard" : "defer";
|
|
2287
|
+
}
|
|
2123
2288
|
/**
|
|
2124
2289
|
* A fork of the main conversation (the away recap) sent on the main thread's
|
|
2125
2290
|
* last compressed prefix: that prefix, then the fork's own messages after it.
|
|
@@ -2301,11 +2466,31 @@ function notePrefixSize(prefix, rec, bytes) {
|
|
|
2301
2466
|
prefix.lastSize = sample;
|
|
2302
2467
|
}
|
|
2303
2468
|
/** Record the reported size of a main-thread request forwarded whole. */
|
|
2304
|
-
function notePassthroughSize(state, sessionId, rec, bytes) {
|
|
2469
|
+
function notePassthroughSize(state, sessionId, model, rec, bytes) {
|
|
2305
2470
|
const sample = reportedSample(rec, bytes);
|
|
2306
2471
|
if (sessionId === undefined || sample === undefined)
|
|
2307
2472
|
return;
|
|
2308
|
-
boundedSet(state.passthroughSizes, sessionId, sample);
|
|
2473
|
+
boundedSet(state.passthroughSizes, sessionSizeKey(sessionId, model), sample);
|
|
2474
|
+
}
|
|
2475
|
+
/** Key of a main-thread size sample (ProxyState.passthroughSizes). */
|
|
2476
|
+
function sessionSizeKey(sessionId, model) {
|
|
2477
|
+
return JSON.stringify([sessionId, typeof model === "string" ? model : null]);
|
|
2478
|
+
}
|
|
2479
|
+
/** Requests whose reported input includes server-injected tool results. */
|
|
2480
|
+
const serverToolRecords = new WeakSet();
|
|
2481
|
+
/**
|
|
2482
|
+
* Whether the request offers server tools (web search, web fetch, code
|
|
2483
|
+
* execution, tool search): their results are added to the input server-side,
|
|
2484
|
+
* so the reported size says nothing about the body's bytes.
|
|
2485
|
+
*/
|
|
2486
|
+
function carriesServerTools(body) {
|
|
2487
|
+
return Array.isArray(body.tools) && body.tools.some((tool) => typeof tool?.type === "string" && SERVER_TOOL_TYPE.test(tool.type));
|
|
2488
|
+
}
|
|
2489
|
+
const SERVER_TOOL_TYPE = /^(web_search|web_fetch|code_execution|tool_search_tool)_/;
|
|
2490
|
+
/** Whether the response reports any server tool use (server_tool_use counts). */
|
|
2491
|
+
function usedServerTools(rec) {
|
|
2492
|
+
const used = rec.usage?.server_tool_use;
|
|
2493
|
+
return !!used && Object.values(used).some((count) => count > 0);
|
|
2309
2494
|
}
|
|
2310
2495
|
/**
|
|
2311
2496
|
* The size Anthropic reported for a body of `bytes`, if it can describe one:
|
|
@@ -2315,6 +2500,8 @@ function notePassthroughSize(state, sessionId, rec, bytes) {
|
|
|
2315
2500
|
function reportedSample(rec, bytes) {
|
|
2316
2501
|
if (changedDeliveryRecords.has(rec))
|
|
2317
2502
|
return undefined;
|
|
2503
|
+
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
2504
|
+
return undefined;
|
|
2318
2505
|
const tokens = reportedInputTokens(rec);
|
|
2319
2506
|
if (tokens === undefined)
|
|
2320
2507
|
return undefined;
|
|
@@ -2334,15 +2521,16 @@ function laneSizeKey(req, routeKey, model) {
|
|
|
2334
2521
|
]);
|
|
2335
2522
|
}
|
|
2336
2523
|
/** The session's passthrough size on the main thread, else the lane's. */
|
|
2337
|
-
function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey) {
|
|
2524
|
+
function wholeRequestSizeSlot(state, isMainRequest, sessionId, sizeKey, model) {
|
|
2338
2525
|
const main = isMainRequest && sessionId !== undefined;
|
|
2526
|
+
const mainKey = main ? sessionSizeKey(sessionId, model) : "";
|
|
2339
2527
|
return {
|
|
2340
|
-
get: () => (main ? state.passthroughSizes.get(
|
|
2528
|
+
get: () => (main ? state.passthroughSizes.get(mainKey) : state.laneSizes.get(sizeKey)),
|
|
2341
2529
|
set: (sample) => {
|
|
2342
2530
|
if (!plausibleSample(sample))
|
|
2343
2531
|
return;
|
|
2344
2532
|
if (main)
|
|
2345
|
-
boundedSet(state.passthroughSizes,
|
|
2533
|
+
boundedSet(state.passthroughSizes, mainKey, sample);
|
|
2346
2534
|
else
|
|
2347
2535
|
boundedSet(state.laneSizes, sizeKey, sample, MEMORY_ROUTE_MAX_LANES);
|
|
2348
2536
|
},
|
|
@@ -2354,9 +2542,9 @@ const noteRideSize = notePrefixSize;
|
|
|
2354
2542
|
* Record the reported size of a request forwarded whole: the session's
|
|
2355
2543
|
* passthrough size on the main thread, else its lane's.
|
|
2356
2544
|
*/
|
|
2357
|
-
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, rec, bytes) {
|
|
2545
|
+
function noteWholeRequestSize(state, isMainRequest, sessionId, sizeKey, model, rec, bytes) {
|
|
2358
2546
|
if (isMainRequest && sessionId !== undefined) {
|
|
2359
|
-
notePassthroughSize(state, sessionId, rec, bytes);
|
|
2547
|
+
notePassthroughSize(state, sessionId, model, rec, bytes);
|
|
2360
2548
|
return;
|
|
2361
2549
|
}
|
|
2362
2550
|
const sample = reportedSample(rec, bytes);
|
|
@@ -2529,7 +2717,7 @@ function planToolCompaction(args) {
|
|
|
2529
2717
|
const sample = ride
|
|
2530
2718
|
? ride.sizeHolder.lastSize
|
|
2531
2719
|
: stableLane
|
|
2532
|
-
? state.passthroughSizes.get(sessionId)
|
|
2720
|
+
? state.passthroughSizes.get(sessionSizeKey(sessionId, body.model))
|
|
2533
2721
|
: state.laneSizes.get(args.sizeKey);
|
|
2534
2722
|
const candidate = args.sizing.register(ride?.raw ?? forwardBody, ride ? "ride" : "original", sample);
|
|
2535
2723
|
if (ride)
|
|
@@ -2921,6 +3109,8 @@ async function runBlockingCompression(args) {
|
|
|
2921
3109
|
: undefined;
|
|
2922
3110
|
const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
|
|
2923
3111
|
const compressStarted = Date.now();
|
|
3112
|
+
const budgetMs = args.longBudget
|
|
3113
|
+
? opts.memtree.longCompressBudgetMs : opts.memtree.compressBudgetMs;
|
|
2924
3114
|
const compressMeta = {
|
|
2925
3115
|
// Model + tools drive the server's model-based memory budget
|
|
2926
3116
|
// (e.g. 800k whole-request target for Fable / Opus 5). Omitting
|
|
@@ -2946,6 +3136,7 @@ async function runBlockingCompression(args) {
|
|
|
2946
3136
|
...(messageTimes && Object.keys(messageTimes).length ? { messageTimes } : {}),
|
|
2947
3137
|
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
2948
3138
|
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3139
|
+
...(args.longBudget ? { timeoutMs: budgetMs } : {}),
|
|
2949
3140
|
};
|
|
2950
3141
|
// Sampled BEFORE the call, while it still describes this call: after the
|
|
2951
3142
|
// await the hash is in the cache regardless of who put it there.
|
|
@@ -2953,7 +3144,7 @@ async function runBlockingCompression(args) {
|
|
|
2953
3144
|
// compress() maps every failure to a resolved null (it never rejects).
|
|
2954
3145
|
// Keep foreground work finite even if a transport ignores its abort signal.
|
|
2955
3146
|
let deadline;
|
|
2956
|
-
const maximumWait = Math.min(30_000, Math.max(1, Number.isFinite(
|
|
3147
|
+
const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : 30_000, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
|
|
2957
3148
|
const result = await Promise.race([
|
|
2958
3149
|
opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, state.shutdownSignal, compressMeta),
|
|
2959
3150
|
new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
|
|
@@ -2977,7 +3168,7 @@ async function runBlockingCompression(args) {
|
|
|
2977
3168
|
// Budget-consumed heuristic: the client maps every failure to null, so a
|
|
2978
3169
|
// null that took (roughly) the whole abort budget was almost certainly
|
|
2979
3170
|
// the AbortSignal timeout, not a fast server error.
|
|
2980
|
-
timedOut: result === null && compressMs >=
|
|
3171
|
+
timedOut: result === null && compressMs >= budgetMs,
|
|
2981
3172
|
};
|
|
2982
3173
|
return {
|
|
2983
3174
|
result,
|
|
@@ -3095,7 +3286,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3095
3286
|
capture(opts, "anthropic-request", args.originalBody);
|
|
3096
3287
|
return forwardRaw(req, res, args.originalBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
3097
3288
|
toolForwarding.settle(delivered);
|
|
3098
|
-
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, rec, args.originalBody.length);
|
|
3289
|
+
noteWholeRequestSize(state, isMainRequest, sessionId, args.sizeKey, body.model, rec, args.originalBody.length);
|
|
3099
3290
|
});
|
|
3100
3291
|
};
|
|
3101
3292
|
// Same downstream-close tracking as the followup path: compression
|
|
@@ -3124,6 +3315,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
3124
3315
|
sizing: args.sizing,
|
|
3125
3316
|
inputCandidate: args.inputCandidate,
|
|
3126
3317
|
...(args.compaction ? { compaction: args.compaction } : {}),
|
|
3318
|
+
longBudget: args.wholeRefused ? await args.wholeRefused() : false,
|
|
3127
3319
|
});
|
|
3128
3320
|
}
|
|
3129
3321
|
catch {
|
|
@@ -3275,6 +3467,10 @@ async function recoverToolRouteMiss(args) {
|
|
|
3275
3467
|
activationAttempted = true;
|
|
3276
3468
|
installedRoute = installed;
|
|
3277
3469
|
installFate = installed ? "installed" : "stale";
|
|
3470
|
+
// Only an installed route makes this answer's memory the reused one.
|
|
3471
|
+
if (installed && linkSeq !== undefined) {
|
|
3472
|
+
noteMemorySource(state, linkSeq, requestSessionId(req), result);
|
|
3473
|
+
}
|
|
3278
3474
|
// A tool continuation can arrive after message_stop while this HTTP
|
|
3279
3475
|
// stream is still draining. Its route is already complete, so a smaller
|
|
3280
3476
|
// window may buy recovery now. Mutate only this attempt's object: a late
|
|
@@ -3539,6 +3735,17 @@ async function selectForwardCandidate(args) {
|
|
|
3539
3735
|
}
|
|
3540
3736
|
return changed ? candidate.body : intended;
|
|
3541
3737
|
}
|
|
3738
|
+
/**
|
|
3739
|
+
* Whether delivery would refuse `candidate` sent whole: it is planned over the
|
|
3740
|
+
* window, and the measurement selection would use (an exact count, else the
|
|
3741
|
+
* same estimate or bytes/4 fallback) does not fit either. Only then is there
|
|
3742
|
+
* no fallback, and only then may a compress take the long budget.
|
|
3743
|
+
*/
|
|
3744
|
+
async function wholeRequestRefused(sizing, candidate, limits) {
|
|
3745
|
+
if (fitsNative(sizing.plan(candidate).tokens, limits))
|
|
3746
|
+
return false;
|
|
3747
|
+
return !fitsMeasured(await sizing.measure(candidate, limits), limits);
|
|
3748
|
+
}
|
|
3542
3749
|
/** Write the bounded fallback refusal (route-fallback.ts); always false. */
|
|
3543
3750
|
function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
3544
3751
|
const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
@@ -3577,6 +3784,9 @@ function noteSelectedCandidateSize(rec) {
|
|
|
3577
3784
|
const tokens = reportedInputTokens(rec);
|
|
3578
3785
|
if (!candidate || !owner || tokens === undefined)
|
|
3579
3786
|
return;
|
|
3787
|
+
// Server tools' injected results inflate the reported input (reportedSample).
|
|
3788
|
+
if (serverToolRecords.has(rec) || usedServerTools(rec))
|
|
3789
|
+
return;
|
|
3580
3790
|
const sample = { tokens, forwardedBytes: candidate.body.length };
|
|
3581
3791
|
if (!plausibleSample(sample))
|
|
3582
3792
|
return;
|