@pyxmate/memory 0.9.3 → 0.11.0

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.
@@ -112,21 +112,36 @@ var MemoryClient = class {
112
112
  * With enrichment callbacks: fetches extracted images, calls describeImage for each,
113
113
  * optionally extracts entities, then submits enrichment data back to the server.
114
114
  */
115
+ // 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
116
  async ingestFile(file, options) {
116
117
  const formData = new FormData();
117
118
  formData.append("file", file);
118
119
  if (options?.description) {
119
120
  formData.append("description", options.description);
120
121
  }
122
+ const wantsTextWindows = Boolean(
123
+ options?.enrichment?.extractEntities || options?.enrichment?.extractEntitiesV2
124
+ );
125
+ const headers = { ...this._authHeaders };
126
+ if (wantsTextWindows) {
127
+ headers["X-Pyx-Enrichment-Capabilities"] = "text_windows_v1";
128
+ }
121
129
  const res = await fetch(`${this.baseUrl}/api/memory/ingest/file`, {
122
130
  method: "POST",
123
131
  body: formData,
124
- headers: this._authHeaders
132
+ headers
125
133
  });
126
134
  const result = await this.parseApiResponse(res);
127
- if (result.enrichment && options?.enrichment?.describeImage) {
128
- const { fileId, token, expiresAt, images } = result.enrichment;
129
- const descriptions = [];
135
+ if (!result.enrichment || !options?.enrichment) {
136
+ return result;
137
+ }
138
+ const enrichment = result.enrichment;
139
+ const { fileId, token, expiresAt, images } = enrichment;
140
+ const isV2 = "version" in enrichment;
141
+ const textWindows = isV2 ? enrichment.textWindows : [];
142
+ const descriptions = [];
143
+ const describeImage = options.enrichment.describeImage;
144
+ if (describeImage && images.length > 0) {
130
145
  const CONCURRENCY = 5;
131
146
  for (let i = 0; i < images.length; i += CONCURRENCY) {
132
147
  const batch = images.slice(i, i + CONCURRENCY);
@@ -143,48 +158,66 @@ var MemoryClient = class {
143
158
  );
144
159
  }
145
160
  const imageBuffer = await imageRes.arrayBuffer();
146
- const description = await options.enrichment.describeImage(imageBuffer, imageMeta);
161
+ const description = await describeImage(imageBuffer, imageMeta);
147
162
  return { imageId: imageMeta.imageId, description };
148
163
  })
149
164
  );
150
165
  descriptions.push(...batchResults);
151
166
  }
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
- );
167
+ }
168
+ let entities;
169
+ let relationships;
170
+ const v2Callback = options.enrichment.extractEntitiesV2;
171
+ const legacyCallback = options.enrichment.extractEntities;
172
+ const imageDescriptionTexts = descriptions.map((d) => d.description);
173
+ if (v2Callback && (textWindows.length > 0 || imageDescriptionTexts.length > 0)) {
174
+ const extracted = await v2Callback({
175
+ textWindows,
176
+ imageDescriptions: imageDescriptionTexts,
177
+ mimeType: file.type,
178
+ filename: file.name
179
+ });
180
+ entities = extracted.entities;
181
+ relationships = extracted.relationships;
182
+ } else if (legacyCallback) {
183
+ const allInputs = [...textWindows, ...imageDescriptionTexts];
184
+ if (allInputs.length > 0) {
185
+ const extracted = await legacyCallback(allInputs);
158
186
  entities = extracted.entities;
159
187
  relationships = extracted.relationships;
160
188
  }
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
189
  }
190
+ const hasGraph = (entities?.length ?? 0) > 0;
191
+ const hasImages = descriptions.length > 0;
192
+ if (!hasGraph && !hasImages) {
193
+ return result;
194
+ }
195
+ const enrichTokenHeader = `${token}:${expiresAt}`;
196
+ const enrichRes = await fetch(`${this.baseUrl}/api/memory/files/${fileId}/enrich`, {
197
+ method: "POST",
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ "X-Enrichment-Token": enrichTokenHeader,
201
+ ...this._authHeaders
202
+ },
203
+ body: JSON.stringify({
204
+ imageDescriptions: descriptions,
205
+ entities,
206
+ relationships
207
+ })
208
+ });
209
+ if (!enrichRes.ok) {
210
+ const body = await enrichRes.json().catch(() => ({}));
211
+ throw new MemoryServerError(
212
+ body.error ?? `Enrichment failed: ${enrichRes.status}`,
213
+ enrichRes.status
214
+ );
215
+ }
216
+ const enrichData = await this.parseApiResponse(enrichRes);
217
+ const enrichCharacters = descriptions.reduce((sum, d) => sum + d.description.length, 0);
218
+ result.entryIds.push(...enrichData.entryIds);
219
+ result.chunks += descriptions.length;
220
+ result.totalCharacters += enrichCharacters;
188
221
  return result;
189
222
  }
