@pyxmate/memory 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -26,7 +26,15 @@ This copies skill files into `.claude/skills/pyx-memory/` so Claude Code auto-di
26
26
  import { MemoryClient } from '@pyxmate/memory';
27
27
  import type { MemoryEntry } from '@pyxmate/memory';
28
28
 
29
- const client = new MemoryClient('https://your-pyx-memory-endpoint');
29
+ // Simple: URL + API key
30
+ const client = new MemoryClient('https://your-pyx-memory-endpoint', process.env.MEMORY_API_KEY);
31
+
32
+ // Multi-tenant: URL + options with default headers
33
+ const client = new MemoryClient('https://your-pyx-memory-endpoint', {
34
+ apiKey: process.env.MEMORY_API_KEY,
35
+ defaultHeaders: { 'X-Tenant-Id': 'tenant-abc' },
36
+ });
37
+
30
38
  await client.initialize();
31
39
 
32
40
  // Store a memory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -6,12 +6,15 @@ description: >
6
6
  remember something, recall prior context, store a decision or bug fix, ingest
7
7
  files or images, or search for what was discussed before. Also use when
8
8
  integrating the pyx-memory SDK into code, working with MemoryClient, or using
9
- the HTTP API.
9
+ the HTTP API. Covers multi-tenant isolation (tenantId, TENANT_MODE),
10
+ sensitivity classification, encryption at rest, and confidence/abstention.
10
11
  Triggers on: 'remember', 'recall', 'store memory', 'search memory',
11
12
  'ingest file', 'store this image', 'remember this document',
12
13
  'what did we decide', 'pyx-memory', 'memory', 'MemoryClient',
13
- 'integrate memory', 'memory consolidation'.
14
- allowed-tools: Read, Grep, Glob, Bash(curl *)
14
+ 'integrate memory', 'memory consolidation', 'multi-tenant',
15
+ 'tenant isolation', 'sensitivity', 'encryption', 'confidence',
16
+ 'abstention'.
17
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash(curl *)
15
18
  argument-hint: "[store|search|ingest] <content or query>"
16
19
  ---
17
20
 
@@ -44,6 +47,16 @@ curl -s -X POST {{ENDPOINT}}/api/memory/ingest \
44
47
  -d '{"content":"WHAT_HAPPENED","type":"long-term","metadata":{"source":"agent","topic":"TOPIC","project":"PROJECT_NAME"}}'
45
48
  ```
46
49
 
50
+ **Multi-tenant mode**: When the server uses `TENANT_MODE=multi`, include `X-Tenant-Id` on all requests:
51
+
52
+ ```bash
53
+ curl -s -X POST {{ENDPOINT}}/api/memory/ingest \
54
+ -H "Authorization: Bearer {{API_KEY}}" \
55
+ -H "Content-Type: application/json" \
56
+ -H "X-Tenant-Id: {{TENANT_ID}}" \
57
+ -d '{"content":"WHAT_HAPPENED","type":"long-term","metadata":{"source":"agent","topic":"TOPIC","project":"PROJECT_NAME"}}'
58
+ ```
59
+
47
60
  When the memory mentions specific people, tools, organizations, or other named subjects, also include `entities` and `relationships` to populate the knowledge graph. See [reference/http-api.md](reference/http-api.md) for entity types, relationship types, and examples.
48
61
 
49
62
  ## Search: before making assumptions
@@ -60,6 +73,8 @@ curl -s "{{ENDPOINT}}/api/memory/search?q=QUERY&limit=5" \
60
73
  -H "Authorization: Bearer {{API_KEY}}"
61
74
  ```
62
75
 
76
+ **Multi-tenant mode**: Add `-H "X-Tenant-Id: {{TENANT_ID}}"` to scope search results to the tenant.
77
+
63
78
  ## Ingest files & images: when a file is worth remembering
64
79
 
65
80
  Upload files directly when the content is worth persisting — diagrams, screenshots, documents, data files.
@@ -21,7 +21,15 @@ const MEMORY_URL = process.env.MEMORY_URL; // e.g., 'http://localhost:7822'
21
21
  let memory: MemoryClient | null = null;
