claude-code-infinite 1.0.29 → 1.0.31
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 +65 -7
- 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-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 +61 -16
- 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/hooks.d.ts +24 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -2
- package/dist/hooks.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-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +4 -0
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree.d.ts +86 -3
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +283 -53
- 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/paused-session.d.ts.map +1 -1
- package/dist/paused-session.js +11 -8
- package/dist/paused-session.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/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 +370 -14
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +797 -94
- 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. */
|
|
@@ -773,8 +795,12 @@ export class MemtreeClient {
|
|
|
773
795
|
lastAnswerUnpaid = false;
|
|
774
796
|
/** Whether the latest call that got an answer, or timed out, refused the key (key-page.ts). */
|
|
775
797
|
lastAnswerKeyInvalid = false;
|
|
798
|
+
/** Whether the latest such call succeeded; undefined before any answer. */
|
|
799
|
+
lastAnswerOk;
|
|
776
800
|
/** Complete compression request key → whether its latest failure arms fuse. */
|
|
777
801
|
compressFailureArming = new Map();
|
|
802
|
+
/** HTTP status of each key's latest failed compress (absent: no response). */
|
|
803
|
+
compressFailureStatus = new Map();
|
|
778
804
|
/**
|
|
779
805
|
* Non-null when the server last answered 402 (unpaid MemTree key): the
|
|
780
806
|
* server's human-readable detail text. Set by both compression and
|
|
@@ -792,6 +818,10 @@ export class MemtreeClient {
|
|
|
792
818
|
get pausedDetail() {
|
|
793
819
|
return this.lastAnswerUnpaid ? this.withShortLink(this.unpaidDetail) : null;
|
|
794
820
|
}
|
|
821
|
+
/** Why the key is paused (payment-link.ts REASON_*) while a fresh short link says, else null. */
|
|
822
|
+
get pausedReason() {
|
|
823
|
+
return this.paymentLinks?.current()?.reason ?? null;
|
|
824
|
+
}
|
|
795
825
|
/**
|
|
796
826
|
* Wait at most `ms` for a short checkout link already being fetched, so a
|
|
797
827
|
* notice about to be built from the 402 detail can show it. Never throws.
|
|
@@ -812,6 +842,13 @@ export class MemtreeClient {
|
|
|
812
842
|
get keyInvalid() {
|
|
813
843
|
return this.lastAnswerKeyInvalid;
|
|
814
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Whether MemTree's latest answer (or timeout) was a success: true, a
|
|
847
|
+
* failure: false, no answer yet: undefined. Positive evidence that it works.
|
|
848
|
+
*/
|
|
849
|
+
get answering() {
|
|
850
|
+
return this.lastAnswerOk;
|
|
851
|
+
}
|
|
815
852
|
/** Where to get a new key for this client's environment (its base URL). */
|
|
816
853
|
get keyPageUrl() {
|
|
817
854
|
return keyPageUrl(this.baseUrl);
|
|
@@ -828,6 +865,18 @@ export class MemtreeClient {
|
|
|
828
865
|
get compressBudgetMs() {
|
|
829
866
|
return this.compressTimeoutMs;
|
|
830
867
|
}
|
|
868
|
+
/** The blocking-compress budget for an upload of `bytes` (COMPRESS_TIMEOUT_MS_PER_KIB). */
|
|
869
|
+
compressBudgetForBytes(bytes) {
|
|
870
|
+
return scaledTimeout(this.compressBudgetMs, bytes);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* When a compress call (not an index-only one) last got a live successful
|
|
874
|
+
* answer (epoch ms; 0: never). A compress timing out while others succeed
|
|
875
|
+
* is its size or a slow reply, not an outage.
|
|
876
|
+
*/
|
|
877
|
+
get lastCompressSuccessAt() {
|
|
878
|
+
return this.compressSuccessAt;
|
|
879
|
+
}
|
|
831
880
|
/**
|
|
832
881
|
* Non-secret identity of the MemTree backend and account this client uses:
|
|
833
882
|
* a hash over the origin and a hash of the key. Persisted state is valid only
|
|
@@ -918,6 +967,13 @@ export class MemtreeClient {
|
|
|
918
967
|
lastCompressFailureArming(hash, modelContextLimit, meta) {
|
|
919
968
|
return (this.compressFailureArming.get(this.compressKey(hash, modelContextLimit, meta)) ?? true);
|
|
920
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* The HTTP status of the latest failed compress of this request, sampled at
|
|
972
|
+
* its settle like lastCompressFailureArming; undefined when it got none.
|
|
973
|
+
*/
|
|
974
|
+
lastCompressFailureStatus(hash, modelContextLimit, meta) {
|
|
975
|
+
return this.compressFailureStatus.get(this.compressKey(hash, modelContextLimit, meta));
|
|
976
|
+
}
|
|
921
977
|
/**
|
|
922
978
|
* Blocking user-turn compression. Returns null on ANY failure (timeout,
|
|
923
979
|
* network, 4xx/5xx including 402) — the caller must degrade to passthrough.
|
|
@@ -935,11 +991,15 @@ export class MemtreeClient {
|
|
|
935
991
|
const inFlightKey = this.inFlightKey(hash, modelContextLimit, meta);
|
|
936
992
|
const promise = this.callContextMemory(messages, modelContextLimit, {
|
|
937
993
|
timeoutMs: meta?.timeoutMs ?? this.compressTimeoutMs,
|
|
994
|
+
// An explicit budget is the caller's deadline (a preparation's waiters,
|
|
995
|
+
// the proxy's own wait): kept as given so both end together.
|
|
996
|
+
scaleTimeout: meta?.timeoutMs === undefined,
|
|
938
997
|
signal,
|
|
939
998
|
model: meta?.model,
|
|
940
999
|
tools: meta?.tools,
|
|
941
1000
|
compressionTargetTokens: meta?.compressionTargetTokens,
|
|
942
1001
|
compressionThresholdTokens: meta?.compressionThresholdTokens,
|
|
1002
|
+
prepare: meta?.prepare,
|
|
943
1003
|
clientInputTokens: meta?.clientInputTokens,
|
|
944
1004
|
clientInputTokensExact: meta?.clientInputTokensExact,
|
|
945
1005
|
messageUsage: meta?.messageUsage,
|
|
@@ -963,6 +1023,17 @@ export class MemtreeClient {
|
|
|
963
1023
|
if (first !== undefined)
|
|
964
1024
|
this.compressFailureArming.delete(first);
|
|
965
1025
|
}
|
|
1026
|
+
this.compressFailureStatus.delete(cacheKey);
|
|
1027
|
+
// A body never sent (over the limit) is no answer of the server's: its 413 must not
|
|
1028
|
+
// read as MemTree's refusal to fit the window, which lets the safety net truncate.
|
|
1029
|
+
if (status !== undefined && err?.bodyLimit !== true) {
|
|
1030
|
+
this.compressFailureStatus.set(cacheKey, status);
|
|
1031
|
+
}
|
|
1032
|
+
if (this.compressFailureStatus.size > DEDUPE_CACHE_MAX) {
|
|
1033
|
+
const first = this.compressFailureStatus.keys().next().value;
|
|
1034
|
+
if (first !== undefined)
|
|
1035
|
+
this.compressFailureStatus.delete(first);
|
|
1036
|
+
}
|
|
966
1037
|
// Don't cache failures — drop the entry so retries (e.g. Claude Code's
|
|
967
1038
|
// automatic retry of an identical request) hit the server again.
|
|
968
1039
|
if (this.compressCache.get(cacheKey) === promise) {
|
|
@@ -1028,7 +1099,49 @@ export class MemtreeClient {
|
|
|
1028
1099
|
if (first !== undefined)
|
|
1029
1100
|
this.indexedHashes.delete(first);
|
|
1030
1101
|
}
|
|
1031
|
-
this.
|
|
1102
|
+
this.enqueueIndex(indexLaneKey(sessionId, messages), { messages, modelContextLimit, opts: { sessionId, clientMeta, messageTimesFor } });
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* At most one background index call per conversation. Each call carries the whole
|
|
1106
|
+
* history, so while one uploads only the newest waiting call is worth sending: a long
|
|
1107
|
+
* session's tool rounds otherwise stacked 25 MB uploads, up to 16 at once, and starved
|
|
1108
|
+
* the compress calls that share the link (long-session test, staging 2026-10-24). A
|
|
1109
|
+
* waiting call is replaced only by one whose history extends it; otherwise it goes now.
|
|
1110
|
+
*/
|
|
1111
|
+
enqueueIndex(lane, job) {
|
|
1112
|
+
if (!this.indexLanesBusy.has(lane)) {
|
|
1113
|
+
this.startLaneIndex(lane, job);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const waiting = this.indexLanesNext.get(lane);
|
|
1117
|
+
if (waiting && !extendsHistory(job.messages, waiting.messages)) {
|
|
1118
|
+
void this.submitIndexOnly(waiting.messages, waiting.modelContextLimit, waiting.opts);
|
|
1119
|
+
}
|
|
1120
|
+
this.indexLanesNext.set(lane, job);
|
|
1121
|
+
}
|
|
1122
|
+
startLaneIndex(lane, job) {
|
|
1123
|
+
this.indexLanesBusy.add(lane);
|
|
1124
|
+
void this.submitIndexOnly(job.messages, job.modelContextLimit, job.opts).finally(() => {
|
|
1125
|
+
this.indexLanesBusy.delete(lane);
|
|
1126
|
+
const next = this.indexLanesNext.get(lane);
|
|
1127
|
+
this.indexLanesNext.delete(lane);
|
|
1128
|
+
// At shutdown drainBackground has already sent (or dropped) what was waiting.
|
|
1129
|
+
if (next && !this.backgroundClosing)
|
|
1130
|
+
this.startLaneIndex(lane, next);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* At shutdown: send every waiting call now, inside the drain, except one whose history
|
|
1135
|
+
* the final index carries (a main session's, unless it was rewound since).
|
|
1136
|
+
*/
|
|
1137
|
+
sendWaitingIndexes() {
|
|
1138
|
+
for (const [lane, job] of this.indexLanesNext) {
|
|
1139
|
+
const final = this.finalIndexLanes.get(lane);
|
|
1140
|
+
if (!final || !extendsHistory(final, job.messages)) {
|
|
1141
|
+
void this.submitIndexOnly(job.messages, job.modelContextLimit, job.opts);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
this.indexLanesNext.clear();
|
|
1032
1145
|
}
|
|
1033
1146
|
/**
|
|
1034
1147
|
* The main lane's latest conversation for a session, kept for its final
|
|
@@ -1078,8 +1191,13 @@ export class MemtreeClient {
|
|
|
1078
1191
|
return;
|
|
1079
1192
|
this.finalDrainDeadline ??= Date.now() + Math.max(0, timeoutMs);
|
|
1080
1193
|
const recent = [...this.mainConversations.keys()].slice(-FINAL_INDEX_EXIT_SESSIONS).reverse();
|
|
1081
|
-
for (const sessionId of recent)
|
|
1194
|
+
for (const sessionId of recent) {
|
|
1195
|
+
const conversation = this.mainConversations.get(sessionId);
|
|
1196
|
+
if (conversation) {
|
|
1197
|
+
this.finalIndexLanes.set(indexLaneKey(sessionId, conversation.messages), conversation.messages);
|
|
1198
|
+
}
|
|
1082
1199
|
this.fireFinalIndex(sessionId, true);
|
|
1200
|
+
}
|
|
1083
1201
|
for (const sessionId of this.pendingFinals.keys())
|
|
1084
1202
|
this.cancelFinalIndex(sessionId);
|
|
1085
1203
|
}
|
|
@@ -1110,7 +1228,7 @@ export class MemtreeClient {
|
|
|
1110
1228
|
}
|
|
1111
1229
|
async sendFinalIndex(sessionId, conversation, reader, signal) {
|
|
1112
1230
|
const reply = await readFinalReply(reader, signal);
|
|
1113
|
-
const prepared = await prepareFinalMessages(withFinalReply(conversation.messages, reply), signal);
|
|
1231
|
+
const prepared = await prepareFinalMessages(withFinalReply(conversation.messages, reply), signal, (message) => this.imageRefs.referenceMessage(message));
|
|
1114
1232
|
if (this.mainConversations.get(sessionId) !== conversation ||
|
|
1115
1233
|
this.finalIndexed.get(sessionId) === prepared.key)
|
|
1116
1234
|
return;
|
|
@@ -1120,7 +1238,7 @@ export class MemtreeClient {
|
|
|
1120
1238
|
await this.callContextMemory(prepared.messages, conversation.modelContextLimit, {
|
|
1121
1239
|
sessionId, clientMeta: conversation.clientMeta, messageTimes,
|
|
1122
1240
|
indexOnly: true, finalIndex: true, signal, timeoutMs: INDEX_TIMEOUT_MS,
|
|
1123
|
-
serializedMessages: prepared.serialized,
|
|
1241
|
+
serializedMessages: prepared.serialized, serializedImageRefs: prepared.refs,
|
|
1124
1242
|
});
|
|
1125
1243
|
}
|
|
1126
1244
|
submitIndexOnly(messages, modelContextLimit, opts) {
|
|
@@ -1152,7 +1270,7 @@ export class MemtreeClient {
|
|
|
1152
1270
|
this.backgroundIndexes.delete(operation);
|
|
1153
1271
|
});
|
|
1154
1272
|
this.backgroundIndexes.set(operation, controller);
|
|
1155
|
-
|
|
1273
|
+
return operation;
|
|
1156
1274
|
}
|
|
1157
1275
|
/**
|
|
1158
1276
|
* Stop accepting background indexes and wait boundedly for those already in
|
|
@@ -1161,7 +1279,10 @@ export class MemtreeClient {
|
|
|
1161
1279
|
* The boolean is true for a graceful drain and false when abort was needed.
|
|
1162
1280
|
*/
|
|
1163
1281
|
async drainBackground(timeoutMs = 2_000) {
|
|
1164
|
-
this.backgroundClosing
|
|
1282
|
+
if (!this.backgroundClosing) {
|
|
1283
|
+
this.backgroundClosing = true;
|
|
1284
|
+
this.sendWaitingIndexes();
|
|
1285
|
+
}
|
|
1165
1286
|
if (this.backgroundIndexes.size === 0)
|
|
1166
1287
|
return true;
|
|
1167
1288
|
const boundedMs = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
@@ -1181,6 +1302,7 @@ export class MemtreeClient {
|
|
|
1181
1302
|
clearTimeout(timer);
|
|
1182
1303
|
if (completed)
|
|
1183
1304
|
return true;
|
|
1305
|
+
this.drainCutting = true;
|
|
1184
1306
|
for (const controller of this.backgroundIndexes.values()) {
|
|
1185
1307
|
controller.abort();
|
|
1186
1308
|
}
|
|
@@ -1216,6 +1338,7 @@ export class MemtreeClient {
|
|
|
1216
1338
|
meta?.compressionTargetTokens ?? null,
|
|
1217
1339
|
meta?.compressionThresholdTokens ?? null,
|
|
1218
1340
|
meta?.clientInputTokens !== undefined && meta.clientInputTokensExact === true,
|
|
1341
|
+
meta?.prepare === true,
|
|
1219
1342
|
];
|
|
1220
1343
|
}
|
|
1221
1344
|
/**
|
|
@@ -1300,6 +1423,8 @@ export class MemtreeClient {
|
|
|
1300
1423
|
if (opts.compressionThresholdTokens !== undefined) {
|
|
1301
1424
|
body.compression_threshold_tokens = opts.compressionThresholdTokens;
|
|
1302
1425
|
}
|
|
1426
|
+
if (opts.prepare)
|
|
1427
|
+
body.prepare = true;
|
|
1303
1428
|
if (opts.clientInputTokens !== undefined) {
|
|
1304
1429
|
body.client_input_tokens = opts.clientInputTokens;
|
|
1305
1430
|
if (opts.clientInputTokensExact)
|
|
@@ -1323,14 +1448,20 @@ export class MemtreeClient {
|
|
|
1323
1448
|
// (within its image allowance) instead of naming them in placeholders.
|
|
1324
1449
|
body.flatten_images = true;
|
|
1325
1450
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1451
|
+
// Stored images as references, and never a body over the HTTP/1 limit (memtree-body.ts).
|
|
1452
|
+
const plan = (refs) => planMemtreeBody({
|
|
1453
|
+
body, messages, indexOnly: opts.indexOnly === true,
|
|
1454
|
+
serialized: refs && opts.serializedMessages !== undefined
|
|
1455
|
+
? { value: opts.serializedMessages, refs: opts.serializedImageRefs ?? [] } : undefined,
|
|
1456
|
+
imageRefs: refs ? this.imageRefs : undefined,
|
|
1457
|
+
});
|
|
1458
|
+
const planned = plan(opts.imageRefs !== false);
|
|
1459
|
+
if (planned.payload === undefined)
|
|
1460
|
+
throw this.oversized(planned.limit, opts);
|
|
1461
|
+
const payload = planned.payload;
|
|
1462
|
+
// The answer named references it could not serve: sent again as its own call.
|
|
1463
|
+
let resent = false;
|
|
1464
|
+
let refusalText;
|
|
1334
1465
|
opts.signal?.throwIfAborted();
|
|
1335
1466
|
const started = Date.now();
|
|
1336
1467
|
let status;
|
|
@@ -1339,35 +1470,35 @@ export class MemtreeClient {
|
|
|
1339
1470
|
let diagnostics = {};
|
|
1340
1471
|
const controller = new AbortController();
|
|
1341
1472
|
const abort = () => controller.abort();
|
|
1342
|
-
|
|
1473
|
+
// A default compress budget grows with what it uploads; an index call's,
|
|
1474
|
+
// and any budget a caller set, is fixed.
|
|
1475
|
+
const timeoutMs = opts.scaleTimeout && !opts.indexOnly
|
|
1476
|
+
? scaledTimeout(opts.timeoutMs, Buffer.byteLength(payload, "utf-8")) : opts.timeoutMs;
|
|
1477
|
+
const timeout = setTimeout(abort, timeoutMs);
|
|
1343
1478
|
timeout.unref();
|
|
1344
1479
|
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
1345
1480
|
if (opts.signal?.aborted)
|
|
1346
1481
|
abort();
|
|
1347
1482
|
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
|
-
});
|
|
1483
|
+
const response = await this.postContextMemory(payload, opts, controller.signal);
|
|
1368
1484
|
status = response.status;
|
|
1485
|
+
if (planned.refs.length && !response.ok) {
|
|
1486
|
+
refusalText = await readUnlessAborted(response, controller.signal);
|
|
1487
|
+
if (this.refusedRefs(response.status, refusalText, planned.refs)) {
|
|
1488
|
+
// A new call with bytes: its own size and request-log record, within what is
|
|
1489
|
+
// left of this call's budget (a caller's deadline is never extended). Read when
|
|
1490
|
+
// its timer starts, after its body is built, and never clamped: a size-scaled
|
|
1491
|
+
// budget adds its allowance to what is left, however negative.
|
|
1492
|
+
resent = true;
|
|
1493
|
+
const budget = opts.timeoutMs;
|
|
1494
|
+
return await this.callContextMemory(messages, modelContextLimit, {
|
|
1495
|
+
...opts, imageRefs: false,
|
|
1496
|
+
get timeoutMs() { return budget - (Date.now() - started); },
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1369
1500
|
if (!response.ok) {
|
|
1370
|
-
const text = await response.text().catch(() => "");
|
|
1501
|
+
const text = refusalText ?? await response.text().catch(() => "");
|
|
1371
1502
|
if (response.status === 402) {
|
|
1372
1503
|
this.unpaidDetail = extract402Detail(text);
|
|
1373
1504
|
// Shared, cached and backed off: one request per hour at most while paused.
|
|
@@ -1379,6 +1510,7 @@ export class MemtreeClient {
|
|
|
1379
1510
|
throw Object.assign(new Error(`context_memory ${response.status}: ${text.slice(0, 300)}`), { status: response.status });
|
|
1380
1511
|
}
|
|
1381
1512
|
const json = (await response.json());
|
|
1513
|
+
this.imageRefs.noteAnswer(json);
|
|
1382
1514
|
// An index-only ack is `{ messages: [], usage: {...zeros}, index_only:
|
|
1383
1515
|
// true }` by contract: the server skips compression and returns no
|
|
1384
1516
|
// messages. Only a compress call needs a non-empty conversation back.
|
|
@@ -1393,6 +1525,8 @@ export class MemtreeClient {
|
|
|
1393
1525
|
const memtreeIndex = response.headers.get("x-polychat-memtree-index") ?? undefined;
|
|
1394
1526
|
this.unpaidDetail = null; // a success proves the key is paid (again)
|
|
1395
1527
|
ok = true;
|
|
1528
|
+
if (!opts.indexOnly)
|
|
1529
|
+
this.compressSuccessAt = Date.now();
|
|
1396
1530
|
diagnostics = {
|
|
1397
1531
|
...(memtreeUrl ? { memtreeUrl } : {}),
|
|
1398
1532
|
...(memtreeIndex ? { memtreeIndex } : {}),
|
|
@@ -1425,33 +1559,107 @@ export class MemtreeClient {
|
|
|
1425
1559
|
// A background index call that failed otherwise (a timeout, a 413 on a big
|
|
1426
1560
|
// body) is no news about payment; a compress call's answer always is.
|
|
1427
1561
|
// A refused key is news from any call, like a 402.
|
|
1428
|
-
const news = status === 402 || keyRefused || ok || !opts.indexOnly;
|
|
1562
|
+
const news = !resent && (status === 402 || keyRefused || ok || !opts.indexOnly);
|
|
1429
1563
|
if (news && (status !== undefined || !opts.signal?.aborted)) {
|
|
1430
1564
|
this.lastAnswerUnpaid = status === 402;
|
|
1431
1565
|
this.lastAnswerKeyInvalid = keyRefused;
|
|
1432
1566
|
}
|
|
1567
|
+
// Availability, unlike payment, counts a failed background index too.
|
|
1568
|
+
if (!resent && (status !== undefined || !opts.signal?.aborted))
|
|
1569
|
+
this.lastAnswerOk = ok;
|
|
1433
1570
|
// One JSONL line per MemTree call, success or failure; `status` stays
|
|
1434
1571
|
// absent when the call never got a response (network error/timeout).
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1572
|
+
if (!resent)
|
|
1573
|
+
this.reqlog?.log({
|
|
1574
|
+
kind: "memtree",
|
|
1575
|
+
indexOnly: opts.indexOnly === true,
|
|
1576
|
+
ms: Date.now() - started,
|
|
1577
|
+
ok,
|
|
1578
|
+
status,
|
|
1579
|
+
requestBytes: Buffer.byteLength(payload),
|
|
1580
|
+
...(planned.limit ? { bodyLimit: planned.limit } : {}),
|
|
1581
|
+
// Aborted by an exit drain that ran out: a known cut, not a server failure.
|
|
1582
|
+
...(!ok && this.drainCutting && controller.signal.aborted
|
|
1583
|
+
? { exitCut: true, finalIndex: opts.finalIndex === true }
|
|
1584
|
+
: !ok && opts.signal?.aborted && callerCut(status) ? { callerAborted: true } : {}),
|
|
1585
|
+
// Present only when the request carried a model for server-side
|
|
1586
|
+
// budget resolution — the client-visible half of the server's
|
|
1587
|
+
// "/v1/context_memory budget" log line.
|
|
1588
|
+
...(opts.model && !opts.indexOnly ? { model: opts.model } : {}),
|
|
1589
|
+
...diagnostics,
|
|
1590
|
+
});
|
|
1448
1591
|
}
|
|
1449
1592
|
}
|
|
1593
|
+
/** One POST of a MemTree call's body. */
|
|
1594
|
+
postContextMemory(payload, opts, signal) {
|
|
1595
|
+
return fetch(`${this.baseUrl}/v1/context_memory`, {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
headers: {
|
|
1598
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
1599
|
+
"content-type": "application/json",
|
|
1600
|
+
"x-client": CLIENT_NAME,
|
|
1601
|
+
"x-client-version": CLIENT_VERSION,
|
|
1602
|
+
"x-client-features": CLIENT_FEATURES,
|
|
1603
|
+
...(opts.sessionId ? { "x-claude-code-session-id": opts.sessionId } : {}),
|
|
1604
|
+
...(opts.clientMeta && Object.keys(opts.clientMeta).length
|
|
1605
|
+
? { "x-client-meta": JSON.stringify(opts.clientMeta) }
|
|
1606
|
+
: {}),
|
|
1607
|
+
// Compress calls only: an index-only call returns no memory.
|
|
1608
|
+
...(this.memtreeTools && !opts.indexOnly
|
|
1609
|
+
? { "x-memtree-tools": this.memtreeTools }
|
|
1610
|
+
: {}),
|
|
1611
|
+
},
|
|
1612
|
+
body: payload,
|
|
1613
|
+
signal,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Whether a failed answer to a call that sent references asks for their bytes: the
|
|
1618
|
+
* server lacks some images (409: those are forgotten), or refuses references (400:
|
|
1619
|
+
* this request's forgotten, or none ever again when the key may not send them).
|
|
1620
|
+
*/
|
|
1621
|
+
refusedRefs(status, text, refs) {
|
|
1622
|
+
const missing = status === MISSING_IMAGES_STATUS ? missingImageShas(text) : undefined;
|
|
1623
|
+
const refused = refusesImageRefs(status, text);
|
|
1624
|
+
if (missing !== undefined) {
|
|
1625
|
+
this.imageRefs.forget(missing.length ? missing : refs);
|
|
1626
|
+
}
|
|
1627
|
+
else if (refused === "never") {
|
|
1628
|
+
this.imageRefs.disable();
|
|
1629
|
+
}
|
|
1630
|
+
else if (refused === "this") {
|
|
1631
|
+
this.imageRefs.forget(refs);
|
|
1632
|
+
}
|
|
1633
|
+
else {
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
this.log(`context_memory ${status} on image references: sending their bytes`);
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* A body over the limit that nothing could shrink (memtree-body.ts): never sent.
|
|
1641
|
+
* Logged like any call, with `bodyLimit` (memtree_body_limit, not memtree_error); the
|
|
1642
|
+
* error carries 413, the status the server's front end would have answered.
|
|
1643
|
+
*/
|
|
1644
|
+
oversized(limit, opts) {
|
|
1645
|
+
this.reqlog?.log({
|
|
1646
|
+
kind: "memtree", indexOnly: opts.indexOnly === true, ms: 0, ok: false,
|
|
1647
|
+
requestBytes: limit?.requestBytes ?? 0, ...(limit ? { bodyLimit: limit } : {}),
|
|
1648
|
+
});
|
|
1649
|
+
this.log(`context_memory body of ${limit?.requestBytes ?? "?"} bytes over the limit: not sent`);
|
|
1650
|
+
return Object.assign(new Error("context_memory body over the size limit (not sent)"), { status: 413, bodyLimit: true });
|
|
1651
|
+
}
|
|
1450
1652
|
log(msg) {
|
|
1451
1653
|
if (this.debug)
|
|
1452
1654
|
console.error(`[ccc proxy] ${msg}`);
|
|
1453
1655
|
}
|
|
1454
1656
|
}
|
|
1657
|
+
/** A failed answer's body; an abort while reading it is thrown, never read as "". */
|
|
1658
|
+
async function readUnlessAborted(response, signal) {
|
|
1659
|
+
const text = await response.text().catch(() => "");
|
|
1660
|
+
signal.throwIfAborted();
|
|
1661
|
+
return text;
|
|
1662
|
+
}
|
|
1455
1663
|
/** 402 bodies are FastAPI JSON: {"detail": "<human-readable payment text>"}. */
|
|
1456
1664
|
function extract402Detail(bodyText) {
|
|
1457
1665
|
try {
|
|
@@ -1470,4 +1678,26 @@ export function withFinalReply(messages, reply) {
|
|
|
1470
1678
|
return messages;
|
|
1471
1679
|
return [...messages, reply];
|
|
1472
1680
|
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Whether a failed call its caller aborted was cut by that abort: before any answer, or while
|
|
1683
|
+
* a 2xx body was still being read. Any other status is the server's own answer.
|
|
1684
|
+
*/
|
|
1685
|
+
function callerCut(status) {
|
|
1686
|
+
return status === undefined || (status >= 200 && status < 300);
|
|
1687
|
+
}
|
|
1688
|
+
/** `baseMs` plus COMPRESS_TIMEOUT_MS_PER_KIB per KiB of `bytes`. */
|
|
1689
|
+
function scaledTimeout(baseMs, bytes) {
|
|
1690
|
+
if (!Number.isFinite(baseMs))
|
|
1691
|
+
return baseMs;
|
|
1692
|
+
return baseMs + Math.ceil(Math.max(0, bytes) / 1024) * COMPRESS_TIMEOUT_MS_PER_KIB;
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* One conversation for background indexing: its session and its first message after the
|
|
1696
|
+
* system prompt, media left out (Claude Code's media eviction keeps it). Subagents share
|
|
1697
|
+
* the session id but not that message.
|
|
1698
|
+
*/
|
|
1699
|
+
function indexLaneKey(sessionId, messages) {
|
|
1700
|
+
const first = messages.find((message) => message.role !== "system");
|
|
1701
|
+
return JSON.stringify([sessionId ?? null, first ? mediaFreeMessageHash(first) : ""]);
|
|
1702
|
+
}
|
|
1473
1703
|
//# sourceMappingURL=memtree.js.map
|