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.
Files changed (95) hide show
  1. package/README.md +65 -7
  2. package/dist/cli-args.d.ts +10 -0
  3. package/dist/cli-args.d.ts.map +1 -1
  4. package/dist/cli-args.js +30 -0
  5. package/dist/cli-args.js.map +1 -1
  6. package/dist/cli-lifecycle.d.ts +45 -1
  7. package/dist/cli-lifecycle.d.ts.map +1 -1
  8. package/dist/cli-lifecycle.js +59 -0
  9. package/dist/cli-lifecycle.js.map +1 -1
  10. package/dist/cli-main.js +61 -16
  11. package/dist/cli-main.js.map +1 -1
  12. package/dist/count-tokens.d.ts +15 -0
  13. package/dist/count-tokens.d.ts.map +1 -1
  14. package/dist/count-tokens.js +23 -1
  15. package/dist/count-tokens.js.map +1 -1
  16. package/dist/diagnostics-schema.d.ts.map +1 -1
  17. package/dist/diagnostics-schema.js +5 -0
  18. package/dist/diagnostics-schema.js.map +1 -1
  19. package/dist/diagnostics.d.ts.map +1 -1
  20. package/dist/diagnostics.js +18 -1
  21. package/dist/diagnostics.js.map +1 -1
  22. package/dist/final-index.d.ts +7 -1
  23. package/dist/final-index.d.ts.map +1 -1
  24. package/dist/final-index.js +10 -3
  25. package/dist/final-index.js.map +1 -1
  26. package/dist/hooks.d.ts +24 -1
  27. package/dist/hooks.d.ts.map +1 -1
  28. package/dist/hooks.js +24 -2
  29. package/dist/hooks.js.map +1 -1
  30. package/dist/image-refs.d.ts +51 -0
  31. package/dist/image-refs.d.ts.map +1 -0
  32. package/dist/image-refs.js +169 -0
  33. package/dist/image-refs.js.map +1 -0
  34. package/dist/image-tokens.d.ts +37 -0
  35. package/dist/image-tokens.d.ts.map +1 -0
  36. package/dist/image-tokens.js +158 -0
  37. package/dist/image-tokens.js.map +1 -0
  38. package/dist/memtree-body.d.ts +52 -0
  39. package/dist/memtree-body.d.ts.map +1 -0
  40. package/dist/memtree-body.js +99 -0
  41. package/dist/memtree-body.js.map +1 -0
  42. package/dist/memtree-mcp.d.ts.map +1 -1
  43. package/dist/memtree-mcp.js +4 -0
  44. package/dist/memtree-mcp.js.map +1 -1
  45. package/dist/memtree.d.ts +86 -3
  46. package/dist/memtree.d.ts.map +1 -1
  47. package/dist/memtree.js +283 -53
  48. package/dist/memtree.js.map +1 -1
  49. package/dist/model-context.d.ts +80 -0
  50. package/dist/model-context.d.ts.map +1 -0
  51. package/dist/model-context.js +192 -0
  52. package/dist/model-context.js.map +1 -0
  53. package/dist/paused-session.d.ts.map +1 -1
  54. package/dist/paused-session.js +11 -8
  55. package/dist/paused-session.js.map +1 -1
  56. package/dist/payment-gate.d.ts +2 -0
  57. package/dist/payment-gate.d.ts.map +1 -1
  58. package/dist/payment-gate.js +1 -1
  59. package/dist/payment-gate.js.map +1 -1
  60. package/dist/prefix-store.d.ts +2 -0
  61. package/dist/prefix-store.d.ts.map +1 -1
  62. package/dist/prefix-store.js +4 -0
  63. package/dist/prefix-store.js.map +1 -1
  64. package/dist/prepared-compression.d.ts +78 -0
  65. package/dist/prepared-compression.d.ts.map +1 -0
  66. package/dist/prepared-compression.js +127 -0
  67. package/dist/prepared-compression.js.map +1 -0
  68. package/dist/proxy.d.ts +370 -14
  69. package/dist/proxy.d.ts.map +1 -1
  70. package/dist/proxy.js +797 -94
  71. package/dist/proxy.js.map +1 -1
  72. package/dist/reqlog.d.ts +55 -0
  73. package/dist/reqlog.d.ts.map +1 -1
  74. package/dist/reqlog.js.map +1 -1
  75. package/dist/request-sizing.d.ts +21 -2
  76. package/dist/request-sizing.d.ts.map +1 -1
  77. package/dist/request-sizing.js +57 -11
  78. package/dist/request-sizing.js.map +1 -1
  79. package/dist/route-identity.d.ts +27 -0
  80. package/dist/route-identity.d.ts.map +1 -1
  81. package/dist/route-identity.js +88 -8
  82. package/dist/route-identity.js.map +1 -1
  83. package/dist/token-estimate.d.ts +3 -20
  84. package/dist/token-estimate.d.ts.map +1 -1
  85. package/dist/token-estimate.js +9 -50
  86. package/dist/token-estimate.js.map +1 -1
  87. package/dist/tool-output-store.d.ts +12 -0
  88. package/dist/tool-output-store.d.ts.map +1 -0
  89. package/dist/tool-output-store.js +146 -0
  90. package/dist/tool-output-store.js.map +1 -0
  91. package/dist/tool-result-truncation.d.ts +32 -0
  92. package/dist/tool-result-truncation.d.ts.map +1 -0
  93. package/dist/tool-result-truncation.js +135 -0
  94. package/dist/tool-result-truncation.js.map +1 -0
  95. 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. */
@@ -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.submitIndexOnly(messages, modelContextLimit, { sessionId, clientMeta, messageTimesFor });
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
- void operation;
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 = true;
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
- 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
- }
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
- const timeout = setTimeout(abort, opts.timeoutMs);
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 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
- });
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
- 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
- });
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