22
22
 
23
23
  if (MEMORY_URL) {
24
+ // Simple: API key only
24
25
  memory = new MemoryClient(MEMORY_URL, process.env.MEMORY_API_KEY);
26
+
27
+ // Multi-tenant: API key + tenant headers
28
+ // memory = new MemoryClient(MEMORY_URL, {
29
+ // apiKey: process.env.MEMORY_API_KEY,
30
+ // defaultHeaders: { 'X-Tenant-Id': 'tenant-abc', 'X-User-Id': 'user-123' },
31
+ // });
32
+
25
33
  await memory.initialize(); // verifies connectivity
26
34
  }
27
35
 
@@ -100,8 +108,11 @@ services:
100
108
  - memory-data:/data
101
109
  environment:
102
110
  - DATA_DIR=/data
111
+ - API_KEY=${MEMORY_API_KEY} # auth for all requests
112
+ # - TENANT_MODE=multi # require X-Tenant-Id on all ops
113
+ # - SENSITIVITY_POLICY=encrypt # encrypt secret entries at rest
114
+ # - ENCRYPTION_KEY=${ENCRYPTION_KEY} # 32 bytes as 64 hex chars
103
115
  # Embedding is internal (BGE-M3, 1024d) — no API keys needed
104
- # - EMBEDDING_DIMENSIONS=1024 # optional override
105
116
 
106
117
  your-app:
107
118
  environment:
@@ -37,6 +37,32 @@ await memory.initialize();
37
37
  // No external embedding provider needed
38
38
  ```
39
39
 
40
+ ## Pattern 2b: Production with Multi-Tenant + Encryption
41
+
42
+ ```typescript
43
+ import { Memory } from '@pyx-memory/core';
44
+
45
+ const memory = new Memory({
46
+ dataDir: './data',
47
+ tenantId: 'tenant-abc', // Auto-scope all operations to this tenant
48
+ encryptionKey: Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'), // 32 bytes for AES-256-GCM
49
+ });
50
+ await memory.initialize();
51
+
52
+ // All store/search/get/delete/list operations are automatically tenant-scoped
53
+ await memory.store({
54
+ content: 'Secret API key: sk-abc123', // auto-classified as 'secret', encrypted at rest
55
+ type: 'long-term',
56
+ metadata: { source: 'config' },
57
+ });
58
+
59
+ // Search respects tenant isolation + sensitivity filtering
60
+ const results = await memory.search({
61
+ query: 'API key',
62
+ maxSensitivity: 'internal', // 'secret' entries are redacted in results
63
+ });
64
+ ```
65
+
40
66
  ## Pattern 3: Production with Store Targets
41
67
 
42
68
  ```typescript
@@ -105,11 +105,130 @@ Communities are leveraged by the hybrid RAG strategy to answer broad "what are t
105
105
 
106
106
  ---
107
107
 
