audrey 0.5.1 → 0.8.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.
package/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  Biological memory architecture for AI agents. Gives agents cognitive memory that decays, consolidates, self-validates, and learns from experience — not just a database.
4
4
 
5
-
6
5
  ## Why Audrey Exists
7
6
 
8
7
  Every AI memory tool today (Mem0, Zep, LangChain Memory) is a filing cabinet. Store stuff, retrieve stuff. None of them do what biological memory actually does:
@@ -46,7 +45,7 @@ npx audrey status
46
45
  npx audrey uninstall
47
46
  ```
48
47
 
49
- Every Claude Code session now has 7 memory tools: `memory_encode`, `memory_recall`, `memory_consolidate`, `memory_introspect`, `memory_resolve_truth`, `memory_export`, `memory_import`.
48
+ Every Claude Code session now has 9 memory tools: `memory_encode`, `memory_recall`, `memory_consolidate`, `memory_introspect`, `memory_resolve_truth`, `memory_export`, `memory_import`, `memory_forget`, `memory_decay`.
50
49
 
51
50
  ### SDK in Your Code
52
51
 
@@ -79,14 +78,26 @@ await brain.encode({
79
78
  const memories = await brain.recall('stripe rate limits', { limit: 5 });
80
79
  // Returns: [{ content, type, confidence, score, ... }]
81
80
 
82
- // 4. Consolidate episodes into principles (the "sleep" cycle)
81
+ // 4. Filtered recall by tag, source, or date range
82
+ const recent = await brain.recall('stripe', {
83
+ tags: ['rate-limit'],
84
+ sources: ['direct-observation'],
85
+ after: '2026-02-01T00:00:00Z',
86
+ });
87
+
88
+ // 5. Consolidate episodes into principles (the "sleep" cycle)
83
89
  await brain.consolidate();
84
90
 
85
- // 5. Check brain health
91
+ // 6. Forget something
92
+ brain.forget(memoryId); // soft-delete
93
+ brain.forget(memoryId, { purge: true }); // hard-delete
94
+ await brain.forgetByQuery('old API endpoint', { minSimilarity: 0.9 });
95
+
96
+ // 7. Check brain health
86
97
  const stats = brain.introspect();
87
98
  // { episodic: 47, semantic: 12, procedural: 3, dormant: 8, ... }
88
99
 
89
- // 6. Clean up
100
+ // 8. Clean up
90
101
  brain.close();
91
102
  ```
92
103
 
@@ -219,6 +230,16 @@ Context-dependent truths are modeled explicitly:
219
230
 
220
231
  New high-confidence evidence can reopen resolved disputes.
221
232
 
233
+ ### Forget and Purge
234
+
235
+ Memories can be explicitly forgotten — by ID or by semantic query:
236
+
237
+ **Soft-delete** (default) — Marks the memory as forgotten/superseded and removes its vector index. The record stays in the database but is excluded from recall. Reversible via direct database access.
238
+
239
+ **Hard-delete** (`purge: true`) — Permanently removes the memory from both the main table and the vector index. Irreversible.
240
+
241
+ **Bulk purge** — Removes all forgotten, dormant, superseded, and rolled-back memories in one operation. Useful for GDPR compliance or storage cleanup.
242
+
222
243
  ### Rollback
223
244
 
224
245
  Bad consolidation? Undo it:
