@pyxmate/memory 0.10.0 → 0.12.1

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.
@@ -11,24 +11,31 @@ var MemoryServerError = class extends Error {
11
11
  return this.status === 404;
12
12
  }
13
13
  };
14
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
14
15
  var MemoryClient = class {
15
16
  baseUrl;
16
17
  _authHeaders;
18
+ _requestTimeoutMs;
17
19
  constructor(memoryUrl, apiKeyOrOptions) {
18
20
  this.baseUrl = memoryUrl.replace(/\/$/, "");
19
21
  let apiKey;
20
22
  let defaultHeaders = {};
23
+ let requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
21
24
  if (typeof apiKeyOrOptions === "string") {
22
25
  apiKey = apiKeyOrOptions;
23
26
  } else if (apiKeyOrOptions) {
24
27
  apiKey = apiKeyOrOptions.apiKey;
25
28
  defaultHeaders = apiKeyOrOptions.defaultHeaders ?? {};
29
+ if (apiKeyOrOptions.requestTimeoutMs !== void 0) {
30
+ requestTimeoutMs = apiKeyOrOptions.requestTimeoutMs;
31
+ }
26
32
  }
27
33
  const trimmed = apiKey?.trim();
28
34
  this._authHeaders = {
29
35
  ...trimmed ? { Authorization: `Bearer ${trimmed}` } : {},
30
36
  ...defaultHeaders
31
37
  };
38
+ this._requestTimeoutMs = requestTimeoutMs;
32
39
  }
33
40
  /** Encode a path segment to prevent URL injection */
34
41
  encodePathSegment(segment) {
@@ -112,21 +119,36 @@ var MemoryClient = class {
112
119
  * With enrichment callbacks: fetches extracted images, calls describeImage for each,
113
120
  * optionally extracts entities, then submits enrichment data back to the server.
114
121
  */
122
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: two-phase enrichment fans out across image-describe + entity-extract + v1/v2 negotiation; each branch maps to a documented case in memory-client-enrichment.test.ts
115
123
  async ingestFile(file, options) {
116
124
  const formData = new FormData();
117
125
  formData.append("file", file);
118
126
  if (options?.description) {
119
127
  formData.append("description", options.description);
120
128
  }
129
+ const wantsTextWindows = Boolean(
130
+ options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
131
+ );
132
+ const headers = { ...this._authHeaders };
133
+ if (wantsTextWindows) {
134
+ headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
135
+ }
121
136
  const res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
122
137
  method: "POST",
123
138
  body: formData,
124
- headers: this._authHeaders
139
+ headers
125
140
  });
126
141
  const result = await this.parseApiResponse(res);
127
- if (result.enrichment && options?.enrichment?.describeImage) {
128
- const { fileId, token, expiresAt, images } = result.enrichment;
129
- const descriptions = [];
142
+ if (!result.enrichment || !options?.enrichment) {
143
+ return result;
144
+ }
145
+ const enrichment = result.enrichment;
146
+ const { fileId, token, expiresAt, images } = enrichment;
147
+ const isV2 = "version" in enrichment;
148
+ const textWindows = isV2 ? enrichment.textWindows : [];
149
+ const descriptions = [];
150
+ const describeImage = options.enrichment.describeImage;
151
+ if (describeImage && images.length > 0) {
130
152
  const CONCURRENCY = 5;
131
153
  for (let i = 0; i < images.length; i += CONCURRENCY) {
132
154
  const batch = images.slice(i, i + CONCURRENCY);
@@ -143,48 +165,66 @@ var MemoryClient = class {
143
165
  );
144
166
  }
145
167
  const imageBuffer = await imageRes.arrayBuffer();
146
- const description = await options.enrichment.describeImage(imageBuffer, imageMeta);
168
+ const description = await describeImage(imageBuffer, imageMeta);
147
169
  return { imageId: imageMeta.imageId, description };
148
170
  })
149
171
  );
150
172
  descriptions.push(...batchResults);
151
173
  }