108
+ ## Multi-Tenant Isolation
109
+
110
+ pyx-memory supports multi-tenant data isolation via three scoping fields: `tenantId`, `userId`, and `teamId`.
111
+
112
+ ### Embedded mode
113
+
114
+ ```typescript
115
+ // Instance-level scoping — auto-scopes ALL operations
116
+ const memory = new Memory({
117
+ dataDir: './data',
118
+ tenantId: 'tenant-abc',
119
+ });
120
+
121
+ // Or per-operation scoping
122
+ await memory.store({
123
+ content: 'Fact for this tenant.',
124
+ tenantId: 'tenant-abc',
125
+ userId: 'user-123',
126
+ teamId: 'team-eng',
127
+ });
128
+
129
+ // Search is auto-scoped to the instance tenantId
130
+ const results = await memory.search({ query: 'facts', limit: 5 });
131
+
132
+ // get/delete/clearSession/stats accept TenantScopeOptions
133
+ const entry = await memory.get('entry-id', { tenantId: 'tenant-abc' });
134
+ ```
135
+
136
+ ### Sidecar mode
137
+
138
+ ```typescript
139
+ // Set tenant headers via MemoryClientOptions
140
+ const memory = new MemoryClient('http://localhost:7822', {
141
+ apiKey: process.env.MEMORY_API_KEY,
142
+ defaultHeaders: { 'X-Tenant-Id': 'tenant-abc', 'X-User-Id': 'user-123' },
143
+ });
144
+
145
+ // Or pass tenantId per-operation in the request body
146
+ await memory.store({
147
+ content: 'Tenant-scoped memory.',
148
+ tenantId: 'tenant-abc',
149
+ userId: 'user-123',
150
+ });
151
+ ```
152
+
153
+ ### Server enforcement
154
+
155
+ Set `TENANT_MODE=multi` to require `X-Tenant-Id` on all operations. Requests without it are rejected (HTTP 400). When `TENANT_MODE=single` (default), tenant fields are stored but not enforced — backward compatible.
156
+
157
+ Content hash dedup is scoped by `tenantId` to prevent cross-tenant collisions.
158
+
159
+ ---
160
+
161
+ ## Sensitivity Classification & Encryption
162
+
163
+ pyx-memory auto-classifies content sensitivity and optionally encrypts sensitive data at rest.
164
+
165
+ ### How it works
166
+
167
+ 1. Every `store()` scans content for credentials and PII
168
+ 2. Auto-classifies as `public`, `internal` (PII), or `secret` (credentials)
169
+ 3. Behavior depends on `SENSITIVITY_POLICY` env var (or embedded config):
170
+ - `flag`: classify only, no content modification (default)
171
+ - `redact`: replace detected credentials with `[REDACTED]`
172
+ - `block`: reject ingestion (HTTP 400)
173
+ - `encrypt`: encrypt `secret` entries at rest with AES-256-GCM
174
+
175
+ ### Embedded encryption
176
+
177
+ ```typescript
178
+ const memory = new Memory({
179
+ dataDir: './data',
180
+ encryptionKey: Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'), // 32 bytes
181
+ });
182
+
183
+ // Entries classified as 'secret' are automatically encrypted before storage
184
+ // Decryption is automatic on get() when the encryption key is available
185
+ ```
186
+
187
+ ### Search access control
188
+
189
+ ```typescript
190
+ // Embedded: filter by max sensitivity
191
+ const results = await memory.search({
192
+ query: 'config',
193
+ maxSensitivity: 'internal', // secret entries are redacted in results
194
+ });
195
+
196
+ // Sidecar: via header
197
+ const memory = new MemoryClient('http://localhost:7822', {
198
+ apiKey: 'key',
199
+ defaultHeaders: { 'X-Caller-Access-Level': 'internal' },
200
+ });
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Confidence / Abstention
206
+
207
+ Search results can include a confidence score that enables "I don't know" responses when retrieval quality is low.
208
+
209
+ ```typescript
210
+ const results = await memory.search({
211
+ query: 'user preferences',
212
+ strategy: 'hybrid',
213
+ abstentionThreshold: 0.5, // 0-1, default 0.3
214
+ });
215
+
216
+ if (results.confidence?.shouldAbstain) {
217
+ // Confidence is below threshold — don't use these results
218
+ return "I don't have enough information to answer that.";
219
+ }
220
+ ```
221
+
222
+ The confidence score is computed from four signals: top score magnitude, score gap (top vs second), score spread (stdDev), and score separation (top vs mean). It's automatically included when using hybrid, graph, or agentic strategies.
223
+
224
+ ---
225
+
108
226
  ## Automatic Behaviors
109
227
 
110
228
  These happen automatically — no configuration needed:
111
229
 
112
230
  - **PII detection**: Every `store()` scans content and sets `metadata.piiDetected` + `metadata.piiTypes` if found
231
+ - **Sensitivity classification**: Every `store()` auto-classifies content as `public`, `internal`, or `secret`
113
232
  - **Content hashing**: Every `store()` computes SHA-256 `contentHash`
114
233
  - **Access tracking**: Every `search()` increments `accessCount` and updates `lastAccessed` on returned entries
115
234
  - **FTS5 sync**: SQLite triggers keep full-text search index in sync with memory_entries
@@ -117,6 +236,7 @@ These happen automatically — no configuration needed:
117
236
  - **Auto-registration on import**: Importing `@pyx-memory/core` registers StubEmbeddingProvider, LanceDBProvider, and NaiveRAGEngine
118
237
  - **Graph/Agentic RAG registration**: Memory constructor auto-registers GraphRAGEngine and AgenticRAGEngine when you pass `graphStore` or `reasoningProvider`
119
238
  - **Agent scoping**: If `agentId` is set in MemoryOptions, all operations auto-filter by that agent
239
+ - **Tenant scoping**: If `tenantId` is set in MemoryOptions, all operations auto-filter by that tenant
120
240
 
121
241
  ---
122
242
 
@@ -36,7 +36,7 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
36
36
  | DELETE | `/api/memory/entries/:id` | Delete entry |
37
37
  | DELETE | `/api/memory/sessions/:sessionId` | Clear session |
38
38
 
39
- ## Graph (4 endpoints)
39
+ ## Graph (5 endpoints)
40
40
 
41
41
  | Method | Endpoint | Description |
42
42
  |--------|----------|-------------|
@@ -44,6 +44,7 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
44
44
  | GET | `/api/memory/graph/edges` | Graph stats |
45
45
  | GET | `/api/memory/graph/relationships` | List relationships |
46
46
  | POST | `/api/memory/graph/query` | Traverse (JSON: `{ nodeId, depth? }`) |
47
+ | POST | `/api/memory/graph/clear` | Clear all graph nodes and edges (admin) |
47
48
 
48
49
  ## Lifecycle (9 endpoints)
49
50
 
@@ -252,6 +253,86 @@ curl -s -X POST {{ENDPOINT}}/api/memory/ingest \
252
253
 
253
254
  ---
254
255
 
256
+ ## Multi-Tenant Isolation
257
+
258
+ When `TENANT_MODE=multi`, the server requires `X-Tenant-Id` on all operations. Requests without it are rejected with HTTP 400.
259
+
260
+ ### Tenant Headers
261
+
262
+ | Header | Description |
263
+ |--------|-------------|
264
+ | `X-Tenant-Id` | **Required** in multi-tenant mode. Tenant ID for data isolation. |
265
+ | `X-User-Id` | Optional. User ID within the tenant. |
266
+ | `X-Team-Id` | Optional. Team/group ID within the tenant. |
267
+
268
+ As a fallback, `tenantId` can be extracted from JWT Bearer token claims (`tenantId`, `tenant_id`, or `tid` field in the payload).
269
+
270
+ ### Example: multi-tenant ingest
271
+
272
+ ```bash
273
+ curl -X POST {{ENDPOINT}}/api/memory/ingest \
274
+ -H "Authorization: Bearer {{API_KEY}}" \
275
+ -H "Content-Type: application/json" \
276
+ -H "X-Tenant-Id: tenant-abc" \
277
+ -H "X-User-Id: user-123" \
278
+ -H "X-Team-Id: team-eng" \
279
+ -d '{"content":"Tenant-scoped memory.","type":"long-term","metadata":{}}'
280
+ ```
281
+
282
+ Tenant scoping is enforced on all operations: ingest, search, list, get, delete, clearSession, stats.
283
+
284
+ When `TENANT_MODE=single` (default), no tenant filtering applies (backward compatible). Tenant fields in the request body are still stored but not enforced.
285
+
286
+ ---
287
+
288
+ ## Sensitivity Classification & Encryption
289
+
290
+ pyx-memory auto-classifies content sensitivity and optionally encrypts sensitive data at rest.
291
+
292
+ ### Sensitivity levels
293
+
294
+ | Level | Auto-detected when |
295
+ |-------|--------------------|
296
+ | `public` | Neither credentials nor PII detected |
297
+ | `internal` | PII detected (email, phone, SSN, etc.) |
298
+ | `secret` | API keys, tokens, connection strings, private keys, passwords detected |
299
+
300
+ ### Sensitivity policy (`SENSITIVITY_POLICY` env var)
301
+
302
+ | Policy | Behavior |
303
+ |--------|----------|
304
+ | `flag` (default) | Detect and classify. Store `sensitivity` field on entry. No content modification. |
305
+ | `redact` | Replace detected credentials with `[REDACTED]` before storage. |
306
+ | `block` | Reject ingest requests containing credentials (HTTP 400). |
307
+ | `encrypt` | Entries classified as `secret` are encrypted at rest with AES-256-GCM. |
308
+
309
+ ### Search access control
310
+
311
+ The `maxSensitivity` search parameter (or `X-Caller-Access-Level` header) filters results by sensitivity level. Entries above the caller's access level have their content replaced with `[REDACTED: sensitive content]`.
312
+
313
+ ```bash
314
+ # Only see public and internal entries (secret entries are redacted)
315
+ curl '{{ENDPOINT}}/api/memory/search?query=config' \
316
+ -H "Authorization: Bearer {{API_KEY}}" \
317
+ -H "X-Caller-Access-Level: internal"
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Confidence / Abstention
323
+
324
+ Search results include optional confidence scoring via the `abstentionThreshold` parameter.
325
+
326
+ ```bash
327
+ # Get confidence scoring with custom threshold (0-1, default 0.3)
328
+ curl '{{ENDPOINT}}/api/memory/search?query=user+preferences&strategy=hybrid&abstentionThreshold=0.5'
329
+ # Response includes: { confidence: { confidence: 0.72, shouldAbstain: false, signals: { ... } } }
330
+ ```
331
+
332
+ When `shouldAbstain` is `true`, the retrieval confidence is below the threshold — the agent should respond with "I don't know" rather than using low-confidence results.
333
+
334
+ ---
335
+
255
336
  ## Response Format