190
223
  /**
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MemoryClient
3
- } from "./chunk-KVRPHFDP.mjs";
3
+ } from "./chunk-5IERAVVW.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-DGVOXKYV.mjs";
15
+ import "./chunk-5IERAVVW.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 {
@@ -255,6 +290,13 @@ interface MemoryEntry {
255
290
  userId?: string;
256
291
  /** Team/group ID within the tenant. */
257
292
  teamId?: string;
293
+ /**
294
+ * When true, consolidation leaves this entry alone — it is never archived by
295
+ * decay and never merged (nor merged-into) by semantic deduplication. Use for
296
+ * stable anchor entries whose ID is referenced by external systems (e.g. a
297
+ * file-catalog row pointed at by another service's foreign key).
298
+ */
299
+ pinned?: boolean;
258
300
  }
259
301
  interface MemorySearchParams {
260
302
  query: string;
@@ -370,6 +412,11 @@ interface MemoryIngestRequest {
370
412
  userId?: string;
371
413
  /** Team/group ID within the tenant. */
372
414
  teamId?: string;
415
+ /**
416
+ * Exclude this entry from consolidation (decay-archive and semantic dedup).
417
+ * See `MemoryEntry.pinned` for full semantics.
418
+ */
419
+ pinned?: boolean;
373
420
  }
374
421
  interface MemoryStats {
375
422
  totalEntries: number;
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-5IERAVVW.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-DGVOXKYV.mjs";
15
+ import "./chunk-5IERAVVW.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.9.3",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -86,6 +86,21 @@ When `consolidate()` runs, it executes a 7-step pipeline:
86
86
 
87
87
  All steps have **non-LLM fallbacks** — consolidation works without an LLM, just less intelligently.
88
88
 
89
+ ### Pinned entries (opt out of consolidation)
90
+
91
+ Entries stored with `pinned: true` are exempt from both the dedup step and the decay-archive step. `Memory.runDecay()` skips them via the same filter. Use for anchor rows whose ID is referenced by external systems — e.g. a file-catalog entry whose ID is stored as a foreign key in a downstream service's database:
92
+
93
+ ```typescript
94
+ await memory.store({
95
+ content: '[File: "report.pdf" (application/pdf)]',
96
+ type: MemoryType.LONG_TERM,
97
+ metadata: { source: 'file-ingestion', filename: 'report.pdf' },
98
+ pinned: true, // external FK points here — must not be merged or archived
99
+ });
100
+ ```
101
+
102
+ Without `pinned`, short near-duplicate anchor content can trip the 0.95 cosine dedup threshold and get hard-deleted, orphaning the external reference. Pinned entries' exemption is SQL-side (`WHERE pinned = 0` filter on consolidation's query) and backed by a partial index `idx_memory_pinned WHERE pinned = 1`. Callers can query with `QueryFilters.excludePinned: true` to reproduce the same exclusion.
103
+
89
104
  ---
90
105
 
91
106
  ## Community Detection
@@ -24,7 +24,7 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
24
24
  |--------|----------|-------------|
25
25
  | GET | `/health` | Public health check (status only — no internals exposed) |
26
26
  | GET | `/admin/health` | Admin health check (version, uptime, embedding provider, memory stats) |
27
- | POST | `/api/memory/ingest` | Store a memory (JSON: `{ content, type, metadata, agentId?, sessionId?, targets?, entities?, relationships?, importance?, source?, eventTime?, id?, parentId?, ingestTime? }`) |
27
+ | POST | `/api/memory/ingest` | Store a memory (JSON: `{ content, type, metadata, agentId?, sessionId?, targets?, entities?, relationships?, importance?, source?, eventTime?, id?, parentId?, ingestTime?, pinned? }`) |
28
28
  | POST | `/api/memory/ingest/file` | Upload file (multipart, 100MB limit). For PDFs with images, returns an `enrichment` block with HMAC token and image metadata for two-phase enrichment. |
29
29
  | GET | `/api/memory/files/download/:filename` | Download uploaded file binary by filename. Returns the original file with proper Content-Type. Images served inline, documents as attachment. |
30
30
  | GET | `/api/memory/files/:fileId/images/:imageId?token=...` | Fetch an extracted PDF image (binary). Requires HMAC token from ingest response. |
@@ -192,6 +192,7 @@ interface MemoryEntry {
192
192
  teamId?: string; // team/group within tenant
193
193
  sensitivity?: SensitivityLevel; // auto-classified: 'public' | 'internal' | 'secret'
194
194
  encrypted?: boolean; // true when content is encrypted at rest
195
+ pinned?: boolean; // exempt from consolidation (decay-archive + dedup-merge); use for stable anchors referenced by external systems
195
196
  }
196
197
  ```
197
198
 
@@ -220,6 +221,7 @@ interface SearchFilters {
220
221
  eventTimeRange?: [string, string]; // [start, end] ISO timestamps
221
222
  parentId?: string;
222
223
  contentType?: string;
224
+ excludePinned?: boolean; // SQL-side `pinned = 0` filter (used internally by consolidation/decay; available to callers too)
223
225
  }
224
226
  ```
225
227