152
- let entities;
153
- let relationships;
154
- if (options.enrichment.extractEntities && descriptions.length > 0) {
155
- const extracted = await options.enrichment.extractEntities(
156
- descriptions.map((d) => d.description)
157
- );
174
+ }
175
+ let entities;
176
+ let relationships;
177
+ const v2Callback = options.enrichment.extractEntitiesV2;
178
+ const legacyCallback = options.enrichment.extractEntities;
179
+ const imageDescriptionTexts = descriptions.map((d) => d.description);
180
+ if (v2Callback && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
181
+ const extracted = await v2Callback({
182
+ textWindows,
183
+ imageDescriptions: imageDescriptionTexts,
184
+ mimeType: file.type,
185
+ filename: file.name
186
+ });
187
+ entities = extracted.entities;
188
+ relationships = extracted.relationships;
189
+ } else if (legacyCallback) {
190
+ const allInputs = [...textWindows, ...imageDescriptionTexts];
191
+ if (allInputs.length > 0) {
192
+ const extracted = await legacyCallback(allInputs);
158
193
  entities = extracted.entities;
159
194
  relationships = extracted.relationships;
160
195
  }
161
- const enrichTokenHeader = `${token}:${expiresAt}`;
162
- const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
163
- method: "POST",
164
- headers: {
165
- "Content-Type": "application/json",
166
- "X-Enrichment-Token": enrichTokenHeader,
167
- ...this._authHeaders
168
- },
169
- body: JSON.stringify({
170
- imageDescriptions: descriptions,
171
- entities,
172
- relationships
173
- })
174
- });
175
- if (!enrichRes.ok) {
176
- const body = await enrichRes.json().catch(() => ({}));
177
- throw new MemoryServerError(
178
- body.error ?? `Enrichment failed: ${enrichRes.status}`,
179
- enrichRes.status
180
- );
181
- }
182
- const enrichData = await this.parseApiResponse(enrichRes);
183
- const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
184
- result.entryIds.push(...enrichData.entryIds);
185
- result.chunks += descriptions.length;
186
- result.totalCharacters += enrichCharacters;
187
196
  }
197
+ const hasGraph = (entities?.length ?? 0) > 0;
198
+ const hasImages = descriptions.length > 0;
199
+ if (!hasGraph && !hasImages) {
200
+ return result;
201
+ }
202
+ const enrichTokenHeader = `${token}:${expiresAt}`;
203
+ const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ "X-Enrichment-Token": enrichTokenHeader,
208
+ ...this._authHeaders
209
+ },
210
+ body: JSON.stringify({
211
+ imageDescriptions: descriptions,
212
+ entities,
213
+ relationships
214
+ })
215
+ });
216
+ if (!enrichRes.ok) {
217
+ const body = await enrichRes.json().catch(() => ({}));
218
+ throw new MemoryServerError(
219
+ body.error ?? `Enrichment failed: ${enrichRes.status}`,
220
+ enrichRes.status
221
+ );
222
+ }
223
+ const enrichData = await this.parseApiResponse(enrichRes);
224
+ const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
225
+ result.entryIds.push(...enrichData.entryIds);
226
+ result.chunks += descriptions.length;
227
+ result.totalCharacters += enrichCharacters;
188
228
  return result;
189
229
  }