256
337
 
257
338
  All responses follow: `{ success: boolean, data?: T, error?: string }`
@@ -274,8 +355,11 @@ All responses follow: `{ success: boolean, data?: T, error?: string }`
274
355
  | `API_KEY` | — | API key for authenticating requests. Unset = open access |
275
356
  | `ADMIN_API_KEY` | — | Separate admin key for destructive ops (DELETE, forget, decay, consolidate, reindex). Falls back to `API_KEY` |
276
357
  | `CORS_ORIGIN` | `*` | CORS allowed origin. Set to specific domain in production |
277
- | `MAX_REQUEST_BODY_MB` | `10` | Maximum request body size in MB |
358
+ | `MAX_REQUEST_BODY_MB` | `512` | Maximum request body size in MB |
278
359
  | `NODE_ENV` | `development` | Set to `production` to mask 5xx error details and enable HSTS |
279
360
  | `PII_POLICY` | `flag` | PII handling: `flag` (detect + tag), `redact` (replace with [REDACTED]), `block` (reject 400) |
361
+ | `SENSITIVITY_POLICY` | `flag` | Credential sensitivity handling: `flag` (detect + classify), `redact` (replace with [REDACTED]), `block` (reject 400), `encrypt` (AES-256-GCM at rest) |
362
+ | `ENCRYPTION_KEY` | — | AES-256-GCM encryption key for sensitive content at rest (32 bytes as 64 hex chars or 44 base64 chars). Required when `SENSITIVITY_POLICY=encrypt` |
280
363
  | `RATE_LIMIT_RPM` | `0` | Requests per minute per IP. 0 = disabled |
