claude-cortex 1.0.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.
Files changed (102) hide show
  1. package/README.md +291 -0
  2. package/dist/api/events.d.ts +134 -0
  3. package/dist/api/events.d.ts.map +1 -0
  4. package/dist/api/events.js +73 -0
  5. package/dist/api/events.js.map +1 -0
  6. package/dist/api/visualization-server.d.ts +11 -0
  7. package/dist/api/visualization-server.d.ts.map +1 -0
  8. package/dist/api/visualization-server.js +653 -0
  9. package/dist/api/visualization-server.js.map +1 -0
  10. package/dist/context/project-context.d.ts +57 -0
  11. package/dist/context/project-context.d.ts.map +1 -0
  12. package/dist/context/project-context.js +135 -0
  13. package/dist/context/project-context.js.map +1 -0
  14. package/dist/database/init.d.ts +49 -0
  15. package/dist/database/init.d.ts.map +1 -0
  16. package/dist/database/init.js +336 -0
  17. package/dist/database/init.js.map +1 -0
  18. package/dist/embeddings/generator.d.ts +20 -0
  19. package/dist/embeddings/generator.d.ts.map +1 -0
  20. package/dist/embeddings/generator.js +77 -0
  21. package/dist/embeddings/generator.js.map +1 -0
  22. package/dist/embeddings/index.d.ts +2 -0
  23. package/dist/embeddings/index.d.ts.map +1 -0
  24. package/dist/embeddings/index.js +2 -0
  25. package/dist/embeddings/index.js.map +1 -0
  26. package/dist/errors.d.ts +74 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +131 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +83 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/memory/activation.d.ts +69 -0
  35. package/dist/memory/activation.d.ts.map +1 -0
  36. package/dist/memory/activation.js +168 -0
  37. package/dist/memory/activation.js.map +1 -0
  38. package/dist/memory/consolidate.d.ts +96 -0
  39. package/dist/memory/consolidate.d.ts.map +1 -0
  40. package/dist/memory/consolidate.js +400 -0
  41. package/dist/memory/consolidate.js.map +1 -0
  42. package/dist/memory/contradiction.d.ts +69 -0
  43. package/dist/memory/contradiction.d.ts.map +1 -0
  44. package/dist/memory/contradiction.js +286 -0
  45. package/dist/memory/contradiction.js.map +1 -0
  46. package/dist/memory/decay.d.ts +62 -0
  47. package/dist/memory/decay.d.ts.map +1 -0
  48. package/dist/memory/decay.js +184 -0
  49. package/dist/memory/decay.js.map +1 -0
  50. package/dist/memory/salience.d.ts +36 -0
  51. package/dist/memory/salience.d.ts.map +1 -0
  52. package/dist/memory/salience.js +200 -0
  53. package/dist/memory/salience.js.map +1 -0
  54. package/dist/memory/similarity.d.ts +57 -0
  55. package/dist/memory/similarity.d.ts.map +1 -0
  56. package/dist/memory/similarity.js +114 -0
  57. package/dist/memory/similarity.js.map +1 -0
  58. package/dist/memory/store.d.ts +170 -0
  59. package/dist/memory/store.d.ts.map +1 -0
  60. package/dist/memory/store.js +973 -0
  61. package/dist/memory/store.js.map +1 -0
  62. package/dist/memory/types.d.ts +91 -0
  63. package/dist/memory/types.d.ts.map +1 -0
  64. package/dist/memory/types.js +30 -0
  65. package/dist/memory/types.js.map +1 -0
  66. package/dist/server.d.ts +12 -0
  67. package/dist/server.d.ts.map +1 -0
  68. package/dist/server.js +466 -0
  69. package/dist/server.js.map +1 -0
  70. package/dist/tools/context.d.ts +135 -0
  71. package/dist/tools/context.d.ts.map +1 -0
  72. package/dist/tools/context.js +273 -0
  73. package/dist/tools/context.js.map +1 -0
  74. package/dist/tools/forget.d.ts +53 -0
  75. package/dist/tools/forget.d.ts.map +1 -0
  76. package/dist/tools/forget.js +179 -0
  77. package/dist/tools/forget.js.map +1 -0
  78. package/dist/tools/recall.d.ts +74 -0
  79. package/dist/tools/recall.d.ts.map +1 -0
  80. package/dist/tools/recall.js +140 -0
  81. package/dist/tools/recall.js.map +1 -0
  82. package/dist/tools/remember.d.ts +65 -0
  83. package/dist/tools/remember.d.ts.map +1 -0
  84. package/dist/tools/remember.js +147 -0
  85. package/dist/tools/remember.js.map +1 -0
  86. package/dist/worker/brain-worker.d.ts +100 -0
  87. package/dist/worker/brain-worker.d.ts.map +1 -0
  88. package/dist/worker/brain-worker.js +261 -0
  89. package/dist/worker/brain-worker.js.map +1 -0
  90. package/dist/worker/link-discovery.d.ts +47 -0
  91. package/dist/worker/link-discovery.d.ts.map +1 -0
  92. package/dist/worker/link-discovery.js +103 -0
  93. package/dist/worker/link-discovery.js.map +1 -0
  94. package/dist/worker/predictive-consolidation.d.ts +46 -0
  95. package/dist/worker/predictive-consolidation.d.ts.map +1 -0
  96. package/dist/worker/predictive-consolidation.js +110 -0
  97. package/dist/worker/predictive-consolidation.js.map +1 -0
  98. package/dist/worker/types.d.ts +91 -0
  99. package/dist/worker/types.d.ts.map +1 -0
  100. package/dist/worker/types.js +22 -0
  101. package/dist/worker/types.js.map +1 -0
  102. package/package.json +59 -0
