claude-code-infinite 1.0.30 → 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 +54 -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-args.d.ts +10 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +30 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-main.js +119 -17
- 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 +41 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +79 -4
- 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 +16 -1
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree.d.ts +9 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +16 -0
- package/dist/memtree.js.map +1 -1
- package/dist/model-context.d.ts +80 -0
- package/dist/model-context.d.ts.map +1 -0
- package/dist/model-context.js +192 -0
- package/dist/model-context.js.map +1 -0
- package/dist/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/payment-gate.d.ts +2 -0
- package/dist/payment-gate.d.ts.map +1 -1
- package/dist/payment-gate.js +1 -1
- package/dist/payment-gate.js.map +1 -1
- package/dist/proxy.d.ts +28 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +332 -42
- 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,15 +54,18 @@ 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";
|
|
58
|
-
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
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, 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";
|
|
61
61
|
import { keyInvalidNoticeText, keyInvalidRefusalMessage } from "./key-status.js";
|
|
62
|
+
import { KEY_INVALID_KIND, ModelContextNotices, PAUSED_KIND, keyInvalidModelContext, lastToldKindInFile, pausedModelContext } from "./model-context.js";
|
|
62
63
|
import { PaymentNoticeThrottle, pausedCompactionText, pausedRefusalMessage, paymentNoticeText, subscribeUrlFromDetail } from "./payment-notice.js";
|
|
63
64
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
64
65
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, knownSideRequestKind, transcriptWarning, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
65
66
|
import { isResumableSessionId } from "./resume-hint.js";
|
|
67
|
+
import { subagentMemoryHintText, subagentStartOutput, } from "./subagent-hint.js";
|
|
68
|
+
import { RESUME_GUIDANCE_HOOK_SUFFIX } from "./hooks.js";
|
|
66
69
|
import { conservativeBodyTokens } from "./token-estimate.js";
|
|
67
70
|
import { bodyImages } from "./image-tokens.js";
|
|
68
71
|
import { claudeAuthFromHeaders, upstreamErrorCode } from "./claude-auth.js";
|
|
@@ -70,6 +73,8 @@ import { rateLimitFromResponse } from "./rate-limit.js";
|
|
|
70
73
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
71
74
|
const DEFAULT_UPSTREAM = "https://api.anthropic.com";
|
|
72
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;
|
|
73
78
|
// After a recovery attempt returns null (MemTree down, 5xx, or a burned
|
|
74
79
|
// timeout budget), suppress the blocking attempt for this long, across every
|
|
75
80
|
// lane. A lane's own growth backoff alone would let each over-budget tool turn
|
|
@@ -336,16 +341,22 @@ export function startProxy(opts) {
|
|
|
336
341
|
pausedNearWindowWarned: new Set(),
|
|
337
342
|
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
338
343
|
pausedDetail: () => opts.memtree.pausedDetail,
|
|
344
|
+
pausedReason: () => opts.memtree.pausedReason,
|
|
345
|
+
memtreeAnswering: () => opts.memtree.answering,
|
|
346
|
+
modelNotices: new ModelContextNotices(),
|
|
339
347
|
usageUrl: opts.memtree.usageDashboardUrl,
|
|
340
348
|
sessionStartNotice: opts.sessionStartNotice,
|
|
341
349
|
promptSubmitNotice: opts.promptSubmitNotice,
|
|
342
350
|
projectTrees: opts.projectTrees,
|
|
351
|
+
subagentHint: opts.subagentHint,
|
|
352
|
+
resumeGuidance: opts.resumeGuidance,
|
|
343
353
|
diagnostics: opts.diagnostics,
|
|
344
354
|
notices: new NoticeDeliveryQueue(),
|
|
345
355
|
mainNoticePending: false,
|
|
346
356
|
mainPromptGeneration: 0,
|
|
347
357
|
promptAccounting: new PromptAccounting(),
|
|
348
358
|
fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
|
|
359
|
+
refusalStreaks: new RefusalStreaks(),
|
|
349
360
|
activeSubagents: new Set(),
|
|
350
361
|
memoryRoutes: new Map(),
|
|
351
362
|
mainRouteEpoch: 0,
|
|
@@ -489,6 +500,12 @@ async function handleRequest(req, res, opts, upstream, state, hookPath) {
|
|
|
489
500
|
res.end(JSON.stringify({ detail: "The ccc proxy only serves programs on this machine" }));
|
|
490
501
|
return;
|
|
491
502
|
}
|
|
503
|
+
if (url.pathname === PRECOMPACT_HOOK_PATH) {
|
|
504
|
+
return handleNoticeHook(req, res, state, opts.reqlog, undefined, "PreCompact");
|
|
505
|
+
}
|
|
506
|
+
if (url.pathname === `${hookPath}${RESUME_GUIDANCE_HOOK_SUFFIX}`) {
|
|
507
|
+
return handleResumeGuidanceHook(req, res, state);
|
|
508
|
+
}
|
|
492
509
|
if (url.pathname === hookPath) {
|
|
493
510
|
return handleNoticeHook(req, res, state, opts.reqlog, (sessionId) => opts.memtree.scheduleFinalIndex(sessionId, (signal) => opts.transcriptUsage?.finalReplyAsync
|
|
494
511
|
? opts.transcriptUsage.finalReplyAsync(sessionId, signal)
|
|
@@ -720,10 +737,45 @@ function noteMainSessionStart(state, source, sessionId) {
|
|
|
720
737
|
state.resumableMainSession = undefined;
|
|
721
738
|
}
|
|
722
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
|
+
}
|
|
723
773
|
/** Serve only validated Claude hook POSTs on the randomized localhost path. */
|
|
724
774
|
async function handleNoticeHook(req, res, state, reqlog,
|
|
725
775
|
/** A main-lane turn ended: the session may be over (its final index). */
|
|
726
|
-
onMainStop
|
|
776
|
+
onMainStop,
|
|
777
|
+
/** The only event this path takes (the token-free PreCompact path). */
|
|
778
|
+
onlyEvent) {
|
|
727
779
|
if (req.method !== "POST") {
|
|
728
780
|
res.writeHead(405, { allow: "POST" });
|
|
729
781
|
res.end();
|
|
@@ -757,19 +809,32 @@ onMainStop) {
|
|
|
757
809
|
res.end();
|
|
758
810
|
return;
|
|
759
811
|
}
|
|
812
|
+
if (onlyEvent !== undefined && parsed.hook_event_name !== onlyEvent) {
|
|
813
|
+
res.writeHead(403);
|
|
814
|
+
res.end();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
760
817
|
if (parsed.hook_event_name === "SessionStart") {
|
|
761
818
|
if (parsed.agent_id === undefined) {
|
|
762
819
|
recordSessionStart(state, parsed.source);
|
|
763
820
|
noteMainSessionStart(state, parsed.source, parsed.session_id);
|
|
821
|
+
// Claude Code's compaction dropped any MemTree notice from the transcript.
|
|
822
|
+
if (parsed.source === "compact")
|
|
823
|
+
state.modelNotices.forget(parsed.session_id);
|
|
764
824
|
}
|
|
765
825
|
// Earlier trees in this project: asked now, so its wait overlaps the resume's.
|
|
766
826
|
const contextPromise = projectTreesContext(state.projectTrees, parsed);
|
|
827
|
+
// A resumed or forked history may carry notices an earlier process gave.
|
|
828
|
+
const restored = parsed.agent_id === undefined &&
|
|
829
|
+
(parsed.source === "resume" || parsed.source === "fork")
|
|
830
|
+
? restoreModelNotices(state, parsed) : Promise.resolve();
|
|
767
831
|
// A resume waits briefly for the newest tree; a fresh session has none to wait for.
|
|
768
832
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
769
833
|
await state.newestTrees?.settle(parsed.session_id);
|
|
770
834
|
}
|
|
771
835
|
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed), extraSessionStartLine(state.sessionStartNotice, parsed));
|
|
772
836
|
const context = await contextPromise;
|
|
837
|
+
await restored;
|
|
773
838
|
if (!line && !context) {
|
|
774
839
|
res.writeHead(204);
|
|
775
840
|
res.end();
|
|
@@ -784,6 +849,25 @@ onMainStop) {
|
|
|
784
849
|
res.end(body);
|
|
785
850
|
return;
|
|
786
851
|
}
|
|
852
|
+
if (parsed.hook_event_name === "PreCompact") {
|
|
853
|
+
// A typed /compact while MemTree compresses: say why it isn't needed, once.
|
|
854
|
+
const reason = parsed.agent_id === undefined && parsed.trigger === "manual"
|
|
855
|
+
? state.modelNotices.blockCompact(parsed.session_id, memtreeWorking(state))
|
|
856
|
+
: undefined;
|
|
857
|
+
if (reason === undefined) {
|
|
858
|
+
res.writeHead(204);
|
|
859
|
+
res.end();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const body = Buffer.from(JSON.stringify({ decision: "block", reason }), "utf-8");
|
|
863
|
+
res.writeHead(200, {
|
|
864
|
+
"content-type": "application/json",
|
|
865
|
+
"content-length": String(body.length),
|
|
866
|
+
"cache-control": "no-store",
|
|
867
|
+
});
|
|
868
|
+
res.end(body);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
787
871
|
if (parsed.hook_event_name === "UserPromptSubmit") {
|
|
788
872
|
// `/memtree-view`: answered here and blocked, so no model turn runs and
|
|
789
873
|
// the prompt never enters the conversation. Not a human turn either, so
|
|
@@ -822,8 +906,11 @@ onMainStop) {
|
|
|
822
906
|
recordMainPromptNotice(state, parsed.prompt_id, parsed.prompt);
|
|
823
907
|
}
|
|
824
908
|
const line = parsed.agent_id === undefined ? promptSubmitLine(state) : undefined;
|
|
825
|
-
|
|
826
|
-
|
|
909
|
+
const context = parsed.agent_id === undefined
|
|
910
|
+
? state.modelNotices.next(parsed.session_id, currentModelNotice(state))
|
|
911
|
+
: undefined;
|
|
912
|
+
if (line || context) {
|
|
913
|
+
const body = Buffer.from(JSON.stringify(promptSubmitOutput(line, context)), "utf-8");
|
|
827
914
|
res.writeHead(200, {
|
|
828
915
|
"content-type": "application/json",
|
|
829
916
|
"content-length": String(body.length),
|
|
@@ -838,8 +925,19 @@ onMainStop) {
|
|
|
838
925
|
}
|
|
839
926
|
if (parsed.hook_event_name === "SubagentStart") {
|
|
840
927
|
state.activeSubagents.add(parsed.agent_id);
|
|
841
|
-
|
|
842
|
-
|
|
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);
|
|
843
941
|
return;
|
|
844
942
|
}
|
|
845
943
|
if (parsed.hook_event_name === "SubagentStop") {
|
|
@@ -937,6 +1035,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
937
1035
|
requestBytes: rawBody.length,
|
|
938
1036
|
claudeAuth: claudeAuthFromHeaders(req.headers),
|
|
939
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 });
|
|
940
1040
|
const logged = async (forward) => {
|
|
941
1041
|
try {
|
|
942
1042
|
await forward;
|
|
@@ -1176,7 +1276,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1176
1276
|
lane: isAwaySummary ? "away" : requestRouteLane === "main" ? "main" : "other" },
|
|
1177
1277
|
...(target !== undefined ? { target } : {}) });
|
|
1178
1278
|
},
|
|
1179
|
-
delivered: () => {
|
|
1279
|
+
delivered: () => {
|
|
1280
|
+
state.fallbackFailures.succeeded(requestRouteKey);
|
|
1281
|
+
state.refusalStreaks.delivered(requestRouteKey);
|
|
1282
|
+
toolForwarding.settle(true);
|
|
1283
|
+
},
|
|
1180
1284
|
});
|
|
1181
1285
|
const rawMsgsForMemtree = messagesWithSystem(messages, body.system);
|
|
1182
1286
|
const msgsForMemtree = normalizeMessagesForMemtree(rawMsgsForMemtree);
|
|
@@ -1765,7 +1869,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1765
1869
|
: {}),
|
|
1766
1870
|
// Riding the old prefix, or sending a raw request that fits, keeps the
|
|
1767
1871
|
// short budget; with neither, a timeout could only end in a refusal.
|
|
1768
|
-
longBudget: !(edge?.kind === "compress" && edge.fallback) &&
|
|
1872
|
+
longBudget: !(edge?.kind === "compress" && edge.fallback && !edge.fallback.uncounted) &&
|
|
1769
1873
|
await wholeRequestRefused(requestSizing, forwardCandidate, sizingLimits()),
|
|
1770
1874
|
});
|
|
1771
1875
|
}
|
|
@@ -1814,6 +1918,28 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1814
1918
|
// Each forwarder installs its own close listener synchronously, so this is
|
|
1815
1919
|
// an event-loop-atomic handoff of downstream-close ownership.
|
|
1816
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
|
+
}
|
|
1817
1943
|
const kept = rideOldPrefix(false);
|
|
1818
1944
|
if (kept)
|
|
1819
1945
|
return kept;
|
|
@@ -1828,8 +1954,6 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1828
1954
|
releaseForOriginalFallback();
|
|
1829
1955
|
sendingWhole();
|
|
1830
1956
|
capture(opts, "anthropic-request", forwardBody);
|
|
1831
|
-
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1832
|
-
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1833
1957
|
const overBudget = reachedBudget(rec.compaction);
|
|
1834
1958
|
if (state.keyInvalid()) {
|
|
1835
1959
|
// A refused key (401/403) says why MemTree is off and how to fix it;
|
|
@@ -1853,26 +1977,6 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1853
1977
|
if (overBudget)
|
|
1854
1978
|
state.nearWindowNotice = handle;
|
|
1855
1979
|
}
|
|
1856
|
-
else if (mayQueueNotice && paymentDetail === null) {
|
|
1857
|
-
// Under the budget the whole request is what compression would have
|
|
1858
|
-
// sent: the turn is not degraded, so the failure stays in the logs.
|
|
1859
|
-
const warn = () => {
|
|
1860
|
-
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
1861
|
-
rec.degradedNotice = "shown";
|
|
1862
|
-
};
|
|
1863
|
-
if (compressionNeeded(rec.compaction)) {
|
|
1864
|
-
warn();
|
|
1865
|
-
}
|
|
1866
|
-
else {
|
|
1867
|
-
// Delivery sizes the request again (an exact count, else a
|
|
1868
|
-
// conservative bound): a turn it finds over the budget warns then.
|
|
1869
|
-
rec.degradedNotice = "quiet-under-budget";
|
|
1870
|
-
quietDegrades.set(rec, () => {
|
|
1871
|
-
if (state.mainPromptGeneration === noticePromptGeneration)
|
|
1872
|
-
warn();
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
1980
|
return logged(forwardRaw(req, res, forwardBody, opts, upstream, state.shutdownSignal, rec).then((delivered) => {
|
|
1877
1981
|
noteWholeSize();
|
|
1878
1982
|
if (delivered)
|
|
@@ -2114,6 +2218,47 @@ function promptSubmitLine(state) {
|
|
|
2114
2218
|
return undefined;
|
|
2115
2219
|
}
|
|
2116
2220
|
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Whether MemTree is known to be compressing: its latest answer a success and
|
|
2223
|
+
* no pause (402) remembered. Before any answer, or after an outage, it is not.
|
|
2224
|
+
*/
|
|
2225
|
+
function memtreeWorking(state) {
|
|
2226
|
+
return state.memtreeAnswering() === true && state.unpaidDetail() === null;
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* What the model should know about MemTree right now: a notice, undefined
|
|
2230
|
+
* when it is known to work, null when neither is known (no answer, an outage).
|
|
2231
|
+
*/
|
|
2232
|
+
function currentModelNotice(state) {
|
|
2233
|
+
if (state.keyInvalid()) {
|
|
2234
|
+
return { kind: KEY_INVALID_KIND, text: keyInvalidModelContext(state.keyPageUrl) };
|
|
2235
|
+
}
|
|
2236
|
+
const detail = state.unpaidDetail();
|
|
2237
|
+
if (detail !== null) {
|
|
2238
|
+
return { kind: PAUSED_KIND, text: pausedModelContext(detail, state.pausedReason()) };
|
|
2239
|
+
}
|
|
2240
|
+
return memtreeWorking(state) ? undefined : null;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* A resumed session's transcript may hold a notice from an earlier ccc process
|
|
2244
|
+
* (paused, then paid in between): start from it, so the change is announced.
|
|
2245
|
+
* A missing or unreadable transcript, or a scan past its time box, starts from
|
|
2246
|
+
* nothing. Never throws.
|
|
2247
|
+
*/
|
|
2248
|
+
async function restoreModelNotices(state, input) {
|
|
2249
|
+
if (!input.transcript_path)
|
|
2250
|
+
return;
|
|
2251
|
+
state.modelNotices.restore(input.session_id, await lastToldKindInFile(input.transcript_path));
|
|
2252
|
+
}
|
|
2253
|
+
/** UserPromptSubmit's answer: a line for the user, context for the model, or both. */
|
|
2254
|
+
function promptSubmitOutput(line, context) {
|
|
2255
|
+
return {
|
|
2256
|
+
...(line ? { systemMessage: line } : {}),
|
|
2257
|
+
...(context
|
|
2258
|
+
? { hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: context } }
|
|
2259
|
+
: {}),
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2117
2262
|
/** The caller's extra SessionStart line; a throwing provider shows nothing. */
|
|
2118
2263
|
function extraSessionStartLine(provider, input) {
|
|
2119
2264
|
if (!provider || input.agent_id !== undefined || input.source === "compact")
|
|
@@ -2139,6 +2284,48 @@ async function projectTreesContext(hint, input) {
|
|
|
2139
2284
|
return undefined;
|
|
2140
2285
|
}
|
|
2141
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
|
+
}
|
|
2142
2329
|
/**
|
|
2143
2330
|
* The SessionStart hook's answer: the banner line for the user
|
|
2144
2331
|
* (`systemMessage`), model context (`additionalContext`), or both.
|
|
@@ -2745,6 +2932,15 @@ function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
|
|
|
2745
2932
|
return record.uncountedOverWindow === true || (record.sizeSource === "reported" &&
|
|
2746
2933
|
(record.estimatedTokens ?? 0) + outputTokens > modelContextLimit);
|
|
2747
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
|
+
}
|
|
2748
2944
|
function calibratedSizeExceedsLimits(record, body, modelContextLimit) {
|
|
2749
2945
|
return (record.sizeSource === "reported" &&
|
|
2750
2946
|
(record.estimatedTokens ?? 0) >= record.budgetTokens) ||
|
|
@@ -2965,7 +3161,14 @@ async function planEdgeCompaction(args) {
|
|
|
2965
3161
|
// image-aware bound), not bytes: a flatten that kept images is mostly
|
|
2966
3162
|
// base64 that costs a fixed amount per image.
|
|
2967
3163
|
const overWindow = calibratedSizeExceedsWindow(compaction, body, modelContextLimit);
|
|
2968
|
-
|
|
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 } : {}) };
|
|
2969
3172
|
if (state.compactNow.has(sessionId))
|
|
2970
3173
|
return forced("manual", fallback);
|
|
2971
3174
|
if (prefix.modelContextLimit !== modelContextLimit ||
|
|
@@ -4481,8 +4684,8 @@ async function selectForwardCandidate(args) {
|
|
|
4481
4684
|
(args.rec.routeLane === "main" && args.opts.toolOutputs
|
|
4482
4685
|
? await truncatedToFit(args.rec, policy.sizing, target, args.limits, args.opts.toolOutputs)
|
|
4483
4686
|
: undefined);
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4687
|
+
const degraded = degradedNotices.get(args.rec);
|
|
4688
|
+
degradedNotices.delete(args.rec);
|
|
4486
4689
|
const lateInstall = lateRouteInstalls.get(args.rec);
|
|
4487
4690
|
if (lateInstall && (!selection || selection.candidate !== target || target === preferred)) {
|
|
4488
4691
|
lateRouteInstalls.delete(args.rec);
|
|
@@ -4510,14 +4713,29 @@ async function selectForwardCandidate(args) {
|
|
|
4510
4713
|
args.rec.lateCountRecovery.outcome =
|
|
4511
4714
|
changed && candidate.kind === "replacement" ? "compressed" : "forwarded";
|
|
4512
4715
|
delete args.rec.overBudgetForward;
|
|
4513
|
-
|
|
4716
|
+
const overBudget = measurement.tokens > args.limits.budget ||
|
|
4514
4717
|
(measurement.source !== "exact" && candidate.kind === "original" &&
|
|
4515
|
-
measurement.estimatedTokens > args.limits.budget)
|
|
4718
|
+
measurement.estimatedTokens > args.limits.budget);
|
|
4719
|
+
if (overBudget) {
|
|
4516
4720
|
args.rec.overBudgetForward = { reason: measurement.source === "exact"
|
|
4517
4721
|
? "exact-count-fits-window" : measurement.source === "bytes"
|
|
4518
4722
|
? "bytes-fallback-fits-window" : "estimate-fits-window" };
|
|
4519
4723
|
warnPausedNearWindow(args.state, args.rec, requestSessionId(args.req));
|
|
4520
|
-
|
|
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;
|
|
4521
4739
|
}
|
|
4522
4740
|
return changed ? candidate.body : intended;
|
|
4523
4741
|
}
|
|
@@ -4585,6 +4803,9 @@ async function truncateCandidate(rec, sizing, base, limits, store) {
|
|
|
4585
4803
|
if (!plan)
|
|
4586
4804
|
return undefined;
|
|
4587
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
|
+
}
|
|
4588
4809
|
const measurement = await sizing.measure(candidate, limits);
|
|
4589
4810
|
if (fitsMeasured(measurement, limits) && measurement.tokens <= goal) {
|
|
4590
4811
|
rec.truncatedToolResult = { originalChars: plan.originalChars,
|
|
@@ -4693,6 +4914,17 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
4693
4914
|
: args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
4694
4915
|
args.rec.forwardedBytes = 0;
|
|
4695
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);
|
|
4696
4928
|
args.state.diagnostics?.record("refusal", {
|
|
4697
4929
|
status: failure.status, attempt: failure.attempt,
|
|
4698
4930
|
input_ktokens: Math.round(inputTokens / 1000), budget_ktokens: Math.round(budgetTokens / 1000),
|
|
@@ -4719,6 +4951,24 @@ function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
|
4719
4951
|
args.state.keyNotice.markShown();
|
|
4720
4952
|
return false;
|
|
4721
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
|
+
}
|
|
4722
4972
|
/**
|
|
4723
4973
|
* The refusal text already says MemTree is paused and how to resume, so it
|
|
4724
4974
|
* counts as the paused notice: the throttle restarts, and this request's
|
|
@@ -4794,7 +5044,7 @@ function reachedBudget(compaction) {
|
|
|
4794
5044
|
* forced compaction (budget, window, `/memtree-compact`, a rebuilt prefix), a
|
|
4795
5045
|
* size at or over the budget, or a size nobody measured. A planned fit is
|
|
4796
5046
|
* provisional: delivery's own sizing can still find the turn over the budget
|
|
4797
|
-
* (
|
|
5047
|
+
* (degradedNotices), since a plan can undercount.
|
|
4798
5048
|
*/
|
|
4799
5049
|
function compressionNeeded(compaction) {
|
|
4800
5050
|
if (compaction === undefined || compaction.reason !== undefined)
|
|
@@ -4804,10 +5054,15 @@ function compressionNeeded(compaction) {
|
|
|
4804
5054
|
return compaction.estimatedTokens >= compaction.budgetTokens;
|
|
4805
5055
|
}
|
|
4806
5056
|
/**
|
|
4807
|
-
* A degraded turn
|
|
4808
|
-
*
|
|
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.
|
|
4809
5060
|
*/
|
|
4810
|
-
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();
|
|
4811
5066
|
/** The paused notice a degraded request queued, until it is refused or settles. */
|
|
4812
5067
|
const pausedNotices = new WeakMap();
|
|
4813
5068
|
const changedDeliveryRecords = new WeakSet();
|
|
@@ -5495,6 +5750,41 @@ function capture(opts, kind, body) {
|
|
|
5495
5750
|
// diagnostics only — never break the proxy path
|
|
5496
5751
|
}
|
|
5497
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
|
+
}
|
|
5498
5788
|
function readBody(req) {
|
|
5499
5789
|
return readAll(req);
|
|
5500
5790
|
}
|