364
+ | `TENANT_MODE` | `single` | Tenant isolation mode: `single` (no tenant filtering, backward compatible) or `multi` (require `X-Tenant-Id` header on all operations) |
281
365
  | `ENRICHMENT_SECRET` | (auto-generated) | HMAC secret for enrichment token signing. Auto-generated if unset (tokens won't survive restarts). Min 32 bytes for production. |
@@ -20,6 +20,10 @@
20
20
  | id (custom) | yes | yes |
21
21
  | parentId | yes | yes |
22
22
  | ingestTime | yes | yes |
23
+ | tenantId | yes | yes |
24
+ | userId | yes | yes |
25
+ | teamId | yes | yes |
26
+ | sensitivity | yes | yes (auto-classified) |
23
27
 
24
28
  **All StoreInput fields are forwarded.** Full parity.
25
29
 
@@ -28,6 +32,9 @@
28
32
  | Param | Embedded `Memory.search()` | Sidecar `MemoryClient.search()` |
29
33
  |-------|---------------------------|--------------------------------|
30
34
  | query, limit, type, agentId, strategy | yes | yes |
35
+ | tenantId, userId, teamId | yes | yes (via `X-Tenant-Id`/`X-User-Id`/`X-Team-Id` headers or `defaultHeaders` — NOT forwarded from per-request params) |
36
+ | maxSensitivity | yes | yes (via `X-Caller-Access-Level` header or `defaultHeaders` — NOT forwarded from per-request params) |
37
+ | abstentionThreshold | yes | yes |
31
38
  | eventTimeRange (bi-temporal search) | yes | yes |
32
39
  | asOf (point-in-time search) | yes | yes |
33
40
  | **filters** (source, importanceMin, parentId, contentType) | yes | **NO** — not forwarded |
@@ -42,6 +49,7 @@
42
49
  |----------------|-------------|-----------------|
43
50
  | All core (9) | yes | yes (inherited) |
44
51
  | Graph nodes/edges/query (3) | yes (concrete methods) | yes (inherited) |
52
+ | Graph clear (admin) | yes (`graphClear()`) | yes (inherited) |
45
53
  | **Graph relationships** | **NO** | yes (`graphRelationships()`) |
46
54
  | All lifecycle (7) | yes | yes (inherited) |
47
55
  | **Consolidation log** | **NO** | yes (`consolidationLog()`) |
@@ -59,5 +67,8 @@
59
67
  | Security headers | Always on (CSP, X-Frame-Options, nosniff) |
60
68
  | HSTS | Auto-enabled when `NODE_ENV=production` |
61
69
  | PII policy | `PII_POLICY` env var (`flag` / `redact` / `block`) |
62
- | Body size limit | `MAX_REQUEST_BODY_MB` env var (default: 10) |
70
+ | Sensitivity policy | `SENSITIVITY_POLICY` env var (`flag` / `redact` / `block` / `encrypt`) |
71
+ | Encryption at rest | `ENCRYPTION_KEY` env var (32 bytes, hex or base64) |
72
+ | Multi-tenant isolation | `TENANT_MODE` env var (`single` / `multi`) |
73
+ | Body size limit | `MAX_REQUEST_BODY_MB` env var (default: 512) |
63
74
  | Error masking | 5xx details hidden when `NODE_ENV=production` |
@@ -77,6 +77,15 @@ const memory = new MemoryClient('http://localhost:7822');
77
77
  // With auth (production)
78
78
  const authedMemory = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_KEY);