@@ -0,0 +1,973 @@
1
+ /**
2
+ * Memory Store
3
+ *
4
+ * Core CRUD operations for the memory database.
5
+ * Handles storage, retrieval, and management of memories.
6
+ */
7
+ import { getDatabase } from '../database/init.js';
8
+ import { DEFAULT_CONFIG, } from './types.js';
9
+ import { calculateSalience, suggestCategory, extractTags, } from './salience.js';
10
+ import { calculateDecayedScore, calculateReinforcementBoost, calculatePriority, } from './decay.js';
11
+ import { activateMemory as spreadActivation, getActivationBoost, } from './activation.js';
12
+ import { jaccardSimilarity } from './similarity.js';
13
+ import { emitMemoryCreated, emitMemoryAccessed, emitMemoryDeleted, emitMemoryUpdated, } from '../api/events.js';
14
+ import { generateEmbedding, cosineSimilarity } from '../embeddings/index.js';
15
+ // Anti-bloat: Maximum content size per memory (10KB)
16
+ const MAX_CONTENT_SIZE = 10 * 1024;
17
+ // Track truncation info globally for the last addMemory call
18
+ let lastTruncationInfo = null;
19
+ /**
20
+ * Truncate content if it exceeds max size
21
+ * Returns both the content and truncation info
22
+ */
23
+ function truncateContent(content) {
24
+ const originalLength = content.length;
25
+ if (originalLength > MAX_CONTENT_SIZE) {
26
+ return {
27
+ content: content.slice(0, MAX_CONTENT_SIZE) + '\n\n[Content truncated - exceeded 10KB limit]',
28
+ wasTruncated: true,
29
+ originalLength,
30
+ };
31
+ }
32
+ return { content, wasTruncated: false, originalLength };
33
+ }
34
+ /**
35
+ * Get truncation info from the last addMemory call
36
+ */
37
+ export function getLastTruncationInfo() {
38
+ return lastTruncationInfo;
39
+ }
40
+ /**
41
+ * Escape FTS5 query to prevent syntax errors
42
+ * FTS5 interprets:
43
+ * - "word-word" as "column:value" syntax
44
+ * - AND, OR, NOT as boolean operators
45
+ * - &, | as boolean operators
46
+ * We quote individual terms to search them literally
47
+ */
48
+ function escapeFts5Query(query) {
49
+ // Split on whitespace, process each term, filter empty, rejoin
50
+ return query
51
+ .split(/\s+/)
52
+ .filter(term => term.length > 0)
53
+ .map(term => {
54
+ // FTS5 boolean operators - quote them to search literally
55
+ const upperTerm = term.toUpperCase();
56
+ if (upperTerm === 'AND' || upperTerm === 'OR' || upperTerm === 'NOT') {
57
+ return `"${term}"`;
58
+ }
59
+ // If term contains special FTS5 characters, quote it
60
+ // Including: - : * ^ ( ) & | and quotes
61
+ if (/[-:*^()&|]/.test(term) || term.includes('"')) {
62
+ // Escape existing quotes and wrap in quotes
63
+ return `"${term.replace(/"/g, '""')}"`;
64
+ }
65
+ return term;
66
+ })
67
+ .join(' ');
68
+ }
69
+ /**
70
+ * Convert database row to Memory object
71
+ */
72
+ export function rowToMemory(row) {
73
+ return {
74
+ id: row.id,
75
+ type: row.type,
76
+ category: row.category,
77
+ title: row.title,
78
+ content: row.content,
79
+ project: row.project,
80
+ tags: JSON.parse(row.tags || '[]'),
81
+ salience: row.salience,
82
+ accessCount: row.access_count,
83
+ lastAccessed: new Date(row.last_accessed),
84
+ createdAt: new Date(row.created_at),
85
+ decayedScore: row.decayed_score ?? row.salience,
86
+ metadata: JSON.parse(row.metadata || '{}'),
87
+ embedding: row.embedding,
88
+ scope: row.scope ?? 'project',
89
+ transferable: Boolean(row.transferable),
90
+ };
91
+ }
92
+ /**
93
+ * Detect if memory content suggests global applicability
94
+ * Used to auto-set scope to 'global' for transferable knowledge
95
+ */
96
+ function detectGlobalPattern(content, category, tags) {
97
+ const globalCategories = ['pattern', 'preference', 'learning'];
98
+ const globalKeywords = ['always', 'never', 'best practice', 'general rule', 'universal'];
99
+ const globalTags = ['universal', 'global', 'general', 'cross-project'];
100
+ if (globalCategories.includes(category))
101
+ return true;
102
+ if (globalKeywords.some(k => content.toLowerCase().includes(k)))
103
+ return true;
104
+ if (tags.some(t => globalTags.includes(t.toLowerCase())))
105
+ return true;
106
+ return false;
107
+ }
108
+ /**
109
+ * Add a new memory
110
+ */
111
+ export function addMemory(input, config = DEFAULT_CONFIG) {
112
+ const db = getDatabase();
113
+ // Calculate salience if not provided
114
+ const salience = input.salience ?? calculateSalience(input);
115
+ // Suggest category if not provided
116
+ const category = input.category ?? suggestCategory(input);
117
+ // Extract tags
118
+ const tags = extractTags(input);
119
+ // Determine type
120
+ const type = input.type ?? (salience >= config.consolidationThreshold ? 'long_term' : 'short_term');
121
+ // Determine scope and transferable flag for cross-project knowledge
122
+ const scope = input.scope ??
123
+ (detectGlobalPattern(input.content, category, tags) ? 'global' : 'project');
124
+ const transferable = input.transferable ?? (scope === 'global' ? 1 : 0);
125
+ const stmt = db.prepare(`
126
+ INSERT INTO memories (type, category, title, content, project, tags, salience, metadata, scope, transferable)
127
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
128
+ `);
129
+ // Anti-bloat: Truncate content if too large
130
+ const truncationResult = truncateContent(input.content);
131
+ // Store truncation info for the remember tool to access
132
+ lastTruncationInfo = {
133
+ wasTruncated: truncationResult.wasTruncated,
134
+ originalLength: truncationResult.originalLength,
135
+ truncatedLength: truncationResult.content.length,
136
+ };
137
+ const result = stmt.run(type, category, input.title, truncationResult.content, input.project || null, JSON.stringify(tags), salience, JSON.stringify(input.metadata || {}), scope, transferable);
138
+ const memory = getMemoryById(result.lastInsertRowid);
139
+ // Emit event for real-time dashboard
140
+ emitMemoryCreated(memory);
141
+ // ORGANIC FEATURE: Auto-link to related memories
142
+ // This builds the knowledge graph automatically as memories are created
143
+ try {
144
+ const relationships = detectRelationships(memory);
145
+ for (const rel of relationships.slice(0, 3)) { // Top 3 most relevant
146
+ createMemoryLink(memory.id, rel.targetId, rel.relationship, rel.strength);
147
+ }
148
+ }
149
+ catch (e) {
150
+ // Don't fail memory creation if linking fails
151
+ console.error('[claude-cortex] Auto-link failed:', e);
152
+ }
153
+ // SEMANTIC SEARCH: Generate embedding asynchronously (don't block INSERT)
154
+ const memoryId = memory.id;
155
+ generateEmbedding(input.title + ' ' + truncationResult.content)
156
+ .then(embedding => {
157
+ try {
158
+ db.prepare('UPDATE memories SET embedding = ? WHERE id = ?')
159
+ .run(Buffer.from(embedding.buffer), memoryId);
160
+ }
161
+ catch (e) {
162
+ console.error('[claude-cortex] Failed to store embedding:', e);
163
+ }
164
+ })
165
+ .catch(e => {
166
+ console.error('[claude-cortex] Failed to generate embedding:', e);
167
+ });
168
+ // Anti-bloat: Check if limits exceeded and trigger async cleanup
169
+ // We use setImmediate to not block the insert response
170
+ setImmediate(() => {
171
+ try {
172
+ const stats = getMemoryStats();
173
+ if (stats.shortTerm > config.maxShortTermMemories ||
174
+ stats.longTerm > config.maxLongTermMemories) {
175
+ // Import dynamically to avoid circular dependency
176
+ import('./consolidate.js').then(({ enforceMemoryLimits }) => {
177
+ enforceMemoryLimits(config);
178
+ }).catch(() => {
179
+ // Silently ignore - consolidation will happen on next scheduled run
180
+ });
181
+ }
182
+ }
183
+ catch {
184
+ // Silently ignore errors in async cleanup
185
+ }
186
+ });
187
+ return memory;
188
+ }
189
+ /**
190
+ * Get a memory by ID
191
+ */
192
+ export function getMemoryById(id) {
193
+ const db = getDatabase();
194
+ const row = db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
195
+ if (!row)
196
+ return null;
197
+ return rowToMemory(row);
198
+ }
199
+ /**
200
+ * Update a memory
201
+ */
202
+ export function updateMemory(id, updates) {
203
+ const db = getDatabase();
204
+ const existing = getMemoryById(id);
205
+ if (!existing)
206
+ return null;
207
+ const fields = [];
208
+ const values = [];
209
+ if (updates.title !== undefined) {
210
+ fields.push('title = ?');
211
+ values.push(updates.title);
212
+ }
213
+ if (updates.content !== undefined) {
214
+ fields.push('content = ?');
215
+ values.push(updates.content);
216
+ }
217
+ if (updates.type !== undefined) {
218
+ fields.push('type = ?');
219
+ values.push(updates.type);
220
+ }
221
+ if (updates.category !== undefined) {
222
+ fields.push('category = ?');
223
+ values.push(updates.category);
224
+ }
225
+ if (updates.project !== undefined) {
226
+ fields.push('project = ?');
227
+ values.push(updates.project);
228
+ }
229
+ if (updates.tags !== undefined) {
230
+ fields.push('tags = ?');
231
+ values.push(JSON.stringify(updates.tags));
232
+ }
233
+ if (updates.salience !== undefined) {
234
+ fields.push('salience = ?');
235
+ values.push(updates.salience);
236
+ }
237
+ if (updates.metadata !== undefined) {
238
+ fields.push('metadata = ?');
239
+ values.push(JSON.stringify(updates.metadata));
240
+ }
241
+ if (fields.length === 0)
242
+ return existing;
243
+ values.push(id);
244
+ db.prepare(`UPDATE memories SET ${fields.join(', ')} WHERE id = ?`).run(...values);
245
+ const updatedMemory = getMemoryById(id);
246
+ // Emit event for real-time dashboard
247
+ emitMemoryUpdated(updatedMemory);
248
+ return updatedMemory;
249
+ }
250
+ /**
251
+ * Delete a memory
252
+ */
253
+ export function deleteMemory(id) {
254
+ const db = getDatabase();
255
+ // Get memory before deletion for event
256
+ const memory = getMemoryById(id);
257
+ const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
258
+ // Emit event for real-time dashboard
259
+ if (result.changes > 0 && memory) {
260
+ emitMemoryDeleted(id, memory.title);
261
+ }
262
+ return result.changes > 0;
263
+ }
264
+ /**
265
+ * Access a memory (updates access count and timestamp, returns reinforced memory)
266
+ */
267
+ export function accessMemory(id, config = DEFAULT_CONFIG) {
268
+ const db = getDatabase();
269
+ const memory = getMemoryById(id);
270
+ if (!memory)
271
+ return null;
272
+ // Calculate new salience with reinforcement
273
+ const newSalience = calculateReinforcementBoost(memory, config);
274
+ db.prepare(`
275
+ UPDATE memories
276
+ SET access_count = access_count + 1,
277
+ last_accessed = CURRENT_TIMESTAMP,
278
+ salience = ?
279
+ WHERE id = ?
280
+ `).run(newSalience, id);
281
+ const updatedMemory = getMemoryById(id);
282
+ // Emit event for real-time dashboard
283
+ emitMemoryAccessed(updatedMemory, newSalience);
284
+ // ORGANIC FEATURE: Link strengthening on co-access
285
+ // If memory A and B are both accessed within 5 minutes, strengthen their link
286
+ // This mimics Hebbian learning: "neurons that fire together, wire together"
287
+ try {
288
+ const recentlyAccessed = db.prepare(`
289
+ SELECT id FROM memories
290
+ WHERE last_accessed > datetime('now', '-5 minutes')
291
+ AND id != ?
292
+ LIMIT 10
293
+ `).all(id);
294
+ for (const recent of recentlyAccessed) {
295
+ // Check if link exists in either direction
296
+ const existingLink = db.prepare(`
297
+ SELECT id, strength FROM memory_links
298
+ WHERE (source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?)
299
+ `).get(id, recent.id, recent.id, id);
300
+ if (existingLink) {
301
+ // Strengthen existing link (cap at 1.0)
302
+ const newStrength = Math.min(1.0, existingLink.strength + 0.05);
303
+ db.prepare('UPDATE memory_links SET strength = ? WHERE id = ?')
304
+ .run(newStrength, existingLink.id);
305
+ }
306
+ else {
307
+ // Create new weak link for co-accessed memories
308
+ createMemoryLink(id, recent.id, 'related', 0.2);
309
+ }
310
+ }
311
+ }
312
+ catch (e) {
313
+ // Don't fail memory access if link strengthening fails
314
+ console.error('[claude-cortex] Link strengthening failed:', e);
315
+ }
316
+ // ORGANIC FEATURE: Spreading Activation (Phase 2)
317
+ // Activate this memory and spread activation to linked memories
318
+ // This makes related memories easier to recall in subsequent searches
319
+ spreadActivation(id);
320
+ return updatedMemory;
321
+ }
322
+ /**
323
+ * Soft access - updates last_accessed without boosting salience
324
+ * Used for search results to close the reinforcement loop
325
+ * ORGANIC FEATURE: This allows searched memories to stay fresh without
326
+ * artificially inflating their salience scores
327
+ */
328
+ export function softAccessMemory(id) {
329
+ const db = getDatabase();
330
+ db.prepare('UPDATE memories SET last_accessed = ? WHERE id = ?')
331
+ .run(new Date().toISOString(), id);
332
+ }
333
+ // ============================================
334
+ // ORGANIC FEATURE: Memory Enrichment (Phase 3)
335
+ // ============================================
336
+ // Enrichment configuration
337
+ const ENRICHMENT_SIMILARITY_THRESHOLD = 0.3; // Min similarity to trigger enrichment
338
+ const ENRICHMENT_COOLDOWN_HOURS = 1; // Don't enrich same memory within 1 hour
339
+ const MAX_ENRICHMENT_SIZE = 2000; // Max chars to add per enrichment
340
+ // Track last enrichment times (in-memory, ephemeral like activation cache)
341
+ const enrichmentTimestamps = new Map();
342
+ /**
343
+ * Enrich a memory with additional context
344
+ *
345
+ * This adds timestamped context to a memory when:
346
+ * 1. The new context is sufficiently related but different (new information)
347
+ * 2. The memory hasn't been enriched recently (cooldown)
348
+ * 3. The content won't exceed the size limit
349
+ *
350
+ * ORGANIC FEATURE: Memories grow with new context over time,
351
+ * mimicking how human memories are reconsolidated with new information
352
+ *
353
+ * @param memoryId - ID of the memory to enrich
354
+ * @param newContext - New context to add
355
+ * @param contextType - Type of context ('search' | 'access' | 'related')
356
+ * @returns EnrichmentResult indicating success or failure with reason
357
+ */
358
+ export function enrichMemory(memoryId, newContext, contextType = 'access') {
359
+ const db = getDatabase();
360
+ const memory = getMemoryById(memoryId);
361
+ if (!memory) {
362
+ return { enriched: false, reason: 'Memory not found' };
363
+ }
364
+ // Check cooldown
365
+ const lastEnrichment = enrichmentTimestamps.get(memoryId);
366
+ const now = Date.now();
367
+ if (lastEnrichment && (now - lastEnrichment) < ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000) {
368
+ return { enriched: false, reason: 'Enrichment cooldown active' };
369
+ }
370
+ // Check similarity - should be related but not too similar (new info)
371
+ const similarity = jaccardSimilarity(memory.content, newContext);
372
+ if (similarity > 0.8) {
373
+ return { enriched: false, reason: 'Context too similar (no new information)' };
374
+ }
375
+ if (similarity < ENRICHMENT_SIMILARITY_THRESHOLD) {
376
+ return { enriched: false, reason: 'Context not sufficiently related' };
377
+ }
378
+ // Truncate context if needed
379
+ const truncatedContext = newContext.slice(0, MAX_ENRICHMENT_SIZE);
380
+ // Build enrichment block with timestamp
381
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
382
+ const enrichmentBlock = `\n\n---\n[${timestamp}] ${contextType}: ${truncatedContext}`;
383
+ // Check size limit (leave 500 char buffer for future enrichments)
384
+ const newContent = memory.content + enrichmentBlock;
385
+ if (newContent.length > MAX_CONTENT_SIZE - 500) {
386
+ return { enriched: false, reason: 'Content size limit reached' };
387
+ }
388
+ // Update memory
389
+ db.prepare(`
390
+ UPDATE memories
391
+ SET content = ?,
392
+ last_accessed = CURRENT_TIMESTAMP
393
+ WHERE id = ?
394
+ `).run(newContent, memoryId);
395
+ // Update cooldown timestamp
396
+ enrichmentTimestamps.set(memoryId, now);
397
+ // Emit update event for dashboard
398
+ const updatedMemory = getMemoryById(memoryId);
399
+ emitMemoryUpdated(updatedMemory);
400
+ return { enriched: true, reason: `Added ${contextType} context (${truncatedContext.length} chars)` };
401
+ }
402
+ /**
403
+ * Clear enrichment cooldown for a memory (for testing)
404
+ */
405
+ export function clearEnrichmentCooldown(memoryId) {
406
+ enrichmentTimestamps.delete(memoryId);
407
+ }
408
+ /**
409
+ * Get enrichment cooldown status for a memory
410
+ */
411
+ export function getEnrichmentCooldownStatus(memoryId) {
412
+ const lastEnrichment = enrichmentTimestamps.get(memoryId);
413
+ if (!lastEnrichment) {
414
+ return { onCooldown: false, remainingMs: 0 };
415
+ }
416
+ const cooldownMs = ENRICHMENT_COOLDOWN_HOURS * 60 * 60 * 1000;
417
+ const elapsed = Date.now() - lastEnrichment;
418
+ const remaining = Math.max(0, cooldownMs - elapsed);
419
+ return {
420
+ onCooldown: remaining > 0,
421
+ remainingMs: remaining,
422
+ };
423
+ }
424
+ /**
425
+ * Update persisted decay scores for all memories
426
+ * Called during consolidation and periodically by the API server
427
+ * Returns the number of memories updated
428
+ */
429
+ export function updateDecayScores() {
430
+ const db = getDatabase();
431
+ // Get all memories
432
+ const memories = db.prepare('SELECT * FROM memories').all();
433
+ let updated = 0;
434
+ const updateStmt = db.prepare('UPDATE memories SET decayed_score = ? WHERE id = ?');
435
+ for (const row of memories) {
436
+ const memory = rowToMemory(row);
437
+ const decayedScore = calculateDecayedScore(memory);
438
+ // Only update if score has changed significantly (saves writes)
439
+ const currentScore = row.decayed_score;
440
+ if (currentScore === null || Math.abs(currentScore - decayedScore) > 0.01) {
441
+ updateStmt.run(decayedScore, memory.id);
442
+ updated++;
443
+ }
444
+ }
445
+ return updated;
446
+ }
447
+ /**
448
+ * Detect the likely category a query is asking about
449
+ */
450
+ function detectQueryCategory(query) {
451
+ const lower = query.toLowerCase();
452
+ if (/architect|design|structure|pattern|system|schema|model/.test(lower)) {
453
+ return 'architecture';
454
+ }
455
+ if (/error|bug|fix|issue|crash|exception|fail|problem/.test(lower)) {
456
+ return 'error';
457
+ }
458
+ if (/prefer|always|never|style|convention|like|want/.test(lower)) {
459
+ return 'preference';
460
+ }
461
+ if (/learn|discover|realiz|found\s+out|turns?\s+out/.test(lower)) {
462
+ return 'learning';
463
+ }
464
+ if (/todo|task|pending|need\s+to|should\s+do/.test(lower)) {
465
+ return 'todo';
466
+ }
467
+ if (/relation|depend|connect|link|reference/.test(lower)) {
468
+ return 'relationship';
469
+ }
470
+ return null;
471
+ }
472
+ /**
473
+ * Calculate a boost for memories linked to high-salience memories
474
+ */
475
+ function calculateLinkBoost(memoryId, db) {
476
+ try {
477
+ // Get linked memories and their salience
478
+ const linked = db.prepare(`
479
+ SELECT m.salience, ml.strength
480
+ FROM memory_links ml
481
+ JOIN memories m ON (m.id = ml.target_id OR m.id = ml.source_id)
482
+ WHERE (ml.source_id = ? OR ml.target_id = ?)
483
+ AND m.id != ?
484
+ `).all(memoryId, memoryId, memoryId);
485
+ if (linked.length === 0)
486
+ return 0;
487
+ // Calculate weighted average of linked memory salience
488
+ const totalWeight = linked.reduce((sum, l) => sum + l.strength, 0);
489
+ if (totalWeight === 0)
490
+ return 0;
491
+ const weightedSalience = linked.reduce((sum, l) => sum + l.salience * l.strength, 0) / totalWeight;
492
+ // Cap boost at 0.15
493
+ return Math.min(0.15, weightedSalience * 0.2);
494
+ }
495
+ catch {
496
+ return 0;
497
+ }
498
+ }
499
+ /**
500
+ * Calculate partial tag match score
501
+ */
502
+ function calculateTagScore(queryTags, memoryTags) {
503
+ if (queryTags.length === 0 || memoryTags.length === 0)
504
+ return 0;
505
+ // Count partial matches (substring matching)
506
+ let matches = 0;
507
+ for (const qt of queryTags) {
508
+ const qtLower = qt.toLowerCase();
509
+ if (memoryTags.some(mt => mt.toLowerCase().includes(qtLower) || qtLower.includes(mt.toLowerCase()))) {
510
+ matches++;
511
+ }
512
+ }
513
+ return (matches / queryTags.length) * 0.1;
514
+ }
515
+ /**
516
+ * Extract potential tags from a query string
517
+ */
518
+ function extractQueryTags(query) {
519
+ // Extract words that might be tags (tech terms, project-specific terms)
520
+ const words = query.toLowerCase().split(/\s+/);
521
+ return words.filter(w => w.length > 2 &&
522
+ /^[a-z][a-z0-9-]*$/.test(w) &&
523
+ !['the', 'and', 'for', 'with', 'how', 'what', 'when', 'where', 'why'].includes(w));
524
+ }
525
+ /**
526
+ * Search memories by vector similarity
527
+ * Returns memories sorted by cosine similarity to the query embedding
528
+ */
529
+ function vectorSearch(queryEmbedding, limit, project, includeGlobal = true) {
530
+ const db = getDatabase();
531
+ // Get memories with embeddings
532
+ let query = `
533
+ SELECT * FROM memories
534
+ WHERE embedding IS NOT NULL
535
+ `;
536
+ const params = [];
537
+ if (project && includeGlobal) {
538
+ query += ` AND (project = ? OR scope = 'global')`;
539
+ params.push(project);
540
+ }
541
+ else if (project) {
542
+ query += ` AND project = ?`;
543
+ params.push(project);
544
+ }
545
+ const rows = db.prepare(query).all(...params);
546
+ // Calculate similarities
547
+ const results = rows
548
+ .map(row => {
549
+ const embeddingBuffer = row.embedding;
550
+ const embedding = new Float32Array(embeddingBuffer.buffer, embeddingBuffer.byteOffset, embeddingBuffer.length / 4);
551
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
552
+ return {
553
+ memory: rowToMemory(row),
554
+ similarity,
555
+ };
556
+ })
557
+ .filter(r => r.similarity > 0.3) // Threshold for relevance
558
+ .sort((a, b) => b.similarity - a.similarity)
559
+ .slice(0, limit);
560
+ return results;
561
+ }
562
+ /**
563
+ * Search memories using full-text search, vector similarity, and filters
564
+ * Now uses hybrid search combining FTS5 keywords with semantic vector matching
565
+ */
566
+ export async function searchMemories(options, config = DEFAULT_CONFIG) {
567
+ const db = getDatabase();
568
+ const limit = options.limit || 20;
569
+ const includeGlobal = options.includeGlobal ?? true;
570
+ // Detect query category for boosting
571
+ const detectedCategory = options.query ? detectQueryCategory(options.query) : null;
572
+ const queryTags = options.query ? extractQueryTags(options.query) : [];
573
+ // SEMANTIC SEARCH: Generate query embedding (may fail on first call while model loads)
574
+ let queryEmbedding = null;
575
+ let vectorResults = new Map(); // memoryId -> similarity
576
+ if (options.query && options.query.trim()) {
577
+ try {
578
+ queryEmbedding = await generateEmbedding(options.query);
579
+ const vectorHits = vectorSearch(queryEmbedding, limit * 2, options.project, includeGlobal);
580
+ for (const hit of vectorHits) {
581
+ vectorResults.set(hit.memory.id, hit.similarity);
582
+ }
583
+ }
584
+ catch (e) {
585
+ // Vector search unavailable - fall back to FTS only
586
+ console.log('[claude-cortex] Vector search unavailable, using FTS only');
587
+ }
588
+ }
589
+ let sql;
590
+ const params = [];
591
+ if (options.query && options.query.trim()) {
592
+ // Use FTS search - escape query to prevent FTS5 syntax errors
593
+ // FTS5 interprets "word-word" as "column:value", so we quote terms
594
+ const escapedQuery = escapeFts5Query(options.query.trim());
595
+ sql = `
596
+ SELECT m.*, fts.rank
597
+ FROM memories m
598
+ JOIN memories_fts fts ON m.id = fts.rowid
599
+ WHERE memories_fts MATCH ?
600
+ `;
601
+ params.push(escapedQuery);
602
+ }
603
+ else {
604
+ // No query, just filter
605
+ sql = `SELECT *, 0 as rank FROM memories m WHERE 1=1`;
606
+ }
607
+ // Add filters - include global memories if enabled
608
+ if (options.project) {
609
+ if (includeGlobal) {
610
+ sql += ` AND (m.project = ? OR m.scope = 'global')`;
611
+ }
612
+ else {
613
+ sql += ' AND m.project = ?';
614
+ }
615
+ params.push(options.project);
616
+ }
617
+ if (options.category) {
618
+ sql += ' AND m.category = ?';
619
+ params.push(options.category);
620
+ }
621
+ if (options.type) {
622
+ sql += ' AND m.type = ?';
623
+ params.push(options.type);
624
+ }
625
+ if (options.minSalience) {
626
+ sql += ' AND m.salience >= ?';
627
+ params.push(options.minSalience);
628
+ }
629
+ if (options.tags && options.tags.length > 0) {
630
+ // Use json_each() for proper JSON array parsing
631
+ // This avoids false positives from LIKE matching (e.g., "api" matching "api-gateway")
632
+ const tagPlaceholders = options.tags.map(() => '?').join(',');
633
+ sql += ` AND EXISTS (
634
+ SELECT 1 FROM json_each(m.tags)
635
+ WHERE json_each.value IN (${tagPlaceholders})
636
+ )`;
637
+ params.push(...options.tags);
638
+ }
639
+ sql += ' ORDER BY m.salience DESC, m.last_accessed DESC LIMIT ?';
640
+ params.push(limit);
641
+ const rows = db.prepare(sql).all(...params);
642
+ // Convert to SearchResult with computed scores
643
+ const results = rows.map(row => {
644
+ const memory = rowToMemory(row);
645
+ const decayedScore = calculateDecayedScore(memory, config);
646
+ memory.decayedScore = decayedScore;
647
+ // Improved FTS score normalization (BM25-style)
648
+ // FTS5 rank is negative, closer to 0 = better match
649
+ const rawRank = row.rank;
650
+ const ftsScore = rawRank ? 1 / (1 + Math.abs(rawRank)) : 0.3;
651
+ // Recency boost for recently accessed memories
652
+ const hoursSinceAccess = (Date.now() - new Date(memory.lastAccessed).getTime()) / (1000 * 60 * 60);
653
+ const recencyBoost = hoursSinceAccess < 1 ? 0.1 : (hoursSinceAccess < 24 ? 0.05 : 0);
654
+ // Category match bonus
655
+ const categoryBoost = detectedCategory && memory.category === detectedCategory ? 0.1 : 0;
656
+ // Link boost - memories connected to high-salience memories rank higher
657
+ const linkBoost = calculateLinkBoost(memory.id, db);
658
+ // Partial tag match bonus
659
+ const tagBoost = calculateTagScore(queryTags, memory.tags);
660
+ // ORGANIC FEATURE: Spreading Activation boost (Phase 2)
661
+ // Recently accessed memories and their linked neighbors get a boost
662
+ const activationBoost = getActivationBoost(memory.id);
663
+ // SEMANTIC SEARCH: Vector similarity boost (Phase 5)
664
+ // If memory was found by vector search, add similarity as a boost
665
+ const vectorSimilarity = vectorResults.get(memory.id) || 0;
666
+ const vectorBoost = vectorSimilarity * 0.3; // 30% weight for vector similarity
667
+ // Combined relevance score (adjusted weights to accommodate vector)
668
+ const relevanceScore = (ftsScore * 0.3 + // Reduced from 0.35
669
+ vectorBoost + // New: 0-0.3 from vector similarity
670
+ decayedScore * 0.25 + // Reduced from 0.35
671
+ calculatePriority(memory) * 0.1 + // Reduced from 0.15
672
+ recencyBoost + categoryBoost + linkBoost + tagBoost + activationBoost);
673
+ return { memory, relevanceScore };
674
+ });
675
+ // Sort by relevance and filter out too-decayed memories
676
+ const sortedResults = results
677
+ .filter(r => options.includeDecayed || r.memory.decayedScore >= config.salienceThreshold)
678
+ .sort((a, b) => b.relevanceScore - a.relevanceScore);
679
+ // ORGANIC FEATURE: Soft-access top results to reinforce useful memories
680
+ // This closes the reinforcement loop - memories that appear in searches stay fresh
681
+ // We only soft-access (update last_accessed, no salience boost) to avoid inflation
682
+ for (const result of sortedResults.slice(0, 5)) {
683
+ softAccessMemory(result.memory.id);
684
+ }
685
+ return sortedResults;
686
+ }
687
+ /**
688
+ * Get all memories for a project
689
+ */
690
+ export function getProjectMemories(project, config = DEFAULT_CONFIG) {
691
+ const db = getDatabase();
692
+ const rows = db.prepare(`
693
+ SELECT * FROM memories
694
+ WHERE project = ?
695
+ ORDER BY salience DESC, last_accessed DESC
696
+ `).all(project);
697
+ return rows.map(row => {
698
+ const memory = rowToMemory(row);
699
+ memory.decayedScore = calculateDecayedScore(memory, config);
700
+ return memory;
701
+ });
702
+ }
703
+ /**
704
+ * Get recent memories
705
+ */
706
+ export function getRecentMemories(limit = 10, project) {
707
+ const db = getDatabase();
708
+ let sql = 'SELECT * FROM memories';
709
+ const params = [];
710
+ if (project) {
711
+ sql += ' WHERE project = ?';
712
+ params.push(project);
713
+ }
714
+ sql += ' ORDER BY last_accessed DESC LIMIT ?';
715
+ params.push(limit);
716
+ const rows = db.prepare(sql).all(...params);
717
+ return rows.map(rowToMemory);
718
+ }
719
+ /**
720
+ * Get memories by type
721
+ */
722
+ export function getMemoriesByType(type, limit = 50) {
723
+ const db = getDatabase();
724
+ const rows = db.prepare(`
725
+ SELECT * FROM memories
726
+ WHERE type = ?
727
+ ORDER BY salience DESC, last_accessed DESC
728
+ LIMIT ?
729
+ `).all(type, limit);
730
+ return rows.map(rowToMemory);
731
+ }
732
+ /**
733
+ * Get high-priority memories (for context injection)
734
+ */
735
+ export function getHighPriorityMemories(limit = 10, project) {
736
+ const db = getDatabase();
737
+ let sql = `
738
+ SELECT * FROM memories
739
+ WHERE salience >= 0.6
740
+ `;
741
+ const params = [];
742
+ if (project) {
743
+ sql += ' AND project = ?';
744
+ params.push(project);
745
+ }
746
+ sql += ' ORDER BY salience DESC, last_accessed DESC LIMIT ?';
747
+ params.push(limit);
748
+ const rows = db.prepare(sql).all(...params);
749
+ return rows.map(rowToMemory);
750
+ }
751
+ /**
752
+ * Promote a memory from short-term to long-term
753
+ */
754
+ export function promoteMemory(id) {
755
+ const db = getDatabase();
756
+ db.prepare(`
757
+ UPDATE memories
758
+ SET type = 'long_term'
759
+ WHERE id = ? AND type = 'short_term'
760
+ `).run(id);
761
+ return getMemoryById(id);
762
+ }
763
+ /**
764
+ * Bulk delete decayed memories
765
+ */
766
+ export function cleanupDecayedMemories(config = DEFAULT_CONFIG) {
767
+ const db = getDatabase();
768
+ // Get all short-term memories and check decay
769
+ const shortTerm = getMemoriesByType('short_term', 1000);
770
+ const toDelete = [];
771
+ for (const memory of shortTerm) {
772
+ const decayedScore = calculateDecayedScore(memory, config);
773
+ if (decayedScore < config.salienceThreshold) {
774
+ toDelete.push(memory.id);
775
+ }
776
+ }
777
+ if (toDelete.length > 0) {
778
+ const placeholders = toDelete.map(() => '?').join(',');
779
+ db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...toDelete);
780
+ }
781
+ return toDelete.length;
782
+ }
783
+ /**
784
+ * Get memory statistics
785
+ */
786
+ export function getMemoryStats(project) {
787
+ const db = getDatabase();
788
+ let whereClause = '';
789
+ const params = [];
790
+ if (project) {
791
+ whereClause = 'WHERE project = ?';
792
+ params.push(project);
793
+ }
794
+ const total = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause}`).get(...params).count;
795
+ const shortTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'short_term'`).get(...params).count;
796
+ const longTerm = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'long_term'`).get(...params).count;
797
+ const episodic = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause} ${whereClause ? 'AND' : 'WHERE'} type = 'episodic'`).get(...params).count;
798
+ const avgResult = db.prepare(`SELECT AVG(salience) as avg FROM memories ${whereClause}`).get(...params);
799
+ const averageSalience = avgResult.avg || 0;
800
+ // Get counts by category
801
+ const categoryRows = db.prepare(`
802
+ SELECT category, COUNT(*) as count
803
+ FROM memories ${whereClause}
804
+ GROUP BY category
805
+ `).all(...params);
806
+ const byCategory = {};
807
+ for (const row of categoryRows) {
808
+ byCategory[row.category] = row.count;
809
+ }
810
+ return {
811
+ total,
812
+ shortTerm,
813
+ longTerm,
814
+ episodic,
815
+ byCategory,
816
+ averageSalience,
817
+ };
818
+ }
819
+ /**
820
+ * Create a link between two memories
821
+ */
822
+ export function createMemoryLink(sourceId, targetId, relationship, strength = 0.5) {
823
+ const db = getDatabase();
824
+ // Verify both memories exist
825
+ const source = getMemoryById(sourceId);
826
+ const target = getMemoryById(targetId);
827
+ if (!source || !target)
828
+ return null;
829
+ // Prevent self-links
830
+ if (sourceId === targetId)
831
+ return null;
832
+ try {
833
+ const result = db.prepare(`
834
+ INSERT INTO memory_links (source_id, target_id, relationship, strength)
835
+ VALUES (?, ?, ?, ?)
836
+ `).run(sourceId, targetId, relationship, strength);
837
+ return {
838
+ id: result.lastInsertRowid,
839
+ sourceId,
840
+ targetId,
841
+ relationship,
842
+ strength,
843
+ createdAt: new Date(),
844
+ };
845
+ }
846
+ catch {
847
+ // Link already exists (UNIQUE constraint)
848
+ return null;
849
+ }
850
+ }
851
+ /**
852
+ * Get all memories related to a given memory
853
+ */
854
+ export function getRelatedMemories(memoryId) {
855
+ const db = getDatabase();
856
+ // Get outgoing links (this memory references others)
857
+ const outgoing = db.prepare(`
858
+ SELECT m.*, ml.relationship, ml.strength
859
+ FROM memory_links ml
860
+ JOIN memories m ON m.id = ml.target_id
861
+ WHERE ml.source_id = ?
862
+ `).all(memoryId);
863
+ // Get incoming links (other memories reference this one)
864
+ const incoming = db.prepare(`
865
+ SELECT m.*, ml.relationship, ml.strength
866
+ FROM memory_links ml
867
+ JOIN memories m ON m.id = ml.source_id
868
+ WHERE ml.target_id = ?
869
+ `).all(memoryId);
870
+ const results = [];
871
+ for (const row of outgoing) {
872
+ results.push({
873
+ memory: rowToMemory(row),
874
+ relationship: row.relationship,
875
+ strength: row.strength,
876
+ direction: 'outgoing',
877
+ });
878
+ }
879
+ for (const row of incoming) {
880
+ results.push({
881
+ memory: rowToMemory(row),
882
+ relationship: row.relationship,
883
+ strength: row.strength,
884
+ direction: 'incoming',
885
+ });
886
+ }
887
+ return results;
888
+ }
889
+ /**
890
+ * Delete a memory link
891
+ */
892
+ export function deleteMemoryLink(sourceId, targetId) {
893
+ const db = getDatabase();
894
+ const result = db.prepare(`
895
+ DELETE FROM memory_links WHERE source_id = ? AND target_id = ?
896
+ `).run(sourceId, targetId);
897
+ return result.changes > 0;
898
+ }
899
+ /**
900
+ * Get all memory links
901
+ */
902
+ export function getAllMemoryLinks() {
903
+ const db = getDatabase();
904
+ const rows = db.prepare(`SELECT * FROM memory_links ORDER BY created_at DESC`).all();
905
+ return rows.map(row => ({
906
+ id: row.id,
907
+ sourceId: row.source_id,
908
+ targetId: row.target_id,
909
+ relationship: row.relationship,
910
+ strength: row.strength,
911
+ createdAt: new Date(row.created_at),
912
+ }));
913
+ }
914
+ /**
915
+ * Detect potential relationships for a new memory
916
+ * Returns memories that might be related based on:
917
+ * - Shared tags
918
+ * - Similar project
919
+ * - Content similarity (keywords)
920
+ */
921
+ export function detectRelationships(memory, maxResults = 5) {
922
+ const db = getDatabase();
923
+ const results = [];
924
+ // Find memories with shared tags
925
+ if (memory.tags.length > 0) {
926
+ const tagPlaceholders = memory.tags.map(() => '?').join(',');
927
+ const tagMatches = db.prepare(`
928
+ SELECT DISTINCT m.id, m.tags
929
+ FROM memories m, json_each(m.tags)
930
+ WHERE json_each.value IN (${tagPlaceholders})
931
+ AND m.id != ?
932
+ LIMIT ?
933
+ `).all(...memory.tags, memory.id, maxResults);
934
+ for (const match of tagMatches) {
935
+ const matchTags = JSON.parse(match.tags);
936
+ const sharedCount = memory.tags.filter(t => matchTags.includes(t)).length;
937
+ const strength = Math.min(0.9, 0.3 + (sharedCount * 0.2));
938
+ results.push({ targetId: match.id, relationship: 'related', strength });
939
+ }
940
+ }
941
+ // Find memories in the same project
942
+ if (memory.project) {
943
+ const projectMatches = db.prepare(`
944
+ SELECT id FROM memories
945
+ WHERE project = ? AND id != ?
946
+ ORDER BY last_accessed DESC
947
+ LIMIT ?
948
+ `).all(memory.project, memory.id, maxResults);
949
+ for (const match of projectMatches) {
950
+ // Only add if not already in results
951
+ if (!results.find(r => r.targetId === match.id)) {
952
+ results.push({ targetId: match.id, relationship: 'related', strength: 0.4 });
953
+ }
954
+ }
955
+ }
956
+ // Find memories with similar category
957
+ const categoryMatches = db.prepare(`
958
+ SELECT id FROM memories
959
+ WHERE category = ? AND id != ?
960
+ ORDER BY salience DESC, last_accessed DESC
961
+ LIMIT ?
962
+ `).all(memory.category, memory.id, 3);
963
+ for (const match of categoryMatches) {
964
+ if (!results.find(r => r.targetId === match.id)) {
965
+ results.push({ targetId: match.id, relationship: 'related', strength: 0.3 });
966
+ }
967
+ }
968
+ // Sort by strength and limit
969
+ return results
970
+ .sort((a, b) => b.strength - a.strength)
971
+ .slice(0, maxResults);
972
+ }
973
+ //# sourceMappingURL=store.js.map