190
230
  /**
@@ -302,16 +342,44 @@ var MemoryClient = class {
302
342
  return result.entries;
303
343
  }
304
344
  async fetchApi(path, options) {
305
- const res = await fetch(`${this.baseUrl}${path}`, {
306
- ...options,
307
- headers: {
308
- "Content-Type": "application/json",
309
- ...options?.headers,
310
- ...this._authHeaders
311
- }
312
- });
345
+ const signal = options?.signal ?? AbortSignal.timeout(this._requestTimeoutMs);
346
+ let res;
347
+ try {
348
+ res = await fetch(`${this.baseUrl}${path}`, {
349
+ ...options,
350
+ headers: {
351
+ "Content-Type": "application/json",
352
+ ...options?.headers,
353
+ ...this._authHeaders
354
+ },
355
+ signal
356
+ });
357
+ } catch (err) {
358
+ throw this.translateFetchError(err, path);
359
+ }
313
360
  return this.parseApiResponse(res);
314
361
  }
362
+ /**
363
+ * Map fetch-layer rejections into a typed `MemoryServerError` so callers
364
+ * can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
365
+ * caller's signal generally fires an `AbortError`. Anything else (DNS,
366
+ * TCP reset, TLS) becomes a wrapped error with status 0.
367
+ */
368
+ translateFetchError(err, path) {
369
+ if (err instanceof Error) {
370
+ if (err.name === "TimeoutError") {
371
+ return new MemoryServerError(
372
+ `Memory server request timed out after ${this._requestTimeoutMs}ms (${path})`,
373
+ 504
374
+ );
375
+ }
376
+ if (err.name === "AbortError") {
377
+ return new MemoryServerError(`Memory server request aborted (${path})`, 499);
378
+ }
379
+ return new MemoryServerError(`Memory server request failed: ${err.message} (${path})`, 0);
380
+ }
381
+ return new MemoryServerError(`Memory server request failed: ${String(err)} (${path})`, 0);
382
+ }
315
383
  /** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
316
384
  async parseApiResponse(res) {
317
385
  let body;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MemoryClient
3
- } from "./chunk-KVRPHFDP.mjs";
3
+ } from "./chunk-4YIKI2BA.mjs";
4
4
 
5
5
  // ../dashboard/src/aggregations/consolidation-analytics.ts
6
6
  function analyzeConsolidationLog(entries) {
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-QFEFLBTN.mjs";
15
- import "./chunk-KVRPHFDP.mjs";
14
+ } from "./chunk-DZZHJ66P.mjs";
15
+ import "./chunk-4YIKI2BA.mjs";
16
16
  export {
17
17
  DashboardClient,
18
18
  Poller,
package/dist/index.d.ts CHANGED
@@ -71,15 +71,50 @@ interface ExtendedMemoryInterface extends MemoryInterface {
71
71
  deleteBySource(source: string): Promise<number>;
72
72
  }
73
73
 
74
- /** Callbacks for two-phase file enrichment. */
74
+ /**
75
+ * Callbacks for two-phase file enrichment. All callbacks are optional so the
76
+ * SDK can support every combination of the v2 flow:
77
+ * - Image-rich PDF + describeImage only → describes images, no entity extraction
78
+ * - Image-rich PDF + describeImage + extractEntities[V2] → describes + extracts
79
+ * - Text-only file + extractEntities[V2] only → extracts from textWindows
80
+ * - Mixed file + all three → describes + extracts from both sources
81
+ *
82
+ * Without ANY callback, ingestFile returns the parsed result without running
83
+ * Phase 2/3 — caller opted out of enrichment entirely.
84
+ */
75
85
  interface EnrichmentCallbacks {
76
- /** Describe an image using LLM vision. Receives the raw image buffer and metadata. */
77
- describeImage: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
78
- /** Optionally extract entities/relationships from all image descriptions. */
86
+ /**
87
+ * Describe an image using LLM vision. Receives the raw image buffer and
88
+ * metadata. Required for image-bearing files; safe to omit for text-only
89
+ * uploads where the server emits zero images.
90
+ */
91
+ describeImage?: (imageBuffer: ArrayBuffer, meta: ExtractedImageMeta) => Promise<string>;
92
+ /**
93
+ * Legacy entity-extraction callback. Receives a flat string array which
94
+ * the SDK constructs as `[...textWindows, ...imageDescriptions]` — callers
95
+ * cannot distinguish the two sources. Kept for backwards compatibility;
96
+ * new code should prefer {@link extractEntitiesV2}.
97
+ */
79
98
  extractEntities?: (descriptions: string[]) => Promise<{
80
99
  entities: IngestEntity$1[];
81
100
  relationships: IngestRelationship$1[];
82
101
  }>;
102
+ /**
103
+ * v2 entity-extraction callback. Receives text windows and image
104
+ * descriptions separately so callers can apply different prompts per
105
+ * source. Takes precedence over {@link extractEntities} when both are
106
+ * defined. Triggers the `X-Pyx-Enrichment-Capabilities: text_windows_v1`
107
+ * negotiation header on ingest.
108
+ */
109
+ extractEntitiesV2?: (input: {
110
+ textWindows: string[];
111
+ imageDescriptions: string[];
112
+ mimeType: string;
113
+ filename: string;
114
+ }) => Promise<{
115
+ entities: IngestEntity$1[];
116
+ relationships: IngestRelationship$1[];
117
+ }>;
83
118
  }