@@ -268,20 +289,38 @@ const id = await brain.encode({
268
289
 
269
290
  Episodes are **immutable**. Corrections create new records with `supersedes` links. The original is preserved.
270
291
 
292
+ ### `brain.encodeBatch(paramsList)` → `Promise<string[]>`
293
+
294
+ Encode multiple episodes in one call. Same params as `encode()`, but as an array.
295
+
296
+ ```js
297
+ const ids = await brain.encodeBatch([
298
+ { content: 'Stripe returned 429', source: 'direct-observation' },
299
+ { content: 'Redis timed out', source: 'tool-result' },
300
+ { content: 'User reports slow checkout', source: 'told-by-user' },
301
+ ]);
302
+ ```
303
+
271
304
  ### `brain.recall(query, options)` → `Promise<Memory[]>`
272
305
 
273
306
  Retrieve memories ranked by `similarity * confidence`.
274
307
 
275
308
  ```js
276
309
  const memories = await brain.recall('stripe rate limits', {
277
- minConfidence: 0.5, // Filter below this confidence
278
- types: ['semantic'], // Filter by memory type
279
- limit: 5, // Max results
280
- includeProvenance: true, // Include evidence chains
281
- includeDormant: false, // Include dormant memories
310
+ limit: 5, // Max results (default 10)
311
+ minConfidence: 0.5, // Filter below this confidence
312
+ types: ['semantic'], // Filter by memory type
313
+ includeProvenance: true, // Include evidence chains
314
+ includeDormant: false, // Include dormant memories
315
+ tags: ['rate-limit'], // Only episodic memories with these tags
316
+ sources: ['direct-observation'], // Only episodic memories from these sources
317
+ after: '2026-02-01T00:00:00Z', // Only memories created after this date
318
+ before: '2026-03-01T00:00:00Z', // Only memories created before this date
282
319
  });
283
320
  ```
284
321
 
322
+ Tag and source filters only apply to episodic memories (semantic and procedural memories don't have tags or sources). Date filters apply to all memory types.
323
+
285
324
  Each result:
286
325
 
287
326
  ```js
@@ -304,21 +343,9 @@ Each result:
304
343
 
305
344
  Retrieval automatically reinforces matched memories (boosts confidence, resets decay clock).
306
345
 
307
- ### `brain.encodeBatch(paramsList)` → `Promise<string[]>`
308
-
309
- Encode multiple episodes in one call. Same params as `encode()`, but as an array.
310
-
311
- ```js
312
- const ids = await brain.encodeBatch([
313
- { content: 'Stripe returned 429', source: 'direct-observation' },
314
- { content: 'Redis timed out', source: 'tool-result' },
315
- { content: 'User reports slow checkout', source: 'told-by-user' },
316
- ]);
317
- ```
318
-
319
346
  ### `brain.recallStream(query, options)` → `AsyncGenerator<Memory>`
320
347
 
321
- Streaming version of `recall()`. Yields results one at a time. Supports early `break`.
348
+ Streaming version of `recall()`. Yields results one at a time. Supports early `break`. Same options as `recall()`.
322
349
 
323
350
  ```js
324
351
  for await (const memory of brain.recallStream('stripe issues', { limit: 10 })) {
@@ -327,6 +354,37 @@ for await (const memory of brain.recallStream('stripe issues', { limit: 10 })) {
327
354
  }
328
355
  ```
329
356
 
357
+ ### `brain.forget(id, options)` → `ForgetResult`
358
+
359
+ Forget a memory by ID. Works on any memory type (episodic, semantic, procedural).
360
+
361
+ ```js
362
+ brain.forget(memoryId); // soft-delete
363
+ brain.forget(memoryId, { purge: true }); // hard-delete (permanent)
364
+ // { id, type: 'episodic', purged: false }
365
+ ```
366
+
367
+ ### `brain.forgetByQuery(query, options)` → `Promise<ForgetResult | null>`
368
+
369
+ Find the closest matching memory by semantic search and forget it. Searches all three memory types, picks the best match.
370
+
371
+ ```js
372
+ const result = await brain.forgetByQuery('old API endpoint', {
373
+ minSimilarity: 0.9, // Threshold for match (default 0.9)
374
+ purge: false, // Hard-delete? (default false)
375
+ });
376
+ // null if no match above threshold
377
+ ```
378
+
379
+ ### `brain.purge()` → `PurgeCounts`
380
+
381
+ Bulk hard-delete all dead memories: forgotten episodes, dormant/superseded/rolled-back semantics and procedures.
382
+
383
+ ```js
384
+ const counts = brain.purge();
385
+ // { episodes: 12, semantics: 3, procedures: 0 }
386
+ ```
387
+
330
388
  ### `brain.consolidate(options)` → `Promise<ConsolidationResult>`
331
389
 
332
390
  Run the consolidation engine manually.
@@ -389,6 +447,15 @@ brain.introspect();
389
447
 
390
448
  Full audit trail of all consolidation runs.
391
449
 
450
+ ### `brain.export()` / `brain.import(snapshot)`
451
+
452
+ Export all memories as a JSON snapshot, or import from one.
453
+
454
+ ```js
455
+ const snapshot = brain.export(); // { version, episodes, semantics, procedures, ... }
456
+ await brain.import(snapshot); // Re-embeds everything with current provider
457
+ ```
458
+
392
459
  ### Events
393
460
 
394
461
  ```js
@@ -398,6 +465,8 @@ brain.on('contradiction', ({ episodeId, contradictionId, semanticId, resolution
398
465
  brain.on('consolidation', ({ runId, principlesExtracted }) => { ... });
399
466
  brain.on('decay', ({ totalEvaluated, transitionedToDormant }) => { ... });
400
467
  brain.on('rollback', ({ runId, rolledBackMemories }) => { ... });
468
+ brain.on('forget', ({ id, type, purged }) => { ... });
469
+ brain.on('purge', ({ episodes, semantics, procedures }) => { ... });
401
470
  brain.on('migration', ({ episodes, semantics, procedures }) => { ... });
402
471
  brain.on('error', (err) => { ... });
403
472
  ```
@@ -410,7 +479,7 @@ Close the database connection.
410
479
 
411
480
  ```
412
481
  audrey-data/
413
- audrey.db Single SQLite file. WAL mode. That's your brain.
482
+ audrey.db <- Single SQLite file. WAL mode. That's your brain.
414
483
  ```
415
484
 
416
485
  ```
@@ -418,15 +487,16 @@ src/
418
487
  audrey.js Main class. EventEmitter. Public API surface.
419
488
  causal.js Causal graph management. LLM-powered mechanism articulation.
420
489
  confidence.js Compositional confidence formula. Pure math.
421
- consolidate.js "Sleep" cycle. KNN clustering LLM extraction promote.
490
+ consolidate.js "Sleep" cycle. KNN clustering -> LLM extraction -> promote.
422
491
  db.js SQLite + sqlite-vec. Schema, vec0 tables, migrations.
423
492
  decay.js Ebbinghaus forgetting curves.
424
493
  embedding.js Pluggable providers (Mock, OpenAI). Batch embedding.
425
494
  encode.js Immutable episodic memory creation + vec0 writes.
495
+ forget.js Soft-delete, hard-delete, query-based forget, bulk purge.
426
496
  introspect.js Health dashboard queries.
427
497
  llm.js Pluggable LLM providers (Mock, Anthropic, OpenAI).
428
498
  prompts.js Structured prompt templates for LLM operations.
429
- recall.js KNN retrieval + confidence scoring + async streaming.
499
+ recall.js KNN retrieval + confidence scoring + filtered recall + streaming.
430
500
  rollback.js Undo consolidation runs.
431
501
  utils.js Date math, safe JSON parse.
432
502
  validate.js KNN validation + LLM contradiction detection.
@@ -437,7 +507,7 @@ src/
437
507
  index.js Barrel export.
438
508
 
439
509
  mcp-server/
440
- index.js MCP tool server (7 tools, stdio transport) + CLI subcommands.
510
+ index.js MCP tool server (9 tools, stdio transport) + CLI subcommands.
441
511
  config.js Shared config (env var parsing, install arg builder).
442
512
  ```
443
513
 
@@ -461,7 +531,7 @@ All mutations use SQLite transactions. CHECK constraints enforce valid states an
461
531
  ## Running Tests
462
532
 
463
533
  ```bash
464
- npm test # 243 tests across 22 files
534
+ npm test # 278 tests across 23 files
465
535
  npm run test:watch
466
536
  ```
467
537
 
@@ -471,115 +541,60 @@ npm run test:watch
471
541
  node examples/stripe-demo.js
472
542
  ```
473
543
 
474
- Demonstrates the full pipeline: encode 3 rate-limit observations consolidate into principle recall proactively.
544
+ Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate into principle, recall proactively.
475
545
 
476
546
  ---
477
547
 
478
- ## Roadmap
548
+ ## Changelog
479
549
 
480
- ### v0.1.0 — Foundation
550
+ ### v0.6.0 — Filtered Recall + Forget (current)
481
551
 
482
- - [x] Immutable episodic memory with append-only records
483
- - [x] Compositional confidence formula (source + evidence + recency + retrieval)
484
- - [x] Ebbinghaus-inspired forgetting curves with configurable half-lives
485
- - [x] Dormancy transitions for low-confidence memories
486
- - [x] Confidence-weighted recall across episodic/semantic/procedural types
487
- - [x] Provenance chains (which episodes contributed to which principles)
488
- - [x] Retrieval reinforcement (frequently accessed memories resist decay)
489
- - [x] Consolidation engine with clustering and principle extraction
490
- - [x] Idempotent consolidation with checkpoint cursors
491
- - [x] Full consolidation audit trail (input/output IDs per run)
492
- - [x] Consolidation rollback (undo bad runs, restore episodes)
493
- - [x] Contradiction lifecycle (open/resolved/context_dependent/reopened)
494
- - [x] Circular self-confirmation defense (model-generated cap at 0.6)
495
- - [x] Source type diversity tracking on semantic memories
496
- - [x] Supersedes links for correcting episodic memories
497
- - [x] Pluggable embedding providers (Mock for tests, OpenAI for production)
498
- - [x] Causal context storage (trigger/consequence per episode)
499
- - [x] Introspection API (memory counts, contradiction stats, consolidation history)
500
- - [x] EventEmitter lifecycle hooks (encode, reinforcement, consolidation, decay, rollback, error)
501
- - [x] SQLite with WAL mode, CHECK constraints, indexes, foreign keys
502
- - [x] Transaction safety on all multi-step mutations
503
- - [x] Input validation on public API (content, salience, tags, source)
504
- - [x] Shared utility extraction (cosine similarity, date math, safe JSON parse)
505
- - [x] 104 tests across 12 test files
506
- - [x] Proof-of-concept demo (Stripe rate limit scenario)
552
+ - Filtered recall: tag, source, and date-range filters on `recall()` and `recallStream()`
553
+ - `forget()` soft-delete any memory by ID
554
+ - `forgetByQuery()` find closest match by semantic search and forget it
555
+ - `purge()` bulk hard-delete all forgotten/dormant/superseded memories
556
+ - `memory_forget` and `memory_decay` MCP tools (9 tools total)
557
+ - 278 tests across 23 files
507
558
 
508
- ### v0.2.0 — LLM Integration
559
+ ### v0.5.0 — Feature Depth
509
560
 
510
- - [x] LLM-powered principle extraction (replace callback with Anthropic/OpenAI calls)
511
- - [x] LLM-based contradiction detection during validation
512
- - [x] Causal mechanism articulation via LLM (not just trigger/consequence)
513
- - [x] Spurious correlation detection (require mechanistic explanation for causal links)
514
- - [x] Context-dependent truth resolution via LLM
515
- - [x] Configurable LLM provider for consolidation (Mock, Anthropic, OpenAI)
516
- - [x] Structured prompt templates for all LLM operations
517
- - [x] 142 tests across 15 test files
561
+ - Configurable confidence weights and decay rates per instance
562
+ - Memory export/import (JSON snapshots with re-embedding)
563
+ - `memory_export` and `memory_import` MCP tools
564
+ - Auto-consolidation scheduling
565
+ - Adaptive consolidation parameter suggestions
566
+ - 243 tests across 22 files
567
+
568
+ ### v0.3.1 MCP Server
569
+
570
+ - MCP tool server via `@modelcontextprotocol/sdk` with stdio transport
571
+ - One-command install: `npx audrey install` (auto-detects API keys)
572
+ - CLI subcommands: `install`, `uninstall`, `status`
573
+ - JSDoc type annotations on all public exports
574
+ - Published to npm
575
+ - 194 tests across 17 files
518
576
 
519
577
  ### v0.3.0 — Vector Performance
520
578
 
521
- - [x] sqlite-vec native vector indexing (vec0 virtual tables with cosine distance)
522
- - [x] KNN queries for recall, validation, and consolidation clustering (all vector math in C)
523
- - [x] SQL-native metadata filtering in KNN (state, source, consolidated)
524
- - [x] Batch encoding API (`encodeBatch` encode N episodes in one call)
525
- - [x] Streaming recall with async generators (`recallStream`)
526
- - [x] Dimension configuration and mismatch validation
527
- - [x] Automatic migration from v0.2.0 embedding BLOBs to vec0 tables
528
- - [x] 168 tests across 16 test files
529
-
530
- ### v0.3.1 MCP Server + JSDoc Types
531
-
532
- - [x] MCP tool server via `@modelcontextprotocol/sdk` with stdio transport
533
- - [x] 5 tools: `memory_encode`, `memory_recall`, `memory_consolidate`, `memory_introspect`, `memory_resolve_truth`
534
- - [x] Configuration via environment variables (data dir, embedding provider, LLM provider)
535
- - [x] One-command install: `npx audrey install` (auto-detects API keys)
536
- - [x] CLI subcommands: `install`, `uninstall`, `status`
537
- - [x] JSDoc type annotations on all public exports (16 source files)
538
- - [x] Published to npm with proper package metadata
539
- - [x] 194 tests across 17 test files
540
-
541
- ### v0.5.0 — Feature Depth (current)
542
-
543
- - [x] Configurable confidence weights per Audrey instance
544
- - [x] Configurable decay rates (half-lives) per Audrey instance
545
- - [x] Confidence config wired through constructor to recall and decay
546
- - [x] Memory export (JSON snapshot of all tables, no raw embeddings)
547
- - [x] Memory import with automatic re-embedding via current provider
548
- - [x] `memory_export` and `memory_import` MCP tools (7 tools total)
549
- - [x] Auto-consolidation scheduling (`startAutoConsolidate` / `stopAutoConsolidate`)
550
- - [x] Consolidation metrics tracking (per-run params and results)
551
- - [x] Adaptive consolidation parameter suggestions based on historical yield
552
- - [x] 243 tests across 22 test files
553
-
554
- ### v0.4.0 — Type Safety & Developer Experience
555
-
556
- - [ ] Full TypeScript conversion with strict mode
557
- - [ ] Published type declarations (.d.ts)
558
- - [ ] Schema versioning and migration system
559
- - [ ] Structured logging (optional, pluggable)
560
-
561
- ### v0.4.5 — Embedding Migration (deferred from v0.3.0)
562
-
563
- - [ ] Embedding migration pipeline (re-embed when models change)
564
- - [ ] Re-consolidation queue (re-run consolidation with new embedding model)
565
-
566
- ### v0.6.0 — Scale
567
-
568
- - [ ] pgvector adapter for PostgreSQL backend
569
- - [ ] Redis adapter for distributed caching
570
- - [ ] Connection pooling for concurrent agent access
571
- - [ ] Pagination on recall queries (cursor-based)
572
- - [ ] Benchmarks: encode throughput, recall latency at 10k/100k/1M memories
573
-
574
- ### v1.0.0 — Production Ready
575
-
576
- - [ ] Comprehensive error handling at all boundaries
577
- - [ ] Rate limiting on embedding API calls
578
- - [ ] Memory usage profiling and optimization
579
- - [ ] Security audit (injection, data isolation)
580
- - [ ] Cross-agent knowledge sharing protocol (Hivemind)
581
- - [ ] Documentation site
582
- - [ ] Integration guides (LangChain, CrewAI, Claude Code, custom agents)
579
+ - sqlite-vec native vector indexing (vec0 virtual tables with cosine distance)
580
+ - KNN queries for recall, validation, and consolidation clustering
581
+ - Batch encoding API and streaming recall with async generators
582
+ - Dimension configuration and automatic migration from v0.2.0
583
+ - 168 tests across 16 files
584
+
585
+ ### v0.2.0 LLM Integration
586
+
587
+ - LLM-powered principle extraction, contradiction detection, causal articulation
588
+ - Context-dependent truth resolution
589
+ - Configurable LLM providers (Mock, Anthropic, OpenAI)
590
+ - 142 tests across 15 files
591
+
592
+ ### v0.1.0 Foundation
593
+
594
+ - Immutable episodic memory, compositional confidence, Ebbinghaus forgetting curves
595
+ - Consolidation engine, contradiction lifecycle, rollback
596
+ - Circular self-confirmation defense, causal context, introspection
597
+ - 104 tests across 12 files
583
598
 
584
599
  ## Design Decisions
585
600
 
@@ -591,7 +606,7 @@ Demonstrates the full pipeline: encode 3 rate-limit observations → consolidate
591
606
 
592
607
  **Why model-generated cap at 0.6?** Prevents the most dangerous exploit in AI memory: circular self-confirmation where an agent's own inferences bootstrap themselves into high-confidence "facts" through repeated retrieval.
593
608
 
594
- **Why no TypeScript yet?** Prototyping speed. TypeScript conversion is on the roadmap for v0.4.0. The pure-math modules (`confidence.js`, `utils.js`) are already type-safe in practice.
609
+ **Why soft-delete by default?** Hard-deletes are irreversible. Soft-delete preserves data integrity and audit trails while excluding the memory from recall. Use `purge: true` or `brain.purge()` when you need permanent removal (GDPR, storage cleanup).
595
610
 
596
611
  ## License
597
612
 
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const VERSION = '0.5.1';
4
+ export const VERSION = '0.8.0';
5
5
  export const SERVER_NAME = 'audrey-memory';
6
6
  export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
7
7
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
@@ -65,7 +65,7 @@ function install() {
65
65
  console.log(`
66
66
  Audrey registered as "${SERVER_NAME}" with Claude Code.
67
67
 
68
- 7 tools available in every session:
68
+ 9 tools available in every session:
69
69
  memory_encode — Store observations, facts, preferences
70
70
  memory_recall — Search memories by semantic similarity
71
71
  memory_consolidate — Extract principles from accumulated episodes
@@ -73,6 +73,8 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
73
73
  memory_resolve_truth — Resolve contradictions between claims
74
74
  memory_export — Export all memories as JSON snapshot
75
75
  memory_import — Import a snapshot into a fresh database
76
+ memory_forget — Forget a specific memory by ID or query
77
+ memory_decay — Apply forgetting curves, transition low-confidence to dormant
76
78
 
77
79
  Data stored in: ${DEFAULT_DATA_DIR}
78
80
  Verify: claude mcp list
@@ -161,10 +163,11 @@ async function main() {
161
163
  source: z.enum(VALID_SOURCES).describe('Source type of the memory'),
162
164
  tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
163
165
  salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
166
+ context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
164
167
  },
165
- async ({ content, source, tags, salience }) => {
168
+ async ({ content, source, tags, salience, context }) => {
166
169
  try {
167
- const id = await audrey.encode({ content, source, tags, salience });
170
+ const id = await audrey.encode({ content, source, tags, salience, context });
168
171
  return toolResult({ id, content, source });
169
172
  } catch (err) {
170
173
  return toolError(err);
@@ -179,13 +182,23 @@ async function main() {
179
182
  limit: z.number().min(1).max(50).optional().describe('Max results (default 10)'),
180
183
  types: z.array(z.enum(VALID_TYPES)).optional().describe('Memory types to search'),
181
184
  min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold'),
185
+ tags: z.array(z.string()).optional().describe('Only return episodic memories with these tags'),
186
+ sources: z.array(z.enum(VALID_SOURCES)).optional().describe('Only return episodic memories from these sources'),
187
+ after: z.string().optional().describe('Only return memories created after this ISO date'),
188
+ before: z.string().optional().describe('Only return memories created before this ISO date'),
189
+ context: z.record(z.string()).optional().describe('Retrieval context — memories encoded in matching context get boosted'),
182
190
  },
183
- async ({ query, limit, types, min_confidence }) => {
191
+ async ({ query, limit, types, min_confidence, tags, sources, after, before, context }) => {
184
192
  try {
185
193
  const results = await audrey.recall(query, {
186
194
  limit: limit ?? 10,
187
195
  types,
188
196
  minConfidence: min_confidence,
197
+ tags,
198
+ sources,
199
+ after,
200
+ before,
201
+ context,
189
202
  });
190
203
  return toolResult(results);
191
204
  } catch (err) {
@@ -279,6 +292,53 @@ async function main() {
279
292
  },
280
293
  );
281
294
 
295
+ server.tool(
296
+ 'memory_forget',
297
+ {
298
+ id: z.string().optional().describe('ID of the memory to forget'),
299
+ query: z.string().optional().describe('Semantic query to find and forget the closest matching memory'),
300
+ min_similarity: z.number().min(0).max(1).optional().describe('Minimum similarity for query-based forget (default 0.9)'),
301
+ purge: z.boolean().optional().describe('Hard-delete the memory permanently (default false, soft-delete)'),
302
+ },
303
+ async ({ id, query, min_similarity, purge }) => {
304
+ try {
305
+ if (!id && !query) {
306
+ return toolError(new Error('Provide either id or query'));
307
+ }
308
+ let result;
309
+ if (id) {
310
+ result = audrey.forget(id, { purge: purge ?? false });
311
+ } else {
312
+ result = await audrey.forgetByQuery(query, {
313
+ minSimilarity: min_similarity ?? 0.9,
314
+ purge: purge ?? false,
315
+ });
316
+ if (!result) {
317
+ return toolResult({ forgotten: false, reason: 'No memory found above similarity threshold' });
318
+ }
319
+ }
320
+ return toolResult({ forgotten: true, ...result });
321
+ } catch (err) {
322
+ return toolError(err);
323
+ }
324
+ },
325
+ );
326
+
327
+ server.tool(
328
+ 'memory_decay',
329
+ {
330
+ dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant (default 0.1)'),
331
+ },
332
+ async ({ dormant_threshold }) => {
333
+ try {
334
+ const result = audrey.decay({ dormantThreshold: dormant_threshold });
335
+ return toolResult(result);
336
+ } catch (err) {
337
+ return toolError(err);
338
+ }
339
+ },
340
+ );
341
+
282
342
  const transport = new StdioServerTransport();
283
343
  await server.connect(transport);
284
344
  console.error('[audrey-mcp] connected via stdio');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audrey",
3
- "version": "0.5.1",
3
+ "version": "0.8.0",
4
4
  "description": "Biological memory architecture for AI agents — encode, consolidate, and recall memories with confidence decay, contradiction detection, and causal graphs",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/audrey.js CHANGED
@@ -8,12 +8,14 @@ import { validateMemory } from './validate.js';
8
8
  import { runConsolidation } from './consolidate.js';
9
9
  import { applyDecay } from './decay.js';
10
10
  import { rollbackConsolidation, getConsolidationHistory } from './rollback.js';
11
+ import { forgetMemory, forgetByQuery as forgetByQueryFn, purgeMemories } from './forget.js';
11
12
  import { introspect as introspectFn } from './introspect.js';
12
13
  import { buildContextResolutionPrompt } from './prompts.js';
13
14
  import { exportMemories } from './export.js';
14
15
  import { importMemories } from './import.js';
15
16
  import { suggestConsolidationParams as suggestParamsFn } from './adaptive.js';
16
17
  import { reembedAll } from './migrate.js';
18
+ import { applyInterference } from './interference.js';
17
19
 
18
20
  /**
19
21
  * @typedef {'direct-observation' | 'told-by-user' | 'tool-result' | 'inference' | 'model-generated'} SourceType
@@ -33,6 +35,10 @@ import { reembedAll } from './migrate.js';
33
35
  * @property {number} [limit]
34
36
  * @property {boolean} [includeProvenance]
35
37
  * @property {boolean} [includeDormant]
38
+ * @property {string[]} [tags]
39
+ * @property {string[]} [sources]
40
+ * @property {string} [after]
41
+ * @property {string} [before]
36
42
  *
37
43
  * @typedef {Object} RecallResult
38
44
  * @property {string} id
@@ -84,6 +90,8 @@ export class Audrey extends EventEmitter {
84
90
  confidence = {},
85
91
  consolidation = {},
86
92
  decay = {},
93
+ interference = {},
94
+ context = {},
87
95
  } = {}) {
88
96
  super();
89
97
 
@@ -108,12 +116,24 @@ export class Audrey extends EventEmitter {
108
116
  weights: confidence.weights,
109
117
  halfLives: confidence.halfLives,
110
118
  sourceReliability: confidence.sourceReliability,
119
+ interferenceWeight: interference.weight ?? 0.1,
120
+ contextWeight: context.weight ?? 0.3,
111
121
  };
112
122
  this.consolidationConfig = {
113
123
  minEpisodes: consolidation.minEpisodes || 3,
114
124
  };
115
125
  this.decayConfig = { dormantThreshold: decay.dormantThreshold || 0.1 };
116
126
  this._autoConsolidateTimer = null;
127
+ this.interferenceConfig = {
128
+ enabled: interference.enabled ?? true,
129
+ k: interference.k ?? 5,
130
+ threshold: interference.threshold ?? 0.6,
131
+ weight: interference.weight ?? 0.1,
132
+ };
133
+ this.contextConfig = {
134
+ enabled: context.enabled ?? true,
135
+ weight: context.weight ?? 0.3,
136
+ };
117
137
  }
118
138
 
119
139
  async _ensureMigrated() {
@@ -155,6 +175,15 @@ export class Audrey extends EventEmitter {
155
175
  await this._ensureMigrated();
156
176
  const id = await encodeEpisode(this.db, this.embeddingProvider, params);
157
177
  this.emit('encode', { id, ...params });
178
+ if (this.interferenceConfig.enabled) {
179
+ applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
180
+ .then(affected => {
181
+ if (affected.length > 0) {
182
+ this.emit('interference', { episodeId: id, affected });
183
+ }
184
+ })
185
+ .catch(err => this.emit('error', err));
186
+ }
158
187
  this._emitValidation(id, params);
159
188
  return id;
160
189
  }
@@ -188,7 +217,7 @@ export class Audrey extends EventEmitter {
188
217
  await this._ensureMigrated();
189
218
  return recallFn(this.db, this.embeddingProvider, query, {
190
219
  ...options,
191
- confidenceConfig: options.confidenceConfig ?? this.confidenceConfig,
220
+ confidenceConfig: this._recallConfig(options),
192
221
  });
193
222
  }
194
223
 
@@ -201,10 +230,17 @@ export class Audrey extends EventEmitter {
201
230
  await this._ensureMigrated();
202
231
  yield* recallStreamFn(this.db, this.embeddingProvider, query, {
203
232
  ...options,
204
- confidenceConfig: options.confidenceConfig ?? this.confidenceConfig,
233
+ confidenceConfig: this._recallConfig(options),
205
234
  });
206
235
  }
207
236
 
237
+ _recallConfig(options) {
238
+ const base = options.confidenceConfig ?? this.confidenceConfig;
239
+ return this.contextConfig.enabled && options.context
240
+ ? { ...base, retrievalContext: options.context }
241
+ : base;
242
+ }
243
+
208
244
  /**
209
245
  * @param {{ minClusterSize?: number, similarityThreshold?: number, extractPrinciple?: Function, llmProvider?: import('./llm.js').LLMProvider }} [options]
210
246
  * @returns {Promise<ConsolidationResult>}
@@ -343,6 +379,25 @@ export class Audrey extends EventEmitter {
343
379
  return suggestParamsFn(this.db);
344
380
  }
345
381
 
382
+ forget(id, options = {}) {
383
+ const result = forgetMemory(this.db, id, options);
384
+ this.emit('forget', result);
385
+ return result;
386
+ }
387
+
388
+ async forgetByQuery(query, options = {}) {
389
+ await this._ensureMigrated();
390
+ const result = await forgetByQueryFn(this.db, this.embeddingProvider, query, options);
391
+ if (result) this.emit('forget', result);
392
+ return result;
393
+ }
394
+
395
+ purge() {
396
+ const result = purgeMemories(this.db);
397
+ this.emit('purge', result);
398
+ return result;
399
+ }
400
+
346
401
  /** @returns {void} */
347
402
  close() {
348
403
  this.stopAutoConsolidate();
package/src/confidence.js CHANGED
@@ -67,8 +67,16 @@ export function recencyDecay(ageDays, halfLifeDays) {
67
67
  */
68
68
  export function retrievalReinforcement(retrievalCount, daysSinceRetrieval) {
69
69
  if (retrievalCount === 0) return 0;
70
- const lambdaRet = Math.LN2 / 14; // 14-day half-life for retrieval decay
71
- return Math.min(1.0, 0.3 * Math.log(1 + retrievalCount) * Math.exp(-lambdaRet * daysSinceRetrieval));
70
+ const lambdaRet = Math.LN2 / 14;
71
+ const baseReinforcement = 0.3 * Math.log(1 + retrievalCount);
72
+ const recencyWeight = Math.exp(-lambdaRet * daysSinceRetrieval);
73
+ const spacedBonus = Math.min(0.15, 0.02 * Math.log(1 + daysSinceRetrieval));
74
+ return Math.min(1.0, baseReinforcement * recencyWeight + spacedBonus);
75
+ }
76
+
77
+ export function salienceModifier(salience) {
78
+ const s = salience ?? 0.5;
79
+ return 0.5 + s;
72
80
  }
73
81
 
74
82
  /**
@@ -152,6 +152,7 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
152
152
  embeddingBuffer,
153
153
  semanticId: generateId(),
154
154
  semanticNow: new Date().toISOString(),
155
+ maxSalience: Math.max(...cluster.map(ep => ep.salience ?? 0.5)),
155
156
  });
156
157
  }
157
158
 
@@ -168,8 +169,8 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
168
169
  id, content, embedding, state, evidence_episode_ids,
169
170
  evidence_count, supporting_count, source_type_diversity,
170
171
  consolidation_checkpoint, embedding_model, embedding_version,
171
- consolidation_model, created_at
172
- ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)
172
+ consolidation_model, created_at, salience
173
+ ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
173
174
  `).run(
174
175
  entry.semanticId,
175
176
  entry.principle.content,
@@ -183,6 +184,7 @@ export async function runConsolidation(db, embeddingProvider, options = {}) {
183
184
  embeddingProvider.modelVersion,
184
185
  llmProvider?.modelName || null,
185
186
  entry.semanticNow,
187
+ entry.maxSalience,
186
188
  );
187
189
 
188
190
  db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)').run(
package/src/context.js ADDED
@@ -0,0 +1,15 @@
1
+ export function contextMatchRatio(encodingContext, retrievalContext) {
2
+ if (!encodingContext || !retrievalContext) return 0;
3
+ const retrievalKeys = Object.keys(retrievalContext);
4
+ if (retrievalKeys.length === 0) return 0;
5
+ const sharedKeys = retrievalKeys.filter(k => k in encodingContext);
6
+ if (sharedKeys.length === 0) return 0;
7
+ const matches = sharedKeys.filter(k => encodingContext[k] === retrievalContext[k]).length;
8
+ return matches / retrievalKeys.length;
9
+ }
10
+
11
+ export function contextModifier(encodingContext, retrievalContext, weight = 0.3) {
12
+ if (!encodingContext || !retrievalContext) return 1.0;
13
+ const ratio = contextMatchRatio(encodingContext, retrievalContext);
14
+ return 1.0 + (weight * ratio);
15
+ }
package/src/db.js CHANGED
@@ -11,6 +11,7 @@ const SCHEMA = `
11
11
  source TEXT NOT NULL CHECK(source IN ('direct-observation','told-by-user','tool-result','inference','model-generated')),
12
12
  source_reliability REAL NOT NULL,
13
13
  salience REAL DEFAULT 0.5,
14
+ context TEXT DEFAULT '{}',
14
15
  tags TEXT,
15
16
  causal_trigger TEXT,
16
17
  causal_consequence TEXT,
@@ -42,7 +43,9 @@ const SCHEMA = `
42
43
  created_at TEXT NOT NULL,
43
44
  last_reinforced_at TEXT,
44
45
  retrieval_count INTEGER DEFAULT 0,
45
- challenge_count INTEGER DEFAULT 0
46
+ challenge_count INTEGER DEFAULT 0,
47
+ interference_count INTEGER DEFAULT 0,
48
+ salience REAL DEFAULT 0.5
46
49
  );
47
50
 
48
51
  CREATE TABLE IF NOT EXISTS procedures (
@@ -58,7 +61,9 @@ const SCHEMA = `
58
61
  embedding_version TEXT,
59
62
  created_at TEXT NOT NULL,
60
63
  last_reinforced_at TEXT,
61
- retrieval_count INTEGER DEFAULT 0
64
+ retrieval_count INTEGER DEFAULT 0,
65
+ interference_count INTEGER DEFAULT 0,
66
+ salience REAL DEFAULT 0.5
62
67
  );
63
68
 
64
69
  CREATE TABLE IF NOT EXISTS causal_links (
package/src/decay.js CHANGED
@@ -1,4 +1,5 @@
1
- import { computeConfidence, DEFAULT_HALF_LIVES } from './confidence.js';
1
+ import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
2
+ import { interferenceModifier } from './interference.js';
2
3
  import { daysBetween } from './utils.js';
3
4
 
4
5
  /**
@@ -13,7 +14,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
13
14
 
14
15
  const semantics = db.prepare(`
15
16
  SELECT id, supporting_count, contradicting_count, created_at,
16
- last_reinforced_at, retrieval_count
17
+ last_reinforced_at, retrieval_count, interference_count, salience
17
18
  FROM semantics WHERE state = 'active'
18
19
  `).all();
19
20
 
@@ -26,7 +27,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
26
27
  ? daysBetween(sem.last_reinforced_at, now)
27
28
  : ageDays;
28
29
 
29
- const confidence = computeConfidence({
30
+ let confidence = computeConfidence({
30
31
  sourceType: 'tool-result',
31
32
  supportingCount: sem.supporting_count || 0,
32
33
  contradictingCount: sem.contradicting_count || 0,
@@ -35,6 +36,9 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
35
36
  retrievalCount: sem.retrieval_count || 0,
36
37
  daysSinceRetrieval,
37
38
  });
39
+ confidence *= interferenceModifier(sem.interference_count || 0);
40
+ confidence *= salienceModifier(sem.salience ?? 0.5);
41
+ confidence = Math.max(0, Math.min(1, confidence));
38
42
 
39
43
  if (confidence < dormantThreshold) {
40
44
  markDormantSem.run('dormant', sem.id);
@@ -44,7 +48,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
44
48
 
45
49
  const procedures = db.prepare(`
46
50
  SELECT id, success_count, failure_count, created_at,
47
- last_reinforced_at, retrieval_count
51
+ last_reinforced_at, retrieval_count, interference_count, salience
48
52
  FROM procedures WHERE state = 'active'
49
53
  `).all();
50
54
 
@@ -57,7 +61,7 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
57
61
  ? daysBetween(proc.last_reinforced_at, now)
58
62
  : ageDays;
59
63
 
60
- const confidence = computeConfidence({
64
+ let confidence = computeConfidence({
61
65
  sourceType: 'tool-result',
62
66
  supportingCount: proc.success_count || 0,
63
67
  contradictingCount: proc.failure_count || 0,
@@ -66,6 +70,9 @@ export function applyDecay(db, { dormantThreshold = 0.1, halfLives } = {}) {
66
70
  retrievalCount: proc.retrieval_count || 0,
67
71
  daysSinceRetrieval,
68
72
  });
73
+ confidence *= interferenceModifier(proc.interference_count || 0);
74
+ confidence *= salienceModifier(proc.salience ?? 0.5);
75
+ confidence = Math.max(0, Math.min(1, confidence));
69
76
 
70
77
  if (confidence < dormantThreshold) {
71
78
  markDormantProc.run('dormant', proc.id);
package/src/encode.js CHANGED
@@ -14,6 +14,7 @@ export async function encodeEpisode(db, embeddingProvider, {
14
14
  causal,
15
15
  tags,
16
16
  supersedes,
17
+ context = {},
17
18
  }) {
18
19
  if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
19
20
  if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
@@ -28,12 +29,13 @@ export async function encodeEpisode(db, embeddingProvider, {
28
29
  const insertAndLink = db.transaction(() => {
29
30
  db.prepare(`
30
31
  INSERT INTO episodes (
31
- id, content, embedding, source, source_reliability, salience,
32
+ id, content, embedding, source, source_reliability, salience, context,
32
33
  tags, causal_trigger, causal_consequence, created_at,
33
34
  embedding_model, embedding_version, supersedes
34
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
36
  `).run(
36
37
  id, content, embeddingBuffer, source, reliability, salience,
38
+ JSON.stringify(context),
37
39
  tags ? JSON.stringify(tags) : null,
38
40
  causal?.trigger || null, causal?.consequence || null,
39
41
  now, embeddingProvider.modelName, embeddingProvider.modelVersion,
package/src/forget.js ADDED
@@ -0,0 +1,111 @@
1
+ export function forgetMemory(db, id, { purge = false } = {}) {
2
+ const episode = db.prepare('SELECT id FROM episodes WHERE id = ?').get(id);
3
+ if (episode) {
4
+ if (purge) {
5
+ db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(id);
6
+ db.prepare('DELETE FROM episodes WHERE id = ?').run(id);
7
+ } else {
8
+ db.prepare("UPDATE episodes SET superseded_by = 'forgotten' WHERE id = ?").run(id);
9
+ db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(id);
10
+ }
11
+ return { id, type: 'episodic', purged: purge };
12
+ }
13
+
14
+ const semantic = db.prepare('SELECT id FROM semantics WHERE id = ?').get(id);
15
+ if (semantic) {
16
+ if (purge) {
17
+ db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(id);
18
+ db.prepare('DELETE FROM semantics WHERE id = ?').run(id);
19
+ } else {
20
+ db.prepare("UPDATE semantics SET state = 'superseded' WHERE id = ?").run(id);
21
+ db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(id);
22
+ }
23
+ return { id, type: 'semantic', purged: purge };
24
+ }
25
+
26
+ const procedure = db.prepare('SELECT id FROM procedures WHERE id = ?').get(id);
27
+ if (procedure) {
28
+ if (purge) {
29
+ db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(id);
30
+ db.prepare('DELETE FROM procedures WHERE id = ?').run(id);
31
+ } else {
32
+ db.prepare("UPDATE procedures SET state = 'superseded' WHERE id = ?").run(id);
33
+ db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(id);
34
+ }
35
+ return { id, type: 'procedural', purged: purge };
36
+ }
37
+
38
+ throw new Error(`Memory not found: ${id}`);
39
+ }
40
+
41
+ export function purgeMemories(db) {
42
+ const deadEpisodes = db.prepare(
43
+ 'SELECT id FROM episodes WHERE superseded_by IS NOT NULL'
44
+ ).all();
45
+ const deadSemantics = db.prepare(
46
+ "SELECT id FROM semantics WHERE state IN ('superseded', 'dormant', 'rolled_back')"
47
+ ).all();
48
+ const deadProcedures = db.prepare(
49
+ "SELECT id FROM procedures WHERE state IN ('superseded', 'dormant', 'rolled_back')"
50
+ ).all();
51
+
52
+ const purgeAll = db.transaction(() => {
53
+ for (const row of deadEpisodes) {
54
+ db.prepare('DELETE FROM vec_episodes WHERE id = ?').run(row.id);
55
+ db.prepare('DELETE FROM episodes WHERE id = ?').run(row.id);
56
+ }
57
+ for (const row of deadSemantics) {
58
+ db.prepare('DELETE FROM vec_semantics WHERE id = ?').run(row.id);
59
+ db.prepare('DELETE FROM semantics WHERE id = ?').run(row.id);
60
+ }
61
+ for (const row of deadProcedures) {
62
+ db.prepare('DELETE FROM vec_procedures WHERE id = ?').run(row.id);
63
+ db.prepare('DELETE FROM procedures WHERE id = ?').run(row.id);
64
+ }
65
+ });
66
+
67
+ purgeAll();
68
+
69
+ return {
70
+ episodes: deadEpisodes.length,
71
+ semantics: deadSemantics.length,
72
+ procedures: deadProcedures.length,
73
+ };
74
+ }
75
+
76
+ export async function forgetByQuery(db, embeddingProvider, query, { minSimilarity = 0.9, purge = false } = {}) {
77
+ const queryVector = await embeddingProvider.embed(query);
78
+ const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
79
+
80
+ const candidates = [];
81
+
82
+ const epMatch = db.prepare(`
83
+ SELECT e.id, (1.0 - v.distance) AS similarity, 'episodic' AS type
84
+ FROM vec_episodes v JOIN episodes e ON e.id = v.id
85
+ WHERE v.embedding MATCH ? AND k = 1 AND e.superseded_by IS NULL
86
+ `).get(queryBuffer);
87
+ if (epMatch) candidates.push(epMatch);
88
+
89
+ const semMatch = db.prepare(`
90
+ SELECT s.id, (1.0 - v.distance) AS similarity, 'semantic' AS type
91
+ FROM vec_semantics v JOIN semantics s ON s.id = v.id
92
+ WHERE v.embedding MATCH ? AND k = 1 AND (v.state = 'active' OR v.state = 'context_dependent')
93
+ `).get(queryBuffer);
94
+ if (semMatch) candidates.push(semMatch);
95
+
96
+ const procMatch = db.prepare(`
97
+ SELECT p.id, (1.0 - v.distance) AS similarity, 'procedural' AS type
98
+ FROM vec_procedures v JOIN procedures p ON p.id = v.id
99
+ WHERE v.embedding MATCH ? AND k = 1 AND (v.state = 'active' OR v.state = 'context_dependent')
100
+ `).get(queryBuffer);
101
+ if (procMatch) candidates.push(procMatch);
102
+
103
+ if (candidates.length === 0) return null;
104
+
105
+ candidates.sort((a, b) => b.similarity - a.similarity);
106
+ const best = candidates[0];
107
+
108
+ if (best.similarity < minSimilarity) return null;
109
+
110
+ return forgetMemory(db, best.id, { purge });
111
+ }
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Audrey } from './audrey.js';
2
- export { computeConfidence, sourceReliability, DEFAULT_SOURCE_RELIABILITY, DEFAULT_WEIGHTS, DEFAULT_HALF_LIVES } from './confidence.js';
2
+ export { computeConfidence, sourceReliability, salienceModifier, DEFAULT_SOURCE_RELIABILITY, DEFAULT_WEIGHTS, DEFAULT_HALF_LIVES } from './confidence.js';
3
3
  export { createEmbeddingProvider, MockEmbeddingProvider, OpenAIEmbeddingProvider } from './embedding.js';
4
4
  export { createLLMProvider, MockLLMProvider, AnthropicLLMProvider, OpenAILLMProvider } from './llm.js';
5
5
  export { recall, recallStream } from './recall.js';
@@ -14,3 +14,6 @@ export { exportMemories } from './export.js';
14
14
  export { importMemories } from './import.js';
15
15
  export { suggestConsolidationParams } from './adaptive.js';
16
16
  export { reembedAll } from './migrate.js';
17
+ export { forgetMemory, forgetByQuery, purgeMemories } from './forget.js';
18
+ export { applyInterference, interferenceModifier } from './interference.js';
19
+ export { contextMatchRatio, contextModifier } from './context.js';
@@ -0,0 +1,51 @@
1
+ export function interferenceModifier(interferenceCount, weight = 0.1) {
2
+ return 1 / (1 + weight * interferenceCount);
3
+ }
4
+
5
+ export async function applyInterference(db, embeddingProvider, episodeId, { content }, config = {}) {
6
+ const { enabled = true, k = 5, threshold = 0.6, weight = 0.1 } = config;
7
+
8
+ if (!enabled) return [];
9
+
10
+ const vector = await embeddingProvider.embed(content);
11
+ const buffer = embeddingProvider.vectorToBuffer(vector);
12
+
13
+ const semanticHits = db.prepare(`
14
+ SELECT s.id, s.interference_count, (1.0 - v.distance) AS similarity
15
+ FROM vec_semantics v
16
+ JOIN semantics s ON s.id = v.id
17
+ WHERE v.embedding MATCH ?
18
+ AND k = ?
19
+ AND (v.state = 'active' OR v.state = 'context_dependent')
20
+ `).all(buffer, k);
21
+
22
+ const proceduralHits = db.prepare(`
23
+ SELECT p.id, p.interference_count, (1.0 - v.distance) AS similarity
24
+ FROM vec_procedures v
25
+ JOIN procedures p ON p.id = v.id
26
+ WHERE v.embedding MATCH ?
27
+ AND k = ?
28
+ AND (v.state = 'active' OR v.state = 'context_dependent')
29
+ `).all(buffer, k);
30
+
31
+ const affected = [];
32
+
33
+ const updateSemantic = db.prepare('UPDATE semantics SET interference_count = ? WHERE id = ?');
34
+ const updateProcedural = db.prepare('UPDATE procedures SET interference_count = ? WHERE id = ?');
35
+
36
+ for (const hit of semanticHits) {
37
+ if (hit.similarity < threshold) continue;
38
+ const newCount = hit.interference_count + 1;
39
+ updateSemantic.run(newCount, hit.id);
40
+ affected.push({ id: hit.id, type: 'semantic', newCount, similarity: hit.similarity });
41
+ }
42
+
43
+ for (const hit of proceduralHits) {
44
+ if (hit.similarity < threshold) continue;
45
+ const newCount = hit.interference_count + 1;
46
+ updateProcedural.run(newCount, hit.id);
47
+ affected.push({ id: hit.id, type: 'procedural', newCount, similarity: hit.similarity });
48
+ }
49
+
50
+ return affected;
51
+ }
package/src/recall.js CHANGED
@@ -1,10 +1,12 @@
1
- import { computeConfidence, DEFAULT_HALF_LIVES } from './confidence.js';
1
+ import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
2
+ import { interferenceModifier } from './interference.js';
3
+ import { contextMatchRatio, contextModifier } from './context.js';
2
4
  import { daysBetween, safeJsonParse } from './utils.js';
3
5
 
4
6
  function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
5
7
  const ageDays = daysBetween(ep.created_at, now);
6
8
  const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
7
- return computeConfidence({
9
+ let confidence = computeConfidence({
8
10
  sourceType: ep.source,
9
11
  supportingCount: 1,
10
12
  contradictingCount: 0,
@@ -15,6 +17,8 @@ function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
15
17
  weights: confidenceConfig.weights,
16
18
  customSourceReliability: confidenceConfig.sourceReliability,
17
19
  });
20
+ confidence *= salienceModifier(ep.salience);
21
+ return Math.max(0, Math.min(1, confidence));
18
22
  }
19
23
 
20
24
  function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
@@ -23,7 +27,7 @@ function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
23
27
  ? daysBetween(sem.last_reinforced_at, now)
24
28
  : ageDays;
25
29
  const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
26
- return computeConfidence({
30
+ let confidence = computeConfidence({
27
31
  sourceType: 'tool-result',
28
32
  supportingCount: sem.supporting_count || 0,
29
33
  contradictingCount: sem.contradicting_count || 0,
@@ -34,6 +38,9 @@ function computeSemanticConfidence(sem, now, confidenceConfig = {}) {
34
38
  weights: confidenceConfig.weights,
35
39
  customSourceReliability: confidenceConfig.sourceReliability,
36
40
  });
41
+ confidence *= interferenceModifier(sem.interference_count || 0, confidenceConfig.interferenceWeight);
42
+ confidence *= salienceModifier(sem.salience);
43
+ return Math.max(0, Math.min(1, confidence));
37
44
  }
38
45
 
39
46
  function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
@@ -42,7 +49,7 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
42
49
  ? daysBetween(proc.last_reinforced_at, now)
43
50
  : ageDays;
44
51
  const halfLives = confidenceConfig.halfLives || DEFAULT_HALF_LIVES;
45
- return computeConfidence({
52
+ let confidence = computeConfidence({
46
53
  sourceType: 'tool-result',
47
54
  supportingCount: proc.success_count || 0,
48
55
  contradictingCount: proc.failure_count || 0,
@@ -53,9 +60,12 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
53
60
  weights: confidenceConfig.weights,
54
61
  customSourceReliability: confidenceConfig.sourceReliability,
55
62
  });
63
+ confidence *= interferenceModifier(proc.interference_count || 0, confidenceConfig.interferenceWeight);
64
+ confidence *= salienceModifier(proc.salience);
65
+ return Math.max(0, Math.min(1, confidence));
56
66
  }
57
67
 
58
- function buildEpisodicEntry(ep, confidence, score, includeProvenance) {
68
+ function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch) {
59
69
  const entry = {
60
70
  id: ep.id,
61
71
  content: ep.content,
@@ -65,6 +75,9 @@ function buildEpisodicEntry(ep, confidence, score, includeProvenance) {
65
75
  source: ep.source,
66
76
  createdAt: ep.created_at,
67
77
  };
78
+ if (contextMatch !== undefined) {
79
+ entry.contextMatch = contextMatch;
80
+ }
68
81
  if (includeProvenance) {
69
82
  entry.provenance = {
70
83
  source: ep.source,
@@ -121,7 +134,19 @@ function buildProceduralEntry(proc, confidence, score, includeProvenance) {
121
134
  return entry;
122
135
  }
123
136
 
124
- function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig) {
137
+ function stateClause(includeDormant) {
138
+ return includeDormant
139
+ ? "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')"
140
+ : "AND (v.state = 'active' OR v.state = 'context_dependent')";
141
+ }
142
+
143
+ function matchesDateFilters(createdAt, filters) {
144
+ if (filters.after && createdAt <= filters.after) return false;
145
+ if (filters.before && createdAt >= filters.before) return false;
146
+ return true;
147
+ }
148
+
149
+ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}) {
125
150
  const rows = db.prepare(`
126
151
  SELECT e.*, (1.0 - v.distance) AS similarity
127
152
  FROM vec_episodes v
@@ -133,34 +158,43 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
133
158
 
134
159
  const results = [];
135
160
  for (const row of rows) {
136
- const confidence = computeEpisodicConfidence(row, now, confidenceConfig);
161
+ if (!matchesDateFilters(row.created_at, filters)) continue;
162
+ if (filters.tags?.length) {
163
+ const rowTags = safeJsonParse(row.tags, []);
164
+ if (!filters.tags.some(t => rowTags.includes(t))) continue;
165
+ }
166
+ if (filters.sources?.length && !filters.sources.includes(row.source)) continue;
167
+ let confidence = computeEpisodicConfidence(row, now, confidenceConfig);
168
+
169
+ let ctxMatch;
170
+ if (confidenceConfig?.retrievalContext) {
171
+ const encodingCtx = safeJsonParse(row.context, {});
172
+ ctxMatch = contextMatchRatio(encodingCtx, confidenceConfig.retrievalContext);
173
+ confidence *= contextModifier(encodingCtx, confidenceConfig.retrievalContext, confidenceConfig.contextWeight);
174
+ confidence = Math.max(0, Math.min(1, confidence));
175
+ }
176
+
137
177
  if (confidence < minConfidence) continue;
138
178
  const score = row.similarity * confidence;
139
- results.push(buildEpisodicEntry(row, confidence, score, includeProvenance));
179
+ results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch));
140
180
  }
141
181
  return results;
142
182
  }
143
183
 
144
- function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig) {
145
- let stateFilter;
146
- if (includeDormant) {
147
- stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
148
- } else {
149
- stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
150
- }
151
-
184
+ function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}) {
152
185
  const rows = db.prepare(`
153
186
  SELECT s.*, (1.0 - v.distance) AS similarity
154
187
  FROM vec_semantics v
155
188
  JOIN semantics s ON s.id = v.id
156
189
  WHERE v.embedding MATCH ?
157
190
  AND k = ?
158
- ${stateFilter}
191
+ ${stateClause(includeDormant)}
159
192
  `).all(queryBuffer, candidateK);
160
193
 
161
194
  const results = [];
162
195
  const matchedIds = [];
163
196
  for (const row of rows) {
197
+ if (!matchesDateFilters(row.created_at, filters)) continue;
164
198
  const confidence = computeSemanticConfidence(row, now, confidenceConfig);
165
199
  if (confidence < minConfidence) continue;
166
200
  const score = row.similarity * confidence;
@@ -170,26 +204,20 @@ function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includePro
170
204
  return { results, matchedIds };
171
205
  }
172
206
 
173
- function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig) {
174
- let stateFilter;
175
- if (includeDormant) {
176
- stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
177
- } else {
178
- stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
179
- }
180
-
207
+ function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters = {}) {
181
208
  const rows = db.prepare(`
182
209
  SELECT p.*, (1.0 - v.distance) AS similarity
183
210
  FROM vec_procedures v
184
211
  JOIN procedures p ON p.id = v.id
185
212
  WHERE v.embedding MATCH ?
186
213
  AND k = ?
187
- ${stateFilter}
214
+ ${stateClause(includeDormant)}
188
215
  `).all(queryBuffer, candidateK);
189
216
 
190
217
  const results = [];
191
218
  const matchedIds = [];
192
219
  for (const row of rows) {
220
+ if (!matchesDateFilters(row.created_at, filters)) continue;
193
221
  const confidence = computeProceduralConfidence(row, now, confidenceConfig);
194
222
  if (confidence < minConfidence) continue;
195
223
  const score = row.similarity * confidence;
@@ -203,7 +231,7 @@ function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeP
203
231
  * @param {import('better-sqlite3').Database} db
204
232
  * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
205
233
  * @param {string} query
206
- * @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean }} [options]
234
+ * @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
207
235
  * @returns {AsyncGenerator<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>}
208
236
  */
209
237
  export async function* recallStream(db, embeddingProvider, query, options = {}) {
@@ -214,24 +242,30 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
214
242
  includeProvenance = false,
215
243
  includeDormant = false,
216
244
  confidenceConfig,
245
+ tags,
246
+ sources,
247
+ after,
248
+ before,
217
249
  } = options;
218
250
 
219
251
  const queryVector = await embeddingProvider.embed(query);
220
252
  const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
221
253
  const searchTypes = types || ['episodic', 'semantic', 'procedural'];
222
254
  const now = new Date();
223
- const candidateK = limit * 3;
255
+ const hasFilters = tags?.length || sources?.length || after || before;
256
+ const candidateK = hasFilters ? limit * 5 : limit * 3;
257
+ const filters = { tags, sources, after, before };
224
258
 
225
259
  const allResults = [];
226
260
 
227
261
  if (searchTypes.includes('episodic')) {
228
- const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig);
262
+ const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters);
229
263
  allResults.push(...episodic);
230
264
  }
231
265
 
232
266
  if (searchTypes.includes('semantic')) {
233
267
  const { results: semResults, matchedIds: semIds } =
234
- knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig);
268
+ knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters);
235
269
  allResults.push(...semResults);
236
270
 
237
271
  if (semIds.length > 0) {
@@ -247,7 +281,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
247
281
 
248
282
  if (searchTypes.includes('procedural')) {
249
283
  const { results: procResults, matchedIds: procIds } =
250
- knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig);
284
+ knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant, confidenceConfig, filters);
251
285
  allResults.push(...procResults);
252
286
 
253
287
  if (procIds.length > 0) {
@@ -272,7 +306,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
272
306
  * @param {import('better-sqlite3').Database} db
273
307
  * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
274
308
  * @param {string} query
275
- * @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean }} [options]
309
+ * @param {{ minConfidence?: number, types?: string[], limit?: number, includeProvenance?: boolean, includeDormant?: boolean, tags?: string[], sources?: string[], after?: string, before?: string }} [options]
276
310
  * @returns {Promise<Array<{ id: string, content: string, type: string, confidence: number, score: number, source: string, createdAt: string }>>}
277
311
  */
278
312
  export async function recall(db, embeddingProvider, query, options = {}) {