79
79
 
80
+ // With auth + multi-tenant headers (production, multi-tenant mode)
81
+ const tenantMemory = new MemoryClient('http://localhost:7822', {
82
+ apiKey: process.env.MEMORY_API_KEY,
83
+ defaultHeaders: {
84
+ 'X-Tenant-Id': 'tenant-abc',
85
+ 'X-User-Id': 'user-123',
86
+ },
87
+ });
88
+
80
89
  await memory.initialize(); // verifies server connectivity via /health
81
90
 
82
91
  await memory.store({ content: 'Important fact', type: 'long-term', metadata: {} });
@@ -122,6 +131,8 @@ Start the server: `bun packages/server/src/index.ts`
122
131
  - **DO** use `':memory:'` dataDir for tests
123
132
  - **DO** handle `MemoryServerError` in sidecar mode (has `.status` and `.isNotFound`)
124
133
  - **DO** implement `DisabledMemory` (no-op) for graceful degradation when memory is unavailable
134
+ - **DO** set `tenantId` on `MemoryOptions` or pass `X-Tenant-Id` header for multi-tenant deployments
135
+ - **DO** use `MemoryClientOptions` with `defaultHeaders` for tenant/access-level headers in sidecar mode
125
136
 
126
137
  ### DON'T
127
138
 
@@ -132,6 +143,7 @@ Start the server: `bun packages/server/src/index.ts`
132
143
  - **DON'T** construct multiple Memory instances with the same `dataDir` — LanceDB singleton causes conflicts
133
144
  - **DON'T** use `':memory:'` in production — LanceDB still writes to `/tmp/autonomy-vectors`
134
145
  - **DON'T** expose the server without configuring `API_KEY` for network deployments
