claude-code-infinite 1.0.31 → 1.0.32
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 +46 -47
- package/dist/claude-auth-status.d.ts +50 -0
- package/dist/claude-auth-status.d.ts.map +1 -0
- package/dist/claude-auth-status.js +213 -0
- package/dist/claude-auth-status.js.map +1 -0
- package/dist/claude-auth.d.ts +2 -1
- package/dist/claude-auth.d.ts.map +1 -1
- package/dist/claude-auth.js +2 -1
- package/dist/claude-auth.js.map +1 -1
- package/dist/claude-plan-warning.d.ts +13 -40
- package/dist/claude-plan-warning.d.ts.map +1 -1
- package/dist/claude-plan-warning.js +24 -75
- package/dist/claude-plan-warning.js.map +1 -1
- package/dist/cli-main.js +107 -16
- package/dist/cli-main.js.map +1 -1
- package/dist/diagnostics-schema.d.ts +5 -2
- package/dist/diagnostics-schema.d.ts.map +1 -1
- package/dist/diagnostics-schema.js +26 -6
- package/dist/diagnostics-schema.js.map +1 -1
- package/dist/diagnostics.d.ts +5 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +7 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/hooks.d.ts +17 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +55 -2
- package/dist/hooks.js.map +1 -1
- package/dist/memtree-mcp.d.ts +7 -0
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +12 -1
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/notices.d.ts +2 -0
- package/dist/notices.d.ts.map +1 -1
- package/dist/notices.js +2 -0
- package/dist/notices.js.map +1 -1
- package/dist/proxy.d.ts +21 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +243 -38
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +11 -2
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +2 -0
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +5 -0
- package/dist/request-sizing.js.map +1 -1
- package/dist/resume-guidance.d.ts +102 -0
- package/dist/resume-guidance.d.ts.map +1 -0
- package/dist/resume-guidance.js +182 -0
- package/dist/resume-guidance.js.map +1 -0
- package/dist/route-fallback.d.ts +23 -0
- package/dist/route-fallback.d.ts.map +1 -1
- package/dist/route-fallback.js +44 -0
- package/dist/route-fallback.js.map +1 -1
- package/dist/sign-in-notice.d.ts +137 -0
- package/dist/sign-in-notice.d.ts.map +1 -0
- package/dist/sign-in-notice.js +300 -0
- package/dist/sign-in-notice.js.map +1 -0
- package/dist/subagent-hint.d.ts +31 -0
- package/dist/subagent-hint.d.ts.map +1 -0
- package/dist/subagent-hint.js +47 -0
- package/dist/subagent-hint.js.map +1 -0
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { capCacheBreakpoints } from "./route-cache.js";
|
|
42
42
|
import { PromptAccounting } from "./prompt-accounting.js";
|
|
43
|
-
import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures, pausedRefusal } from "./route-fallback.js";
|
|
43
|
+
import { INDEX_WAIT_RETRY_AFTER_SECONDS, RefusalStreaks, 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
46
|
import { extendsHistory, mediaFreeMessageHash, prefixIdentity, prefixMessageMatches, } from "./route-identity.js";
|
|
@@ -54,7 +54,7 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
54
54
|
import { join } from "node:path";
|
|
55
55
|
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, 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
|
-
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
|
|
57
|
+
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, SLOW_RETRY_NOTICE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
|
|
58
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";
|
|
59
59
|
import { USAGE_LABEL, USAGE_SIGN_IN_NOTE } from "./usage-link.js";
|
|
60
60
|
import { hyperlink } from "./payment-gate.js";
|
|
@@ -64,6 +64,8 @@ import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paym
|
|
|
64
64
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
65
65
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
66
66
|
import { isResumableSessionId } from "./resume-hint.js";
|
|
67
|
+
import { subagentMemoryHintText, subagentStartOutput, } from "./subagent-hint.js";
|
|
68
|
+
import { RESUME_GUIDANCE_HOOK_SUFFIX } from "./hooks.js";
|
|
67
69
|
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
68
70
|
import { bodyImages } from "./image-tokens.js";
|
|
69
71
|
import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
|
|
@@ -71,6 +73,8 @@ import { rateLimitFromResponse } from "./rate-limit.js";
|
|
|
71
73
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
72
74
|
const DEFAULT_UPSTREAM = "https://api.anthropic.com";
|
|
73
75
|
const HOOK_BODY_LIMIT = 64 * 1024;
|
|
76
|
+
/** A hook body still arriving after this is dropped (the hook itself waits 5 s). */
|
|
77
|
+
const HOOK_BODY_TIMEOUT_MS = 3_000;
|
|
74
78
|
// After a recovery attempt returns null (MemTree down, 5xx, or a burned
|
|
75
79
|
// timeout budget), suppress the blocking attempt for this long, across every
|
|
76
80
|
// lane. A lane's own growth backoff alone would let each over-budget tool turn
|
|
@@ -344,12 +348,15 @@ export function startProxy(opts) {
|
|
|
344
348
|
sessionStartNotice: opts.sessionStartNotice,
|
|
345
349
|
promptSubmitNotice: opts.promptSubmitNotice,
|
|
346
350
|
projectTrees: opts.projectTrees,
|
|
351
|
+
subagentHint: opts.subagentHint,
|
|
352
|
+
resumeGuidance: opts.resumeGuidance,
|
|
347
353
|
diagnostics: opts.diagnostics,
|
|
348
354
|
notices: new NoticeDeliveryQueue(),
|
|
349
355
|
mainNoticePending: false,
|
|
350
356
|
mainPromptGeneration: 0,
|
|
351
357
|
promptAccounting: new PromptAccounting(),
|
|
352
358
|
fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
|
|
359
|
+
refusalStreaks: new RefusalStreaks(),
|
|
353
360
|
activeSubagents: new Set(),
|
|
354
361
|
memoryRoutes: new Map(),
|
|
355
362
|
mainRouteEpoch: 0,
|
|
@@ -496,6 +503,9 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
|
|
|
496
503
|
if (url.pathname === PRECOMPACT_HOOK_PATH) {
|
|
497
504
|
return handleNoticeHook(req, res, state, opts.reqlog, undefined, "PreCompact");
|
|
498
505
|
}
|
|
506
|
+
if (url.pathname === `${hookPath}${RESUME_GUIDANCE_HOOK_SUFFIX}`) {
|
|
507
|
+
return handleResumeGuidanceHook(req, res, state);
|
|
508
|
+
}
|
|
499
509
|
if (url.pathname === hookPath) {
|
|
500
510
|
return handleNoticeHook(req, res, state, opts.reqlog, (sessionId) => opts.memtree.scheduleFinalIndex(sessionId, (signal) => opts.transcriptUsage?.finalReplyAsync
|
|
501
511
|
? opts.transcriptUsage.finalReplyAsync(sessionId, signal)
|
|
@@ -727,6 +737,39 @@ function noteMainSessionStart(state, source, sessionId) {
|
|
|
727
737
|
state.resumableMainSession = undefined;
|
|
728
738
|
}
|
|
729
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* SessionStart on its own hook (resume-guidance.ts): only the current MemTree
|
|
742
|
+
* guidance, so Claude Code sees it as one context of its own and adds an
|
|
743
|
+
* identical one only once. Never fails the hook: anything unexpected is 204.
|
|
744
|
+
*/
|
|
745
|
+
async function handleResumeGuidanceHook(req, res, state) {
|
|
746
|
+
const raw = req.method === "POST" ? await readBodyWithin(req, HOOK_BODY_LIMIT) : undefined;
|
|
747
|
+
if (req.method !== "POST")
|
|
748
|
+
req.resume();
|
|
749
|
+
let input = null;
|
|
750
|
+
try {
|
|
751
|
+
if (raw !== undefined && raw.length > 0) {
|
|
752
|
+
input = parseNoticeHookInput(JSON.parse(raw.toString("utf-8")));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
input = null;
|
|
757
|
+
}
|
|
758
|
+
const context = input?.hook_event_name === "SessionStart"
|
|
759
|
+
? await resumeGuidanceContext(state, input) : undefined;
|
|
760
|
+
if (!context) {
|
|
761
|
+
res.writeHead(204);
|
|
762
|
+
res.end();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const body = Buffer.from(JSON.stringify(sessionStartOutput(undefined, context)), "utf-8");
|
|
766
|
+
res.writeHead(200, {
|
|
767
|
+
"content-type": "application/json",
|
|
768
|
+
"content-length": String(body.length),
|
|
769
|
+
"cache-control": "no-store",
|
|
770
|
+
});
|
|
771
|
+
res.end(body);
|
|
772
|
+
}
|
|
730
773
|
/** Serve only validated Claude hook POSTs on the randomized localhost path. */
|
|
731
774
|
async function handleNoticeHook(req, res, state, reqlog,
|
|
732
775
|
/** A main-lane turn ended: the session may be over (its final index). */
|
|
@@ -882,8 +925,19 @@ onlyEvent) {
|
|
|
882
925
|
}
|
|
883
926
|
if (parsed.hook_event_name === "SubagentStart") {
|
|
884
927
|
state.activeSubagents.add(parsed.agent_id);
|
|
885
|
-
|
|
886
|
-
|
|
928
|
+
const hint = await subagentHintContext(state, parsed.session_id);
|
|
929
|
+
if (hint === undefined) {
|
|
930
|
+
res.writeHead(204);
|
|
931
|
+
res.end();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const body = Buffer.from(JSON.stringify(subagentStartOutput(hint)), "utf-8");
|
|
935
|
+
res.writeHead(200, {
|
|
936
|
+
"content-type": "application/json",
|
|
937
|
+
"content-length": String(body.length),
|
|
938
|
+
"cache-control": "no-store",
|
|
939
|
+
});
|
|
940
|
+
res.end(body);
|
|
887
941
|
return;
|
|
888
942
|
}
|
|
889
943
|
if (parsed.hook_event_name === "SubagentStop") {
|
|
@@ -981,6 +1035,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
981
1035
|
requestBytes: rawBody.length,
|
|
982
1036
|
claudeAuth: claudeAuthFromHeaders(req.headers),
|
|
983
1037
|
};
|
|
1038
|
+
// The prompt this request was made for, before any await (Codex r2 P2).
|
|
1039
|
+
noticeContexts.set(rec, { promptId: state.mainPromptId, generation: state.mainPromptGeneration });
|
|
984
1040
|
const logged = async (forward) => {
|
|
985
1041
|
try {
|
|
986
1042
|
await forward;
|
|
@@ -1220,7 +1276,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1220
1276
|
lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
|
|
1221
1277
|
...(target !== undefined ? { target } : {}) });
|
|
1222
1278
|
},
|
|
1223
|
-
delivered: () => {
|
|
1279
|
+
delivered: () => {
|
|
1280
|
+
state.fallbackFailures.succeeded(requestRouteKey);
|
|
1281
|
+
state.refusalStreaks.delivered(requestRouteKey);
|
|
1282
|
+
toolForwarding.settle(true);
|
|
1283
|
+
},
|
|
1224
1284
|
});
|
|
1225
1285
|
const rawMsgsForMemtree = messagesWithSystem(messages, body.system);
|
|
1226
1286
|
const msgsForMemtree = normalizeMessagesForMemtree(rawMsgsForMemtree);
|
|
@@ -1809,7 +1869,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1809
1869
|
: {}),
|
|
1810
1870
|
// Riding the old prefix, or sending a raw request that fits, keeps the
|
|
1811
1871
|
// short budget; with neither, a timeout could only end in a refusal.
|
|
1812
|
-
longBudget: !(edge?.kind === "compress" && edge.fallback) &&
|
|
1872
|
+
longBudget: !(edge?.kind === "compress" && edge.fallback && !edge.fallback.uncounted) &&
|
|
1813
1873
|
await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
|
|
1814
1874
|
});
|
|
1815
1875
|
}
|
|
@@ -1858,6 +1918,28 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1858
1918
|
// Each forwarder installs its own close listener synchronously, so this is
|
|
1859
1919
|
// an event-loop-atomic handoff of downstream-close ownership.
|
|
1860
1920
|
if (!result) {
|
|
1921
|
+
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1922
|
+
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1923
|
+
if (mayQueueNotice && paymentDetail === null && !state.keyInvalid()) {
|
|
1924
|
+
// Under the budget the whole request is what compression would have
|
|
1925
|
+
// sent: the turn is not degraded, so the failure stays in the logs.
|
|
1926
|
+
// Delivery decides the rest (selectForwardCandidate): only a whole
|
|
1927
|
+
// history actually sent warns. A refusal (Claude Code retries it) or a
|
|
1928
|
+
// ride on a compressed prefix did not run uncompressed (2026-10-28).
|
|
1929
|
+
const eager = compressionNeeded(rec.compaction);
|
|
1930
|
+
// Delivery sizes the request again (an exact count, else a
|
|
1931
|
+
// conservative bound): a planned fit it finds over the budget warns then.
|
|
1932
|
+
if (!eager)
|
|
1933
|
+
rec.degradedNotice = "quiet-under-budget";
|
|
1934
|
+
degradedNotices.set(rec, { eager, warn: () => {
|
|
1935
|
+
// A 402 or refused key meanwhile keeps its own, actionable notice (Codex r2 P2).
|
|
1936
|
+
if (state.mainPromptGeneration !== noticePromptGeneration ||
|
|
1937
|
+
opts.memtree.paymentRequiredDetail !== null || state.keyInvalid())
|
|
1938
|
+
return;
|
|
1939
|
+
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
1940
|
+
rec.degradedNotice = "shown";
|
|
1941
|
+
} });
|
|
1942
|
+
}
|
|
1861
1943
|
const kept = rideOldPrefix(false);
|
|
1862
1944
|
if (kept)
|
|
1863
1945
|
return kept;
|
|
@@ -1872,8 +1954,6 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1872
1954
|
releaseForOriginalFallback();
|
|
1873
1955
|
sendingWhole();
|
|
1874
1956
|
capture(opts, "anthropic-request", forwardBody);
|
|
1875
|
-
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1876
|
-
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1877
1957
|
const overBudget = reachedBudget(rec.compaction);
|
|
1878
1958
|
if (state.keyInvalid()) {
|
|
1879
1959
|
// A refused key (401/403) says why MemTree is off and how to fix it;
|
|
@@ -1897,26 +1977,6 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1897
1977
|
if (overBudget)
|
|
1898
1978
|
state.nearWindowNotice = handle;
|
|
1899
1979
|
}
|
|
1900
|
-
else if (mayQueueNotice && paymentDetail === null) {
|
|
1901
|
-
// Under the budget the whole request is what compression would have
|
|
1902
|
-
// sent: the turn is not degraded, so the failure stays in the logs.
|
|
1903
|
-
const warn = () => {
|
|
1904
|
-
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
1905
|
-
rec.degradedNotice = "shown";
|
|
1906
|
-
};
|
|
1907
|
-
if (compressionNeeded(rec.compaction)) {
|
|
1908
|
-
warn();
|
|
1909
|
-
}
|
|
1910
|
-
else {
|
|
1911
|
-
// Delivery sizes the request again (an exact count, else a
|
|
1912
|
-
// conservative bound): a turn it finds over the budget warns then.
|
|
1913
|
-
rec.degradedNotice = "quiet-under-budget";
|
|
1914
|
-
quietDegrades.set(rec, () => {
|
|
1915
|
-
if (state.mainPromptGeneration === noticePromptGeneration)
|
|
1916
|
-
warn();
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1920
1980
|
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1921
1981
|
noteWholeSize();
|
|
1922
1982
|
if (delivered)
|
|
@@ -2224,6 +2284,48 @@ async function projectTreesContext(hint, input) {
|
|
|
2224
2284
|
return undefined;
|
|
2225
2285
|
}
|
|
2226
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* The parent's memory hint for a starting subagent: only while MemTree works
|
|
2289
|
+
* and the session has the tools, naming the parent's newest tree (a bounded
|
|
2290
|
+
* wait when no answer is cached). Undefined otherwise; never throws.
|
|
2291
|
+
*/
|
|
2292
|
+
async function subagentHintContext(state, sessionId) {
|
|
2293
|
+
if (!state.subagentHint?.enabled || !memtreeWorking(state))
|
|
2294
|
+
return undefined;
|
|
2295
|
+
try {
|
|
2296
|
+
// A cached tree will do (peek refreshes it in the background); wait only for none.
|
|
2297
|
+
const tree = state.newestTrees?.peek(sessionId) ?? await state.newestTrees?.settle(sessionId);
|
|
2298
|
+
// MemTree may have paused or failed while the lookup was on its way.
|
|
2299
|
+
if (!tree || !state.subagentHint.enabled || !memtreeWorking(state))
|
|
2300
|
+
return undefined;
|
|
2301
|
+
return subagentMemoryHintText(sessionId, tree.key);
|
|
2302
|
+
}
|
|
2303
|
+
catch {
|
|
2304
|
+
return undefined;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
/** The current MemTree guidance for a resumed main session (resume-guidance.ts). */
|
|
2308
|
+
async function resumeGuidanceContext(state, input) {
|
|
2309
|
+
try {
|
|
2310
|
+
return await state.resumeGuidance?.contextFor(input, {
|
|
2311
|
+
working: () => resumeMemtreeWorking(state),
|
|
2312
|
+
compressed: () => currentMemtreePage(state, input.session_id)?.compressed,
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
return undefined;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Whether MemTree works as a session resumes: not paused, key not refused,
|
|
2321
|
+
* and its latest answer, if any, a success. A new `ccc --resume` process has
|
|
2322
|
+
* no answer yet; its startup key check already turned a refused or unpaid
|
|
2323
|
+
* key into no guidance (resumeGuidanceFor).
|
|
2324
|
+
*/
|
|
2325
|
+
function resumeMemtreeWorking(state) {
|
|
2326
|
+
return state.memtreeAnswering() !== false && state.unpaidDetail() === null &&
|
|
2327
|
+
!state.keyInvalid();
|
|
2328
|
+
}
|
|
2227
2329
|
/**
|
|
2228
2330
|
* The SessionStart hook's answer: the banner line for the user
|
|
2229
2331
|
* (`systemMessage`), model context (`additionalContext`), or both.
|
|
@@ -2830,6 +2932,15 @@ function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
|
|
|
2830
2932
|
return record.uncountedOverWindow === true || (record.sizeSource === "reported" &&
|
|
2831
2933
|
(record.estimatedTokens ?? 0) + outputTokens > modelContextLimit);
|
|
2832
2934
|
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Over the window (calibratedSizeExceedsWindow) only by the uncounted
|
|
2937
|
+
* conservative bound: a calibrated estimate that fits the window with the
|
|
2938
|
+
* full max_tokens says otherwise.
|
|
2939
|
+
*/
|
|
2940
|
+
function uncountedOnlyOverWindow(record, body, modelContextLimit) {
|
|
2941
|
+
return record.uncountedOverWindow === true && record.sizeSource === "reported" &&
|
|
2942
|
+
(record.estimatedTokens ?? 0) + bodyOutputReservation(body, false) <= modelContextLimit;
|
|
2943
|
+
}
|
|
2833
2944
|
function calibratedSizeExceedsLimits(record, body, modelContextLimit) {
|
|
2834
2945
|
return (record.sizeSource === "reported" &&
|
|
2835
2946
|
(record.estimatedTokens ?? 0) >= record.budgetTokens) ||
|
|
@@ -3050,7 +3161,14 @@ async function planEdgeCompaction(args) {
|
|
|
3050
3161
|
// image-aware bound), not bytes: a flatten that kept images is mostly
|
|
3051
3162
|
// base64 that costs a fixed amount per image.
|
|
3052
3163
|
const overWindow = calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
|
|
3053
|
-
|
|
3164
|
+
// Over the window only by the conservative bound of a ride whose planning
|
|
3165
|
+
// count timed out: delivery counts it again before sending it, so it stays
|
|
3166
|
+
// the fallback a failed compress rides (2026-10-28: a 528k ride whose
|
|
3167
|
+
// count timed out was dropped, and the failed compress refused the turn).
|
|
3168
|
+
const uncounted = overWindow && uncountedOnlyOverWindow(compaction, body, modelContextLimit) &&
|
|
3169
|
+
args.sizing.canRetryCount(prefixCandidate);
|
|
3170
|
+
const fallback = overWindow && !uncounted
|
|
3171
|
+
? undefined : { prefix, routed, ...(uncounted ? { uncounted: true } : {}) };
|
|
3054
3172
|
if (state.compactNow.has(sessionId))
|
|
3055
3173
|
return forced("manual", fallback);
|
|
3056
3174
|
if (prefix.modelContextLimit !== modelContextLimit ||
|
|
@@ -4566,8 +4684,8 @@ async function selectForwardCandidate(args) {
|
|
|
4566
4684
|
(args.rec.routeLane === "main" && args.opts.toolOutputs
|
|
4567
4685
|
? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
|
|
4568
4686
|
: undefined);
|
|
4569
|
-
const
|
|
4570
|
-
|
|
4687
|
+
const degraded = degradedNotices.get(args.rec);
|
|
4688
|
+
degradedNotices.delete(args.rec);
|
|
4571
4689
|
const lateInstall = lateRouteInstalls.get(args.rec);
|
|
4572
4690
|
if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
|
|
4573
4691
|
lateRouteInstalls.delete(args.rec);
|
|
@@ -4595,14 +4713,29 @@ async function selectForwardCandidate(args) {
|
|
|
4595
4713
|
args.rec.lateCountRecovery.outcome =
|
|
4596
4714
|
changed && candidate.kind === "replacement" ? "compressed" : "forwarded";
|
|
4597
4715
|
delete args.rec.overBudgetForward;
|
|
4598
|
-
|
|
4716
|
+
const overBudget = measurement.tokens > args.limits.budget ||
|
|
4599
4717
|
(measurement.source !== "exact" && candidate.kind === "original" &&
|
|
4600
|
-
measurement.estimatedTokens > args.limits.budget)
|
|
4718
|
+
measurement.estimatedTokens > args.limits.budget);
|
|
4719
|
+
if (overBudget) {
|
|
4601
4720
|
args.rec.overBudgetForward = { reason: measurement.source === "exact"
|
|
4602
4721
|
? "exact-count-fits-window" : measurement.source === "bytes"
|
|
4603
4722
|
? "bytes-fallback-fits-window" : "estimate-fits-window" };
|
|
4604
4723
|
warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
|
|
4605
|
-
|
|
4724
|
+
}
|
|
4725
|
+
// Uncompressed only when the whole history goes out (or a truncation of
|
|
4726
|
+
// it), not a compressed ride.
|
|
4727
|
+
const whole = candidate.kind === "original" || wholeHistoryCandidates.has(candidate);
|
|
4728
|
+
if (degraded && whole) {
|
|
4729
|
+
// A failed compress's ride that delivery could not send went out whole.
|
|
4730
|
+
if (args.rec.turnType === "followup-prefix") {
|
|
4731
|
+
args.rec.turnType = "followup-degraded";
|
|
4732
|
+
delete args.rec.compaction?.keptPrefix;
|
|
4733
|
+
}
|
|
4734
|
+
if (degraded.eager || overBudget)
|
|
4735
|
+
degraded.warn();
|
|
4736
|
+
}
|
|
4737
|
+
else if (degraded) {
|
|
4738
|
+
delete args.rec.degradedNotice;
|
|
4606
4739
|
}
|
|
4607
4740
|
return changed ? candidate.body : intended;
|
|
4608
4741
|
}
|
|
@@ -4670,6 +4803,9 @@ async function truncateCandidate(rec, sizing, base, limits, store) {
|
|
|
4670
4803
|
if (!plan)
|
|
4671
4804
|
return undefined;
|
|
4672
4805
|
const candidate = sizing.register(Buffer.from(JSON.stringify(plan.body), "utf-8"), "replacement");
|
|
4806
|
+
if (base.kind === "original" || wholeHistoryCandidates.has(base)) {
|
|
4807
|
+
wholeHistoryCandidates.add(candidate);
|
|
4808
|
+
}
|
|
4673
4809
|
const measurement = await sizing.measure(candidate, limits);
|
|
4674
4810
|
if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
|
|
4675
4811
|
rec.truncatedToolResult = { originalChars: plan.originalChars,
|
|
@@ -4778,6 +4914,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
4778
4914
|
: args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
4779
4915
|
args.rec.forwardedBytes = 0;
|
|
4780
4916
|
args.rec.approxInputTokens = 0;
|
|
4917
|
+
// Nothing went out uncompressed: Claude Code retries it (2026-10-28).
|
|
4918
|
+
// A failed compress's ride that could not be sent either (review r2 P3).
|
|
4919
|
+
const refusedRide = args.rec.turnType === "followup-prefix" &&
|
|
4920
|
+
args.rec.compaction?.keptPrefix === true;
|
|
4921
|
+
if (args.rec.turnType === "followup-degraded" || refusedRide) {
|
|
4922
|
+
args.rec.turnType = "followup-refused";
|
|
4923
|
+
delete args.rec.compaction?.keptPrefix;
|
|
4924
|
+
}
|
|
4925
|
+
delete args.rec.degradedNotice;
|
|
4926
|
+
if (unpaid === null && !keyInvalid)
|
|
4927
|
+
noteSlowRetry(args, failure);
|
|
4781
4928
|
args.state.diagnostics?.record("refusal", {
|
|
4782
4929
|
status: failure.status, attempt: failure.attempt,
|
|
4783
4930
|
input_ktokens: Math.round(inputTokens / 1000), budget_ktokens: Math.round(budgetTokens / 1000),
|
|
@@ -4804,6 +4951,24 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
4804
4951
|
args.state.keyNotice.markShown();
|
|
4805
4952
|
return false;
|
|
4806
4953
|
}
|
|
4954
|
+
/**
|
|
4955
|
+
* A repeated retryable refusal on the main thread: say once per streak that
|
|
4956
|
+
* MemTree is slow and Claude Code is retrying, unless another notice waits.
|
|
4957
|
+
*/
|
|
4958
|
+
function noteSlowRetry(args, failure) {
|
|
4959
|
+
const { state, rec } = args;
|
|
4960
|
+
const prompt = noticeContexts.get(rec);
|
|
4961
|
+
// Not for a stale prompt or a closed response, nor an index wait (its 503
|
|
4962
|
+
// says why), nor a final refusal (its own message shows).
|
|
4963
|
+
const canShow = failure.status === 503 && rec.routeLane === "main" &&
|
|
4964
|
+
rec.indexBehind !== true && prompt !== undefined &&
|
|
4965
|
+
prompt.generation === state.mainPromptGeneration && !args.res.destroyed &&
|
|
4966
|
+
!state.notices.hasPending();
|
|
4967
|
+
if (!state.refusalStreaks.refused(args.routeKey, Date.now(), canShow))
|
|
4968
|
+
return;
|
|
4969
|
+
state.notices.queueSuffix(SLOW_RETRY_NOTICE, undefined, prompt.promptId);
|
|
4970
|
+
rec.slowRetryNotice = true;
|
|
4971
|
+
}
|
|
4807
4972
|
/**
|
|
4808
4973
|
* The refusal text already says MemTree is paused and how to resume, so it
|
|
4809
4974
|
* counts as the paused notice: the throttle restarts, and this request's
|
|
@@ -4879,7 +5044,7 @@ function reachedBudget(compaction) {
|
|
|
4879
5044
|
* forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
|
|
4880
5045
|
* size at or over the budget, or a size nobody measured. A planned fit is
|
|
4881
5046
|
* provisional: delivery's own sizing can still find the turn over the budget
|
|
4882
|
-
* (
|
|
5047
|
+
* (degradedNotices), since a plan can undercount.
|
|
4883
5048
|
*/
|
|
4884
5049
|
function compressionNeeded(compaction) {
|
|
4885
5050
|
if (compaction === undefined || compaction.reason !== undefined)
|
|
@@ -4889,10 +5054,15 @@ function compressionNeeded(compaction) {
|
|
|
4889
5054
|
return compaction.estimatedTokens >= compaction.budgetTokens;
|
|
4890
5055
|
}
|
|
4891
5056
|
/**
|
|
4892
|
-
* A degraded turn
|
|
4893
|
-
*
|
|
5057
|
+
* A degraded turn's notice, queued by delivery only when the whole request
|
|
5058
|
+
* goes out: at once when planning said it needed compression (`eager`), else
|
|
5059
|
+
* when delivery's sizing finds it over the budget after all.
|
|
4894
5060
|
*/
|
|
4895
|
-
const
|
|
5061
|
+
const degradedNotices = new WeakMap();
|
|
5062
|
+
/** Truncations (truncatedToFit) of the whole history: they still run uncompressed. */
|
|
5063
|
+
const wholeHistoryCandidates = new WeakSet();
|
|
5064
|
+
/** The prompt a request was made for, for notices its refusal may queue. */
|
|
5065
|
+
const noticeContexts = new WeakMap();
|
|
4896
5066
|
/** The paused notice a degraded request queued, until it is refused or settles. */
|
|
4897
5067
|
const pausedNotices = new WeakMap();
|
|
4898
5068
|
const changedDeliveryRecords = new WeakSet();
|
|
@@ -5580,6 +5750,41 @@ function capture(opts, kind, body) {
|
|
|
5580
5750
|
// diagnostics only — never break the proxy path
|
|
5581
5751
|
}
|
|
5582
5752
|
}
|
|
5753
|
+
/**
|
|
5754
|
+
* The body, or undefined once it passes `limit` bytes (declared or streamed)
|
|
5755
|
+
* or takes longer than HOOK_BODY_TIMEOUT_MS: the rest is drained unkept.
|
|
5756
|
+
*/
|
|
5757
|
+
function readBodyWithin(req, limit) {
|
|
5758
|
+
return new Promise((resolve) => {
|
|
5759
|
+
const chunks = [];
|
|
5760
|
+
let size = 0;
|
|
5761
|
+
let done = false;
|
|
5762
|
+
const finish = (body) => {
|
|
5763
|
+
if (done)
|
|
5764
|
+
return;
|
|
5765
|
+
done = true;
|
|
5766
|
+
clearTimeout(timer);
|
|
5767
|
+
if (body === undefined) {
|
|
5768
|
+
chunks.length = 0;
|
|
5769
|
+
req.removeAllListeners("data");
|
|
5770
|
+
req.resume();
|
|
5771
|
+
}
|
|
5772
|
+
resolve(body);
|
|
5773
|
+
};
|
|
5774
|
+
const timer = setTimeout(() => finish(undefined), HOOK_BODY_TIMEOUT_MS);
|
|
5775
|
+
if (Number(req.headers["content-length"] ?? 0) > limit)
|
|
5776
|
+
return finish(undefined);
|
|
5777
|
+
req.on("data", (chunk) => {
|
|
5778
|
+
size += chunk.length;
|
|
5779
|
+
if (size > limit)
|
|
5780
|
+
finish(undefined);
|
|
5781
|
+
else
|
|
5782
|
+
chunks.push(chunk);
|
|
5783
|
+
});
|
|
5784
|
+
req.on("end", () => finish(Buffer.concat(chunks)));
|
|
5785
|
+
req.on("error", () => finish(undefined));
|
|
5786
|
+
});
|
|
5787
|
+
}
|
|
5583
5788
|
function readBody(req) {
|
|
5584
5789
|
return readAll(req);
|
|
5585
5790
|
}
|