claude-code-infinite 1.0.29 → 1.0.30
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 +57 -7
- package/dist/cli-lifecycle.d.ts +45 -1
- package/dist/cli-lifecycle.d.ts.map +1 -1
- package/dist/cli-lifecycle.js +59 -0
- package/dist/cli-lifecycle.js.map +1 -1
- package/dist/cli-main.js +49 -15
- package/dist/cli-main.js.map +1 -1
- package/dist/count-tokens.d.ts +15 -0
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +23 -1
- package/dist/count-tokens.js.map +1 -1
- package/dist/diagnostics-schema.d.ts.map +1 -1
- package/dist/diagnostics-schema.js +5 -0
- package/dist/diagnostics-schema.js.map +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +18 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/final-index.d.ts +7 -1
- package/dist/final-index.d.ts.map +1 -1
- package/dist/final-index.js +10 -3
- package/dist/final-index.js.map +1 -1
- package/dist/image-refs.d.ts +51 -0
- package/dist/image-refs.d.ts.map +1 -0
- package/dist/image-refs.js +169 -0
- package/dist/image-refs.js.map +1 -0
- package/dist/image-tokens.d.ts +37 -0
- package/dist/image-tokens.d.ts.map +1 -0
- package/dist/image-tokens.js +158 -0
- package/dist/image-tokens.js.map +1 -0
- package/dist/memtree-body.d.ts +52 -0
- package/dist/memtree-body.d.ts.map +1 -0
- package/dist/memtree-body.js +99 -0
- package/dist/memtree-body.js.map +1 -0
- package/dist/memtree.d.ts +77 -3
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +267 -53
- package/dist/memtree.js.map +1 -1
- package/dist/paused-session.d.ts.map +1 -1
- package/dist/paused-session.js +11 -8
- package/dist/paused-session.js.map +1 -1
- package/dist/prefix-store.d.ts +2 -0
- package/dist/prefix-store.d.ts.map +1 -1
- package/dist/prefix-store.js +4 -0
- package/dist/prefix-store.js.map +1 -1
- package/dist/prepared-compression.d.ts +78 -0
- package/dist/prepared-compression.d.ts.map +1 -0
- package/dist/prepared-compression.js +127 -0
- package/dist/prepared-compression.js.map +1 -0
- package/dist/proxy.d.ts +363 -14
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +708 -90
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +55 -0
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +21 -2
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +57 -11
- package/dist/request-sizing.js.map +1 -1
- package/dist/route-identity.d.ts +27 -0
- package/dist/route-identity.d.ts.map +1 -1
- package/dist/route-identity.js +88 -8
- package/dist/route-identity.js.map +1 -1
- package/dist/token-estimate.d.ts +3 -20
- package/dist/token-estimate.d.ts.map +1 -1
- package/dist/token-estimate.js +9 -50
- package/dist/token-estimate.js.map +1 -1
- package/dist/tool-output-store.d.ts +12 -0
- package/dist/tool-output-store.d.ts.map +1 -0
- package/dist/tool-output-store.js +146 -0
- package/dist/tool-output-store.js.map +1 -0
- package/dist/tool-result-truncation.d.ts +32 -0
- package/dist/tool-result-truncation.d.ts.map +1 -0
- package/dist/tool-result-truncation.js +135 -0
- package/dist/tool-result-truncation.js.map +1 -0
- package/package.json +1 -1
package/dist/memtree.js
CHANGED
|
@@ -20,17 +20,21 @@ import { createHash } from "node:crypto";
|
|
|
20
20
|
import { prepareFinalMessages, readFinalReply } from "./final-index.js";
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
22
|
import { usageDashboardUrl } from "./usage-link.js";
|
|
23
|
+
import { extendsHistory, mediaFreeMessageHash } from "./route-identity.js";
|
|
23
24
|
import { withShortPaymentUrl } from "./payment-gate.js";
|
|
24
25
|
import { isKeyRejected, keyPageUrl } from "./key-page.js";
|
|
26
|
+
import { IMAGE_REFS_FEATURE, ImageRefs, MISSING_IMAGES_STATUS, missingImageShas, refusesImageRefs, } from "./image-refs.js";
|
|
27
|
+
import { planMemtreeBody } from "./memtree-body.js";
|
|
25
28
|
import { stripCcSystemReminders, } from "./turns.js";
|
|
26
29
|
export const CLIENT_NAME = "cc-infinite";
|
|
27
30
|
/**
|
|
28
|
-
* Sent as `x-client-features` on
|
|
31
|
+
* Sent as `x-client-features` on every MemTree call. `near-window-budget`: ccc lifts
|
|
29
32
|
* Claude Code's local refusal near the window (claude-env.ts) and compresses
|
|
30
33
|
* mid-turn, so the server may give it the 900k budget on 1M-window models
|
|
31
|
-
* (older ccc keeps 800k).
|
|
34
|
+
* (older ccc keeps 800k). `image-refs`: ccc reads the stored images an answer
|
|
35
|
+
* lists and sends those images as references afterwards (image-refs.ts).
|
|
32
36
|
*/
|
|
33
|
-
export const CLIENT_FEATURES =
|
|
37
|
+
export const CLIENT_FEATURES = `near-window-budget,${IMAGE_REFS_FEATURE}`;
|
|
34
38
|
// Sent on every MemTree call so the server can detect/warn stale clients
|
|
35
39
|
// (plan: "API versioning").
|
|
36
40
|
export const CLIENT_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -39,6 +43,13 @@ export const CLIENT_VERSION = createRequire(import.meta.url)("../package.json").
|
|
|
39
43
|
// server-side compression — so this is a circuit breaker for a hung server, not a
|
|
40
44
|
// latency target. Override via CCC_COMPRESS_TIMEOUT_MS (read below).
|
|
41
45
|
const DEFAULT_COMPRESS_TIMEOUT_MS = 15000;
|
|
46
|
+
/**
|
|
47
|
+
* Added to a compress call's budget per KiB of its upload: a large session's
|
|
48
|
+
* body (20+ MB) takes seconds to send on a slow link before the server even
|
|
49
|
+
* starts, so a flat budget times out on size alone (2026-10-23: every tool
|
|
50
|
+
* turn's compress of a 21.5 MB body timed out at 15 s with MemTree healthy).
|
|
51
|
+
*/
|
|
52
|
+
export const COMPRESS_TIMEOUT_MS_PER_KIB = 1;
|
|
42
53
|
/**
|
|
43
54
|
* The budget for a compress with nothing else to send: no compressed prefix to
|
|
44
55
|
* ride and a raw request known not to fit the window (the first turn after
|
|
@@ -736,6 +747,7 @@ export class MemtreeClient {
|
|
|
736
747
|
baseUrl;
|
|
737
748
|
apiKey;
|
|
738
749
|
compressTimeoutMs;
|
|
750
|
+
compressSuccessAt = 0;
|
|
739
751
|
longCompressTimeoutMs;
|
|
740
752
|
debug;
|
|
741
753
|
reqlog;
|
|
@@ -756,8 +768,18 @@ export class MemtreeClient {
|
|
|
756
768
|
indexedHashes = new Set();
|
|
757
769
|
/** In-flight index-only calls, tracked so shutdown cannot outrun their logs. */
|
|
758
770
|
backgroundIndexes = new Map();
|
|
771
|
+
/** Conversations (indexLaneKey) with a background index call in flight. */
|
|
772
|
+
indexLanesBusy = new Set();
|
|
773
|
+
/** Per busy conversation, the newest index call waiting for the one in flight. */
|
|
774
|
+
indexLanesNext = new Map();
|
|
775
|
+
/** Per conversation, the history the exit's final index carries (flushFinalIndexes). */
|
|
776
|
+
finalIndexLanes = new Map();
|
|
759
777
|
/** Once draining begins, no later request may create another log producer. */
|
|
760
778
|
backgroundClosing = false;
|
|
779
|
+
/** drainBackground ran out and is aborting what is left (exit_index_cut, not an error). */
|
|
780
|
+
drainCutting = false;
|
|
781
|
+
/** Images MemTree stores, sent as references instead of their bytes. */
|
|
782
|
+
imageRefs = new ImageRefs();
|
|
761
783
|
/** Session → its main lane's latest conversation, for the final index. */
|
|
762
784
|
mainConversations = new Map();
|
|
763
785
|
/** Session → its final index waiting for the session to stay idle. */
|
|
@@ -775,6 +797,8 @@ export class MemtreeClient {
|
|
|
775
797
|
lastAnswerKeyInvalid = false;
|
|
776
798
|
/** Complete compression request key → whether its latest failure arms fuse. */
|
|
777
799
|
compressFailureArming = new Map();
|
|
800
|
+
/** HTTP status of each key's latest failed compress (absent: no response). */
|
|
801
|
+
compressFailureStatus = new Map();
|
|
778
802
|
/**
|
|
779
803
|
* Non-null when the server last answered 402 (unpaid MemTree key): the
|
|
780
804
|
* server's human-readable detail text. Set by both compression and
|
|
@@ -828,6 +852,18 @@ export class MemtreeClient {
|
|
|
828
852
|
get compressBudgetMs() {
|
|
829
853
|
return this.compressTimeoutMs;
|
|
830
854
|
}
|
|
855
|
+
/** The blocking-compress budget for an upload of `bytes` (COMPRESS_TIMEOUT_MS_PER_KIB). */
|
|
856
|
+
compressBudgetForBytes(bytes) {
|
|
857
|
+
return scaledTimeout(this.compressBudgetMs, bytes);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* When a compress call (not an index-only one) last got a live successful
|
|
861
|
+
* answer (epoch ms; 0: never). A compress timing out while others succeed
|
|
862
|
+
* is its size or a slow reply, not an outage.
|
|
863
|
+
*/
|
|
864
|
+
get lastCompressSuccessAt() {
|
|
865
|
+
return this.compressSuccessAt;
|
|
866
|
+
}
|
|
831
867
|
/**
|
|
832
868
|
* Non-secret identity of the MemTree backend and account this client uses:
|
|
833
869
|
* a hash over the origin and a hash of the key. Persisted state is valid only
|
|
@@ -918,6 +954,13 @@ export class MemtreeClient {
|
|
|
918
954
|
lastCompressFailureArming(hash, modelContextLimit, meta) {
|
|
919
955
|
return (this.compressFailureArming.get(this.compressKey(hash, modelContextLimit, meta)) ?? true);
|
|
920
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* The HTTP status of the latest failed compress of this request, sampled at
|
|
959
|
+
* its settle like lastCompressFailureArming; undefined when it got none.
|
|
960
|
+
*/
|
|
961
|
+
lastCompressFailureStatus(hash, modelContextLimit, meta) {
|
|
962
|
+
return this.compressFailureStatus.get(this.compressKey(hash, modelContextLimit, meta));
|
|
963
|
+
}
|
|
921
964
|
/**
|
|
922
965
|
* Blocking user-turn compression. Returns null on ANY failure (timeout,
|
|
923
966
|
* network, 4xx/5xx including 402) — the caller must degrade to passthrough.
|
|
@@ -935,11 +978,15 @@ export class MemtreeClient {
|
|
|
935
978
|
const inFlightKey = this.inFlightKey(hash, modelContextLimit, meta);
|
|
936
979
|
const promise = this.callContextMemory(messages, modelContextLimit, {
|
|
937
980
|
timeoutMs: meta?.timeoutMs ?? this.compressTimeoutMs,
|
|
981
|
+
// An explicit budget is the caller's deadline (a preparation's waiters,
|
|
982
|
+
// the proxy's own wait): kept as given so both end together.
|
|
983
|
+
scaleTimeout: meta?.timeoutMs === undefined,
|
|
938
984
|
signal,
|
|
939
985
|
model: meta?.model,
|
|
940
986
|
tools: meta?.tools,
|
|
941
987
|
compressionTargetTokens: meta?.compressionTargetTokens,
|
|
942
988
|
compressionThresholdTokens: meta?.compressionThresholdTokens,
|
|
989
|
+
prepare: meta?.prepare,
|
|
943
990
|
clientInputTokens: meta?.clientInputTokens,
|
|
944
991
|
clientInputTokensExact: meta?.clientInputTokensExact,
|
|
945
992
|
messageUsage: meta?.messageUsage,
|
|
@@ -963,6 +1010,17 @@ export class MemtreeClient {
|
|
|
963
1010
|
if (first !== undefined)
|
|
964
1011
|
this.compressFailureArming.delete(first);
|
|
965
1012
|
}
|
|
1013
|
+
this.compressFailureStatus.delete(cacheKey);
|
|
1014
|
+
// A body never sent (over the limit) is no answer of the server's: its 413 must not
|
|
1015
|
+
// read as MemTree's refusal to fit the window, which lets the safety net truncate.
|
|
1016
|
+
if (status !== undefined && err?.bodyLimit !== true) {
|
|
1017
|
+
this.compressFailureStatus.set(cacheKey, status);
|
|
1018
|
+
}
|
|
1019
|
+
if (this.compressFailureStatus.size > DEDUPE_CACHE_MAX) {
|
|
1020
|
+
const first = this.compressFailureStatus.keys().next().value;
|
|
1021
|
+
if (first !== undefined)
|
|
1022
|
+
this.compressFailureStatus.delete(first);
|
|
1023
|
+
}
|
|
966
1024
|
// Don't cache failures — drop the entry so retries (e.g. Claude Code's
|
|
967
1025
|
// automatic retry of an identical request) hit the server again.
|
|
968
1026
|
if (this.compressCache.get(cacheKey) === promise) {
|
|
@@ -1028,7 +1086,49 @@ export class MemtreeClient {
|
|
|
1028
1086
|
if (first !== undefined)
|
|
1029
1087
|
this.indexedHashes.delete(first);
|
|
1030
1088
|
}
|
|
1031
|
-
this.
|
|
1089
|
+
this.enqueueIndex(indexLaneKey(sessionId, messages), { messages, modelContextLimit, opts: { sessionId, clientMeta, messageTimesFor } });
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* At most one background index call per conversation. Each call carries the whole
|
|
1093
|
+
* history, so while one uploads only the newest waiting call is worth sending: a long
|
|
1094
|
+
* session's tool rounds otherwise stacked 25 MB uploads, up to 16 at once, and starved
|
|
1095
|
+
* the compress calls that share the link (long-session test, staging 2026-10-24). A
|
|
1096
|
+
* waiting call is replaced only by one whose history extends it; otherwise it goes now.
|
|
1097
|
+
*/
|
|
1098
|
+
enqueueIndex(lane, job) {
|
|
1099
|
+
if (!this.indexLanesBusy.has(lane)) {
|
|
1100
|
+
this.startLaneIndex(lane, job);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const waiting = this.indexLanesNext.get(lane);
|
|
1104
|
+
if (waiting && !extendsHistory(job.messages, waiting.messages)) {
|
|
1105
|
+
void this.submitIndexOnly(waiting.messages, waiting.modelContextLimit, waiting.opts);
|
|
1106
|
+
}
|
|
1107
|
+
this.indexLanesNext.set(lane, job);
|
|
1108
|
+
}
|
|
1109
|
+
startLaneIndex(lane, job) {
|
|
1110
|
+
this.indexLanesBusy.add(lane);
|
|
1111
|
+
void this.submitIndexOnly(job.messages, job.modelContextLimit, job.opts).finally(() => {
|
|
1112
|
+
this.indexLanesBusy.delete(lane);
|
|
1113
|
+
const next = this.indexLanesNext.get(lane);
|
|
1114
|
+
this.indexLanesNext.delete(lane);
|
|
1115
|
+
// At shutdown drainBackground has already sent (or dropped) what was waiting.
|
|
1116
|
+
if (next && !this.backgroundClosing)
|
|
1117
|
+
this.startLaneIndex(lane, next);
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* At shutdown: send every waiting call now, inside the drain, except one whose history
|
|
1122
|
+
* the final index carries (a main session's, unless it was rewound since).
|
|
1123
|
+
*/
|
|
1124
|
+
sendWaitingIndexes() {
|
|
1125
|
+
for (const [lane, job] of this.indexLanesNext) {
|
|
1126
|
+
const final = this.finalIndexLanes.get(lane);
|
|
1127
|
+
if (!final || !extendsHistory(final, job.messages)) {
|
|
1128
|
+
void this.submitIndexOnly(job.messages, job.modelContextLimit, job.opts);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
this.indexLanesNext.clear();
|
|
1032
1132
|
}
|
|
1033
1133
|
/**
|
|
1034
1134
|
* The main lane's latest conversation for a session, kept for its final
|
|
@@ -1078,8 +1178,13 @@ export class MemtreeClient {
|
|
|
1078
1178
|
return;
|
|
1079
1179
|
this.finalDrainDeadline ??= Date.now() + Math.max(0, timeoutMs);
|
|
1080
1180
|
const recent = [...this.mainConversations.keys()].slice(-FINAL_INDEX_EXIT_SESSIONS).reverse();
|
|
1081
|
-
for (const sessionId of recent)
|
|
1181
|
+
for (const sessionId of recent) {
|
|
1182
|
+
const conversation = this.mainConversations.get(sessionId);
|
|
1183
|
+
if (conversation) {
|
|
1184
|
+
this.finalIndexLanes.set(indexLaneKey(sessionId, conversation.messages), conversation.messages);
|
|
1185
|
+
}
|
|
1082
1186
|
this.fireFinalIndex(sessionId, true);
|
|
1187
|
+
}
|
|
1083
1188
|
for (const sessionId of this.pendingFinals.keys())
|
|
1084
1189
|
this.cancelFinalIndex(sessionId);
|
|
1085
1190
|
}
|
|
@@ -1110,7 +1215,7 @@ export class MemtreeClient {
|
|
|
1110
1215
|
}
|
|
1111
1216
|
async sendFinalIndex(sessionId, conversation, reader, signal) {
|
|
1112
1217
|
const reply = await readFinalReply(reader, signal);
|
|
1113
|
-
const prepared = await prepareFinalMessages(withFinalReply(conversation.messages, reply), signal);
|
|
1218
|
+
const prepared = await prepareFinalMessages(withFinalReply(conversation.messages, reply), signal, (message) => this.imageRefs.referenceMessage(message));
|
|
1114
1219
|
if (this.mainConversations.get(sessionId) !== conversation ||
|
|
1115
1220
|
this.finalIndexed.get(sessionId) === prepared.key)
|
|
1116
1221
|
return;
|
|
@@ -1120,7 +1225,7 @@ export class MemtreeClient {
|
|
|
1120
1225
|
await this.callContextMemory(prepared.messages, conversation.modelContextLimit, {
|
|
1121
1226
|
sessionId, clientMeta: conversation.clientMeta, messageTimes,
|
|
1122
1227
|
indexOnly: true, finalIndex: true, signal, timeoutMs: INDEX_TIMEOUT_MS,
|
|
1123
|
-
serializedMessages: prepared.serialized,
|
|
1228
|
+
serializedMessages: prepared.serialized, serializedImageRefs: prepared.refs,
|
|
1124
1229
|
});
|
|
1125
1230
|
}
|
|
1126
1231
|
submitIndexOnly(messages, modelContextLimit, opts) {
|
|
@@ -1152,7 +1257,7 @@ export class MemtreeClient {
|
|
|
1152
1257
|
this.backgroundIndexes.delete(operation);
|
|
1153
1258
|
});
|
|
1154
1259
|
this.backgroundIndexes.set(operation, controller);
|
|
1155
|
-
|
|
1260
|
+
return operation;
|
|
1156
1261
|
}
|
|
1157
1262
|
/**
|
|
1158
1263
|
* Stop accepting background indexes and wait boundedly for those already in
|
|
@@ -1161,7 +1266,10 @@ export class MemtreeClient {
|
|
|
1161
1266
|
* The boolean is true for a graceful drain and false when abort was needed.
|
|
1162
1267
|
*/
|
|
1163
1268
|
async drainBackground(timeoutMs = 2_000) {
|
|
1164
|
-
this.backgroundClosing
|
|
1269
|
+
if (!this.backgroundClosing) {
|
|
1270
|
+
this.backgroundClosing = true;
|
|
1271
|
+
this.sendWaitingIndexes();
|
|
1272
|
+
}
|
|
1165
1273
|
if (this.backgroundIndexes.size === 0)
|
|
1166
1274
|
return true;
|
|
1167
1275
|
const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
@@ -1181,6 +1289,7 @@ export class MemtreeClient {
|
|
|
1181
1289
|
clearTimeout(timer);
|
|
1182
1290
|
if (completed)
|
|
1183
1291
|
return true;
|
|
1292
|
+
this.drainCutting = true;
|
|
1184
1293
|
for (const controller of this.backgroundIndexes.values()) {
|
|
1185
1294
|
controller.abort();
|
|
1186
1295
|
}
|
|
@@ -1216,6 +1325,7 @@ export class MemtreeClient {
|
|
|
1216
1325
|
meta?.compressionTargetTokens ?? null,
|
|
1217
1326
|
meta?.compressionThresholdTokens ?? null,
|
|
1218
1327
|
meta?.clientInputTokens !== undefined && meta.clientInputTokensExact === true,
|
|
1328
|
+
meta?.prepare === true,
|
|
1219
1329
|
];
|
|
1220
1330
|
}
|
|
1221
1331
|
/**
|
|
@@ -1300,6 +1410,8 @@ export class MemtreeClient {
|
|
|
1300
1410
|
if (opts.compressionThresholdTokens !== undefined) {
|
|
1301
1411
|
body.compression_threshold_tokens = opts.compressionThresholdTokens;
|
|
1302
1412
|
}
|
|
1413
|
+
if (opts.prepare)
|
|
1414
|
+
body.prepare = true;
|
|
1303
1415
|
if (opts.clientInputTokens !== undefined) {
|
|
1304
1416
|
body.client_input_tokens = opts.clientInputTokens;
|
|
1305
1417
|
if (opts.clientInputTokensExact)
|
|
@@ -1323,14 +1435,20 @@ export class MemtreeClient {
|
|
|
1323
1435
|
// (within its image allowance) instead of naming them in placeholders.
|
|
1324
1436
|
body.flatten_images = true;
|
|
1325
1437
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1438
|
+
// Stored images as references, and never a body over the HTTP/1 limit (memtree-body.ts).
|
|
1439
|
+
const plan = (refs) => planMemtreeBody({
|
|
1440
|
+
body, messages, indexOnly: opts.indexOnly === true,
|
|
1441
|
+
serialized: refs && opts.serializedMessages !== undefined
|
|
1442
|
+
? { value: opts.serializedMessages, refs: opts.serializedImageRefs ?? [] } : undefined,
|
|
1443
|
+
imageRefs: refs ? this.imageRefs : undefined,
|
|
1444
|
+
});
|
|
1445
|
+
const planned = plan(opts.imageRefs !== false);
|
|
1446
|
+
if (planned.payload === undefined)
|
|
1447
|
+
throw this.oversized(planned.limit, opts);
|
|
1448
|
+
const payload = planned.payload;
|
|
1449
|
+
// The answer named references it could not serve: sent again as its own call.
|
|
1450
|
+
let resent = false;
|
|
1451
|
+
let refusalText;
|
|
1334
1452
|
opts.signal?.throwIfAborted();
|
|
1335
1453
|
const started = Date.now();
|
|
1336
1454
|
let status;
|
|
@@ -1339,35 +1457,35 @@ export class MemtreeClient {
|
|
|
1339
1457
|
let diagnostics = {};
|
|
1340
1458
|
const controller = new AbortController();
|
|
1341
1459
|
const abort = () => controller.abort();
|
|
1342
|
-
|
|
1460
|
+
// A default compress budget grows with what it uploads; an index call's,
|
|
1461
|
+
// and any budget a caller set, is fixed.
|
|
1462
|
+
const timeoutMs = opts.scaleTimeout && !opts.indexOnly
|
|
1463
|
+
? scaledTimeout(opts.timeoutMs, Buffer.byteLength(payload, "utf-8")) : opts.timeoutMs;
|
|
1464
|
+
const timeout = setTimeout(abort, timeoutMs);
|
|
1343
1465
|
timeout.unref();
|
|
1344
1466
|
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
1345
1467
|
if (opts.signal?.aborted)
|
|
1346
1468
|
abort();
|
|
1347
1469
|
try {
|
|
1348
|
-
const response = await
|
|
1349
|
-
method: "POST",
|
|
1350
|
-
headers: {
|
|
1351
|
-
authorization: `Bearer ${this.apiKey}`,
|
|
1352
|
-
"content-type": "application/json",
|
|
1353
|
-
"x-client": CLIENT_NAME,
|
|
1354
|
-
"x-client-version": CLIENT_VERSION,
|
|
1355
|
-
"x-client-features": CLIENT_FEATURES,
|
|
1356
|
-
...(opts.sessionId ? { "x-claude-code-session-id": opts.sessionId } : {}),
|
|
1357
|
-
...(opts.clientMeta && Object.keys(opts.clientMeta).length
|
|
1358
|
-
? { "x-client-meta": JSON.stringify(opts.clientMeta) }
|
|
1359
|
-
: {}),
|
|
1360
|
-
// Compress calls only: an index-only call returns no memory.
|
|
1361
|
-
...(this.memtreeTools && !opts.indexOnly
|
|
1362
|
-
? { "x-memtree-tools": this.memtreeTools }
|
|
1363
|
-
: {}),
|
|
1364
|
-
},
|
|
1365
|
-
body: payload,
|
|
1366
|
-
signal: controller.signal,
|
|
1367
|
-
});
|
|
1470
|
+
const response = await this.postContextMemory(payload, opts, controller.signal);
|
|
1368
1471
|
status = response.status;
|
|
1472
|
+
if (planned.refs.length && !response.ok) {
|
|
1473
|
+
refusalText = await readUnlessAborted(response, controller.signal);
|
|
1474
|
+
if (this.refusedRefs(response.status, refusalText, planned.refs)) {
|
|
1475
|
+
// A new call with bytes: its own size and request-log record, within what is
|
|
1476
|
+
// left of this call's budget (a caller's deadline is never extended). Read when
|
|
1477
|
+
// its timer starts, after its body is built, and never clamped: a size-scaled
|
|
1478
|
+
// budget adds its allowance to what is left, however negative.
|
|
1479
|
+
resent = true;
|
|
1480
|
+
const budget = opts.timeoutMs;
|
|
1481
|
+
return await this.callContextMemory(messages, modelContextLimit, {
|
|
1482
|
+
...opts, imageRefs: false,
|
|
1483
|
+
get timeoutMs() { return budget - (Date.now() - started); },
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1369
1487
|
if (!response.ok) {
|
|
1370
|
-
const text = await response.text().catch(() => "");
|
|
1488
|
+
const text = refusalText ?? await response.text().catch(() => "");
|
|
1371
1489
|
if (response.status === 402) {
|
|
1372
1490
|
this.unpaidDetail = extract402Detail(text);
|
|
1373
1491
|
// Shared, cached and backed off: one request per hour at most while paused.
|
|
@@ -1379,6 +1497,7 @@ export class MemtreeClient {
|
|
|
1379
1497
|
throw Object.assign(new Error(`context_memory ${response.status}: ${text.slice(0, 300)}`), { status: response.status });
|
|
1380
1498
|
}
|
|
1381
1499
|
const json = (await response.json());
|
|
1500
|
+
this.imageRefs.noteAnswer(json);
|
|
1382
1501
|
// An index-only ack is `{ messages: [], usage: {...zeros}, index_only:
|
|
1383
1502
|
// true }` by contract: the server skips compression and returns no
|
|
1384
1503
|
// messages. Only a compress call needs a non-empty conversation back.
|
|
@@ -1393,6 +1512,8 @@ export class MemtreeClient {
|
|
|
1393
1512
|
const memtreeIndex = response.headers.get("x-polychat-memtree-index") ?? undefined;
|
|
1394
1513
|
this.unpaidDetail = null; // a success proves the key is paid (again)
|
|
1395
1514
|
ok = true;
|
|
1515
|
+
if (!opts.indexOnly)
|
|
1516
|
+
this.compressSuccessAt = Date.now();
|
|
1396
1517
|
diagnostics = {
|
|
1397
1518
|
...(memtreeUrl ? { memtreeUrl } : {}),
|
|
1398
1519
|
...(memtreeIndex ? { memtreeIndex } : {}),
|
|
@@ -1425,33 +1546,104 @@ export class MemtreeClient {
|
|
|
1425
1546
|
// A background index call that failed otherwise (a timeout, a 413 on a big
|
|
1426
1547
|
// body) is no news about payment; a compress call's answer always is.
|
|
1427
1548
|
// A refused key is news from any call, like a 402.
|
|
1428
|
-
const news = status === 402 || keyRefused || ok || !opts.indexOnly;
|
|
1549
|
+
const news = !resent && (status === 402 || keyRefused || ok || !opts.indexOnly);
|
|
1429
1550
|
if (news && (status !== undefined || !opts.signal?.aborted)) {
|
|
1430
1551
|
this.lastAnswerUnpaid = status === 402;
|
|
1431
1552
|
this.lastAnswerKeyInvalid = keyRefused;
|
|
1432
1553
|
}
|
|
1433
1554
|
// One JSONL line per MemTree call, success or failure; `status` stays
|
|
1434
1555
|
// absent when the call never got a response (network error/timeout).
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1556
|
+
if (!resent)
|
|
1557
|
+
this.reqlog?.log({
|
|
1558
|
+
kind: "memtree",
|
|
1559
|
+
indexOnly: opts.indexOnly === true,
|
|
1560
|
+
ms: Date.now() - started,
|
|
1561
|
+
ok,
|
|
1562
|
+
status,
|
|
1563
|
+
requestBytes: Buffer.byteLength(payload),
|
|
1564
|
+
...(planned.limit ? { bodyLimit: planned.limit } : {}),
|
|
1565
|
+
// Aborted by an exit drain that ran out: a known cut, not a server failure.
|
|
1566
|
+
...(!ok && this.drainCutting && controller.signal.aborted
|
|
1567
|
+
? { exitCut: true, finalIndex: opts.finalIndex === true }
|
|
1568
|
+
: !ok && opts.signal?.aborted && callerCut(status) ? { callerAborted: true } : {}),
|
|
1569
|
+
// Present only when the request carried a model for server-side
|
|
1570
|
+
// budget resolution — the client-visible half of the server's
|
|
1571
|
+
// "/v1/context_memory budget" log line.
|
|
1572
|
+
...(opts.model && !opts.indexOnly ? { model: opts.model } : {}),
|
|
1573
|
+
...diagnostics,
|
|
1574
|
+
});
|
|
1448
1575
|
}
|
|
1449
1576
|
}
|
|
1577
|
+
/** One POST of a MemTree call's body. */
|
|
1578
|
+
postContextMemory(payload, opts, signal) {
|
|
1579
|
+
return fetch(`${this.baseUrl}/v1/context_memory`, {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
headers: {
|
|
1582
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
1583
|
+
"content-type": "application/json",
|
|
1584
|
+
"x-client": CLIENT_NAME,
|
|
1585
|
+
"x-client-version": CLIENT_VERSION,
|
|
1586
|
+
"x-client-features": CLIENT_FEATURES,
|
|
1587
|
+
...(opts.sessionId ? { "x-claude-code-session-id": opts.sessionId } : {}),
|
|
1588
|
+
...(opts.clientMeta && Object.keys(opts.clientMeta).length
|
|
1589
|
+
? { "x-client-meta": JSON.stringify(opts.clientMeta) }
|
|
1590
|
+
: {}),
|
|
1591
|
+
// Compress calls only: an index-only call returns no memory.
|
|
1592
|
+
...(this.memtreeTools && !opts.indexOnly
|
|
1593
|
+
? { "x-memtree-tools": this.memtreeTools }
|
|
1594
|
+
: {}),
|
|
1595
|
+
},
|
|
1596
|
+
body: payload,
|
|
1597
|
+
signal,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Whether a failed answer to a call that sent references asks for their bytes: the
|
|
1602
|
+
* server lacks some images (409: those are forgotten), or refuses references (400:
|
|
1603
|
+
* this request's forgotten, or none ever again when the key may not send them).
|
|
1604
|
+
*/
|
|
1605
|
+
refusedRefs(status, text, refs) {
|
|
1606
|
+
const missing = status === MISSING_IMAGES_STATUS ? missingImageShas(text) : undefined;
|
|
1607
|
+
const refused = refusesImageRefs(status, text);
|
|
1608
|
+
if (missing !== undefined) {
|
|
1609
|
+
this.imageRefs.forget(missing.length ? missing : refs);
|
|
1610
|
+
}
|
|
1611
|
+
else if (refused === "never") {
|
|
1612
|
+
this.imageRefs.disable();
|
|
1613
|
+
}
|
|
1614
|
+
else if (refused === "this") {
|
|
1615
|
+
this.imageRefs.forget(refs);
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
return false;
|
|
1619
|
+
}
|
|
1620
|
+
this.log(`context_memory ${status} on image references: sending their bytes`);
|
|
1621
|
+
return true;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* A body over the limit that nothing could shrink (memtree-body.ts): never sent.
|
|
1625
|
+
* Logged like any call, with `bodyLimit` (memtree_body_limit, not memtree_error); the
|
|
1626
|
+
* error carries 413, the status the server's front end would have answered.
|
|
1627
|
+
*/
|
|
1628
|
+
oversized(limit, opts) {
|
|
1629
|
+
this.reqlog?.log({
|
|
1630
|
+
kind: "memtree", indexOnly: opts.indexOnly === true, ms: 0, ok: false,
|
|
1631
|
+
requestBytes: limit?.requestBytes ?? 0, ...(limit ? { bodyLimit: limit } : {}),
|
|
1632
|
+
});
|
|
1633
|
+
this.log(`context_memory body of ${limit?.requestBytes ?? "?"} bytes over the limit: not sent`);
|
|
1634
|
+
return Object.assign(new Error("context_memory body over the size limit (not sent)"), { status: 413, bodyLimit: true });
|
|
1635
|
+
}
|
|
1450
1636
|
log(msg) {
|
|
1451
1637
|
if (this.debug)
|
|
1452
1638
|
console.error(`[ccc proxy] ${msg}`);
|
|
1453
1639
|
}
|
|
1454
1640
|
}
|
|
1641
|
+
/** A failed answer's body; an abort while reading it is thrown, never read as "". */
|
|
1642
|
+
async function readUnlessAborted(response, signal) {
|
|
1643
|
+
const text = await response.text().catch(() => "");
|
|
1644
|
+
signal.throwIfAborted();
|
|
1645
|
+
return text;
|
|
1646
|
+
}
|
|
1455
1647
|
/** 402 bodies are FastAPI JSON: {"detail": "<human-readable payment text>"}. */
|
|
1456
1648
|
function extract402Detail(bodyText) {
|
|
1457
1649
|
try {
|
|
@@ -1470,4 +1662,26 @@ export function withFinalReply(messages, reply) {
|
|
|
1470
1662
|
return messages;
|
|
1471
1663
|
return [...messages, reply];
|
|
1472
1664
|
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Whether a failed call its caller aborted was cut by that abort: before any answer, or while
|
|
1667
|
+
* a 2xx body was still being read. Any other status is the server's own answer.
|
|
1668
|
+
*/
|
|
1669
|
+
function callerCut(status) {
|
|
1670
|
+
return status === undefined || (status >= 200 && status < 300);
|
|
1671
|
+
}
|
|
1672
|
+
/** `baseMs` plus COMPRESS_TIMEOUT_MS_PER_KIB per KiB of `bytes`. */
|
|
1673
|
+
function scaledTimeout(baseMs, bytes) {
|
|
1674
|
+
if (!Number.isFinite(baseMs))
|
|
1675
|
+
return baseMs;
|
|
1676
|
+
return baseMs + Math.ceil(Math.max(0, bytes) / 1024) * COMPRESS_TIMEOUT_MS_PER_KIB;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* One conversation for background indexing: its session and its first message after the
|
|
1680
|
+
* system prompt, media left out (Claude Code's media eviction keeps it). Subagents share
|
|
1681
|
+
* the session id but not that message.
|
|
1682
|
+
*/
|
|
1683
|
+
function indexLaneKey(sessionId, messages) {
|
|
1684
|
+
const first = messages.find((message) => message.role !== "system");
|
|
1685
|
+
return JSON.stringify([sessionId ?? null, first ? mediaFreeMessageHash(first) : ""]);
|
|
1686
|
+
}
|
|
1473
1687
|
//# sourceMappingURL=memtree.js.map
|