claude-code-infinite 1.0.32 → 1.0.33
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 +25 -9
- package/dist/cli-args.d.ts +7 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +19 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-main.js +10 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/count-tokens.d.ts +7 -3
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +6 -2
- package/dist/count-tokens.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +1 -1
- package/dist/memtree.d.ts +86 -3
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +305 -18
- package/dist/memtree.js.map +1 -1
- package/dist/open-url.d.ts +49 -0
- package/dist/open-url.d.ts.map +1 -0
- package/dist/open-url.js +100 -0
- package/dist/open-url.js.map +1 -0
- package/dist/proxy.d.ts +56 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +375 -39
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +10 -0
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -52,10 +52,10 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
52
52
|
import { brotliDecompressSync, createBrotliDecompress, createGunzip, createInflate, gunzipSync, inflateSync, } from "node:zlib";
|
|
53
53
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
54
54
|
import { join } from "node:path";
|
|
55
|
-
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
55
|
+
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, losesMedia, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
56
56
|
import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
|
|
57
57
|
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, SLOW_RETRY_NOTICE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
|
|
58
|
-
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines,
|
|
58
|
+
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, PRECOMPACT_HOOK_PATH, } from "./hooks.js";
|
|
59
59
|
import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
|
|
60
60
|
import { hyperlink } from "./payment-gate.js";
|
|
61
61
|
import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
|
|
@@ -86,8 +86,11 @@ const HOOK_BODY_TIMEOUT_MS = 3_000;
|
|
|
86
86
|
const TOOL_RECOVERY_FAILURE_COOLDOWN_MS = 60_000;
|
|
87
87
|
/** Safety cap on a long-budget compress, should a transport ignore its abort. */
|
|
88
88
|
const MAX_LONG_COMPRESS_WAIT_MS = 10 * 60_000;
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The most a blocking compress with a fallback may wait (its budget grows with its upload):
|
|
91
|
+
* a 30 MB upload's scaled budget (15 s + 2.5 ms/KiB, ~90 s) fits under it.
|
|
92
|
+
*/
|
|
93
|
+
const MAX_COMPRESS_WAIT_MS = 120_000;
|
|
91
94
|
const SKIP_REQUEST_HEADERS = new Set([
|
|
92
95
|
"host",
|
|
93
96
|
"connection",
|
|
@@ -347,6 +350,7 @@ export function startProxy(opts) {
|
|
|
347
350
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
348
351
|
sessionStartNotice: opts.sessionStartNotice,
|
|
349
352
|
promptSubmitNotice: opts.promptSubmitNotice,
|
|
353
|
+
openUrl: opts.openUrl,
|
|
350
354
|
projectTrees: opts.projectTrees,
|
|
351
355
|
subagentHint: opts.subagentHint,
|
|
352
356
|
resumeGuidance: opts.resumeGuidance,
|
|
@@ -358,6 +362,8 @@ export function startProxy(opts) {
|
|
|
358
362
|
fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
|
|
359
363
|
refusalStreaks: new RefusalStreaks(),
|
|
360
364
|
activeSubagents: new Set(),
|
|
365
|
+
deferredAgentIndexes: new Map(),
|
|
366
|
+
adoptingPrepared: new Map(),
|
|
361
367
|
memoryRoutes: new Map(),
|
|
362
368
|
mainRouteEpoch: 0,
|
|
363
369
|
mainRouteDecisionGeneration: 0,
|
|
@@ -453,8 +459,10 @@ export function startProxy(opts) {
|
|
|
453
459
|
]);
|
|
454
460
|
if (timer)
|
|
455
461
|
clearTimeout(timer);
|
|
456
|
-
if (completed)
|
|
462
|
+
if (completed) {
|
|
463
|
+
sendDeferredAgentIndexes(state);
|
|
457
464
|
return true;
|
|
465
|
+
}
|
|
458
466
|
// The grace period protects useful in-flight delivery. Once it
|
|
459
467
|
// expires, signal every forwarding path first so each can
|
|
460
468
|
// classify the close as proxy-owned, then tear down any request that was
|
|
@@ -463,6 +471,7 @@ export function startProxy(opts) {
|
|
|
463
471
|
for (const accepted of acceptedRequests)
|
|
464
472
|
cancelAcceptedRequest(accepted);
|
|
465
473
|
await quiesced;
|
|
474
|
+
sendDeferredAgentIndexes(state);
|
|
466
475
|
return false;
|
|
467
476
|
};
|
|
468
477
|
return new Promise((resolve, reject) => {
|
|
@@ -942,6 +951,7 @@ onlyEvent) {
|
|
|
942
951
|
}
|
|
943
952
|
if (parsed.hook_event_name === "SubagentStop") {
|
|
944
953
|
state.activeSubagents.delete(parsed.agent_id);
|
|
954
|
+
sendDeferredAgentIndex(state, parsed.agent_id);
|
|
945
955
|
res.writeHead(204);
|
|
946
956
|
res.end();
|
|
947
957
|
return;
|
|
@@ -1409,7 +1419,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1409
1419
|
// prefix, else whole) with no compress call; at it, compress once to
|
|
1410
1420
|
// the target (planToolCompaction). A "replay" is a client retry of the
|
|
1411
1421
|
// request that installed the route, forwarded verbatim.
|
|
1412
|
-
const
|
|
1422
|
+
const planArgs = {
|
|
1413
1423
|
opts,
|
|
1414
1424
|
state,
|
|
1415
1425
|
body,
|
|
@@ -1424,7 +1434,24 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1424
1434
|
rec,
|
|
1425
1435
|
ride: toolRide,
|
|
1426
1436
|
canCompress,
|
|
1427
|
-
}
|
|
1437
|
+
};
|
|
1438
|
+
let plan = await planToolCompaction(planArgs);
|
|
1439
|
+
// Over the window with nothing to ride while the lane compresses: going without it
|
|
1440
|
+
// can only be refused (staging 2026-10-30: a 120 s compress, every tool turn meanwhile
|
|
1441
|
+
// refused). Wait for it, then plan again on what it installed.
|
|
1442
|
+
if (plan.kind === "compress" && plan.overWindow && !plan.fallback &&
|
|
1443
|
+
state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight) {
|
|
1444
|
+
// Stops early if Claude Code drops this request meanwhile.
|
|
1445
|
+
const clientGone = new AbortController();
|
|
1446
|
+
const onClose = () => clientGone.abort();
|
|
1447
|
+
res.once("close", onClose);
|
|
1448
|
+
rec.laneWaitMs = await waitForLaneIdle(state, requestRouteKey, Math.min(MAX_LONG_COMPRESS_WAIT_MS, opts.memtree.longCompressBudgetMs), clientGone.signal);
|
|
1449
|
+
res.off("close", onClose);
|
|
1450
|
+
// Plan on what the lane holds now: the route its compression installed (or, on the
|
|
1451
|
+
// main lane, the session's new stable prefix), never the ride read before the wait.
|
|
1452
|
+
plan = await planToolCompaction({ ...planArgs,
|
|
1453
|
+
ride: currentLaneRide(state, requestRouteKey, body, messages, toolSessionId, isMainRequest) });
|
|
1454
|
+
}
|
|
1428
1455
|
if (isMainRequest && toolSessionId !== undefined && canCompress &&
|
|
1429
1456
|
(plan.kind === "ride" || plan.kind === "pass")) {
|
|
1430
1457
|
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash,
|
|
@@ -1482,6 +1509,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1482
1509
|
// the lane's backoff.
|
|
1483
1510
|
rec.routeRecovery = { outcome: "cooldown" };
|
|
1484
1511
|
}
|
|
1512
|
+
else if (state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight) {
|
|
1513
|
+
// Claimed while the cooldown check above awaited a count (a crossing prompt).
|
|
1514
|
+
rec.routeRecovery = { outcome: "in-flight" };
|
|
1515
|
+
}
|
|
1485
1516
|
else {
|
|
1486
1517
|
const recoveryAttempt = { modelContextLimit, inFlight: true };
|
|
1487
1518
|
if (!prior && state.toolRecoveryAttemptedLanes.size >= 128) {
|
|
@@ -1548,7 +1579,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1548
1579
|
requestSendPolicies.get(req)?.allowed.add(toolRide.raw);
|
|
1549
1580
|
const sendBody = toolRide?.raw ?? (toolNeedsOriginal ? rawBody : forwardBody);
|
|
1550
1581
|
recordTurn(rec, toolRide ? toolRide.turnType : isUserTurn ? "first-user" : "tool", sendBody);
|
|
1551
|
-
opts
|
|
1582
|
+
indexTurn({ opts, state, req, rec, hash, msgsForMemtree, modelContextLimit,
|
|
1583
|
+
sessionId: toolSessionId, clientMeta });
|
|
1552
1584
|
capture(opts, toolRide ? "anthropic-request-memory-tool" : "anthropic-request", sendBody);
|
|
1553
1585
|
// The size Anthropic reports is the next budget check's anchor: a ride
|
|
1554
1586
|
// reports to its prefix (or route), a whole request to the session's (or
|
|
@@ -1619,6 +1651,32 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1619
1651
|
rec.compaction.prepared ??= prepared;
|
|
1620
1652
|
}
|
|
1621
1653
|
}
|
|
1654
|
+
// Never beside the main lane's compression in flight either (a tool turn's that Claude
|
|
1655
|
+
// Code dropped, or another prompt's, both after the preparation they waited for failed):
|
|
1656
|
+
// wait for it, then plan again on what it left: a prefix to ride, or a preparation.
|
|
1657
|
+
// Bounded; stops early if Claude Code drops this request (release 1.0.33 E2E).
|
|
1658
|
+
const crossing = () => edge?.kind === "compress" && isMainRequest &&
|
|
1659
|
+
isAutoCrossing(rec, body.model, modelContextLimit);
|
|
1660
|
+
for (let i = 0; i < 2 && crossing() &&
|
|
1661
|
+
state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight; i++) {
|
|
1662
|
+
const clientGone = new AbortController();
|
|
1663
|
+
const onClose = () => clientGone.abort();
|
|
1664
|
+
res.once("close", onClose);
|
|
1665
|
+
if (res.destroyed)
|
|
1666
|
+
clientGone.abort();
|
|
1667
|
+
rec.laneWaitMs = (rec.laneWaitMs ?? 0) + await waitForLaneIdle(state, requestRouteKey, Math.min(MAX_LONG_COMPRESS_WAIT_MS, opts.memtree.longCompressBudgetMs), clientGone.signal);
|
|
1668
|
+
res.off("close", onClose);
|
|
1669
|
+
await awaitPreparation(state, sessionId, opts.memtree.longCompressBudgetMs, rec.compaction);
|
|
1670
|
+
edge = await planEdge();
|
|
1671
|
+
}
|
|
1672
|
+
// Claude Code dropped it while it waited: the lane's compression in flight already carries
|
|
1673
|
+
// this session's crossing (the next turn rides it), so none beside it for nobody (r3 P1).
|
|
1674
|
+
if (crossing() && state.toolRecoveryAttemptedLanes.get(requestRouteKey)?.inFlight &&
|
|
1675
|
+
res.destroyed && !res.writableFinished) {
|
|
1676
|
+
releaseUncommittedRouteDecision();
|
|
1677
|
+
recordTurn(rec, "followup-client-closed", Buffer.alloc(0));
|
|
1678
|
+
return logged(Promise.resolve());
|
|
1679
|
+
}
|
|
1622
1680
|
// Not beside a blocking compress call: the two would overlap on the server.
|
|
1623
1681
|
if (sessionId !== undefined && edge && (edge.kind === "ride" || edge.kind === "pass")) {
|
|
1624
1682
|
maybePrepareCompression({ opts, state, body, messages, msgsForMemtree, hash, sessionId,
|
|
@@ -1847,9 +1905,17 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1847
1905
|
// The hidden away-summary request compresses too, but its page is not the
|
|
1848
1906
|
// conversation the user is looking at.
|
|
1849
1907
|
const linkSeq = isMainRequest && !isAwaySummary ? nextMemtreeCallSeq(state) : undefined;
|
|
1908
|
+
const edgeCarry = edge?.kind === "compress" && !isAwaySummary
|
|
1909
|
+
? carryFor({ state, isMainRequest, sessionId, body, messages, msgsForMemtree,
|
|
1910
|
+
modelContextLimit, rec,
|
|
1911
|
+
targetTokens: edge.targetTokens, explicitTarget: edge.explicitTarget, linkSeq })
|
|
1912
|
+
: undefined;
|
|
1850
1913
|
let compression;
|
|
1914
|
+
// A main-thread compaction holds its lane, claimed with no await since the check above:
|
|
1915
|
+
// a concurrent turn of the lane waits for it or rides its fallback, never compresses beside it.
|
|
1916
|
+
const edgeLane = (run) => crossing() ? holdingLane(state, requestRouteKey, run) : run();
|
|
1851
1917
|
try {
|
|
1852
|
-
compression = await runBlockingCompression({
|
|
1918
|
+
compression = await edgeLane(async () => runBlockingCompression({
|
|
1853
1919
|
opts,
|
|
1854
1920
|
state,
|
|
1855
1921
|
body,
|
|
@@ -1871,7 +1937,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1871
1937
|
// short budget; with neither, a timeout could only end in a refusal.
|
|
1872
1938
|
longBudget: !(edge?.kind === "compress" && edge.fallback && !edge.fallback.uncounted) &&
|
|
1873
1939
|
await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
|
|
1874
|
-
|
|
1940
|
+
...(edgeCarry ? { carry: edgeCarry } : {}),
|
|
1941
|
+
}));
|
|
1875
1942
|
}
|
|
1876
1943
|
catch {
|
|
1877
1944
|
const kept = rideOldPrefix(false);
|
|
@@ -1911,6 +1978,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1911
1978
|
if (downstreamClosedDuringCompression ||
|
|
1912
1979
|
(res.destroyed && !res.writableFinished)) {
|
|
1913
1980
|
releaseUncommittedRouteDecision();
|
|
1981
|
+
// Claude Code gave up on the turn, not on the compaction: the next turn rides it.
|
|
1982
|
+
if (result && edgeCarry)
|
|
1983
|
+
carryOverCompression(state, edgeCarry, Promise.resolve(result));
|
|
1914
1984
|
recordTurn(rec, "followup-client-closed", Buffer.alloc(0));
|
|
1915
1985
|
return logged(Promise.resolve());
|
|
1916
1986
|
}
|
|
@@ -2353,15 +2423,14 @@ function sessionCommandReply(state, sessionId, prompt) {
|
|
|
2353
2423
|
: pausedCompactionText(viewUrl(subscribeUrlFromDetail(unpaid)));
|
|
2354
2424
|
if (sessionCommandArgs(prompt, MEMTREE_HELP_COMMAND) !== undefined) {
|
|
2355
2425
|
return [
|
|
2356
|
-
"• /memtree-view ·
|
|
2426
|
+
"• /memtree-view · open this session's MemTree page and show its link",
|
|
2357
2427
|
paused !== undefined ? `• ${paused}`
|
|
2358
2428
|
: `• /memtree-compact [tokens | off] · compact this session on your next message (default half the budget, at least ${MEMTREE_COMPACT_MIN_TOKENS / 1000}k)`,
|
|
2359
2429
|
usageLines(state),
|
|
2360
2430
|
].filter((line) => line !== undefined).join("\n");
|
|
2361
2431
|
}
|
|
2362
|
-
if (isMemtreeViewCommand(prompt))
|
|
2363
|
-
return
|
|
2364
|
-
}
|
|
2432
|
+
if (isMemtreeViewCommand(prompt))
|
|
2433
|
+
return memtreeView(state, sessionId);
|
|
2365
2434
|
const args = sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND);
|
|
2366
2435
|
if (args === undefined)
|
|
2367
2436
|
return undefined;
|
|
@@ -2405,10 +2474,8 @@ export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main sessi
|
|
|
2405
2474
|
* is refused rather than applied to the main conversation from here.
|
|
2406
2475
|
*/
|
|
2407
2476
|
function subagentCommandReply(state, sessionId, prompt) {
|
|
2408
|
-
if (isMemtreeViewCommand(prompt))
|
|
2409
|
-
|
|
2410
|
-
return joinNoticeLines(view, usageLines(state)) ?? view;
|
|
2411
|
-
}
|
|
2477
|
+
if (isMemtreeViewCommand(prompt))
|
|
2478
|
+
return `${memtreeView(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
|
|
2412
2479
|
if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
|
|
2413
2480
|
return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
|
|
2414
2481
|
}
|
|
@@ -2423,8 +2490,13 @@ export function parseTokenCount(text) {
|
|
|
2423
2490
|
const value = Math.round(Number(match[1]) * scale);
|
|
2424
2491
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
2425
2492
|
}
|
|
2426
|
-
/**
|
|
2427
|
-
|
|
2493
|
+
/**
|
|
2494
|
+
* The `/memtree-view` answer: one link, this session's newest page (the
|
|
2495
|
+
* newest completed tree, which covers the whole session, rather than the
|
|
2496
|
+
* possibly stale page its memory was compressed from), opened in the browser
|
|
2497
|
+
* where the CLI allows it; or why there is none.
|
|
2498
|
+
*/
|
|
2499
|
+
function memtreeView(state, sessionId) {
|
|
2428
2500
|
const inMemory = state.memtreePages.get(sessionId);
|
|
2429
2501
|
const page = inMemory && (inMemory.sessionId === undefined || inMemory.sessionId === sessionId)
|
|
2430
2502
|
? inMemory
|
|
@@ -2433,15 +2505,17 @@ function memtreeViewLine(state, sessionId) {
|
|
|
2433
2505
|
if (!link) {
|
|
2434
2506
|
return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
|
|
2435
2507
|
}
|
|
2508
|
+
try {
|
|
2509
|
+
state.openUrl?.(link.link);
|
|
2510
|
+
}
|
|
2511
|
+
catch {
|
|
2512
|
+
// The printed link below is the fallback.
|
|
2513
|
+
}
|
|
2436
2514
|
// Blocked-prompt reasons need an explicit hyperlink in styled terminals.
|
|
2437
|
-
|
|
2438
|
-
// The memory the agent works from can be an older tree than the newest one.
|
|
2439
|
-
return page?.compressed && page.url !== link.link
|
|
2440
|
-
? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
|
|
2441
|
-
: lines;
|
|
2515
|
+
return linkLines(LINK_LABEL, viewUrl(link.link), link.note);
|
|
2442
2516
|
}
|
|
2443
2517
|
/**
|
|
2444
|
-
* The usage dashboard link that `/memtree`
|
|
2518
|
+
* The usage dashboard link that `/memtree` ends with,
|
|
2445
2519
|
* formatted like the MemTree link; undefined when the environment has none.
|
|
2446
2520
|
*/
|
|
2447
2521
|
function usageLines(state) {
|
|
@@ -3292,6 +3366,16 @@ async function planToolCompaction(args) {
|
|
|
3292
3366
|
return { kind: "ride",
|
|
3293
3367
|
ride: { raw: adopted.routed.raw, turnType: "tool-prefix", sizeHolder: adopted.prefix } };
|
|
3294
3368
|
}
|
|
3369
|
+
// Another turn that waited for the same preparation installed it meanwhile: plan again on
|
|
3370
|
+
// it (once) rather than compress beside it (Codex r1 P1, release 1.0.33).
|
|
3371
|
+
if (stableLane && !args.replanned && state.stablePrefixes.get(sessionId) !== current) {
|
|
3372
|
+
const waited = compaction.preparedWaitMs;
|
|
3373
|
+
const plan = await planToolCompaction({ ...args, replanned: true,
|
|
3374
|
+
ride: currentLaneRide(state, args.routeKey, body, messages, sessionId, true) });
|
|
3375
|
+
if (waited !== undefined && rec.compaction)
|
|
3376
|
+
rec.compaction.preparedWaitMs = waited;
|
|
3377
|
+
return plan;
|
|
3378
|
+
}
|
|
3295
3379
|
// Settings current after any wait for a preparation (a /memtree-compact N meanwhile).
|
|
3296
3380
|
const modeNow = compactionMode(opts, state, sessionId);
|
|
3297
3381
|
if (modeNow.mode === "off")
|
|
@@ -3351,10 +3435,49 @@ async function adoptPreparedPrefix(args) {
|
|
|
3351
3435
|
const { state, sessionId } = args;
|
|
3352
3436
|
const base = state.stablePrefixes.get(sessionId);
|
|
3353
3437
|
await awaitPreparation(state, sessionId, args.waitMs, args.compaction);
|
|
3438
|
+
// Checked and taken with no await between: two turns resuming together see one owner.
|
|
3439
|
+
while (state.adoptingPrepared.has(sessionId))
|
|
3440
|
+
await awaitAdoption(state, sessionId, args.compaction);
|
|
3354
3441
|
const entry = state.prepared.get(sessionId);
|
|
3355
3442
|
if (!entry)
|
|
3356
3443
|
return undefined;
|
|
3357
3444
|
state.prepared.discard(sessionId);
|
|
3445
|
+
const release = holdAdoption(state, sessionId);
|
|
3446
|
+
try {
|
|
3447
|
+
return await adoptEntry(args, entry, base);
|
|
3448
|
+
}
|
|
3449
|
+
finally {
|
|
3450
|
+
release();
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Another turn of the session is adopting its prepared prefix (two turns waited for the same
|
|
3455
|
+
* preparation): wait for it to finish, so this turn plans on the prefix it installs instead of
|
|
3456
|
+
* compressing again (Codex r1 P1, release 1.0.33). Counted as preparation wait, so a human
|
|
3457
|
+
* turn plans again after it.
|
|
3458
|
+
*/
|
|
3459
|
+
async function awaitAdoption(state, sessionId, compaction) {
|
|
3460
|
+
const adopting = state.adoptingPrepared.get(sessionId);
|
|
3461
|
+
if (!adopting)
|
|
3462
|
+
return;
|
|
3463
|
+
const started = Date.now();
|
|
3464
|
+
await adopting;
|
|
3465
|
+
compaction.preparedWaitMs = (compaction.preparedWaitMs ?? 0) + (Date.now() - started);
|
|
3466
|
+
}
|
|
3467
|
+
/** Mark the session's prepared prefix as being adopted; the returned function ends it. */
|
|
3468
|
+
function holdAdoption(state, sessionId) {
|
|
3469
|
+
let done;
|
|
3470
|
+
const adopting = new Promise((resolve) => { done = resolve; });
|
|
3471
|
+
state.adoptingPrepared.set(sessionId, adopting);
|
|
3472
|
+
return () => {
|
|
3473
|
+
if (state.adoptingPrepared.get(sessionId) === adopting)
|
|
3474
|
+
state.adoptingPrepared.delete(sessionId);
|
|
3475
|
+
done();
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
/** adoptPreparedPrefix's check and install of the entry it took. */
|
|
3479
|
+
async function adoptEntry(args, entry, base) {
|
|
3480
|
+
const { state, sessionId } = args;
|
|
3358
3481
|
const fit = await preparedFit(args, entry);
|
|
3359
3482
|
// Another request installed a prefix, or `/memtree-compact` asked for its
|
|
3360
3483
|
// own target, while this one waited or counted: that wins.
|
|
@@ -3483,17 +3606,98 @@ function maybePrepareCompression(args) {
|
|
|
3483
3606
|
});
|
|
3484
3607
|
boundedSet(state.preparing, sessionId, pending);
|
|
3485
3608
|
}
|
|
3609
|
+
/**
|
|
3610
|
+
* A subagent's share of its budget from which it is indexed on every tool turn, as the
|
|
3611
|
+
* main thread is; below it, only once, when it ends. 2026-10-30: 29 subagents ran ~32
|
|
3612
|
+
* index jobs each ($45.63 a week for one session), and none came near its budget, so no
|
|
3613
|
+
* subagent tree was ever used to compress. Its tree is still built (at SubagentStop, or
|
|
3614
|
+
* at exit), so MemTree search and the page still find it.
|
|
3615
|
+
*/
|
|
3616
|
+
const AGENT_PER_TURN_INDEX_SHARE = 0.5;
|
|
3617
|
+
/** Subagents whose unsent index is kept; the oldest past this is sent at once. */
|
|
3618
|
+
const DEFERRED_AGENT_INDEX_MAX = 64;
|
|
3619
|
+
/**
|
|
3620
|
+
* A tool turn's background index call: now on the main thread and for a subagent past
|
|
3621
|
+
* AGENT_PER_TURN_INDEX_SHARE of its budget; otherwise kept, replacing the subagent's
|
|
3622
|
+
* previous one, until it ends (sendDeferredAgentIndex).
|
|
3623
|
+
*/
|
|
3624
|
+
function indexTurn(args) {
|
|
3625
|
+
const { opts, state, req, sessionId } = args;
|
|
3626
|
+
const send = (mode) => opts.memtree.indexInBackground(args.hash, args.msgsForMemtree, args.modelContextLimit, sessionId, args.clientMeta, transcriptTimesFor(opts, sessionId, agentAttributionId(req)), mode === "final" ? { finalIndex: true } : mode === "flush" ? { force: true } : {});
|
|
3627
|
+
// Only a request with its own agent id (the one SubagentStop names) waits for its end; one
|
|
3628
|
+
// attributed by its parent's id only is indexed per turn as before.
|
|
3629
|
+
const agentId = firstNonEmptyHeader(req, "x-claude-code-agent-id");
|
|
3630
|
+
if (agentId === undefined || sessionId === undefined)
|
|
3631
|
+
return send();
|
|
3632
|
+
const compaction = args.rec.compaction;
|
|
3633
|
+
if (compaction?.estimatedTokens !== undefined &&
|
|
3634
|
+
compaction.estimatedTokens >= compaction.budgetTokens * AGENT_PER_TURN_INDEX_SHARE) {
|
|
3635
|
+
send();
|
|
3636
|
+
// Kept too: its last turns under the text minimum still go at its end (forced).
|
|
3637
|
+
keepDeferredIndex(state, deferredIndexKey(agentId, args.msgsForMemtree), { messages: args.msgsForMemtree, send });
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
// Per agent and conversation: a side request made under the subagent's (or its parent's)
|
|
3641
|
+
// id is another conversation and must not replace the subagent's own.
|
|
3642
|
+
keepDeferredIndex(state, deferredIndexKey(agentId, args.msgsForMemtree), { messages: args.msgsForMemtree, send });
|
|
3643
|
+
}
|
|
3644
|
+
/** Keep `entry` as its key's newest unsent index call (bounded). */
|
|
3645
|
+
function keepDeferredIndex(state, key, entry) {
|
|
3646
|
+
const kept = state.deferredAgentIndexes.get(key);
|
|
3647
|
+
// A rewind or another branch: the kept history is not part of this one, so it goes now.
|
|
3648
|
+
// So does one holding media Claude Code has evicted since (never uploaded otherwise).
|
|
3649
|
+
if (kept && (!extendsHistory(entry.messages, kept.messages) ||
|
|
3650
|
+
losesMedia(kept.messages, entry.messages)))
|
|
3651
|
+
sendDeferredIndex(state, key, "flush");
|
|
3652
|
+
state.deferredAgentIndexes.delete(key);
|
|
3653
|
+
state.deferredAgentIndexes.set(key, entry);
|
|
3654
|
+
while (state.deferredAgentIndexes.size > DEFERRED_AGENT_INDEX_MAX) {
|
|
3655
|
+
const oldest = state.deferredAgentIndexes.keys().next().value;
|
|
3656
|
+
if (oldest === undefined)
|
|
3657
|
+
break;
|
|
3658
|
+
sendDeferredIndex(state, oldest, "flush");
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
/** A deferred index's key: the agent id and its conversation's first message. */
|
|
3662
|
+
function deferredIndexKey(agentId, messages) {
|
|
3663
|
+
const first = messages.find((message) => message.role !== "system");
|
|
3664
|
+
return JSON.stringify([agentId, first ? mediaFreeMessageHash(first) : ""]);
|
|
3665
|
+
}
|
|
3666
|
+
/** A subagent ended: send each of its conversations' kept index call, whatever its size. */
|
|
3667
|
+
function sendDeferredAgentIndex(state, agentId) {
|
|
3668
|
+
for (const key of [...state.deferredAgentIndexes.keys()]) {
|
|
3669
|
+
if (JSON.parse(key)[0] === agentId)
|
|
3670
|
+
sendDeferredIndex(state, key, "final");
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
function sendDeferredIndex(state, key, mode) {
|
|
3674
|
+
const kept = state.deferredAgentIndexes.get(key);
|
|
3675
|
+
state.deferredAgentIndexes.delete(key);
|
|
3676
|
+
kept?.send(mode);
|
|
3677
|
+
}
|
|
3678
|
+
/** At exit: every subagent's kept index calls. */
|
|
3679
|
+
function sendDeferredAgentIndexes(state) {
|
|
3680
|
+
for (const key of [...state.deferredAgentIndexes.keys()])
|
|
3681
|
+
sendDeferredIndex(state, key, "final");
|
|
3682
|
+
}
|
|
3486
3683
|
/** The background prepare call and the prefix it yields (maybePrepareCompression). */
|
|
3487
3684
|
async function prepareInBackground(args) {
|
|
3488
|
-
const { opts, state, body, msgsForMemtree, modelContextLimit
|
|
3685
|
+
const { opts, state, body, msgsForMemtree, modelContextLimit } = args;
|
|
3489
3686
|
const result = await opts.memtree.compress(args.hash, msgsForMemtree, modelContextLimit, state.backgroundSignal, preparedCompressMeta(args));
|
|
3490
3687
|
noteServerBudget(state, body.model, modelContextLimit, result);
|
|
3491
3688
|
if (!result)
|
|
3492
3689
|
return undefined;
|
|
3493
3690
|
state.prepared.noteServerAnswer(result.prepared === true);
|
|
3494
|
-
if (result.prepared !== true
|
|
3495
|
-
|
|
3691
|
+
if (result.prepared !== true)
|
|
3692
|
+
return undefined;
|
|
3693
|
+
return preparedFromResult(args, result);
|
|
3694
|
+
}
|
|
3695
|
+
/** A compressed answer for a session's history, as its prepared prefix (or undefined). */
|
|
3696
|
+
function preparedFromResult(args, result) {
|
|
3697
|
+
const { body, sessionId, modelContextLimit } = args;
|
|
3698
|
+
if (!didMemtreeCompress(result) || !checkCompressedHistory(result, args.msgsForMemtree).usable) {
|
|
3496
3699
|
return undefined;
|
|
3700
|
+
}
|
|
3497
3701
|
const built = buildCompressedBody(body, result);
|
|
3498
3702
|
if (!built)
|
|
3499
3703
|
return undefined;
|
|
@@ -3508,11 +3712,83 @@ async function prepareInBackground(args) {
|
|
|
3508
3712
|
explicitTarget: args.explicitTarget,
|
|
3509
3713
|
modelContextLimit,
|
|
3510
3714
|
};
|
|
3511
|
-
const source = result.memtreeUrl && result.memtreeIndex
|
|
3715
|
+
const source = result.memtreeUrl && result.memtreeIndex && args.linkSeq !== undefined
|
|
3512
3716
|
? { url: result.memtreeUrl, index: result.memtreeIndex, seq: args.linkSeq } : undefined;
|
|
3513
3717
|
return { prefix, atTokens: args.tokens, triggerTokens: args.triggerTokens,
|
|
3514
3718
|
...(source ? { source } : {}) };
|
|
3515
3719
|
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Whether a main-thread compaction is a budget crossing at the automatic target where
|
|
3722
|
+
* preparations exist: the turns that may race one (a dropped crossing turn and the next
|
|
3723
|
+
* prompt) are serialized on its lane, as carryFor carries it.
|
|
3724
|
+
*/
|
|
3725
|
+
function isAutoCrossing(rec, model, modelContextLimit) {
|
|
3726
|
+
const compaction = rec.compaction;
|
|
3727
|
+
return compaction?.mode === "auto" &&
|
|
3728
|
+
(compaction.reason === "budget" || compaction.reason === "prefix-mismatch") &&
|
|
3729
|
+
preparesCompression(model, modelContextLimit);
|
|
3730
|
+
}
|
|
3731
|
+
/** A main-thread compaction's CarryOver, when its plan has the automatic target. */
|
|
3732
|
+
function carryFor(args) {
|
|
3733
|
+
const compaction = args.rec.compaction;
|
|
3734
|
+
// Where prepared prefixes exist (preparesCompression): the turn after rides it. Not a
|
|
3735
|
+
// /memtree-compact (adoption refuses it) nor beside a preparation (the carry would be
|
|
3736
|
+
// refused, its call left running for nothing).
|
|
3737
|
+
if (!args.isMainRequest || args.sessionId === undefined || args.explicitTarget !== false ||
|
|
3738
|
+
args.targetTokens === undefined || compaction?.mode !== "auto" ||
|
|
3739
|
+
// A crossing only: a call the server decides (maybe a passthrough) is not waited for.
|
|
3740
|
+
(compaction.reason !== "budget" && compaction.reason !== "prefix-mismatch") || args.state.compactNow.has(args.sessionId) ||
|
|
3741
|
+
args.state.preparing.has(args.sessionId) || args.state.prepared.isInFlight(args.sessionId) ||
|
|
3742
|
+
args.state.backgroundSignal.aborted ||
|
|
3743
|
+
!preparesCompression(args.body.model, args.modelContextLimit))
|
|
3744
|
+
return undefined;
|
|
3745
|
+
return { body: args.body, messages: args.messages, msgsForMemtree: args.msgsForMemtree,
|
|
3746
|
+
sessionId: args.sessionId, modelContextLimit: args.modelContextLimit,
|
|
3747
|
+
tokens: compaction.estimatedTokens ?? 0, triggerTokens: compaction.budgetTokens,
|
|
3748
|
+
targetTokens: args.targetTokens, explicitTarget: false,
|
|
3749
|
+
...(args.linkSeq !== undefined ? { linkSeq: args.linkSeq } : {}),
|
|
3750
|
+
base: args.state.stablePrefixes.get(args.sessionId) };
|
|
3751
|
+
}
|
|
3752
|
+
/**
|
|
3753
|
+
* A main-thread compaction its turn stopped waiting for (its budget ran out) becomes the
|
|
3754
|
+
* session's preparation: it runs on to the long budget, the session's next turn at the
|
|
3755
|
+
* budget waits for it (awaitPreparation) instead of starting another, and rides its
|
|
3756
|
+
* answer as a prepared prefix. Before, each turn gave up on its call and started a new
|
|
3757
|
+
* one beside it: 2026-10-30, eleven 26 MB compressions in ten minutes, each billed, none
|
|
3758
|
+
* used. False when the session already has a preparation in flight (it serves instead).
|
|
3759
|
+
*/
|
|
3760
|
+
function carryOverCompression(state, carry, pending) {
|
|
3761
|
+
const { sessionId, base } = carry;
|
|
3762
|
+
// A prefix installed since the call started (another turn's) supersedes it already.
|
|
3763
|
+
if (state.backgroundSignal.aborted || state.preparing.has(sessionId) ||
|
|
3764
|
+
state.stablePrefixes.get(sessionId) !== base ||
|
|
3765
|
+
!state.prepared.begin(sessionId, carry.tokens, carry.triggerTokens))
|
|
3766
|
+
return false;
|
|
3767
|
+
const done = pending
|
|
3768
|
+
.then((result) => {
|
|
3769
|
+
// A late answer clears the cooldown; a late failure is no new outage evidence (a
|
|
3770
|
+
// refusal or a slow server's timeout must not arm it, as runBlockingCompression).
|
|
3771
|
+
if (result)
|
|
3772
|
+
noteMemtreeHealth(state, { result, liveHealth: "success" });
|
|
3773
|
+
return result ? preparedFromResult(carry, result) : undefined;
|
|
3774
|
+
})
|
|
3775
|
+
.catch(() => undefined)
|
|
3776
|
+
.then((prepared) => {
|
|
3777
|
+
// A prefix installed meanwhile supersedes it.
|
|
3778
|
+
const current = state.stablePrefixes.get(sessionId) === base;
|
|
3779
|
+
state.prepared.finish(sessionId, current ? prepared : undefined);
|
|
3780
|
+
if (state.preparing.get(sessionId) === done)
|
|
3781
|
+
state.preparing.delete(sessionId);
|
|
3782
|
+
});
|
|
3783
|
+
boundedSet(state.preparing, sessionId, done);
|
|
3784
|
+
return true;
|
|
3785
|
+
}
|
|
3786
|
+
/** How long a carried compaction may run: the long budget, or its upload's if longer. */
|
|
3787
|
+
/** How much longer than its turn's wait a carried call must be allowed to run. */
|
|
3788
|
+
const CARRY_MIN_EXTRA_MS = 5_000;
|
|
3789
|
+
function carriedBudgetMs(opts, scaledMs) {
|
|
3790
|
+
return Math.min(MAX_LONG_COMPRESS_WAIT_MS, Math.max(opts.memtree.longCompressBudgetMs, scaledMs));
|
|
3791
|
+
}
|
|
3516
3792
|
/** What a prepare call sends: a forced compression to the target, no threshold. */
|
|
3517
3793
|
function preparedCompressMeta(args) {
|
|
3518
3794
|
const { opts, body, sessionId, msgsForMemtree, modelContextLimit } = args;
|
|
@@ -3931,9 +4207,13 @@ async function runBlockingCompression(args) {
|
|
|
3931
4207
|
: undefined;
|
|
3932
4208
|
const messageTimes = transcriptTimesFor(opts, args.sessionId, args.agentId)?.(msgsForMemtree);
|
|
3933
4209
|
const compressStarted = Date.now();
|
|
3934
|
-
// The upload is about the request's size: the budget grows with it
|
|
3935
|
-
|
|
3936
|
-
|
|
4210
|
+
// The upload is about the request's size: the budget grows with it (the long one
|
|
4211
|
+
// too, never below the short one's).
|
|
4212
|
+
const scaledMs = opts.memtree.compressBudgetForBytes(rec.requestBytes);
|
|
4213
|
+
const budgetMs = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, args.longBudget ? Math.max(opts.memtree.longCompressBudgetMs, scaledMs) : scaledMs);
|
|
4214
|
+
// Carried only when running on gives it meaningfully more time than this wait.
|
|
4215
|
+
const carry = args.carry && carriedBudgetMs(opts, scaledMs) >= budgetMs + CARRY_MIN_EXTRA_MS
|
|
4216
|
+
? args.carry : undefined;
|
|
3937
4217
|
const compressMeta = {
|
|
3938
4218
|
// Model + tools drive the server's model-based memory budget
|
|
3939
4219
|
// (e.g. 800k whole-request target for Fable / Opus 5). Omitting
|
|
@@ -3962,8 +4242,8 @@ async function runBlockingCompression(args) {
|
|
|
3962
4242
|
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
|
|
3963
4243
|
...(args.clientMeta ? { clientMeta: args.clientMeta } : {}),
|
|
3964
4244
|
// The deadline this wait uses (the upload-scaled budget included), so the
|
|
3965
|
-
// call and its waiters end together.
|
|
3966
|
-
timeoutMs: budgetMs,
|
|
4245
|
+
// call and its waiters end together; a carried call runs on to the long one.
|
|
4246
|
+
timeoutMs: carry ? Math.max(budgetMs, carriedBudgetMs(opts, scaledMs)) : budgetMs,
|
|
3967
4247
|
};
|
|
3968
4248
|
// Sampled BEFORE the call, while it still describes this call: after the
|
|
3969
4249
|
// await the hash is in the cache regardless of who put it there.
|
|
@@ -3972,12 +4252,21 @@ async function runBlockingCompression(args) {
|
|
|
3972
4252
|
// Keep foreground work finite even if a transport ignores its abort signal.
|
|
3973
4253
|
let deadline;
|
|
3974
4254
|
const maximumWait = Math.min(args.longBudget ? MAX_LONG_COMPRESS_WAIT_MS : MAX_COMPRESS_WAIT_MS, Math.max(1, Number.isFinite(budgetMs) ? budgetMs : 15_000));
|
|
4255
|
+
let settled = false;
|
|
4256
|
+
// A refused carry is not aborted: the call may be shared (in-flight join) with a
|
|
4257
|
+
// request that carried it.
|
|
4258
|
+
const call = opts.memtree.compress(hash, msgsForMemtree, modelContextLimit, args.signal ?? state.shutdownSignal, compressMeta);
|
|
4259
|
+
const settle = () => { settled = true; };
|
|
4260
|
+
call.then(settle, settle);
|
|
3975
4261
|
const result = await Promise.race([
|
|
3976
|
-
|
|
4262
|
+
call,
|
|
3977
4263
|
new Promise((resolve) => { deadline = setTimeout(() => resolve(null), maximumWait); }),
|
|
3978
4264
|
]).finally(() => { if (deadline)
|
|
3979
4265
|
clearTimeout(deadline); });
|
|
3980
4266
|
const compressMs = Date.now() - compressStarted;
|
|
4267
|
+
// Given up on but still running: the session's next turn waits for it, not a new call.
|
|
4268
|
+
const carried = result === null && !settled && carry !== undefined &&
|
|
4269
|
+
carryOverCompression(state, carry, call);
|
|
3981
4270
|
noteServerBudget(state, body.model, modelContextLimit, result);
|
|
3982
4271
|
// Sampled at this call's settle, before a concurrent same-request failure of
|
|
3983
4272
|
// another class can overwrite the complete-key entry.
|
|
@@ -3993,16 +4282,20 @@ async function runBlockingCompression(args) {
|
|
|
3993
4282
|
// Budget-consumed heuristic: the client maps every failure to null, so a
|
|
3994
4283
|
// null that took (roughly) the whole abort budget was almost certainly
|
|
3995
4284
|
// the AbortSignal timeout, not a fast server error.
|
|
3996
|
-
|
|
4285
|
+
// A few ms of slack: a timer can fire a millisecond early by the wall clock (CI flake).
|
|
4286
|
+
const timedOut = result === null &&
|
|
4287
|
+
compressMs + TIMER_SLACK_MS >= Math.min(budgetMs, maximumWait);
|
|
3997
4288
|
// A timeout while other compress calls answered (one succeeded after this
|
|
3998
4289
|
// one started) is this call's size or the server's slow reply, not an outage: it arms no cooldown (2026-10-23:
|
|
3999
4290
|
// healthy-server timeouts kept the fuse lit and a screenshot turn was refused).
|
|
4000
4291
|
const othersAnswered = timedOut && (opts.memtree.lastCompressSuccessAt ?? 0) >= compressStarted;
|
|
4001
|
-
|
|
4292
|
+
// A carried call has not failed: it is still running (its answer reports its health).
|
|
4293
|
+
const hadLiveFailure = !cached && result === null && failureArming && !othersAnswered && !carried;
|
|
4002
4294
|
rec.compress = {
|
|
4003
4295
|
ms: compressMs,
|
|
4004
4296
|
ok: result !== null,
|
|
4005
4297
|
timedOut,
|
|
4298
|
+
...(carried ? { carriedOver: true } : {}),
|
|
4006
4299
|
...(!cached && failureStatus === OVER_CONTEXT_STATUS ? { refused: true } : {}),
|
|
4007
4300
|
};
|
|
4008
4301
|
return {
|
|
@@ -4189,6 +4482,10 @@ async function recoverToolRouteMiss(args) {
|
|
|
4189
4482
|
};
|
|
4190
4483
|
res.once("close", markDownstreamClosed);
|
|
4191
4484
|
const linkSeq = isMainRequest ? nextMemtreeCallSeq(state) : undefined;
|
|
4485
|
+
const toolCarry = stable ? carryFor({ state, isMainRequest, sessionId, body, messages,
|
|
4486
|
+
msgsForMemtree,
|
|
4487
|
+
modelContextLimit, rec, targetTokens: stable.targetTokens, explicitTarget: stable.explicitTarget,
|
|
4488
|
+
linkSeq }) : undefined;
|
|
4192
4489
|
let compression;
|
|
4193
4490
|
try {
|
|
4194
4491
|
compression = await runBlockingCompression({
|
|
@@ -4206,6 +4503,7 @@ async function recoverToolRouteMiss(args) {
|
|
|
4206
4503
|
inputCandidate: args.inputCandidate,
|
|
4207
4504
|
...(args.compaction ? { compaction: args.compaction } : {}),
|
|
4208
4505
|
longBudget: args.wholeRefused ? await args.wholeRefused() : false,
|
|
4506
|
+
...(toolCarry ? { carry: toolCarry } : {}),
|
|
4209
4507
|
});
|
|
4210
4508
|
}
|
|
4211
4509
|
catch {
|
|
@@ -4227,9 +4525,11 @@ async function recoverToolRouteMiss(args) {
|
|
|
4227
4525
|
if (downstreamClosedDuringCompression ||
|
|
4228
4526
|
(res.destroyed && !res.writableFinished)) {
|
|
4229
4527
|
// The MemTree work keeps its cache/index value, but a dead client gets
|
|
4230
|
-
// no Anthropic request and no route.
|
|
4528
|
+
// no Anthropic request and no route. The session's next turn rides it.
|
|
4231
4529
|
rec.routeRecovery = { conversationBytes, outcome: "client-closed" };
|
|
4232
4530
|
releaseOwnReservation();
|
|
4531
|
+
if (result && toolCarry)
|
|
4532
|
+
carryOverCompression(state, toolCarry, Promise.resolve(result));
|
|
4233
4533
|
// Same hazard class as the post-forward fates "upstream-failed" /
|
|
4234
4534
|
// "client-aborted": no route was installed and the client's identical
|
|
4235
4535
|
// retry is imminent, so no backoff is set and the attempt mark is
|
|
@@ -4249,7 +4549,9 @@ async function recoverToolRouteMiss(args) {
|
|
|
4249
4549
|
// submission: its longer independent budget can still warm the index for
|
|
4250
4550
|
// a later turn. An unpaid key (402, possibly set by this very compress)
|
|
4251
4551
|
// or a shutting-down proxy gets no retry.
|
|
4552
|
+
// A carried compression still runs on this history (and indexes it): no upload beside it.
|
|
4252
4553
|
if (!state.shutdownSignal.aborted &&
|
|
4554
|
+
rec.compress?.carriedOver !== true &&
|
|
4253
4555
|
opts.memtree.paymentRequiredDetail === null &&
|
|
4254
4556
|
!opts.memtree.keyInvalid // a refused key refuses the index too
|
|
4255
4557
|
) {
|
|
@@ -4850,6 +5152,40 @@ function lateRecoveryPlan(state, rec, preferred, measured, limits, routeKey) {
|
|
|
4850
5152
|
return preferred.kind === "original" && !fitsMeasured(measured, limits)
|
|
4851
5153
|
? { tokens: measured.tokens } : undefined;
|
|
4852
5154
|
}
|
|
5155
|
+
/**
|
|
5156
|
+
* Wait (up to `maxMs`, or until `signal` aborts) for the lane's in-flight compression to
|
|
5157
|
+
* end; the milliseconds waited.
|
|
5158
|
+
*/
|
|
5159
|
+
async function waitForLaneIdle(state, routeKey, maxMs, signal) {
|
|
5160
|
+
const started = Date.now();
|
|
5161
|
+
while (state.toolRecoveryAttemptedLanes.get(routeKey)?.inFlight && !signal.aborted &&
|
|
5162
|
+
!state.shutdownSignal.aborted && !state.backgroundSignal.aborted &&
|
|
5163
|
+
Date.now() - started < maxMs) {
|
|
5164
|
+
// Unref'd: a wait never holds the process open.
|
|
5165
|
+
await new Promise((resolve) => setTimeout(resolve, LANE_WAIT_POLL_MS).unref());
|
|
5166
|
+
}
|
|
5167
|
+
return Date.now() - started;
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
5170
|
+
* The ride the lane's current memory route gives this request, read afresh (after a wait):
|
|
5171
|
+
* undefined when there is none, it does not match this history, or (main lane) a newer
|
|
5172
|
+
* stable prefix superseded it, which the planner then rides itself.
|
|
5173
|
+
*/
|
|
5174
|
+
function currentLaneRide(state, routeKey, body, messages, sessionId, isMainRequest) {
|
|
5175
|
+
const route = getMemoryRoute(state, routeKey);
|
|
5176
|
+
if (!route)
|
|
5177
|
+
return undefined;
|
|
5178
|
+
if (isMainRequest && sessionId !== undefined &&
|
|
5179
|
+
route.stablePrefix !== state.stablePrefixes.get(sessionId))
|
|
5180
|
+
return undefined;
|
|
5181
|
+
const raw = memoryRoutedToolBody(body, messages, route, state.mainRouteEpoch, sessionId);
|
|
5182
|
+
return raw ? { raw, turnType: "tool-memory", sizeHolder: route.stablePrefix ?? route }
|
|
5183
|
+
: undefined;
|
|
5184
|
+
}
|
|
5185
|
+
/** Wall-clock slack when judging whether a call ran its whole budget (timers fire early). */
|
|
5186
|
+
const TIMER_SLACK_MS = 5;
|
|
5187
|
+
/** How often a turn waiting for its lane's compression looks again. */
|
|
5188
|
+
const LANE_WAIT_POLL_MS = 100;
|
|
4853
5189
|
/**
|
|
4854
5190
|
* Run a delivery-time compression as the lane's in-flight operation, so a
|
|
4855
5191
|
* concurrent request of the lane neither starts its own (planning sees
|