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.
- package/README.md +291 -0
- package/dist/api/events.d.ts +134 -0
- package/dist/api/events.d.ts.map +1 -0
- package/dist/api/events.js +73 -0
- package/dist/api/events.js.map +1 -0
- package/dist/api/visualization-server.d.ts +11 -0
- package/dist/api/visualization-server.d.ts.map +1 -0
- package/dist/api/visualization-server.js +653 -0
- package/dist/api/visualization-server.js.map +1 -0
- package/dist/context/project-context.d.ts +57 -0
- package/dist/context/project-context.d.ts.map +1 -0
- package/dist/context/project-context.js +135 -0
- package/dist/context/project-context.js.map +1 -0
- package/dist/database/init.d.ts +49 -0
- package/dist/database/init.d.ts.map +1 -0
- package/dist/database/init.js +336 -0
- package/dist/database/init.js.map +1 -0
- package/dist/embeddings/generator.d.ts +20 -0
- package/dist/embeddings/generator.d.ts.map +1 -0
- package/dist/embeddings/generator.js +77 -0
- package/dist/embeddings/generator.js.map +1 -0
- package/dist/embeddings/index.d.ts +2 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +2 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/activation.d.ts +69 -0
- package/dist/memory/activation.d.ts.map +1 -0
- package/dist/memory/activation.js +168 -0
- package/dist/memory/activation.js.map +1 -0
- package/dist/memory/consolidate.d.ts +96 -0
- package/dist/memory/consolidate.d.ts.map +1 -0
- package/dist/memory/consolidate.js +400 -0
- package/dist/memory/consolidate.js.map +1 -0
- package/dist/memory/contradiction.d.ts +69 -0
- package/dist/memory/contradiction.d.ts.map +1 -0
- package/dist/memory/contradiction.js +286 -0
- package/dist/memory/contradiction.js.map +1 -0
- package/dist/memory/decay.d.ts +62 -0
- package/dist/memory/decay.d.ts.map +1 -0
- package/dist/memory/decay.js +184 -0
- package/dist/memory/decay.js.map +1 -0
- package/dist/memory/salience.d.ts +36 -0
- package/dist/memory/salience.d.ts.map +1 -0
- package/dist/memory/salience.js +200 -0
- package/dist/memory/salience.js.map +1 -0
- package/dist/memory/similarity.d.ts +57 -0
- package/dist/memory/similarity.d.ts.map +1 -0
- package/dist/memory/similarity.js +114 -0
- package/dist/memory/similarity.js.map +1 -0
- package/dist/memory/store.d.ts +170 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +973 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/types.d.ts +91 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +30 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +466 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/context.d.ts +135 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +273 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/forget.d.ts +53 -0
- package/dist/tools/forget.d.ts.map +1 -0
- package/dist/tools/forget.js +179 -0
- package/dist/tools/forget.js.map +1 -0
- package/dist/tools/recall.d.ts +74 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +140 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/remember.d.ts +65 -0
- package/dist/tools/remember.d.ts.map +1 -0
- package/dist/tools/remember.js +147 -0
- package/dist/tools/remember.js.map +1 -0
- package/dist/worker/brain-worker.d.ts +100 -0
- package/dist/worker/brain-worker.d.ts.map +1 -0
- package/dist/worker/brain-worker.js +261 -0
- package/dist/worker/brain-worker.js.map +1 -0
- package/dist/worker/link-discovery.d.ts +47 -0
- package/dist/worker/link-discovery.d.ts.map +1 -0
- package/dist/worker/link-discovery.js +103 -0
- package/dist/worker/link-discovery.js.map +1 -0
- package/dist/worker/predictive-consolidation.d.ts +46 -0
- package/dist/worker/predictive-consolidation.d.ts.map +1 -0
- package/dist/worker/predictive-consolidation.js +110 -0
- package/dist/worker/predictive-consolidation.js.map +1 -0
- package/dist/worker/types.d.ts +91 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +22 -0
- package/dist/worker/types.js.map +1 -0
- 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
|