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.
Files changed (76) hide show
  1. package/README.md +57 -7
  2. package/dist/cli-lifecycle.d.ts +45 -1
  3. package/dist/cli-lifecycle.d.ts.map +1 -1
  4. package/dist/cli-lifecycle.js +59 -0
  5. package/dist/cli-lifecycle.js.map +1 -1
  6. package/dist/cli-main.js +49 -15
  7. package/dist/cli-main.js.map +1 -1
  8. package/dist/count-tokens.d.ts +15 -0
  9. package/dist/count-tokens.d.ts.map +1 -1
  10. package/dist/count-tokens.js +23 -1
  11. package/dist/count-tokens.js.map +1 -1
  12. package/dist/diagnostics-schema.d.ts.map +1 -1
  13. package/dist/diagnostics-schema.js +5 -0
  14. package/dist/diagnostics-schema.js.map +1 -1
  15. package/dist/diagnostics.d.ts.map +1 -1
  16. package/dist/diagnostics.js +18 -1
  17. package/dist/diagnostics.js.map +1 -1
  18. package/dist/final-index.d.ts +7 -1
  19. package/dist/final-index.d.ts.map +1 -1
  20. package/dist/final-index.js +10 -3
  21. package/dist/final-index.js.map +1 -1
  22. package/dist/image-refs.d.ts +51 -0
  23. package/dist/image-refs.d.ts.map +1 -0
  24. package/dist/image-refs.js +169 -0
  25. package/dist/image-refs.js.map +1 -0
  26. package/dist/image-tokens.d.ts +37 -0
  27. package/dist/image-tokens.d.ts.map +1 -0
  28. package/dist/image-tokens.js +158 -0
  29. package/dist/image-tokens.js.map +1 -0
  30. package/dist/memtree-body.d.ts +52 -0
  31. package/dist/memtree-body.d.ts.map +1 -0
  32. package/dist/memtree-body.js +99 -0
  33. package/dist/memtree-body.js.map +1 -0
  34. package/dist/memtree.d.ts +77 -3
  35. package/dist/memtree.d.ts.map +1 -1
  36. package/dist/memtree.js +267 -53
  37. package/dist/memtree.js.map +1 -1
  38. package/dist/paused-session.d.ts.map +1 -1
  39. package/dist/paused-session.js +11 -8
  40. package/dist/paused-session.js.map +1 -1
  41. package/dist/prefix-store.d.ts +2 -0
  42. package/dist/prefix-store.d.ts.map +1 -1
  43. package/dist/prefix-store.js +4 -0
  44. package/dist/prefix-store.js.map +1 -1
  45. package/dist/prepared-compression.d.ts +78 -0
  46. package/dist/prepared-compression.d.ts.map +1 -0
  47. package/dist/prepared-compression.js +127 -0
  48. package/dist/prepared-compression.js.map +1 -0
  49. package/dist/proxy.d.ts +363 -14
  50. package/dist/proxy.d.ts.map +1 -1
  51. package/dist/proxy.js +708 -90
  52. package/dist/proxy.js.map +1 -1
  53. package/dist/reqlog.d.ts +55 -0
  54. package/dist/reqlog.d.ts.map +1 -1
  55. package/dist/reqlog.js.map +1 -1
  56. package/dist/request-sizing.d.ts +21 -2
  57. package/dist/request-sizing.d.ts.map +1 -1
  58. package/dist/request-sizing.js +57 -11
  59. package/dist/request-sizing.js.map +1 -1
  60. package/dist/route-identity.d.ts +27 -0
  61. package/dist/route-identity.d.ts.map +1 -1
  62. package/dist/route-identity.js +88 -8
  63. package/dist/route-identity.js.map +1 -1
  64. package/dist/token-estimate.d.ts +3 -20
  65. package/dist/token-estimate.d.ts.map +1 -1
  66. package/dist/token-estimate.js +9 -50
  67. package/dist/token-estimate.js.map +1 -1
  68. package/dist/tool-output-store.d.ts +12 -0
  69. package/dist/tool-output-store.d.ts.map +1 -0
  70. package/dist/tool-output-store.js +146 -0
  71. package/dist/tool-output-store.js.map +1 -0
  72. package/dist/tool-result-truncation.d.ts +32 -0
  73. package/dist/tool-result-truncation.d.ts.map +1 -0
  74. package/dist/tool-result-truncation.js +135 -0
  75. package/dist/tool-result-truncation.js.map +1 -0
  76. 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 compress calls. `near-window-budget`: ccc lifts
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 = "near-window-budget";
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.submitIndexOnly(messages, modelContextLimit, { sessionId, clientMeta, messageTimesFor });
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
- void operation;
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 = true;
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
- let payload;
1327
- if (opts.serializedMessages !== undefined) {
1328
- delete body.messages;
1329
- payload = JSON.stringify(body).slice(0, -1) + ',"messages":' + opts.serializedMessages + "}";
1330
- }
1331
- else {
1332
- payload = JSON.stringify(body);
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
- const timeout = setTimeout(abort, opts.timeoutMs);
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 fetch(`${this.baseUrl}/v1/context_memory`, {
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
- this.reqlog?.log({
1436
- kind: "memtree",
1437
- indexOnly: opts.indexOnly === true,
1438
- ms: Date.now() - started,
1439
- ok,
1440
- status,
1441
- requestBytes: Buffer.byteLength(payload),
1442
- // Present only when the request carried a model for server-side
1443
- // budget resolution — the client-visible half of the server's
1444
- // "/v1/context_memory budget" log line.
1445
- ...(opts.model && !opts.indexOnly ? { model: opts.model } : {}),
1446
- ...diagnostics,
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