@pyxmate/memory 1.17.15 → 1.17.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import {
2
+ assertGraphExtractionPayload,
2
3
  mergeExtractedEntities
3
- } from "./chunk-A3L46P2G.mjs";
4
+ } from "./chunk-3OLH3HYR.mjs";
4
5
 
5
6
  // ../shared/src/constants/defaults.ts
6
7
  var DEFAULTS = {
@@ -10,6 +11,21 @@ var DEFAULTS = {
10
11
  };
11
12
  var TAXONOMY_MAX_CATEGORIES = 10;
12
13
 
14
+ // ../shared/src/document-source.ts
15
+ import { createHash } from "crypto";
16
+ function documentSource(prefix, documentKey) {
17
+ return `${prefix}:${createHash("sha256").update(documentKey).digest("hex")}`;
18
+ }
19
+ function documentGraphSource(documentKey) {
20
+ return documentSource("document-graph", documentKey);
21
+ }
22
+ function documentContentSource(documentKey) {
23
+ return documentSource("document-content", documentKey);
24
+ }
25
+ function documentImageSource(documentKey) {
26
+ return documentSource("document-image", documentKey);
27
+ }
28
+
13
29
  // ../shared/src/mcp/search-response.ts
14
30
  var STRIPPED_ENTRY_FIELDS = /* @__PURE__ */ new Set(["contentHash", "embedding", "tenantId", "userId", "teamId"]);
15
31
  function isRecord(value) {
@@ -227,12 +243,54 @@ var DisabledMemory = class {
227
243
  };
228
244
 
229
245
  // ../client/src/memory-client.ts
246
+ function isRecord3(value) {
247
+ return value !== null && typeof value === "object" && !Array.isArray(value);
248
+ }
249
+ function normalizeRetryAfter(retryAfter, retryAfterSeconds) {
250
+ const exact = typeof retryAfter === "string" ? retryAfter.trim() : "";
251
+ if (exact) {
252
+ const parsed = /^\d+$/.test(exact) ? Number(exact) : Math.ceil((Date.parse(exact) - Date.now()) / 1e3);
253
+ return {
254
+ retryAfter: exact,
255
+ ...Number.isSafeInteger(parsed) && parsed >= 0 ? { retryAfterSeconds: parsed } : {}
256
+ };
257
+ }
258
+ return Number.isSafeInteger(retryAfterSeconds) && retryAfterSeconds >= 0 ? { retryAfterSeconds } : {};
259
+ }
260
+ function structuralHttpStatus(error) {
261
+ if (!isRecord3(error) || typeof error.status !== "number") return void 0;
262
+ return Number.isSafeInteger(error.status) && error.status >= 400 && error.status < 600 ? error.status : void 0;
263
+ }
264
+ function isFileIngestResult(value) {
265
+ if (!isRecord3(value)) return false;
266
+ if (typeof value.filename !== "string" || typeof value.fileType !== "string" || typeof value.chunks !== "number" || !Number.isSafeInteger(value.chunks) || value.chunks < 0 || !Array.isArray(value.entryIds) || !value.entryIds.every((id) => typeof id === "string") || typeof value.totalCharacters !== "number" || !Number.isSafeInteger(value.totalCharacters) || value.totalCharacters < 0) {
267
+ return false;
268
+ }
269
+ if (value.graphAnchorEntryId !== void 0 && typeof value.graphAnchorEntryId !== "string") {
270
+ return false;
271
+ }
272
+ if (value.enrichment !== void 0 && !isRecord3(value.enrichment)) return false;
273
+ return true;
274
+ }
275
+ function hasEnrichmentCallback(callbacks) {
276
+ return Boolean(callbacks?.describeImage || callbacks?.extractEntitiesV2);
277
+ }
230
278
  var MemoryServerError = class extends Error {
231
279
  status;
232
- constructor(message, status) {
280
+ /** Stable machine-readable discriminator returned by the memory server. */
281
+ code;
282
+ /** Exact HTTP Retry-After value returned by the memory server. */
283
+ retryAfter;
284
+ /** Retry delay normalized to seconds when Retry-After is parseable. */
285
+ retryAfterSeconds;
286
+ constructor(message, status, code, retryAfter) {
233
287
  super(message);
234
288
  this.name = "MemoryServerError";
235
289
  this.status = status;
290
+ this.code = code;
291
+ const retry = normalizeRetryAfter(retryAfter);
292
+ this.retryAfter = retry.retryAfter;
293
+ this.retryAfterSeconds = retry.retryAfterSeconds;
236
294
  }
237
295
  /** True when the server returned HTTP 404 (not found). */
238
296
  get isNotFound() {
@@ -272,12 +330,33 @@ var MemoryClient = class {
272
330
  }
273
331
  authorityHeaders(options) {
274
332
  if (!options) return {};
333
+ const headers = {};
275
334
  const key = options.idempotencyKey;
276
- if (key === void 0) return {};
277
- if (typeof key !== "string" || key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
278
- throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
335
+ if (key !== void 0) {
336
+ if (typeof key !== "string" || key.length === 0 || !key.isWellFormed() || new TextEncoder().encode(key).byteLength > 256) {
337
+ throw new Error("idempotencyKey must be 1-256 well-formed UTF-8 bytes");
338
+ }
339
+ headers["Idempotency-Key"] = key;
340
+ }
341
+ for (const [headerName, value] of [
342
+ ["X-Tenant-Id", options.tenantId],
343
+ ["X-Namespace-Id", options.namespaceId]
344
+ ]) {
345
+ if (value === void 0) continue;
346
+ const configured = Object.entries(this._authHeaders).find(
347
+ ([name]) => name.toLowerCase() === headerName.toLowerCase()
348
+ )?.[1];
349
+ if (configured !== void 0) {
350
+ if (configured !== value) {
351
+ throw new Error(
352
+ `${headerName} conflicts with the MemoryClient default scope; create a client for the requested scope`
353
+ );
354
+ }
355
+ continue;
356
+ }
357
+ headers[headerName] = value;
279
358
  }
280
- return { "Idempotency-Key": key };
359
+ return headers;
281
360
  }
282
361
  async initialize() {
283
362
  const response = await fetch(`${this.baseUrl}/health`, {
@@ -424,8 +503,17 @@ var MemoryClient = class {
424
503
  * collect the terminal event; there is no separate `ingestFile()` Promise
425
504
  * method by design (one wire format, one SDK method).
426
505
  */
427
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: NDJSON dispatch + SDK enrichment fan-out is two cooperating paths; documented inline and tested in memory-client-events.test.ts.
428
506
  async *ingestFileEvents(file, options) {
507
+ if (options?.documentKey !== void 0) {
508
+ if (typeof options.documentKey !== "string" || options.documentKey.trim().length === 0) {
509
+ throw new Error("ingestFileEvents requires a non-empty documentKey when provided");
510
+ }
511
+ if (!options.enrichment?.extractEntitiesV2) {
512
+ throw new Error(
513
+ "ingestFileEvents with documentKey requires enrichment.extractEntitiesV2 so the stable graph replacement can be finalized"
514
+ );
515
+ }
516
+ }
429
517
  const controller = new AbortController();
430
518
  const relayAbort = () => controller.abort(options?.signal?.reason);
431
519
  if (options?.signal?.aborted) relayAbort();
@@ -435,243 +523,434 @@ var MemoryClient = class {
435
523
  formData.append("file", file);
436
524
  if (options?.description) formData.append("description", options.description);
437
525
  if (options?.namespaceId) formData.append("namespaceId", options.namespaceId);
526
+ if (options?.documentKey !== void 0) {
527
+ formData.append("documentKey", options.documentKey);
528
+ }
529
+ if (options?.catalogEntryId !== void 0) {
530
+ formData.append("catalogEntryId", options.catalogEntryId);
531
+ }
438
532
  const wantsTextWindows = Boolean(options?.enrichment?.extractEntitiesV2);
439
533
  const headers = {
440
534
  Accept: "application/x-ndjson",
441
535
  ...this._authHeaders
442
536
  };
443
537
  if (wantsTextWindows) headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
444
- let res;
445
- try {
446
- res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
447
- method: "POST",
448
- body: formData,
449
- headers,
450
- signal: controller.signal
451
- });
452
- } catch (err) {
453
- yield this.ingestErrorEvent(
454
- this.translateFetchError(err, "/api/memory/ingest/file"),
455
- "parsing"
456
- );
457
- return;
458
- }
459
- const contentType = res.headers.get("content-type") ?? "";
460
- if (!contentType.includes("application/x-ndjson")) {
461
- yield this.ingestErrorEvent(
462
- new MemoryServerError(
463
- `Memory server returned ${contentType || "unknown content-type"} instead of application/x-ndjson \u2014 server is older than v0.15.0`,
464
- res.status
465
- ),
466
- "parsing"
467
- );
468
- return;
538
+ const raw = yield* this.streamNdjsonUpload(
539
+ "/api/memory/ingest/file",
540
+ formData,
541
+ headers,
542
+ controller.signal
543
+ );
544
+ if (!raw) return;
545
+ const serverResult = this.fileIngestResultFromEvent({
546
+ ...raw,
547
+ schemaVersion: 1,
548
+ type: "result",
549
+ stage: "complete"
550
+ });
551
+ yield* this.completeIngestFileEvents(file, serverResult, options, controller.signal);
552
+ } finally {
553
+ options?.signal?.removeEventListener("abort", relayAbort);
554
+ controller.abort();
555
+ }
556
+ }
557
+ /**
558
+ * Graph-only re-enrichment for a document whose chunks are already stored
559
+ * and searchable but whose graph build failed. Uploads the original to
560
+ * `/api/memory/graph/enrich/file` (prepare-only — the server performs zero
561
+ * store/delete before the final graph write), runs the SAME enrichment
562
+ * callback engine as {@link ingestFileEvents}, then finalizes into one
563
+ * stable graph anchor keyed by `documentKey`. Repeating the same key
564
+ * replaces the document's graph references idempotently.
565
+ *
566
+ * The terminal `result` is a {@link GraphEnrichResult} event carrying the
567
+ * ACTUAL persisted graph counts; abort and failures yield a terminal
568
+ * `error` event instead (the server retains the pending session for retry).
569
+ */
570
+ async *graphEnrichFileEvents(file, options) {
571
+ if (typeof options?.documentKey !== "string" || options.documentKey.trim().length === 0) {
572
+ throw new Error("graphEnrichFileEvents requires a non-empty documentKey");
573
+ }
574
+ if (!options.enrichment?.extractEntitiesV2) {
575
+ throw new Error(
576
+ "graphEnrichFileEvents requires enrichment.extractEntitiesV2 \u2014 graph-only re-enrichment is caller-extraction by definition"
577
+ );
578
+ }
579
+ const controller = new AbortController();
580
+ const relayAbort = () => controller.abort(options.signal?.reason);
581
+ if (options.signal?.aborted) relayAbort();
582
+ options.signal?.addEventListener("abort", relayAbort, { once: true });
583
+ try {
584
+ const formData = new FormData();
585
+ formData.append("file", file);
586
+ formData.append("documentKey", options.documentKey);
587
+ if (options.catalogEntryId !== void 0) {
588
+ formData.append("catalogEntryId", options.catalogEntryId);
469
589
  }
470
- if (!res.body) {
471
- yield this.ingestErrorEvent(
472
- new MemoryServerError("Memory server returned an empty stream", res.status),
473
- "parsing"
590
+ if (options.namespaceId) formData.append("namespaceId", options.namespaceId);
591
+ const headers = {
592
+ Accept: "application/x-ndjson",
593
+ "X-Pyx-Enrichment-Capabilities": "text_windows_v1",
594
+ ...this._authHeaders
595
+ };
596
+ const raw = yield* this.streamNdjsonUpload(
597
+ "/api/memory/graph/enrich/file",
598
+ formData,
599
+ headers,
600
+ controller.signal
601
+ );
602
+ if (!raw) return;
603
+ try {
604
+ const prepared = raw;
605
+ if (prepared.purpose !== "graph-only" || !prepared.enrichment) {
606
+ throw new MemoryServerError(
607
+ "Graph enrichment prepare returned an unexpected terminal result",
608
+ 0
609
+ );
610
+ }
611
+ const outcome = yield* this.runEnrichmentCallbacks(
612
+ file,
613
+ prepared.enrichment,
614
+ options.enrichment,
615
+ controller.signal,
616
+ "graph-only"
474
617
  );
475
- return;
618
+ const data = outcome.data;
619
+ if (data?.purpose !== "graph-only" || !Array.isArray(data.entryIds) || data.entryIds.length !== 1 || typeof data.entryIds[0] !== "string" || data.entryIds[0].length === 0 || typeof data.graphAnchorEntryId !== "string" || data.graphAnchorEntryId.length === 0) {
620
+ throw new MemoryServerError(
621
+ "Graph enrichment finalization returned an unexpected result",
622
+ 0
623
+ );
624
+ }
625
+ yield { schemaVersion: 1, type: "result", stage: "complete", ...data };
626
+ } catch (err) {
627
+ yield this.ingestErrorEvent(err, "enrichment");
476
628
  }
477
- const reader = res.body.getReader();
478
- const decoder = new TextDecoder();
479
- let buffer = "";
480
- let currentStage = "parsing";
481
- let serverResult = null;
629
+ } finally {
630
+ options.signal?.removeEventListener("abort", relayAbort);
631
+ controller.abort();
632
+ }
633
+ }
634
+ /**
635
+ * POST a multipart body to an NDJSON streaming endpoint and relay its
636
+ * progress/heartbeat events. Returns the raw terminal `result` record, or
637
+ * null after yielding a terminal error (transport failure, server error
638
+ * event, non-NDJSON response, stream ending without a result). One
639
+ * implementation for both streaming surfaces so the wire protocol cannot
640
+ * fork.
641
+ */
642
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one stateful NDJSON reader validates and relays both ingest and graph-only stream envelopes
643
+ async *streamNdjsonUpload(path, formData, headers, signal) {
644
+ let res;
645
+ try {
646
+ res = await fetch(`${this.baseUrl}${path}`, {
647
+ method: "POST",
648
+ body: formData,
649
+ headers,
650
+ signal
651
+ });
652
+ } catch (err) {
653
+ yield this.ingestErrorEvent(this.translateFetchError(err, path), "parsing");
654
+ return null;
655
+ }
656
+ const contentType = res.headers.get("content-type") ?? "";
657
+ if (!res.ok) {
658
+ let error;
482
659
  try {
483
- while (true) {
484
- const { done, value } = await reader.read();
485
- if (done) break;
486
- buffer += decoder.decode(value, { stream: true });
487
- const lines = buffer.split("\n");
488
- buffer = lines.pop() ?? "";
489
- for (const line of lines) {
490
- if (!line.trim()) continue;
491
- const raw = JSON.parse(line);
492
- const type = raw.type;
493
- if (type === "progress" || type === "heartbeat") {
494
- const stage = this.normalizeActiveIngestStage(raw.stage);
495
- if (!stage) continue;
496
- currentStage = stage;
497
- yield { ...raw, schemaVersion: 1, type, stage };
498
- continue;
499
- }
500
- if (type === "result") {
501
- serverResult = this.fileIngestResultFromEvent({
502
- ...raw,
503
- schemaVersion: 1,
504
- type: "result",
505
- stage: "complete"
506
- });
507
- break;
508
- }
509
- if (type === "error") {
510
- yield {
511
- schemaVersion: 1,
512
- type: "error",
513
- stage: this.normalizeActiveIngestStage(raw.stage) ?? currentStage,
514
- error: typeof raw.error === "string" ? raw.error : "File ingest failed",
515
- message: typeof raw.message === "string" ? raw.message : void 0,
516
- code: typeof raw.code === "string" || typeof raw.code === "number" ? raw.code : void 0,
517
- status: typeof raw.status === "number" ? raw.status : void 0
518
- };
519
- return;
520
- }
660
+ await this.parseApiResponse(res);
661
+ error = new MemoryServerError(`Memory server error: ${res.status}`, res.status);
662
+ } catch (cause) {
663
+ error = cause;
664
+ }
665
+ yield this.ingestErrorEvent(error, "parsing");
666
+ return null;
667
+ }
668
+ if (!contentType.includes("application/x-ndjson")) {
669
+ yield this.ingestErrorEvent(
670
+ new MemoryServerError(
671
+ `Memory server returned ${contentType || "unknown content-type"} instead of application/x-ndjson \u2014 server is older than v0.15.0`,
672
+ res.status
673
+ ),
674
+ "parsing"
675
+ );
676
+ return null;
677
+ }
678
+ if (!res.body) {
679
+ yield this.ingestErrorEvent(
680
+ new MemoryServerError("Memory server returned an empty stream", res.status),
681
+ "parsing"
682
+ );
683
+ return null;
684
+ }
685
+ const reader = res.body.getReader();
686
+ const decoder = new TextDecoder();
687
+ let buffer = "";
688
+ let currentStage = "parsing";
689
+ let serverResult = null;
690
+ try {
691
+ while (true) {
692
+ const { done, value } = await reader.read();
693
+ if (done) break;
694
+ buffer += decoder.decode(value, { stream: true });
695
+ const lines = buffer.split("\n");
696
+ buffer = lines.pop() ?? "";
697
+ for (const line of lines) {
698
+ if (!line.trim()) continue;
699
+ const raw = JSON.parse(line);
700
+ const type = raw.type;
701
+ if (type === "progress" || type === "heartbeat") {
702
+ const stage = this.normalizeActiveIngestStage(raw.stage);
703
+ if (!stage) continue;
704
+ currentStage = stage;
705
+ yield { ...raw, schemaVersion: 1, type, stage };
706
+ continue;
707
+ }
708
+ if (type === "result") {
709
+ serverResult = raw;
710
+ break;
711
+ }
712
+ if (type === "error") {
713
+ const partialResult = isFileIngestResult(raw.partialResult) ? raw.partialResult : void 0;
714
+ const retry = normalizeRetryAfter(raw.retryAfter, raw.retryAfterSeconds);
715
+ yield {
716
+ schemaVersion: 1,
717
+ type: "error",
718
+ stage: this.normalizeActiveIngestStage(raw.stage) ?? currentStage,
719
+ error: typeof raw.error === "string" ? raw.error : "File ingest failed",
720
+ message: typeof raw.message === "string" ? raw.message : void 0,
721
+ code: typeof raw.code === "string" || typeof raw.code === "number" ? raw.code : void 0,
722
+ status: typeof raw.status === "number" ? raw.status : void 0,
723
+ ...retry,
724
+ ...partialResult ? { partialResult } : {}
725
+ };
726
+ return null;
521
727
  }
522
- if (serverResult) break;
523
728
  }
524
- } catch (err) {
525
- yield this.ingestErrorEvent(err, currentStage);
526
- return;
527
- } finally {
528
- reader.releaseLock();
729
+ if (serverResult) break;
529
730
  }
530
- if (!serverResult) {
531
- yield this.ingestErrorEvent(
532
- new MemoryServerError("File ingest stream ended without a server result", 0),
533
- currentStage
534
- );
535
- return;
536
- }
537
- yield* this.completeIngestFileEvents(file, serverResult, options, controller.signal);
731
+ } catch (err) {
732
+ yield this.ingestErrorEvent(err, currentStage);
733
+ return null;
538
734
  } finally {
539
- options?.signal?.removeEventListener("abort", relayAbort);
540
- controller.abort();
735
+ reader.releaseLock();
736
+ }
737
+ if (!serverResult) {
738
+ yield this.ingestErrorEvent(
739
+ new MemoryServerError("File ingest stream ended without a server result", 0),
740
+ currentStage
741
+ );
742
+ return null;
541
743
  }
744
+ return serverResult;
542
745
  }
543
746
  /**
544
- * Run the SDK-side enrichment phase (image-describe entity-extract
545
- * `/enrich` POST) for a server result, emitting progress + heartbeat
546
- * events around the slow steps and yielding the single terminal
547
- * {@link IngestResultEvent} at the end. Skips work cleanly when the
548
- * server emitted no enrichment block or the caller wired no callbacks.
747
+ * Run the SDK-side enrichment phase for an ingest result and yield the
748
+ * single terminal {@link IngestResultEvent} at the end. Skips work cleanly
749
+ * when the server emitted no enrichment block or the caller wired no
750
+ * callbacks. The callback work itself lives in
751
+ * {@link runEnrichmentCallbacks} shared with the graph-only surface.
549
752
  */
550
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / no-op are documented decision branches; the fan-out covers the v0.15.0 enrichment phase + heartbeat plumbing.
551
753
  async *completeIngestFileEvents(file, result, options, signal) {
552
754
  try {
553
- if (!result.enrichment || !options?.enrichment) {
755
+ const callbacks = options?.enrichment;
756
+ if (options?.documentKey !== void 0 && !result.enrichment) {
757
+ throw new MemoryServerError(
758
+ "documentKey-aware ingest response omitted required enrichment preparation; stable graph finalization did not run",
759
+ 502
760
+ );
761
+ }
762
+ if (!result.enrichment || !hasEnrichmentCallback(callbacks)) {
554
763
  yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
555
764
  return;
556
765
  }
557
- const enrichment = result.enrichment;
558
- const { fileId, token, expiresAt, images } = enrichment;
559
- const isV2 = "version" in enrichment;
560
- const textWindows = isV2 ? enrichment.textWindows : [];
561
- const descriptions = [];
562
- const describeImage = options.enrichment.describeImage;
563
- if (describeImage && images.length > 0) {
564
- yield {
565
- schemaVersion: 1,
566
- type: "progress",
567
- stage: "enrichment",
568
- filename: file.name,
569
- imagesTotal: images.length,
570
- imagesDescribed: 0,
571
- message: `Describing extracted images (0/${images.length})`
572
- };
573
- const CONCURRENCY = 5;
574
- for (let i = 0; i < images.length; i += CONCURRENCY) {
575
- const batch = images.slice(i, i + CONCURRENCY);
576
- const batchResults = yield* this.withSdkHeartbeats(
577
- "enrichment",
578
- Promise.all(
579
- batch.map(async (imageMeta) => {
580
- const imageRes = await fetch(
581
- `${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
582
- { headers: this._authHeaders, signal }
583
- );
584
- if (!imageRes.ok) {
585
- throw new MemoryServerError(
586
- `Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
587
- imageRes.status
588
- );
589
- }
590
- const imageBuffer = await imageRes.arrayBuffer();
591
- const description = await describeImage(imageBuffer, imageMeta);
592
- return { imageId: imageMeta.imageId, description };
593
- })
594
- ),
595
- signal
766
+ const outcome = yield* this.runEnrichmentCallbacks(
767
+ file,
768
+ result.enrichment,
769
+ callbacks,
770
+ signal,
771
+ "ingest",
772
+ options?.documentKey !== void 0
773
+ );
774
+ if (outcome.posted) {
775
+ const enrichData = outcome.data;
776
+ if (options?.documentKey !== void 0 && !enrichData.graphAnchorEntryId) {
777
+ throw new MemoryServerError(
778
+ "documentKey-aware enrichment response omitted graphAnchorEntryId; server does not support stable document replacement",
779
+ 502
596
780
  );
597
- descriptions.push(...batchResults.filter((d) => d.description.trim().length > 0));
598
- yield {
599
- schemaVersion: 1,
600
- type: "progress",
601
- stage: "enrichment",
602
- filename: file.name,
603
- imagesTotal: images.length,
604
- imagesDescribed: descriptions.length,
605
- message: `Described ${descriptions.length}/${images.length} images`
606
- };
607
781
  }
782
+ result.entryIds.push(...enrichData.entryIds);
783
+ if (enrichData.graphAnchorEntryId) {
784
+ result.graphAnchorEntryId = enrichData.graphAnchorEntryId;
785
+ }
786
+ if ((enrichData.relationshipsDropped ?? 0) > 0) {
787
+ result.relationshipsDropped = enrichData.relationshipsDropped;
788
+ result.droppedRelationships = enrichData.droppedRelationships;
789
+ }
790
+ result.chunks += outcome.descriptions.length;
791
+ result.totalCharacters += outcome.descriptions.reduce(
792
+ (sum, d) => sum + d.description.length,
793
+ 0
794
+ );
608
795
  }
609
- let entities;
610
- let relationships;
611
- const imageDescriptionTexts = descriptions.map((d) => d.description);
612
- if (options.enrichment.extractEntitiesV2 && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
796
+ delete result.enrichment;
797
+ yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
798
+ } catch (err) {
799
+ yield this.ingestErrorEvent(err, "enrichment", result);
800
+ }
801
+ }
802
+ /**
803
+ * The ONE enrichment callback engine, shared by {@link ingestFileEvents}
804
+ * and {@link graphEnrichFileEvents}: fetches extracted images, invokes
805
+ * `describeImage` with bounded concurrency, invokes `extractEntitiesV2`
806
+ * exactly once, and POSTs `/files/{fileId}/enrich` — emitting progress and
807
+ * heartbeat events around each slow step. Throws on any failure; the
808
+ * purpose-specific wrappers translate that into their terminal error.
809
+ *
810
+ * Legacy ingest keeps its existing partial add-on behavior. Graph-only and
811
+ * documentKey-aware full ingest may replace a prior graph only from a
812
+ * complete input set (no truncated text windows or undescribed images). A
813
+ * complete extractor result containing zero entities remains a valid clear.
814
+ */
815
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: image-describe / extract-entities-v2 / no-op are documented decision branches; the fan-out covers the v0.15.0 enrichment phase + heartbeat plumbing.
816
+ async *runEnrichmentCallbacks(file, enrichment, callbacks, signal, purpose, replaceEmptyGraph = false) {
817
+ const { fileId, token, expiresAt, images } = enrichment;
818
+ const isV2 = "version" in enrichment;
819
+ const textWindows = isV2 ? enrichment.textWindows : [];
820
+ const textWindowsTruncation = isV2 ? enrichment.textWindowsTruncation : void 0;
821
+ const requiresCompleteGraphReplacement = purpose === "graph-only" || replaceEmptyGraph;
822
+ if (requiresCompleteGraphReplacement && textWindowsTruncation?.truncated) {
823
+ throw new MemoryServerError(
824
+ `Graph enrichment input was truncated (${textWindowsTruncation.reason ?? "unknown limit"}); prior graph state was preserved and the pending session remains retryable`,
825
+ 422
826
+ );
827
+ }
828
+ const descriptions = [];
829
+ const describeImage = callbacks.describeImage;
830
+ if (describeImage && images.length > 0) {
831
+ yield {
832
+ schemaVersion: 1,
833
+ type: "progress",
834
+ stage: "enrichment",
835
+ filename: file.name,
836
+ imagesTotal: images.length,
837
+ imagesDescribed: 0,
838
+ message: `Describing extracted images (0/${images.length})`
839
+ };
840
+ const CONCURRENCY = 5;
841
+ for (let i = 0; i < images.length; i += CONCURRENCY) {
842
+ const batch = images.slice(i, i + CONCURRENCY);
843
+ const batchResults = yield* this.withSdkHeartbeats(
844
+ "enrichment",
845
+ Promise.all(
846
+ batch.map(async (imageMeta) => {
847
+ const imageRes = await fetch(
848
+ `${this.baseUrl}/api/memory/files/${fileId}/images/${imageMeta.imageId}?token=${encodeURIComponent(token)}`,
849
+ { headers: this._authHeaders, signal }
850
+ );
851
+ if (!imageRes.ok) {
852
+ throw new MemoryServerError(
853
+ `Failed to fetch image ${imageMeta.imageId}: ${imageRes.status}`,
854
+ imageRes.status
855
+ );
856
+ }
857
+ const imageBuffer = await imageRes.arrayBuffer();
858
+ const description = await describeImage(imageBuffer, imageMeta);
859
+ return { imageId: imageMeta.imageId, description };
860
+ })
861
+ ),
862
+ signal
863
+ );
864
+ descriptions.push(...batchResults.filter((d) => d.description.trim().length > 0));
613
865
  yield {
614
866
  schemaVersion: 1,
615
867
  type: "progress",
616
868
  stage: "enrichment",
617
869
  filename: file.name,
618
- message: "Extracting entities"
870
+ imagesTotal: images.length,
871
+ imagesDescribed: descriptions.length,
872
+ message: `Described ${descriptions.length}/${images.length} images`
619
873
  };
620
- const extracted = yield* this.withSdkHeartbeats(
621
- "enrichment",
622
- options.enrichment.extractEntitiesV2({
623
- textWindows,
624
- imageDescriptions: imageDescriptionTexts,
625
- mimeType: file.type,
626
- filename: file.name
627
- }),
628
- signal
629
- );
630
- entities = extracted.entities;
631
- relationships = extracted.relationships;
632
- }
633
- const hasGraph = (entities?.length ?? 0) > 0;
634
- const hasImages = descriptions.length > 0;
635
- if (!hasGraph && !hasImages) {
636
- yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
637
- return;
638
874
  }
875
+ }
876
+ if (requiresCompleteGraphReplacement && descriptions.length < images.length) {
877
+ throw new MemoryServerError(
878
+ `Graph enrichment described only ${descriptions.length}/${images.length} images; prior graph state was preserved and the pending session remains retryable`,
879
+ 422
880
+ );
881
+ }
882
+ let entities;
883
+ let relationships;
884
+ const imageDescriptionTexts = descriptions.map((d) => d.description);
885
+ if (callbacks.extractEntitiesV2 && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
639
886
  yield {
640
887
  schemaVersion: 1,
641
888
  type: "progress",
642
889
  stage: "enrichment",
643
890
  filename: file.name,
644
- message: "Persisting enrichment"
891
+ message: "Extracting entities"
645
892
  };
646
- const enrichRes = yield* this.withSdkHeartbeats(
893
+ const rawExtracted = yield* this.withSdkHeartbeats(
647
894
  "enrichment",
648
- fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
649
- method: "POST",
650
- headers: {
651
- "Content-Type": "application/json",
652
- "X-Enrichment-Token": `${token}:${expiresAt}`,
653
- ...this._authHeaders
654
- },
655
- signal,
656
- body: JSON.stringify({ imageDescriptions: descriptions, entities, relationships })
895
+ callbacks.extractEntitiesV2({
896
+ textWindows,
897
+ imageDescriptions: imageDescriptionTexts,
898
+ mimeType: file.type,
899
+ filename: file.name
657
900
  }),
658
901
  signal
659
902
  );
660
- if (!enrichRes.ok) {
661
- const body = await enrichRes.json().catch(() => ({}));
903
+ let extracted;
904
+ try {
905
+ extracted = assertGraphExtractionPayload(rawExtracted, "extractEntitiesV2 result");
906
+ } catch (error) {
662
907
  throw new MemoryServerError(
663
- body.error ?? `Enrichment failed: ${enrichRes.status}`,
664
- enrichRes.status
908
+ error instanceof Error ? error.message : "extractEntitiesV2 returned an invalid result",
909
+ 422
665
910
  );
666
911
  }
667
- const enrichData = await this.parseApiResponse(enrichRes);
668
- result.entryIds.push(...enrichData.entryIds);
669
- result.chunks += descriptions.length;
670
- result.totalCharacters += descriptions.reduce((sum, d) => sum + d.description.length, 0);
671
- yield { schemaVersion: 1, type: "result", stage: "complete", ...result };
672
- } catch (err) {
673
- yield this.ingestErrorEvent(err, "enrichment", result);
912
+ entities = extracted.entities;
913
+ relationships = extracted.relationships;
914
+ }
915
+ const hadExtractionInput = textWindows.length > 0 || imageDescriptionTexts.length > 0;
916
+ if (purpose === "graph-only" && !hadExtractionInput) {
917
+ throw new MemoryServerError(
918
+ "Graph enrichment produced no extractable text or image descriptions; prior graph state was preserved",
919
+ 422
920
+ );
921
+ }
922
+ const hasGraph = (entities?.length ?? 0) > 0;
923
+ const hasImages = descriptions.length > 0;
924
+ if (!hasGraph && !hasImages && purpose === "ingest" && !replaceEmptyGraph) {
925
+ return { posted: false, data: null, descriptions };
674
926
  }
927
+ yield {
928
+ schemaVersion: 1,
929
+ type: "progress",
930
+ stage: "enrichment",
931
+ filename: file.name,
932
+ message: "Persisting enrichment"
933
+ };
934
+ const enrichRes = yield* this.withSdkHeartbeats(
935
+ "enrichment",
936
+ fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
937
+ method: "POST",
938
+ headers: {
939
+ "Content-Type": "application/json",
940
+ "X-Enrichment-Token": `${token}:${expiresAt}`,
941
+ ...this._authHeaders
942
+ },
943
+ signal,
944
+ body: JSON.stringify({
945
+ imageDescriptions: descriptions,
946
+ entities: entities ?? [],
947
+ relationships: relationships ?? []
948
+ })
949
+ }),
950
+ signal
951
+ );
952
+ const data = await this.parseApiResponse(enrichRes);
953
+ return { posted: true, data, descriptions };
675
954
  }
676
955
  /**
677
956
  * Race a Promise against a periodic heartbeat tick. Yields a heartbeat
@@ -735,15 +1014,20 @@ var MemoryClient = class {
735
1014
  return result;
736
1015
  }
737
1016
  ingestErrorEvent(error, stage, partialResult) {
738
- const status = error instanceof MemoryServerError ? error.status : error instanceof Error && error.name === "AbortError" ? 499 : void 0;
1017
+ const structuralError = isRecord3(error) ? error : void 0;
1018
+ const status = error instanceof MemoryServerError ? error.status : error instanceof Error && error.name === "AbortError" ? 499 : structuralHttpStatus(error);
739
1019
  const message = error instanceof Error ? error.message : String(error);
1020
+ const code = error instanceof MemoryServerError ? error.code : void 0;
1021
+ const retry = error instanceof MemoryServerError ? { retryAfter: error.retryAfter, retryAfterSeconds: error.retryAfterSeconds } : normalizeRetryAfter(structuralError?.retryAfter, structuralError?.retryAfterSeconds);
740
1022
  return {
741
1023
  schemaVersion: 1,
742
1024
  type: "error",
743
1025
  stage,
744
1026
  error: message,
745
1027
  message,
746
- ...status != null ? { status, code: status } : {},
1028
+ ...status != null ? { status, code: code ?? status } : {},
1029
+ ...retry.retryAfter !== void 0 ? { retryAfter: retry.retryAfter } : {},
1030
+ ...retry.retryAfterSeconds !== void 0 ? { retryAfterSeconds: retry.retryAfterSeconds } : {},
747
1031
  ...partialResult ? { partialResult } : {}
748
1032
  };
749
1033
  }
@@ -751,15 +1035,24 @@ var MemoryClient = class {
751
1035
  * Get the download URL for an uploaded file.
752
1036
  * Returns a URL that serves the original file binary with proper Content-Type.
753
1037
  */
754
- getFileDownloadUrl(filename) {
755
- return `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`;
1038
+ getFileDownloadUrl(filename, options) {
1039
+ const url = new URL(
1040
+ `${this.baseUrl}/api/memory/files/download/${encodeURIComponent(filename)}`
1041
+ );
1042
+ if (options?.documentKey !== void 0) {
1043
+ url.searchParams.set("documentKey", options.documentKey);
1044
+ }
1045
+ if (options?.namespaceId !== void 0) {
1046
+ url.searchParams.set("namespaceId", options.namespaceId);
1047
+ }
1048
+ return url.toString();
756
1049
  }
757
1050
  /**
758
1051
  * Download an uploaded file by filename.
759
1052
  * Returns the raw Response (caller handles the body — arrayBuffer, blob, stream, etc.).
760
1053
  */
761
- async downloadFile(filename) {
762
- const url = this.getFileDownloadUrl(filename);
1054
+ async downloadFile(filename, options) {
1055
+ const url = this.getFileDownloadUrl(filename, options);
763
1056
  const res = await fetch(url, { headers: this._authHeaders });
764
1057
  if (!res.ok) {
765
1058
  throw new MemoryServerError(`File download failed: ${res.status}`, res.status);
@@ -837,10 +1130,10 @@ var MemoryClient = class {
837
1130
  async repairGraph() {
838
1131
  return this.fetchApi("/api/memory/graph/repair", { method: "POST" });
839
1132
  }
840
- async deleteBySource(source) {
1133
+ async deleteBySource(source, authority) {
841
1134
  const result = await this.fetchApi(
842
1135
  `/api/memory/source/${this.encodePathSegment(source)}`,
843
- { method: "DELETE" }
1136
+ { method: "DELETE", headers: this.authorityHeaders(authority) }
844
1137
  );
845
1138
  return result.deleted;
846
1139
  }
@@ -853,14 +1146,20 @@ var MemoryClient = class {
853
1146
  }
854
1147
  );
855
1148
  }
856
- async queryAsOf(asOfDate, filters = {}) {
1149
+ async queryAsOf(asOfDate, filters = {}, authority) {
1150
+ if (filters.cursor !== void 0 && filters.offset !== void 0) {
1151
+ throw new Error("queryAsOf cursor and offset are mutually exclusive");
1152
+ }
857
1153
  const params = new URLSearchParams({ asOf: asOfDate });
858
1154
  if (filters.type) params.set("type", filters.type);
859
1155
  if (filters.agentId) params.set("agentId", filters.agentId);
860
1156
  if (filters.source) params.set("source", filters.source);
861
1157
  if (filters.limit) params.set("limit", String(filters.limit));
1158
+ if (filters.offset !== void 0) params.set("offset", String(filters.offset));
1159
+ if (filters.cursor !== void 0) params.set("cursor", filters.cursor);
862
1160
  const result = await this.fetchApi(
863
- `/api/memory/query-as-of?${params}`
1161
+ `/api/memory/query-as-of?${params}`,
1162
+ { headers: this.authorityHeaders(authority) }
864
1163
  );
865
1164
  return result.entries;
866
1165
  }
@@ -902,11 +1201,15 @@ var MemoryClient = class {
902
1201
  return result.entries;
903
1202
  }
904
1203
  async queryByEventTime(startTime, endTime, filters = {}) {
1204
+ if (filters.cursor !== void 0) {
1205
+ throw new Error("queryByEventTime does not support a createdAt cursor; use offset");
1206
+ }
905
1207
  const params = new URLSearchParams({ startTime, endTime });
906
1208
  if (filters.type) params.set("type", filters.type);
907
1209
  if (filters.agentId) params.set("agentId", filters.agentId);
908
1210
  if (filters.source) params.set("source", filters.source);
909
1211
  if (filters.limit) params.set("limit", String(filters.limit));
1212
+ if (filters.offset !== void 0) params.set("offset", String(filters.offset));
910
1213
  const result = await this.fetchApi(
911
1214
  `/api/memory/query-by-event-time?${params}`
912
1215
  );
@@ -1030,23 +1333,33 @@ var MemoryClient = class {
1030
1333
  }
1031
1334
  /** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
1032
1335
  async parseApiResponse(res) {
1336
+ const retryAfter = res.headers.get("Retry-After") ?? void 0;
1033
1337
  let body;
1034
1338
  try {
1035
1339
  body = await res.json();
1036
1340
  } catch {
1037
1341
  throw new MemoryServerError(
1038
1342
  `Memory server error: invalid JSON response (${res.status})`,
1039
- res.status
1343
+ res.status,
1344
+ void 0,
1345
+ retryAfter
1040
1346
  );
1041
1347
  }
1042
- if (!body?.success || body.data == null) {
1348
+ if (!res.ok || !body?.success || body.data == null) {
1043
1349
  if (res.ok && body?.error === void 0) {
1044
1350
  throw new MemoryServerError(
1045
1351
  `Memory server returned HTTP ${res.status} without the expected { success, data } API envelope. Check that the base URL points to a pyx-memory instance server; hosted pyx-memory access uses MCP.`,
1046
- res.status
1352
+ res.status,
1353
+ void 0,
1354
+ retryAfter
1047
1355
  );
1048
1356
  }
1049
- throw new MemoryServerError(body?.error ?? `Memory server error: ${res.status}`, res.status);
1357
+ throw new MemoryServerError(
1358
+ body?.error ?? `Memory server error: ${res.status}`,
1359
+ res.status,
1360
+ body?.code,
1361
+ retryAfter
1362
+ );
1050
1363
  }
1051
1364
  return body.data;
1052
1365
  }
@@ -1055,6 +1368,9 @@ var MemoryClient = class {
1055
1368
  export {
1056
1369
  DEFAULTS,
1057
1370
  TAXONOMY_MAX_CATEGORIES,
1371
+ documentGraphSource,
1372
+ documentContentSource,
1373
+ documentImageSource,
1058
1374
  projectSearchResponseForMcp,
1059
1375
  secretElevationNoticeFor,
1060
1376
  withSecretElevationNotice,