84
119
  /** Error thrown by MemoryClient when the server returns a non-success response. */
85
120
  declare class MemoryServerError extends Error {
@@ -93,10 +128,26 @@ interface MemoryClientOptions {
93
128
  apiKey?: string;
94
129
  /** Additional headers to send with every request (e.g., X-Caller-Access-Level). */
95
130
  defaultHeaders?: Record<string, string>;
131
+ /**
132
+ * Default per-request timeout in milliseconds. Without this, a wedged
133
+ * memory server (e.g. event-loop blocked by inference) makes every
134
+ * caller hang forever — that was the Korens demo wedge in 2026-04 where
135
+ * a 161-second pyx-memory stall propagated through the runtime to the
136
+ * browser. Defaults to 30 s, which is high enough that normal
137
+ * `/search` and `/stats` requests never hit it but low enough that a
138
+ * stuck server fails loudly.
139
+ *
140
+ * Only applied when the caller does NOT pass their own `signal` via
141
+ * RequestInit. Long-running operations (large `consolidate`, `reindex`,
142
+ * file ingest with enrichment) should pass their own AbortSignal —
143
+ * that signal fully replaces the default ceiling.
144
+ */
145
+ requestTimeoutMs?: number;
96
146
  }
97
147
  declare class MemoryClient implements ExtendedMemoryInterface {
98
148
  protected baseUrl: string;
99
149
  private readonly _authHeaders;
150
+ private readonly _requestTimeoutMs;
100
151
  constructor(memoryUrl: string, apiKeyOrOptions?: string | MemoryClientOptions);
101
152
  /** Encode a path segment to prevent URL injection */
102
153
  private encodePathSegment;
@@ -156,6 +207,13 @@ declare class MemoryClient implements ExtendedMemoryInterface {
156
207
  queryAsOf(asOfDate: string, filters?: TemporalQueryFilters): Promise<MemoryEntry$1[]>;
157
208
  queryByEventTime(startTime: string, endTime: string, filters?: TemporalQueryFilters): Promise<MemoryEntry$1[]>;
158
209
  protected fetchApi<T>(path: string, options?: RequestInit): Promise<T>;
210
+ /**
211
+ * Map fetch-layer rejections into a typed `MemoryServerError` so callers
212
+ * can react uniformly. AbortSignal.timeout fires a `TimeoutError`; the
213
+ * caller's signal generally fires an `AbortError`. Anything else (DNS,
214
+ * TCP reset, TLS) becomes a wrapped error with status 0.
215
+ */
216
+ private translateFetchError;
159
217
  /** Parse and validate a JSON API response, throwing MemoryServerError on any failure. */
160
218
  private parseApiResponse;
161
219
  }
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryClient,
3
3
  MemoryServerError
4
- } from "./chunk-KVRPHFDP.mjs";
4
+ } from "./chunk-4YIKI2BA.mjs";
5
5
 
6
6
  // ../shared/src/constants/defaults.ts
7
7
  var DEFAULTS = {
package/dist/react.mjs CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  toGraphologyFormat,
12
12
  transformGraphData,
13
13
  unreachableHealth
14
- } from "./chunk-QFEFLBTN.mjs";
15
- import "./chunk-KVRPHFDP.mjs";
14
+ } from "./chunk-DZZHJ66P.mjs";
15
+ import "./chunk-4YIKI2BA.mjs";
16
16
 
17
17
  // ../dashboard/src/hooks/use-consolidation-log.ts
18
18
  import { useCallback as useCallback2, useMemo } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.10.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",