@nuzo/memory-core 0.1.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.
@@ -0,0 +1,455 @@
1
+ import { NuzoMemoryError } from "./errors.js";
2
+ import { memoryKinds } from "./types.js";
3
+ export function createMemoryService(dependencies) {
4
+ const { auditLog, clock, ids, policy, searchIndex, store, transactions } = dependencies;
5
+ const runTransaction = transactions
6
+ ? (operation) => transactions.run(operation)
7
+ : (operation) => operation();
8
+ async function forgetMemory(input) {
9
+ assertActor(input.actor);
10
+ const memory = await store.findById(input.id);
11
+ if (!memory) {
12
+ throw new NuzoMemoryError("MEMORY_NOT_FOUND", "Memory was not found.", { id: input.id });
13
+ }
14
+ const mode = input.mode ?? "archive";
15
+ const now = clock.now();
16
+ if (mode === "delete") {
17
+ if (input.confirm !== true) {
18
+ throw new NuzoMemoryError("MEMORY_DELETE_CONFIRMATION_REQUIRED", "Hard delete requires explicit confirmation.", { id: input.id });
19
+ }
20
+ await runTransaction(async () => {
21
+ await store.delete(input.id);
22
+ await searchIndex.remove(input.id);
23
+ await auditLog.append({
24
+ id: ids.eventId(),
25
+ memoryId: input.id,
26
+ eventType: "memory.deleted",
27
+ actor: input.actor,
28
+ payload: { reason: input.reason ?? null },
29
+ createdAt: now,
30
+ });
31
+ });
32
+ return;
33
+ }
34
+ await runTransaction(async () => {
35
+ await store.archive(input.id, now);
36
+ await searchIndex.remove(input.id);
37
+ await auditLog.append({
38
+ id: ids.eventId(),
39
+ memoryId: input.id,
40
+ eventType: "memory.archived",
41
+ actor: input.actor,
42
+ payload: { reason: input.reason ?? null },
43
+ createdAt: now,
44
+ });
45
+ });
46
+ }
47
+ return {
48
+ async remember(input) {
49
+ await policy.assertCanRemember(input);
50
+ const now = clock.now();
51
+ const memory = {
52
+ id: ids.memoryId(),
53
+ scope: input.scope,
54
+ kind: input.kind,
55
+ content: input.content.trim(),
56
+ tags: [...new Set(input.tags ?? [])],
57
+ source: input.source,
58
+ confidence: input.confidence ?? 1,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ lastUsedAt: null,
62
+ archivedAt: null,
63
+ };
64
+ await runTransaction(async () => {
65
+ await store.create(memory);
66
+ await searchIndex.index(memory);
67
+ await auditLog.append({
68
+ id: ids.eventId(),
69
+ memoryId: memory.id,
70
+ eventType: "memory.created",
71
+ actor: input.source,
72
+ payload: { kind: memory.kind, scope: memory.scope, tags: memory.tags },
73
+ createdAt: now,
74
+ });
75
+ });
76
+ return memory;
77
+ },
78
+ async recall(input) {
79
+ await policy.assertCanRecall(input);
80
+ const results = await searchIndex.search({
81
+ ...input,
82
+ limit: input.limit ?? 8,
83
+ });
84
+ if (input.recordUsage === false) {
85
+ return results;
86
+ }
87
+ const now = clock.now();
88
+ await runTransaction(async () => {
89
+ for (const result of results) {
90
+ await store.update({
91
+ ...result.memory,
92
+ lastUsedAt: now,
93
+ });
94
+ await auditLog.append({
95
+ id: ids.eventId(),
96
+ memoryId: result.memory.id,
97
+ eventType: "memory.recalled",
98
+ actor: "core",
99
+ payload: { query: input.query, score: result.score },
100
+ createdAt: now,
101
+ });
102
+ }
103
+ });
104
+ return results;
105
+ },
106
+ async list(input = {}) {
107
+ await policy.assertCanList(input);
108
+ return store.list(input);
109
+ },
110
+ async update(input) {
111
+ const current = await store.findById(input.id);
112
+ if (!current) {
113
+ throw new NuzoMemoryError("MEMORY_NOT_FOUND", "Memory was not found.", { id: input.id });
114
+ }
115
+ const hasChanges = input.content !== undefined ||
116
+ input.kind !== undefined ||
117
+ input.scope !== undefined ||
118
+ input.tags !== undefined ||
119
+ input.confidence !== undefined;
120
+ if (!hasChanges) {
121
+ throw new NuzoMemoryError("MEMORY_UPDATE_EMPTY", "At least one memory field must be updated.", {
122
+ id: input.id,
123
+ });
124
+ }
125
+ await policy.assertCanUpdate(input, current);
126
+ const updated = {
127
+ ...current,
128
+ content: input.content?.trim() ?? current.content,
129
+ kind: input.kind ?? current.kind,
130
+ scope: input.scope ?? current.scope,
131
+ tags: input.tags ? [...new Set(input.tags)] : current.tags,
132
+ confidence: input.confidence ?? current.confidence,
133
+ updatedAt: clock.now(),
134
+ };
135
+ await runTransaction(async () => {
136
+ await store.update(updated);
137
+ await searchIndex.index(updated);
138
+ await auditLog.append({
139
+ id: ids.eventId(),
140
+ memoryId: updated.id,
141
+ eventType: "memory.updated",
142
+ actor: input.actor,
143
+ payload: {
144
+ changed: {
145
+ content: input.content !== undefined,
146
+ kind: input.kind !== undefined,
147
+ scope: input.scope !== undefined,
148
+ tags: input.tags !== undefined,
149
+ confidence: input.confidence !== undefined,
150
+ },
151
+ },
152
+ createdAt: updated.updatedAt,
153
+ });
154
+ });
155
+ return updated;
156
+ },
157
+ async history(memoryId) {
158
+ if (memoryId.trim().length === 0) {
159
+ throw new NuzoMemoryError("MEMORY_ID_EMPTY", "Memory ID cannot be empty.");
160
+ }
161
+ return auditLog.list(memoryId);
162
+ },
163
+ async exportMemories(input) {
164
+ assertActor(input.actor);
165
+ await policy.assertCanList(input);
166
+ const memories = await store.list(input);
167
+ const now = clock.now();
168
+ await auditLog.append({
169
+ id: ids.eventId(),
170
+ memoryId: null,
171
+ eventType: "memory.exported",
172
+ actor: input.actor,
173
+ payload: {
174
+ scope: input.scope ?? null,
175
+ tags: input.tags ?? [],
176
+ includeArchived: input.includeArchived === true,
177
+ count: memories.length,
178
+ },
179
+ createdAt: now,
180
+ });
181
+ return {
182
+ format: "nuzo-memory-export",
183
+ version: 1,
184
+ exported_at: now.toISOString(),
185
+ memories: memories.map(toExportItem),
186
+ };
187
+ },
188
+ async importMemories(input) {
189
+ assertActor(input.actor);
190
+ assertExportDocument(input.document);
191
+ const planned = [];
192
+ const duplicateKeysByScope = new Map();
193
+ let skipped = 0;
194
+ for (const item of input.document.memories) {
195
+ const scope = input.scope ?? item.scope;
196
+ await policy.assertCanRemember({
197
+ content: item.content,
198
+ kind: item.kind,
199
+ scope,
200
+ tags: item.tags,
201
+ source: item.source,
202
+ confidence: item.confidence,
203
+ });
204
+ const tags = [...new Set(item.tags)];
205
+ let duplicateKeys = duplicateKeysByScope.get(scope);
206
+ if (!duplicateKeys) {
207
+ const existing = await store.list({ scope, includeArchived: true });
208
+ duplicateKeys = new Set(existing.map(toImportDuplicateKey));
209
+ duplicateKeysByScope.set(scope, duplicateKeys);
210
+ }
211
+ const duplicateKey = toImportDuplicateKey({
212
+ scope,
213
+ kind: item.kind,
214
+ content: item.content,
215
+ tags,
216
+ });
217
+ if (duplicateKeys.has(duplicateKey)) {
218
+ skipped += 1;
219
+ continue;
220
+ }
221
+ duplicateKeys.add(duplicateKey);
222
+ planned.push({ item, scope, tags });
223
+ }
224
+ if (input.dryRun === true) {
225
+ return {
226
+ imported: planned.length,
227
+ skipped,
228
+ dryRun: true,
229
+ };
230
+ }
231
+ await runTransaction(async () => {
232
+ for (const { item, scope, tags } of planned) {
233
+ const memory = {
234
+ id: ids.memoryId(),
235
+ scope,
236
+ kind: item.kind,
237
+ content: item.content.trim(),
238
+ tags,
239
+ source: item.source,
240
+ confidence: item.confidence,
241
+ createdAt: parseExportDate(item.created_at, "created_at"),
242
+ updatedAt: parseExportDate(item.updated_at, "updated_at"),
243
+ lastUsedAt: item.last_used_at ? parseExportDate(item.last_used_at, "last_used_at") : null,
244
+ archivedAt: item.archived_at ? parseExportDate(item.archived_at, "archived_at") : null,
245
+ };
246
+ await store.create(memory);
247
+ await searchIndex.index(memory);
248
+ await auditLog.append({
249
+ id: ids.eventId(),
250
+ memoryId: memory.id,
251
+ eventType: "memory.imported",
252
+ actor: input.actor,
253
+ payload: {
254
+ originalScope: item.scope,
255
+ scope,
256
+ archived: memory.archivedAt !== null,
257
+ },
258
+ createdAt: clock.now(),
259
+ });
260
+ }
261
+ });
262
+ return {
263
+ imported: planned.length,
264
+ skipped,
265
+ dryRun: false,
266
+ };
267
+ },
268
+ forget: forgetMemory,
269
+ async forgetMany(input) {
270
+ const hasScope = input.scope !== undefined;
271
+ const hasTags = (input.tags?.length ?? 0) > 0;
272
+ const selectsAll = input.all === true;
273
+ if (!selectsAll && !hasScope && !hasTags) {
274
+ throw new NuzoMemoryError("MEMORY_BULK_SELECTOR_REQUIRED", "Bulk forget requires a scope, at least one tag, or all.");
275
+ }
276
+ if (selectsAll && (hasScope || hasTags)) {
277
+ throw new NuzoMemoryError("MEMORY_BULK_SELECTOR_CONFLICT", "Bulk forget all cannot be combined with scope or tags.");
278
+ }
279
+ assertActor(input.actor);
280
+ await policy.assertCanList({
281
+ ...(input.scope === undefined ? {} : { scope: input.scope }),
282
+ ...(input.tags === undefined ? {} : { tags: input.tags }),
283
+ });
284
+ const mode = input.mode ?? "archive";
285
+ const dryRun = input.dryRun !== false;
286
+ if (!dryRun && mode === "delete" && input.confirm !== true) {
287
+ throw new NuzoMemoryError("MEMORY_DELETE_CONFIRMATION_REQUIRED", "Hard delete requires explicit confirmation.");
288
+ }
289
+ const filter = {};
290
+ if (input.scope !== undefined) {
291
+ filter.scope = input.scope;
292
+ }
293
+ if (hasTags) {
294
+ filter.tags = input.tags;
295
+ }
296
+ const memories = await store.list(filter);
297
+ const memoryIds = memories.map((memory) => memory.id);
298
+ if (!dryRun) {
299
+ for (const memory of memories) {
300
+ const forgetInput = {
301
+ id: memory.id,
302
+ mode,
303
+ actor: input.actor,
304
+ };
305
+ if (input.confirm !== undefined) {
306
+ forgetInput.confirm = input.confirm;
307
+ }
308
+ if (input.reason !== undefined) {
309
+ forgetInput.reason = input.reason;
310
+ }
311
+ await forgetMemory(forgetInput);
312
+ }
313
+ }
314
+ return {
315
+ matched: memories.length,
316
+ affected: dryRun ? 0 : memories.length,
317
+ mode,
318
+ dryRun,
319
+ ids: memoryIds,
320
+ };
321
+ },
322
+ };
323
+ }
324
+ function assertActor(actor) {
325
+ if (actor.trim().length === 0) {
326
+ throw new NuzoMemoryError("MEMORY_ACTOR_EMPTY", "Memory actor cannot be empty.");
327
+ }
328
+ }
329
+ function toExportItem(memory) {
330
+ return {
331
+ scope: memory.scope,
332
+ kind: memory.kind,
333
+ content: memory.content,
334
+ tags: [...memory.tags],
335
+ source: memory.source,
336
+ confidence: memory.confidence,
337
+ created_at: memory.createdAt.toISOString(),
338
+ updated_at: memory.updatedAt.toISOString(),
339
+ last_used_at: memory.lastUsedAt?.toISOString() ?? null,
340
+ archived_at: memory.archivedAt?.toISOString() ?? null,
341
+ };
342
+ }
343
+ function assertExportDocument(document) {
344
+ const value = document;
345
+ if (!isRecord(value)) {
346
+ throw new NuzoMemoryError("MEMORY_EXPORT_INVALID", "Memory export document is invalid.");
347
+ }
348
+ if (value.format !== "nuzo-memory-export" || value.version !== 1) {
349
+ throw new NuzoMemoryError("MEMORY_EXPORT_UNSUPPORTED", "Memory export format is not supported.", {
350
+ format: value.format,
351
+ version: value.version,
352
+ });
353
+ }
354
+ parseExportDate(getStringField(value, "exported_at", "document"), "exported_at");
355
+ if (!Array.isArray(value.memories)) {
356
+ throw new NuzoMemoryError("MEMORY_EXPORT_INVALID", "Memory export document is invalid.");
357
+ }
358
+ value.memories.forEach(assertExportItem);
359
+ }
360
+ function assertExportItem(item, index) {
361
+ if (!isRecord(item)) {
362
+ throwInvalidExportItem(index, "item must be an object");
363
+ }
364
+ getStringField(item, "scope", `memories[${index}]`);
365
+ const kind = getStringField(item, "kind", `memories[${index}]`);
366
+ if (!memoryKinds.includes(kind)) {
367
+ throwInvalidExportItem(index, "kind is not supported", { kind });
368
+ }
369
+ getStringField(item, "content", `memories[${index}]`);
370
+ getStringArrayField(item, "tags", `memories[${index}]`);
371
+ getStringField(item, "source", `memories[${index}]`);
372
+ const confidence = getNumberField(item, "confidence", `memories[${index}]`);
373
+ if (confidence < 0 || confidence > 1) {
374
+ throwInvalidExportItem(index, "confidence must be between 0 and 1", {
375
+ confidence,
376
+ });
377
+ }
378
+ const createdAt = getStringField(item, "created_at", `memories[${index}]`);
379
+ const updatedAt = getStringField(item, "updated_at", `memories[${index}]`);
380
+ const lastUsedAt = getNullableStringField(item, "last_used_at", `memories[${index}]`);
381
+ const archivedAt = getNullableStringField(item, "archived_at", `memories[${index}]`);
382
+ parseExportDate(createdAt, `memories[${index}].created_at`);
383
+ parseExportDate(updatedAt, `memories[${index}].updated_at`);
384
+ if (lastUsedAt !== null) {
385
+ parseExportDate(lastUsedAt, `memories[${index}].last_used_at`);
386
+ }
387
+ if (archivedAt !== null) {
388
+ parseExportDate(archivedAt, `memories[${index}].archived_at`);
389
+ }
390
+ }
391
+ function isRecord(value) {
392
+ return typeof value === "object" && value !== null && !Array.isArray(value);
393
+ }
394
+ function getStringField(record, field, path) {
395
+ if (typeof record[field] !== "string") {
396
+ throwInvalidExportField(path, field, "must be a string", { value: record[field] });
397
+ }
398
+ return record[field];
399
+ }
400
+ function getNullableStringField(record, field, path) {
401
+ if (record[field] !== null && typeof record[field] !== "string") {
402
+ throwInvalidExportField(path, field, "must be a string or null", { value: record[field] });
403
+ }
404
+ return record[field];
405
+ }
406
+ function getStringArrayField(record, field, path) {
407
+ if (!Array.isArray(record[field]) || !record[field].every((value) => typeof value === "string")) {
408
+ throwInvalidExportField(path, field, "must be an array of strings", { value: record[field] });
409
+ }
410
+ return record[field];
411
+ }
412
+ function getNumberField(record, field, path) {
413
+ if (typeof record[field] !== "number" || !Number.isFinite(record[field])) {
414
+ throwInvalidExportField(path, field, "must be a finite number", { value: record[field] });
415
+ }
416
+ return record[field];
417
+ }
418
+ function throwInvalidExportField(path, field, reason, details = {}) {
419
+ throw new NuzoMemoryError("MEMORY_EXPORT_INVALID", "Memory export document is invalid.", {
420
+ path: `${path}.${field}`,
421
+ reason,
422
+ ...details,
423
+ });
424
+ }
425
+ function throwInvalidExportItem(index, reason, details = {}) {
426
+ throw new NuzoMemoryError("MEMORY_EXPORT_INVALID", "Memory export document is invalid.", {
427
+ path: `memories[${index}]`,
428
+ reason,
429
+ ...details,
430
+ });
431
+ }
432
+ function parseExportDate(value, field) {
433
+ const date = new Date(value);
434
+ if (Number.isNaN(date.getTime())) {
435
+ throw new NuzoMemoryError("MEMORY_EXPORT_INVALID", "Memory export contains an invalid date.", {
436
+ field,
437
+ value,
438
+ });
439
+ }
440
+ return date;
441
+ }
442
+ function toImportDuplicateKey(memory) {
443
+ return JSON.stringify([
444
+ memory.scope,
445
+ memory.kind,
446
+ normalizeContent(memory.content),
447
+ normalizeTags(memory.tags),
448
+ ]);
449
+ }
450
+ function normalizeContent(content) {
451
+ return content.trim().replace(/\s+/g, " ");
452
+ }
453
+ function normalizeTags(tags) {
454
+ return [...new Set(tags)].sort();
455
+ }
@@ -0,0 +1,26 @@
1
+ import Database from "better-sqlite3";
2
+ import type { AuditLog, MemoryStore, SearchIndex, TransactionManager } from "../ports.js";
3
+ import type { ListMemoriesInput, MemoryEvent, MemoryRecord, RecallMemoriesInput, RecallMemoryResult } from "../types.js";
4
+ export interface SQLiteMemoryDatabaseOptions {
5
+ path: string;
6
+ }
7
+ export declare class SQLiteMemoryDatabase implements MemoryStore, SearchIndex, AuditLog, TransactionManager {
8
+ readonly database: Database.Database;
9
+ private transactionQueue;
10
+ constructor(options: SQLiteMemoryDatabaseOptions);
11
+ close(): void;
12
+ getSchemaVersion(): number;
13
+ run<T>(operation: () => Promise<T>): Promise<T>;
14
+ create(memory: MemoryRecord): Promise<void>;
15
+ update(memory: MemoryRecord): Promise<void>;
16
+ findById(id: string): Promise<MemoryRecord | null>;
17
+ archive(id: string, archivedAt: Date): Promise<void>;
18
+ delete(id: string): Promise<void>;
19
+ index(memory: MemoryRecord): Promise<void>;
20
+ remove(memoryId: string): Promise<void>;
21
+ search(input: RecallMemoriesInput): Promise<RecallMemoryResult[]>;
22
+ append(event: MemoryEvent): Promise<void>;
23
+ list(filter: ListMemoriesInput): Promise<MemoryRecord[]>;
24
+ list(memoryId: string): Promise<MemoryEvent[]>;
25
+ private listMemories;
26
+ }