claude-code-infinite 1.0.14 → 1.0.17
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/dist/cli-main.js +3 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/hooks.d.ts +20 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +35 -8
- package/dist/hooks.js.map +1 -1
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +7 -2
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.d.ts +21 -0
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +8 -1
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree.d.ts +32 -0
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +58 -3
- package/dist/memtree.js.map +1 -1
- package/dist/notices.js +2 -2
- package/dist/notices.js.map +1 -1
- package/dist/payment-notice.d.ts +47 -0
- package/dist/payment-notice.d.ts.map +1 -0
- package/dist/payment-notice.js +72 -0
- package/dist/payment-notice.js.map +1 -0
- package/dist/proxy.d.ts +14 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +180 -58
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +10 -2
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +22 -0
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +31 -2
- package/dist/request-sizing.js.map +1 -1
- package/dist/route-fallback.d.ts +42 -4
- package/dist/route-fallback.d.ts.map +1 -1
- package/dist/route-fallback.js +101 -24
- package/dist/route-fallback.js.map +1 -1
- package/dist/turns.d.ts +4 -0
- package/dist/turns.d.ts.map +1 -1
- package/dist/turns.js +41 -9
- package/dist/turns.js.map +1 -1
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { capCacheBreakpoints } from "./route-cache.js";
|
|
42
42
|
import { PromptAccounting } from "./prompt-accounting.js";
|
|
43
|
-
import { RouteFallbackFailures } from "./route-fallback.js";
|
|
43
|
+
import { INDEX_WAIT_RETRY_AFTER_SECONDS, RouteFallbackFailures } from "./route-fallback.js";
|
|
44
44
|
import { createCountTokensSession, CountTokensCooldowns, plausibleSample, } from "./count-tokens.js";
|
|
45
|
-
import { RequestSizing, estimateRequestTokens, fitsNative } from "./request-sizing.js";
|
|
45
|
+
import { RequestSizing, estimateRequestTokens, fitsMeasured, fitsNative, outputReservation } from "./request-sizing.js";
|
|
46
46
|
import { routeMessageHash, stablePrefixMessageHash } from "./route-identity.js";
|
|
47
47
|
import http from "node:http";
|
|
48
48
|
import https from "node:https";
|
|
@@ -50,11 +50,12 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
50
50
|
import { brotliDecompressSync, createBrotliDecompress, createGunzip, createInflate, gunzipSync, inflateSync, } from "node:zlib";
|
|
51
51
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
52
52
|
import { join } from "node:path";
|
|
53
|
-
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
53
|
+
import { cachedPromptTokenCount, checkCompressedHistory, didMemtreeCompress, indexLagEvidence, MemtreeClient, modelBudgetTokens, normalizeMessagesForMemtree, serverFlattenedMessages, rawPromptTokenCount, } from "./memtree.js";
|
|
54
54
|
import { contextLimitForModel, hasEarlierNonToolUserMessage, isAwaySummaryUserMessage, isLocalBashCommandTurn, isNonToolUserMessage, isToolResultUserMessage, lastNonSystemMessage, messagesWithSystem, modelForMemtree, stripSystemReminderText, } from "./turns.js";
|
|
55
|
-
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText,
|
|
56
|
-
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
55
|
+
import { COMPRESSED_NOTICE, compressedTotalsText, DEGRADED_NOTICE, NOT_COMPRESSED_NOTE, recapLinkText, SseNoticeRewriter, stripNoticeBlocks, stripNoticeSystem, } from "./notices.js";
|
|
56
|
+
import { NoticeDeliveryQueue, MEMTREE_COMPACT_COMMAND, MEMTREE_HELP_COMMAND, TRAILER_LABEL, LINK_LABEL, linkLines, urlLine, terminalSupportsHyperlinks, isMemtreeViewCommand, sessionCommandArgs, parseNoticeHookInput, } from "./hooks.js";
|
|
57
57
|
import { hyperlink } from "./payment-gate.js";
|
|
58
|
+
import { PaymentNoticeThrottle, paymentNoticeText } from "./payment-notice.js";
|
|
58
59
|
import { MEMTREE_LINKS_MAX_SESSIONS } from "./memtree-links.js";
|
|
59
60
|
import { describeClaudeCodeRequest, inspectMonitorTranscript, isClaudeCodeSideRequest, memtreeClientMeta, sessionTag, } from "./cc-request.js";
|
|
60
61
|
import { approxTokensFromBytes, mergeUsageFromJsonBody, mergeUsageFromSseEvent, } from "./reqlog.js";
|
|
@@ -135,6 +136,31 @@ function checkAwaitedIndex(opts, attempt, shutdownSignal) {
|
|
|
135
136
|
waiting.checking = false;
|
|
136
137
|
});
|
|
137
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Record lag evidence from `result` (indexLagEvidence): a refusal of this
|
|
141
|
+
* request then waits for the index, and the cached answer expires after the
|
|
142
|
+
* retry interval so a later retry asks MemTree again — at most one fresh
|
|
143
|
+
* compress per interval for the same request.
|
|
144
|
+
*/
|
|
145
|
+
function noteIndexLag(opts, rec, result) {
|
|
146
|
+
if (!indexLagEvidence(result))
|
|
147
|
+
return false;
|
|
148
|
+
rec.indexBehind = true;
|
|
149
|
+
opts.memtree.expireCompressResult?.(result, opts.indexWaitRefreshMs ?? INDEX_WAIT_RETRY_AFTER_SECONDS * 1000);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
/** Mark a lane as waiting for the tree behind `result`'s MemTree page. */
|
|
153
|
+
function awaitIndex(opts, attempt, record, result) {
|
|
154
|
+
const pageId = result.memtreeUrl ? memtreePageId(result.memtreeUrl) : undefined;
|
|
155
|
+
if (!pageId)
|
|
156
|
+
return;
|
|
157
|
+
attempt.awaitingIndex = {
|
|
158
|
+
pageId,
|
|
159
|
+
checking: false,
|
|
160
|
+
deadline: Date.now() + (opts.awaitedIndexWaitTimeoutMs ?? 60_000),
|
|
161
|
+
};
|
|
162
|
+
record.awaitingIndex = true;
|
|
163
|
+
}
|
|
138
164
|
/** Growth (share of the budget) a lane waits for after an attempt that failed. */
|
|
139
165
|
const TOOL_COMPACTION_RETRY_BUDGET_RATIO = 0.05;
|
|
140
166
|
/**
|
|
@@ -235,12 +261,13 @@ export function startProxy(opts) {
|
|
|
235
261
|
const acceptedRequests = new Set();
|
|
236
262
|
const shutdownAbort = new AbortController();
|
|
237
263
|
const state = {
|
|
238
|
-
|
|
264
|
+
paymentNotice: new PaymentNoticeThrottle(opts.paymentNoticeIntervalMs),
|
|
265
|
+
unpaidDetail: () => opts.memtree.paymentRequiredDetail,
|
|
239
266
|
notices: new NoticeDeliveryQueue(),
|
|
240
267
|
mainNoticePending: false,
|
|
241
268
|
mainPromptGeneration: 0,
|
|
242
269
|
promptAccounting: new PromptAccounting(),
|
|
243
|
-
fallbackFailures: new RouteFallbackFailures(),
|
|
270
|
+
fallbackFailures: new RouteFallbackFailures(undefined, opts.indexWaitDeadlineMs),
|
|
244
271
|
activeSubagents: new Set(),
|
|
245
272
|
memoryRoutes: new Map(),
|
|
246
273
|
mainRouteEpoch: 0,
|
|
@@ -252,6 +279,7 @@ export function startProxy(opts) {
|
|
|
252
279
|
shutdownSignal: shutdownAbort.signal,
|
|
253
280
|
routeInstallFault: opts.routeInstallFault,
|
|
254
281
|
memtreePages: new Map(),
|
|
282
|
+
memorySourcePages: new Map(),
|
|
255
283
|
memtreeCallSeq: 0,
|
|
256
284
|
memtreeLinkStore: opts.memtreeLinkStore,
|
|
257
285
|
newestTrees: opts.newestTrees,
|
|
@@ -522,7 +550,7 @@ async function handleMemTreeFinder(req, res, opts, upstreamPath, url) {
|
|
|
522
550
|
*/
|
|
523
551
|
async function handleMemTreeCurrent(req, res, opts, state, url) {
|
|
524
552
|
const sessionId = url.searchParams.get("session") || undefined;
|
|
525
|
-
const page =
|
|
553
|
+
const page = currentMemorySourcePage(state, sessionId);
|
|
526
554
|
const id = page ? memtreePageId(page.url) : undefined;
|
|
527
555
|
if (!page || !id) {
|
|
528
556
|
res.writeHead(404, { "content-type": "application/json" });
|
|
@@ -554,6 +582,12 @@ async function handleMemTreeCurrent(req, res, opts, state, url) {
|
|
|
554
582
|
}
|
|
555
583
|
}
|
|
556
584
|
const MEMTREE_CURRENT_PATH = `${MEMTREE_PASSTHROUGH_PREFIX}current`;
|
|
585
|
+
/** The page whose memory is in the prompt, else (none adopted yet) the newest page. */
|
|
586
|
+
function currentMemorySourcePage(state, sessionId) {
|
|
587
|
+
if (!sessionId)
|
|
588
|
+
return undefined;
|
|
589
|
+
return state.memorySourcePages.get(sessionId) ?? currentMemtreePage(state, sessionId);
|
|
590
|
+
}
|
|
557
591
|
function currentMemtreePage(state, sessionId) {
|
|
558
592
|
if (!sessionId)
|
|
559
593
|
return undefined;
|
|
@@ -615,7 +649,7 @@ onMainStop) {
|
|
|
615
649
|
if (parsed.agent_id === undefined && parsed.source === "resume") {
|
|
616
650
|
await state.newestTrees?.settle(parsed.session_id);
|
|
617
651
|
}
|
|
618
|
-
const line = resumeLinkLine(state, parsed);
|
|
652
|
+
const line = joinNoticeLines(resumeLinkLine(state, parsed), resumePaymentNotice(state, parsed));
|
|
619
653
|
if (!line) {
|
|
620
654
|
res.writeHead(204);
|
|
621
655
|
res.end();
|
|
@@ -634,16 +668,16 @@ onMainStop) {
|
|
|
634
668
|
// `/memtree-view`: answered here and blocked, so no model turn runs and
|
|
635
669
|
// the prompt never enters the conversation. Not a human turn either, so
|
|
636
670
|
// none of the turn state below is touched.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
671
|
+
// `/memtree-view` waits briefly for the newest tree; a prompt only warms it.
|
|
672
|
+
// In a subagent's view the command is answered too (with the main
|
|
673
|
+
// session's page): unanswered, the plugin's fallback body would run.
|
|
674
|
+
if (isMemtreeViewCommand(parsed.prompt))
|
|
675
|
+
await state.newestTrees?.settle(parsed.session_id);
|
|
676
|
+
else if (parsed.agent_id === undefined)
|
|
677
|
+
state.newestTrees?.peek(parsed.session_id);
|
|
644
678
|
const commandReply = parsed.agent_id === undefined
|
|
645
679
|
? sessionCommandReply(state, parsed.session_id, parsed.prompt)
|
|
646
|
-
:
|
|
680
|
+
: subagentCommandReply(state, parsed.session_id, parsed.prompt);
|
|
647
681
|
if (commandReply !== undefined) {
|
|
648
682
|
const body = Buffer.from(JSON.stringify({
|
|
649
683
|
decision: "block",
|
|
@@ -919,7 +953,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
919
953
|
const sizingLimits = () => ({
|
|
920
954
|
budget: resolveBudget(opts, state, body.model, modelContextLimit).tokens,
|
|
921
955
|
context: modelContextLimit,
|
|
922
|
-
output:
|
|
956
|
+
output: bodyOutputReservation(body),
|
|
957
|
+
recountOutput: outputReservation(undefined, body.max_tokens), // full max_tokens
|
|
923
958
|
});
|
|
924
959
|
const rides = state.memoryRoutes.has(requestRouteKey) ||
|
|
925
960
|
(isMainRequest && requestSessionKey !== undefined && state.stablePrefixes.has(requestSessionKey));
|
|
@@ -983,7 +1018,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
983
1018
|
const measured = await requestSizing.measure(candidate, sizingLimits());
|
|
984
1019
|
// A validated ride is still eligible for bounded late recovery. Final
|
|
985
1020
|
// selection considers it together with the original and any replacement.
|
|
986
|
-
return candidate.kind === "ride" ||
|
|
1021
|
+
return candidate.kind === "ride" || fitsMeasured(measured, sizingLimits());
|
|
987
1022
|
},
|
|
988
1023
|
recoverLate: async (buffer, tokens) => {
|
|
989
1024
|
if (rec.compress || compactionMode(opts, state, requestSessionKey).mode === "off" ||
|
|
@@ -1014,6 +1049,8 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1014
1049
|
// Hook state controls notices only. Hidden requests cannot consume main notices.
|
|
1015
1050
|
if (typeof body.model === "string")
|
|
1016
1051
|
rec.model = body.model;
|
|
1052
|
+
if (typeof body.max_tokens === "number")
|
|
1053
|
+
rec.maxTokens = body.max_tokens;
|
|
1017
1054
|
rec.stream = body.stream === true;
|
|
1018
1055
|
// Observation only: which requests carry Claude Code's turn origin.
|
|
1019
1056
|
if (clientInfo)
|
|
@@ -1045,7 +1082,7 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1045
1082
|
candidateSizeOwners.set(routedCandidate, owner);
|
|
1046
1083
|
// Advisory window pressure can request recovery, but never removes a
|
|
1047
1084
|
// structurally valid candidate before request-local counting/selection.
|
|
1048
|
-
if (!
|
|
1085
|
+
if (!fitsMeasured(plannedMeasurement(requestSizing, routedCandidate), sizingLimits())) {
|
|
1049
1086
|
routeMiss = "rejected";
|
|
1050
1087
|
regrantSmallerWindowRecovery(state, requestRouteKey, modelContextLimit);
|
|
1051
1088
|
}
|
|
@@ -1319,6 +1356,9 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1319
1356
|
*/
|
|
1320
1357
|
const forwardCompressed = async (compressedBody, compressedRaw, turnType, stable, result) => {
|
|
1321
1358
|
if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
|
|
1359
|
+
// A lagging index left history verbatim: a refusal waits for indexing.
|
|
1360
|
+
if (result)
|
|
1361
|
+
noteIndexLag(opts, rec, result);
|
|
1322
1362
|
releaseUncommittedRouteDecision();
|
|
1323
1363
|
recordTurn(rec, "followup-degraded", rawBody);
|
|
1324
1364
|
return logged(forwardRaw(req, res, rawBody, opts, upstream, state.shutdownSignal, rec));
|
|
@@ -1359,6 +1399,11 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1359
1399
|
return;
|
|
1360
1400
|
}
|
|
1361
1401
|
const installed = installMemoryRoute(state, requestRouteKey, req, body, messages, compressedBody, routeEpoch, routeDecisionGeneration, stable);
|
|
1402
|
+
// The answer's memory is now what future requests reuse; a ride keeps
|
|
1403
|
+
// the prior source, and a failed or stale forward adopts nothing.
|
|
1404
|
+
if (installed && result && linkSeq !== undefined) {
|
|
1405
|
+
noteMemorySource(state, linkSeq, sessionId, result);
|
|
1406
|
+
}
|
|
1362
1407
|
commitRouteDecision();
|
|
1363
1408
|
// Leave this false if installMemoryRoute unexpectedly throws: the
|
|
1364
1409
|
// delivery-complete fallback then gets one safe retry.
|
|
@@ -1565,21 +1610,17 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1565
1610
|
// it. Degrade to passthrough and queue a display-only hook notice for a
|
|
1566
1611
|
// visible turn. The hidden away-summary request deliberately stays quiet.
|
|
1567
1612
|
// Unpaid key (402, from this compress OR an earlier background index) gets
|
|
1568
|
-
// a payment-specific notice instead of the generic
|
|
1569
|
-
//
|
|
1613
|
+
// a payment-specific notice with the subscribe link instead of the generic
|
|
1614
|
+
// degraded one: on the first 402, then at most once per throttle interval
|
|
1615
|
+
// (counted from actual delivery). In between, unpaid turns stay quiet.
|
|
1570
1616
|
recordTurn(rec, "followup-degraded", forwardBody);
|
|
1571
1617
|
releaseForOriginalFallback();
|
|
1572
1618
|
sendingWhole();
|
|
1573
1619
|
capture(opts, "anthropic-request", forwardBody);
|
|
1574
1620
|
const paymentDetail = opts.memtree.paymentRequiredDetail;
|
|
1575
1621
|
const mayQueueNotice = displayForThisTurn && state.mainPromptGeneration === noticePromptGeneration;
|
|
1576
|
-
if (mayQueueNotice && paymentDetail !== null &&
|
|
1577
|
-
|
|
1578
|
-
state.notices.queueSuffix(detailFirstLine
|
|
1579
|
-
? `${PAYMENT_REQUIRED_NOTICE}\n${detailFirstLine}`
|
|
1580
|
-
: PAYMENT_REQUIRED_NOTICE, () => {
|
|
1581
|
-
state.paymentNoticeShown = true;
|
|
1582
|
-
}, noticePromptId);
|
|
1622
|
+
if (mayQueueNotice && paymentDetail !== null && state.paymentNotice.due()) {
|
|
1623
|
+
state.notices.queueSuffix(paymentNoticeText(paymentDetail, recapLink(state, sessionId)?.link), () => state.paymentNotice.markShown(), noticePromptId);
|
|
1583
1624
|
}
|
|
1584
1625
|
else if (mayQueueNotice && paymentDetail === null) {
|
|
1585
1626
|
state.notices.queueSuffix(DEGRADED_NOTICE, undefined, noticePromptId);
|
|
@@ -1598,6 +1639,10 @@ async function handleMessages(req, res, opts, upstream, state) {
|
|
|
1598
1639
|
usable: historyCheck.usable,
|
|
1599
1640
|
};
|
|
1600
1641
|
if (!actuallyCompressed || !historyCheck.usable) {
|
|
1642
|
+
// A pass-through from a cold index: if the history cannot go out whole,
|
|
1643
|
+
// the refusal waits for the tree instead of spending its strikes.
|
|
1644
|
+
if (!actuallyCompressed)
|
|
1645
|
+
noteIndexLag(opts, rec, result);
|
|
1601
1646
|
const kept = rideOldPrefix(!actuallyCompressed);
|
|
1602
1647
|
if (kept)
|
|
1603
1648
|
return kept;
|
|
@@ -1796,6 +1841,25 @@ function resumeLinkLine(state, input) {
|
|
|
1796
1841
|
}
|
|
1797
1842
|
return state.notices.resumeLine(link, sessionId);
|
|
1798
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* The unpaid-key notice for a resumed main session whose key the server has
|
|
1846
|
+
* already answered 402 for: a resume is a fresh look at the session, so it
|
|
1847
|
+
* says again why it runs uncompressed, regardless of the throttle. The link
|
|
1848
|
+
* line above it already names the session's MemTree page.
|
|
1849
|
+
*/
|
|
1850
|
+
function resumePaymentNotice(state, input) {
|
|
1851
|
+
if (input.agent_id !== undefined || input.source !== "resume")
|
|
1852
|
+
return undefined;
|
|
1853
|
+
const detail = state.unpaidDetail();
|
|
1854
|
+
if (detail === null)
|
|
1855
|
+
return undefined;
|
|
1856
|
+
state.paymentNotice.markShown();
|
|
1857
|
+
return state.notices.warningText(paymentNoticeText(detail));
|
|
1858
|
+
}
|
|
1859
|
+
function joinNoticeLines(...parts) {
|
|
1860
|
+
const present = parts.filter((part) => !!part);
|
|
1861
|
+
return present.length ? present.join("\n") : undefined;
|
|
1862
|
+
}
|
|
1799
1863
|
export const MEMTREE_COMPACT_MIN_TOKENS = 20_000;
|
|
1800
1864
|
/** The reply to a ccc slash command, or undefined for an ordinary prompt. */
|
|
1801
1865
|
function sessionCommandReply(state, sessionId, prompt) {
|
|
@@ -1837,6 +1901,22 @@ function sessionCommandReply(state, sessionId, prompt) {
|
|
|
1837
1901
|
state.compactNow.add(sessionId);
|
|
1838
1902
|
return `${TRAILER_LABEL} compacting: your next message is sent compressed to about ${Math.round(target / 1000)}k tokens, ${keep}`;
|
|
1839
1903
|
}
|
|
1904
|
+
export const SUBAGENT_VIEW_NOTE = "(main session's page; subagent pages aren't linked yet)";
|
|
1905
|
+
export const SUBAGENT_COMPACT_REPLY = "MemTree compact applies to the main session only.";
|
|
1906
|
+
/**
|
|
1907
|
+
* The reply to a ccc slash command typed in a subagent's view. `sessionId` is
|
|
1908
|
+
* the parent session's: the view shows its page, and compaction (per session)
|
|
1909
|
+
* is refused rather than applied to the main conversation from here.
|
|
1910
|
+
*/
|
|
1911
|
+
function subagentCommandReply(state, sessionId, prompt) {
|
|
1912
|
+
if (isMemtreeViewCommand(prompt)) {
|
|
1913
|
+
return `${memtreeViewLine(state, sessionId)}\n${SUBAGENT_VIEW_NOTE}`;
|
|
1914
|
+
}
|
|
1915
|
+
if (sessionCommandArgs(prompt, MEMTREE_COMPACT_COMMAND) !== undefined) {
|
|
1916
|
+
return `${TRAILER_LABEL} ${SUBAGENT_COMPACT_REPLY}`;
|
|
1917
|
+
}
|
|
1918
|
+
return sessionCommandReply(state, sessionId, prompt);
|
|
1919
|
+
}
|
|
1840
1920
|
/** "50k", "50000", "1.5m" → tokens; undefined when not a positive count. */
|
|
1841
1921
|
export function parseTokenCount(text) {
|
|
1842
1922
|
const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(text.trim());
|
|
@@ -1857,13 +1937,16 @@ function memtreeViewLine(state, sessionId) {
|
|
|
1857
1937
|
return `${TRAILER_LABEL} no page yet: this session has not been indexed. The link appears once it has.`;
|
|
1858
1938
|
}
|
|
1859
1939
|
// Blocked-prompt reasons need an explicit hyperlink in styled terminals.
|
|
1860
|
-
const
|
|
1861
|
-
const lines = linkLines(LINK_LABEL, url, link.note);
|
|
1940
|
+
const lines = linkLines(LINK_LABEL, viewUrl(link.link), link.note);
|
|
1862
1941
|
// The memory the agent works from can be an older tree than the newest one.
|
|
1863
1942
|
return page?.compressed && page.url !== link.link
|
|
1864
|
-
? `${lines}\n
|
|
1943
|
+
? `${lines}\n${urlLine(viewUrl(page.url), "memory from ")}`
|
|
1865
1944
|
: lines;
|
|
1866
1945
|
}
|
|
1946
|
+
/** A `/memtree-view` URL: an explicit OSC 8 link where the terminal has them. */
|
|
1947
|
+
function viewUrl(url) {
|
|
1948
|
+
return terminalSupportsHyperlinks() ? hyperlink(url, url) : url;
|
|
1949
|
+
}
|
|
1867
1950
|
/**
|
|
1868
1951
|
* The link shown for a session: its newest completed tree when the server
|
|
1869
1952
|
* named one (newestTrees), else `source`, the page its memory came from. The
|
|
@@ -1912,12 +1995,30 @@ function noteMemtreePage(state, seq, sessionId, result) {
|
|
|
1912
1995
|
// A cached newest tree may predate this page; ask again.
|
|
1913
1996
|
state.newestTrees?.invalidate(sessionId);
|
|
1914
1997
|
}
|
|
1915
|
-
/**
|
|
1998
|
+
/**
|
|
1999
|
+
* Record a compress answer's page as the source of the memory now forwarded
|
|
2000
|
+
* (memorySourcePages), unless a later-submitted call's memory already is.
|
|
2001
|
+
*/
|
|
2002
|
+
function noteMemorySource(state, seq, sessionId, result) {
|
|
2003
|
+
const url = result.memtreeUrl;
|
|
2004
|
+
const index = result.memtreeIndex;
|
|
2005
|
+
if (!sessionId || !url || !index)
|
|
2006
|
+
return;
|
|
2007
|
+
const latest = state.memorySourcePages.get(sessionId);
|
|
2008
|
+
if (latest && latest.seq >= seq)
|
|
2009
|
+
return;
|
|
2010
|
+
const page = { sessionId, url, index, compressed: didMemtreeCompress(result), seq };
|
|
2011
|
+
rememberPage(state.memorySourcePages, sessionId, page);
|
|
2012
|
+
}
|
|
1916
2013
|
function rememberMemtreePage(state, sessionId, page) {
|
|
1917
|
-
state.memtreePages
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2014
|
+
rememberPage(state.memtreePages, sessionId, page);
|
|
2015
|
+
}
|
|
2016
|
+
/** Bound optional page state independently of how many sessions use the proxy. */
|
|
2017
|
+
function rememberPage(pages, sessionId, page) {
|
|
2018
|
+
pages.delete(sessionId);
|
|
2019
|
+
pages.set(sessionId, page);
|
|
2020
|
+
while (pages.size > MEMTREE_LINKS_MAX_SESSIONS) {
|
|
2021
|
+
pages.delete(pages.keys().next().value);
|
|
1921
2022
|
}
|
|
1922
2023
|
}
|
|
1923
2024
|
/**
|
|
@@ -2225,8 +2326,8 @@ function laneCompaction(opts, state, sessionId, model, modelContextLimit) {
|
|
|
2225
2326
|
}
|
|
2226
2327
|
/** Calibrated input must leave room for the requested output as well. */
|
|
2227
2328
|
function calibratedSizeExceedsWindow(record, body, modelContextLimit) {
|
|
2228
|
-
|
|
2229
|
-
|
|
2329
|
+
// A planning estimate, so the full max_tokens (the floor needs an exact count).
|
|
2330
|
+
const outputTokens = bodyOutputReservation(body, false);
|
|
2230
2331
|
return record.sizeSource === "reported" &&
|
|
2231
2332
|
(record.estimatedTokens ?? 0) + outputTokens > modelContextLimit;
|
|
2232
2333
|
}
|
|
@@ -2487,8 +2588,9 @@ function planToolCompaction(args) {
|
|
|
2487
2588
|
}
|
|
2488
2589
|
}
|
|
2489
2590
|
if (!ride) {
|
|
2490
|
-
const
|
|
2491
|
-
overWindow ||=
|
|
2591
|
+
const original = args.sizing.register(forwardBody, "original");
|
|
2592
|
+
overWindow ||= args.sizing.plan(original).tokens +
|
|
2593
|
+
bodyOutputReservation(body, args.sizing.isExact(original)) > modelContextLimit;
|
|
2492
2594
|
}
|
|
2493
2595
|
const sample = ride
|
|
2494
2596
|
? ride.sizeHolder.lastSize
|
|
@@ -2503,7 +2605,7 @@ function planToolCompaction(args) {
|
|
|
2503
2605
|
compaction.estimatedBytes = (ride?.raw ?? forwardBody).length;
|
|
2504
2606
|
compaction.sizeSource = size.source;
|
|
2505
2607
|
overWindow ||= !fitsNative(size.tokens, { budget: budget.tokens, context: modelContextLimit,
|
|
2506
|
-
output:
|
|
2608
|
+
output: bodyOutputReservation(body, args.sizing.isExact(candidate)) });
|
|
2507
2609
|
if (!args.canCompress) {
|
|
2508
2610
|
if (overWindow)
|
|
2509
2611
|
return { kind: "pass", original: true };
|
|
@@ -2567,12 +2669,21 @@ function memoryRoutedToolBody(body, messages, route, routeEpoch, sessionId) {
|
|
|
2567
2669
|
}
|
|
2568
2670
|
/** Size the assembled input (including system/tools) and reserve output room. */
|
|
2569
2671
|
function routedBodyExceedsContext(body, routedBody, modelContextLimit) {
|
|
2570
|
-
const outputTokens =
|
|
2571
|
-
Number.isFinite(body.max_tokens) ? Math.max(0, body.max_tokens) : 0;
|
|
2672
|
+
const outputTokens = bodyOutputReservation(body, false);
|
|
2572
2673
|
// This is the existing local byte estimate, not a tokenizer or a hard cap.
|
|
2573
2674
|
// Final forwarding checks can replace this estimate with Count Tokens.
|
|
2574
2675
|
return approxTokensFromBytes(routedBody.length) + outputTokens > modelContextLimit;
|
|
2575
2676
|
}
|
|
2677
|
+
/** Output room a native-fit check reserves: the model's floor for an exact
|
|
2678
|
+
* count, else (an estimate) the full max_tokens. */
|
|
2679
|
+
function bodyOutputReservation(body, exact = true) {
|
|
2680
|
+
return outputReservation(exact && typeof body.model === "string" ? body.model : undefined, body.max_tokens);
|
|
2681
|
+
}
|
|
2682
|
+
/** A planned size as a measurement, exact only when Count Tokens measured it. */
|
|
2683
|
+
function plannedMeasurement(sizing, candidate) {
|
|
2684
|
+
const planned = sizing.plan(candidate);
|
|
2685
|
+
return { tokens: planned.tokens, source: sizing.isExact(candidate) ? "exact" : planned.source };
|
|
2686
|
+
}
|
|
2576
2687
|
function regrantSmallerWindowRecovery(state, key, modelContextLimit) {
|
|
2577
2688
|
const previous = state.toolRecoveryAttemptedLanes.get(key);
|
|
2578
2689
|
if (previous && !previous.inFlight && modelContextLimit < previous.modelContextLimit) {
|
|
@@ -3031,9 +3142,10 @@ async function recoverToolRouteMiss(args) {
|
|
|
3031
3142
|
* The attempt produced nothing: send what the turn would have sent without
|
|
3032
3143
|
* it — the old ride (kept prefix) or the whole history — and back off.
|
|
3033
3144
|
*/
|
|
3034
|
-
const forwardOriginal = () => {
|
|
3145
|
+
const forwardOriginal = (growthBackoff = true) => {
|
|
3035
3146
|
requestSendPolicies.get(req).failedRebuild = true;
|
|
3036
|
-
|
|
3147
|
+
if (growthBackoff)
|
|
3148
|
+
backOff();
|
|
3037
3149
|
if (fallback) {
|
|
3038
3150
|
requestSendPolicies.get(req)?.allowed.add(fallback.raw);
|
|
3039
3151
|
if (rec.compaction)
|
|
@@ -3146,17 +3258,11 @@ async function recoverToolRouteMiss(args) {
|
|
|
3146
3258
|
};
|
|
3147
3259
|
// No tree yet (nothing indexed): wait for it to be built rather than
|
|
3148
3260
|
// retrying on growth, which on a fast-growing loop is every tool turn.
|
|
3149
|
-
const pageId = result.memtreeUrl ? memtreePageId(result.memtreeUrl) : undefined;
|
|
3150
|
-
if (!actuallyCompressed && !(cachedPromptTokenCount(result) ?? 0) && pageId) {
|
|
3151
|
-
recoveryAttempt.awaitingIndex = {
|
|
3152
|
-
pageId,
|
|
3153
|
-
checking: false,
|
|
3154
|
-
deadline: Date.now() + (opts.awaitedIndexWaitTimeoutMs ?? 60_000),
|
|
3155
|
-
};
|
|
3156
|
-
rec.routeRecovery.awaitingIndex = true;
|
|
3157
|
-
}
|
|
3158
3261
|
releaseOwnReservation();
|
|
3159
|
-
|
|
3262
|
+
if (actuallyCompressed || !noteIndexLag(opts, rec, result))
|
|
3263
|
+
return forwardOriginal();
|
|
3264
|
+
awaitIndex(opts, recoveryAttempt, rec.routeRecovery, result);
|
|
3265
|
+
return forwardOriginal(false);
|
|
3160
3266
|
}
|
|
3161
3267
|
// Serializing the compressed body is the one synchronous step here that can
|
|
3162
3268
|
// realistically throw: these are the multi-megabyte payloads that court
|
|
@@ -3194,7 +3300,14 @@ async function recoverToolRouteMiss(args) {
|
|
|
3194
3300
|
if (!(await requestSendPolicies.get(req).fitsNative(compressedRaw))) {
|
|
3195
3301
|
rec.routeRecovery = { conversationBytes, outcome: "unusable" };
|
|
3196
3302
|
releaseOwnReservation();
|
|
3197
|
-
|
|
3303
|
+
if (!noteIndexLag(opts, rec, result))
|
|
3304
|
+
return forwardOriginal();
|
|
3305
|
+
// The index trails the conversation, so this result kept most of it
|
|
3306
|
+
// verbatim. Growth cannot help; the next compress after indexing can.
|
|
3307
|
+
// Skip the growth backoff so retries compress again, and let a refusal
|
|
3308
|
+
// wait for the index rather than spend the strike budget.
|
|
3309
|
+
awaitIndex(opts, recoveryAttempt, rec.routeRecovery, result);
|
|
3310
|
+
return forwardOriginal(false);
|
|
3198
3311
|
}
|
|
3199
3312
|
if (compressedRaw.length >= (fallback?.raw ?? forwardBody).length) {
|
|
3200
3313
|
// The final transformed body is the proof of payload recovery; a result
|
|
@@ -3228,6 +3341,10 @@ async function recoverToolRouteMiss(args) {
|
|
|
3228
3341
|
activationAttempted = true;
|
|
3229
3342
|
installedRoute = installed;
|
|
3230
3343
|
installFate = installed ? "installed" : "stale";
|
|
3344
|
+
// Only an installed route makes this answer's memory the reused one.
|
|
3345
|
+
if (installed && linkSeq !== undefined) {
|
|
3346
|
+
noteMemorySource(state, linkSeq, requestSessionId(req), result);
|
|
3347
|
+
}
|
|
3231
3348
|
// A tool continuation can arrive after message_stop while this HTTP
|
|
3232
3349
|
// stream is still draining. Its route is already complete, so a smaller
|
|
3233
3350
|
// window may buy recovery now. Mutate only this attempt's object: a late
|
|
@@ -3432,6 +3549,10 @@ async function recoverLateCount(args) {
|
|
|
3432
3549
|
compaction: { target: compactionTarget(compactionMode(args.opts, args.state, requestSessionId(args.req)), budget) },
|
|
3433
3550
|
});
|
|
3434
3551
|
noteMemtreeHealth(args.state, outcome);
|
|
3552
|
+
// Like every compress path: a refusal after a lagging answer waits for
|
|
3553
|
+
// the index, and the cached answer expires so a retry asks again.
|
|
3554
|
+
if (outcome.result)
|
|
3555
|
+
noteIndexLag(args.opts, args.rec, outcome.result);
|
|
3435
3556
|
if (!outcome.result || !didMemtreeCompress(outcome.result) ||
|
|
3436
3557
|
!checkCompressedHistory(outcome.result, messages).usable)
|
|
3437
3558
|
return undefined;
|
|
@@ -3490,12 +3611,13 @@ async function selectForwardCandidate(args) {
|
|
|
3490
3611
|
}
|
|
3491
3612
|
/** Write the bounded fallback refusal (route-fallback.ts); always false. */
|
|
3492
3613
|
function refuseWholeRequest(args, inputTokens, budgetTokens) {
|
|
3493
|
-
const failure = args.state.fallbackFailures.fail(args.routeKey);
|
|
3614
|
+
const failure = args.state.fallbackFailures.fail(args.routeKey, { awaitingIndex: args.rec.indexBehind === true });
|
|
3494
3615
|
args.rec.forwardedBytes = 0;
|
|
3495
3616
|
args.rec.approxInputTokens = 0;
|
|
3496
3617
|
console.error("[ccc proxy] request exceeds native context window; recovery failed", {
|
|
3497
3618
|
lane: args.routeKey, attempt: failure.attempt, status: failure.status,
|
|
3498
3619
|
inputTokens, outputTokens: args.body.max_tokens ?? 0,
|
|
3620
|
+
outputReservation: bodyOutputReservation(args.body), indexBehind: args.rec.indexBehind === true,
|
|
3499
3621
|
budgetTokens, modelContextLimit: args.modelContextLimit,
|
|
3500
3622
|
});
|
|
3501
3623
|
if (args.res.headersSent || args.res.destroyed)
|