146
+ - **DON'T** run `TENANT_MODE=multi` without `API_KEY` — multi-tenant without auth is dangerous
135
147
  - **DON'T** import `@pyx-memory/core` in consumer projects — use `@pyx-memory/client`
136
148
  - **DON'T** expect `filters`, `enableHyDE`, or `enableRerank` to work in sidecar mode
137
149
 
@@ -25,7 +25,10 @@
25
25
  | `IngestRelationship` | `{ source, target, type, properties? }` | `@pyx-memory/shared` |
26
26
  | `EntityType` | `'PERSON' \| 'ORGANIZATION' \| 'CONCEPT' \| 'TOOL' \| 'LOCATION' \| 'EVENT'` | `@pyx-memory/core` |
27
27
  | `RelationType` | `'USES' \| 'OWNS' \| 'DEPENDS_ON' \| 'RELATED_TO' \| 'CREATED_BY' \| 'PART_OF' \| 'IS_A' \| 'WORKS_AT' \| 'LOCATED_IN'` | `@pyx-memory/core` |
28
- | `MemoryListParams` | `{ page?, limit?, type?, agentId? }` | `@pyx-memory/client` |
28
+ | `TenantScopeOptions` | `{ tenantId? }` | `@pyx-memory/client` |
29
+ | `SensitivityLevel` | `'public' \| 'internal' \| 'secret'` | `@pyx-memory/shared` |
30
+ | `MemoryClientOptions` | `{ apiKey?, defaultHeaders? }` | `@pyx-memory/client` |
31
+ | `MemoryListParams` | `{ page?, limit?, type?, agentId?, tenantId? }` | `@pyx-memory/client` |
29
32
  | `MemoryListResult` | `{ entries, totalCount, page, limit }` | `@pyx-memory/client` |
30
33
  | `IngestionResult` | `{ filename, chunks, totalCharacters }` | `@pyx-memory/client` |
31
34
  | `FileIngestResult` | `IngestionResult & { enrichment?: EnrichmentPending }` | `@pyx-memory/shared` |
@@ -49,10 +52,12 @@ interface MemoryInterface {
49
52
  store(entry: Omit<MemoryEntry, 'id' | 'createdAt'> & { id?: string; createdAt?: string; targets?: StoreTarget[]; entities?: IngestEntity[]; relationships?: IngestRelationship[] }): Promise<MemoryEntry>;
50
53
  search(params: MemorySearchParams): Promise<MemorySearchResult>;
51
54
  list(params?: MemoryListParams): Promise<MemoryListResult>; // paginated entry listing
52
- get(id: string): Promise<MemoryEntry | null>;
53
- delete(id: string): Promise<boolean>;
54
- clearSession(sessionId: string): Promise<number>;
55
- stats(): Promise<MemoryStats>;
55
+ get(id: string, options?: TenantScopeOptions): Promise<MemoryEntry | null>;
56
+ delete(id: string, options?: TenantScopeOptions): Promise<boolean>;
57
+ clearSession(sessionId: string, options?: TenantScopeOptions): Promise<number>;
58
+ stats(options?: TenantScopeOptions): Promise<MemoryStats>;
59
+ queryAsOf(asOfDate: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;
60
+ queryByEventTime(startTime: string, endTime: string, filters?: TemporalQueryFilters): Promise<MemoryEntry[]>;
56
61
  shutdown(): Promise<void>;
57
62
  }
58
63
  ```
@@ -73,13 +78,22 @@ interface ExtendedMemoryInterface extends MemoryInterface {
73
78
  ## MemoryClient Constructor and Concrete Methods
74
79
 
75
80
  ```typescript
76
- // Constructor: URL required, apiKey optional
81
+ // Constructor: URL required, second param is string (apiKey) or MemoryClientOptions
77
82
  const client = new MemoryClient('http://localhost:7822'); // no auth
78
- const client = new MemoryClient('http://localhost:7822', 'my-api-key'); // with auth
83
+ const client = new MemoryClient('http://localhost:7822', 'my-api-key'); // with auth (string shorthand)
79
84
  const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_KEY); // from env
85
+
86
+ // Advanced: options object with default headers (e.g., multi-tenant access control)
87
+ const client = new MemoryClient('http://localhost:7822', {
88
+ apiKey: 'my-api-key',
89
+ defaultHeaders: {
90
+ 'X-Tenant-Id': 'tenant-abc',
91
+ 'X-Caller-Access-Level': 'internal',
92
+ },
93
+ });
80
94
  ```
81
95
 
82
- When `apiKey` is provided, all requests include `Authorization: Bearer <key>`. Empty or whitespace-only keys are ignored.
96
+ When `apiKey` is provided, all requests include `Authorization: Bearer <key>`. Empty or whitespace-only keys are ignored. Default headers are merged into every request.
83
97
 
84
98
  `MemoryClient` implements `ExtendedMemoryInterface` AND has additional methods not on any interface.
85
99
  Graph queries and file ingestion are available without `@pyx-memory/core`.
@@ -138,6 +152,18 @@ interface MemorySearchResult {
138
152
  totalCount: number;
139
153
  strategy: RAGStrategy;
140
154
  scoredEntries?: Array<{ entry: MemoryEntry; score: number }>; // ranked results with relevance scores
155
+ confidence?: { // present when abstentionThreshold is set or strategy uses scoring
156
+ confidence: number; // 0-1 confidence score
157
+ shouldAbstain: boolean; // true when confidence < abstentionThreshold
158
+ signals: {
159
+ topScore: number; // magnitude of the highest-scoring result
160
+ scoreGap: number; // difference between #1 and #2 results
161
+ scoreStdDev: number; // spread of scores (high = clear winner)
162
+ resultCount: number; // how many results were returned
163
+ aboveThresholdRatio: number; // fraction of results above threshold
164
+ scoreSeparation: number; // how much top-1 stands out from the pack
165
+ };
166
+ };
141
167
  }
142
168
  ```
143
169
 
@@ -161,6 +187,11 @@ interface MemoryEntry {
161
187
  source?: string; // filename, URL, session ID
162
188
  eventTime?: string; // when event happened (bi-temporal)
163
189
  ingestTime?: string; // when stored (bi-temporal)
190
+ tenantId?: string; // multi-tenant isolation
191
+ userId?: string; // user within tenant
192
+ teamId?: string; // team/group within tenant
193
+ sensitivity?: SensitivityLevel; // auto-classified: 'public' | 'internal' | 'secret'
194
+ encrypted?: boolean; // true when content is encrypted at rest
164
195
  }
165
196
  ```
166
197
 
@@ -176,6 +207,11 @@ interface MemorySearchParams {
176
207
  filters?: SearchFilters;
177
208
  enableHyDE?: boolean; // Hypothetical Document Embedding for query expansion
178
209
  enableRerank?: boolean; // Cross-encoder reranking of results
210
+ tenantId?: string; // multi-tenant scoping
211
+ userId?: string; // user-level scoping
212
+ teamId?: string; // team-level scoping
213
+ maxSensitivity?: SensitivityLevel; // filter by max sensitivity level
214
+ abstentionThreshold?: number; // 0-1, below this confidence → shouldAbstain=true (default: 0.3)
179
215
  }
180
216
 
181
217
  interface SearchFilters {
@@ -201,6 +237,8 @@ interface SearchFilters {
201
237
  | `llm` | `LLMCallback` | no | `undefined` | Enables LLM-powered lifecycle (consolidation, summarization, scoring) |
202
238
  | `skipDuplicates` | `boolean` | no | `false` | Content-hash dedup on store |
203
239
  | `agentId` | `string` | no | `undefined` | Auto-scopes ALL store/search/stats/decay operations to this agent |
240
+ | `tenantId` | `string` | no | `undefined` | Auto-scopes ALL operations to this tenant (multi-tenant isolation) |
241
+ | `encryptionKey` | `Buffer` | no | `undefined` | 32-byte AES-256-GCM key for encrypting `secret` entries at rest |
204
242
  | `qdrantUrl` | `string` | no | `undefined` | @deprecated — Qdrant support is vestigial |
205
243
 
206
244
  ## Embedding Dimension Defaults by Provider