lynkr 2.0.0 → 3.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 +226 -15
- package/docs/index.md +230 -11
- package/install.sh +260 -0
- package/package.json +4 -3
- package/src/clients/databricks.js +158 -0
- package/src/clients/routing.js +13 -1
- package/src/config/index.js +68 -1
- package/src/db/index.js +118 -0
- package/src/memory/extractor.js +350 -0
- package/src/memory/index.js +55 -0
- package/src/memory/retriever.js +266 -0
- package/src/memory/search.js +239 -0
- package/src/memory/store.js +411 -0
- package/src/memory/surprise.js +306 -0
- package/src/memory/tools.js +348 -0
- package/src/orchestrator/index.js +170 -0
- package/test/llamacpp-integration.test.js +686 -0
- package/test/memory/extractor.test.js +360 -0
- package/test/memory/retriever.test.js +583 -0
- package/test/memory/search.test.js +389 -0
- package/test/memory/store.test.js +312 -0
- package/test/memory/surprise.test.js +300 -0
- package/test/memory-performance.test.js +472 -0
- package/test/openai-integration.test.js +681 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const db = require("../db");
|
|
2
|
+
const logger = require("../logger");
|
|
3
|
+
const store = require("./store");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Search memories using FTS5 full-text search
|
|
7
|
+
*/
|
|
8
|
+
function searchMemories(options) {
|
|
9
|
+
const {
|
|
10
|
+
query,
|
|
11
|
+
limit = 10,
|
|
12
|
+
types = null, // Filter by memory types
|
|
13
|
+
categories = null, // Filter by categories
|
|
14
|
+
sessionId = null, // Filter by session
|
|
15
|
+
minImportance = null,
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
if (!query || typeof query !== "string") {
|
|
19
|
+
logger.warn("Search query must be a non-empty string");
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Build FTS5 query - escape special characters
|
|
24
|
+
const ftsQuery = prepareFTS5Query(query);
|
|
25
|
+
|
|
26
|
+
// Build SQL with filters
|
|
27
|
+
let sql = `
|
|
28
|
+
SELECT m.id, m.session_id, m.content, m.type, m.category,
|
|
29
|
+
m.importance, m.surprise_score, m.access_count, m.decay_factor,
|
|
30
|
+
m.source_turn_id, m.created_at, m.updated_at, m.last_accessed_at, m.metadata,
|
|
31
|
+
fts.rank
|
|
32
|
+
FROM memories_fts fts
|
|
33
|
+
JOIN memories m ON m.id = fts.rowid
|
|
34
|
+
WHERE memories_fts MATCH ?
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const params = [ftsQuery];
|
|
38
|
+
|
|
39
|
+
// Add filters
|
|
40
|
+
if (sessionId) {
|
|
41
|
+
sql += ` AND (m.session_id = ? OR m.session_id IS NULL)`;
|
|
42
|
+
params.push(sessionId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (types && Array.isArray(types) && types.length > 0) {
|
|
46
|
+
const placeholders = types.map(() => "?").join(",");
|
|
47
|
+
sql += ` AND m.type IN (${placeholders})`;
|
|
48
|
+
params.push(...types);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (categories && Array.isArray(categories) && categories.length > 0) {
|
|
52
|
+
const placeholders = categories.map(() => "?").join(",");
|
|
53
|
+
sql += ` AND m.category IN (${placeholders})`;
|
|
54
|
+
params.push(...categories);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (minImportance !== null && typeof minImportance === "number") {
|
|
58
|
+
sql += ` AND m.importance >= ?`;
|
|
59
|
+
params.push(minImportance);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Order by FTS5 rank and importance
|
|
63
|
+
sql += ` ORDER BY fts.rank, m.importance DESC LIMIT ?`;
|
|
64
|
+
params.push(limit);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const stmt = db.prepare(sql);
|
|
68
|
+
const rows = stmt.all(...params);
|
|
69
|
+
|
|
70
|
+
return rows.map((row) => ({
|
|
71
|
+
id: row.id,
|
|
72
|
+
sessionId: row.session_id ?? null,
|
|
73
|
+
content: row.content,
|
|
74
|
+
type: row.type,
|
|
75
|
+
category: row.category ?? null,
|
|
76
|
+
importance: row.importance ?? 0.5,
|
|
77
|
+
surpriseScore: row.surprise_score ?? 0.0,
|
|
78
|
+
accessCount: row.access_count ?? 0,
|
|
79
|
+
decayFactor: row.decay_factor ?? 1.0,
|
|
80
|
+
sourceTurnId: row.source_turn_id ?? null,
|
|
81
|
+
createdAt: row.created_at,
|
|
82
|
+
updatedAt: row.updated_at,
|
|
83
|
+
lastAccessedAt: row.last_accessed_at ?? null,
|
|
84
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
85
|
+
rank: row.rank, // FTS5 relevance score
|
|
86
|
+
}));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error({ err, query: ftsQuery }, "FTS5 search failed");
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Prepare FTS5 query - handle special characters and phrases
|
|
95
|
+
*/
|
|
96
|
+
function prepareFTS5Query(query) {
|
|
97
|
+
// Remove or escape FTS5 special characters: " * ( ) AND OR NOT
|
|
98
|
+
// For simple queries, just escape quotes and use phrase matching
|
|
99
|
+
let cleaned = query.trim();
|
|
100
|
+
|
|
101
|
+
// If query looks like a phrase, wrap in quotes for exact matching
|
|
102
|
+
if (!cleaned.includes('"') && cleaned.split(/\s+/).length > 1) {
|
|
103
|
+
// Multi-word query - use phrase search
|
|
104
|
+
cleaned = `"${cleaned.replace(/"/g, '""')}"`;
|
|
105
|
+
} else {
|
|
106
|
+
// Single word or already has quotes - just escape
|
|
107
|
+
cleaned = cleaned.replace(/"/g, '""');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return cleaned;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Search with keyword expansion (extract key terms)
|
|
115
|
+
*/
|
|
116
|
+
function searchWithExpansion(options) {
|
|
117
|
+
const { query, limit = 10 } = options;
|
|
118
|
+
|
|
119
|
+
// Extract keywords from query
|
|
120
|
+
const keywords = extractKeywords(query);
|
|
121
|
+
|
|
122
|
+
// Search with original query
|
|
123
|
+
const results = searchMemories({ ...options, limit: limit * 2 });
|
|
124
|
+
|
|
125
|
+
// If not enough results, try individual keywords
|
|
126
|
+
if (results.length < limit && keywords.length > 1) {
|
|
127
|
+
const seen = new Set(results.map((r) => r.id));
|
|
128
|
+
|
|
129
|
+
for (const keyword of keywords) {
|
|
130
|
+
if (results.length >= limit) break;
|
|
131
|
+
|
|
132
|
+
const kwResults = searchMemories({
|
|
133
|
+
...options,
|
|
134
|
+
query: keyword,
|
|
135
|
+
limit: limit - results.length,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const result of kwResults) {
|
|
139
|
+
if (!seen.has(result.id)) {
|
|
140
|
+
results.push(result);
|
|
141
|
+
seen.add(result.id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return results.slice(0, limit);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract keywords from text (simple tokenization)
|
|
152
|
+
*/
|
|
153
|
+
function extractKeywords(text) {
|
|
154
|
+
if (!text) return [];
|
|
155
|
+
|
|
156
|
+
// Simple keyword extraction:
|
|
157
|
+
// - Split on whitespace
|
|
158
|
+
// - Remove stopwords
|
|
159
|
+
// - Keep words > 3 characters
|
|
160
|
+
// - Lowercase
|
|
161
|
+
|
|
162
|
+
const stopwords = new Set([
|
|
163
|
+
"the",
|
|
164
|
+
"is",
|
|
165
|
+
"at",
|
|
166
|
+
"which",
|
|
167
|
+
"on",
|
|
168
|
+
"and",
|
|
169
|
+
"or",
|
|
170
|
+
"not",
|
|
171
|
+
"this",
|
|
172
|
+
"that",
|
|
173
|
+
"with",
|
|
174
|
+
"from",
|
|
175
|
+
"for",
|
|
176
|
+
"to",
|
|
177
|
+
"in",
|
|
178
|
+
"of",
|
|
179
|
+
"a",
|
|
180
|
+
"an",
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
return text
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.split(/\s+/)
|
|
186
|
+
.map((word) => word.replace(/[^\w]/g, ""))
|
|
187
|
+
.filter((word) => word.length > 3 && !stopwords.has(word));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Find similar memories by keyword overlap
|
|
192
|
+
*/
|
|
193
|
+
function findSimilar(memoryId, limit = 5) {
|
|
194
|
+
const memory = store.getMemory(memoryId);
|
|
195
|
+
if (!memory) return [];
|
|
196
|
+
|
|
197
|
+
const keywords = extractKeywords(memory.content);
|
|
198
|
+
if (keywords.length === 0) return [];
|
|
199
|
+
|
|
200
|
+
// Build OR query for keywords
|
|
201
|
+
const query = keywords.join(" OR ");
|
|
202
|
+
|
|
203
|
+
const results = searchMemories({
|
|
204
|
+
query,
|
|
205
|
+
limit: limit + 1, // +1 to exclude self
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Filter out the original memory
|
|
209
|
+
return results.filter((r) => r.id !== memoryId).slice(0, limit);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Search by content similarity (simple keyword-based)
|
|
214
|
+
*/
|
|
215
|
+
function searchByContent(content, options = {}) {
|
|
216
|
+
const keywords = extractKeywords(content);
|
|
217
|
+
if (keywords.length === 0) return [];
|
|
218
|
+
|
|
219
|
+
const query = keywords.slice(0, 5).join(" OR "); // Top 5 keywords
|
|
220
|
+
return searchMemories({ ...options, query });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Count search results without fetching them
|
|
225
|
+
*/
|
|
226
|
+
function countSearchResults(query, options = {}) {
|
|
227
|
+
const results = searchMemories({ ...options, query, limit: 1000 });
|
|
228
|
+
return results.length;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
searchMemories,
|
|
233
|
+
searchWithExpansion,
|
|
234
|
+
extractKeywords,
|
|
235
|
+
findSimilar,
|
|
236
|
+
searchByContent,
|
|
237
|
+
countSearchResults,
|
|
238
|
+
prepareFTS5Query,
|
|
239
|
+
};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
const db = require("../db");
|
|
2
|
+
const logger = require("../logger");
|
|
3
|
+
|
|
4
|
+
// Prepared statements for memory operations
|
|
5
|
+
const insertMemoryStmt = db.prepare(`
|
|
6
|
+
INSERT INTO memories (
|
|
7
|
+
session_id, content, type, category, importance, surprise_score,
|
|
8
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
9
|
+
last_accessed_at, metadata
|
|
10
|
+
) VALUES (
|
|
11
|
+
@session_id, @content, @type, @category, @importance, @surprise_score,
|
|
12
|
+
@access_count, @decay_factor, @source_turn_id, @created_at, @updated_at,
|
|
13
|
+
@last_accessed_at, @metadata
|
|
14
|
+
)
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
const getMemoryStmt = db.prepare(`
|
|
18
|
+
SELECT
|
|
19
|
+
id, session_id, content, type, category, importance, surprise_score,
|
|
20
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
21
|
+
last_accessed_at, metadata
|
|
22
|
+
FROM memories
|
|
23
|
+
WHERE id = ?
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
const updateMemoryStmt = db.prepare(`
|
|
27
|
+
UPDATE memories
|
|
28
|
+
SET content = @content,
|
|
29
|
+
type = @type,
|
|
30
|
+
category = @category,
|
|
31
|
+
importance = @importance,
|
|
32
|
+
surprise_score = @surprise_score,
|
|
33
|
+
decay_factor = @decay_factor,
|
|
34
|
+
updated_at = @updated_at,
|
|
35
|
+
metadata = @metadata
|
|
36
|
+
WHERE id = @id
|
|
37
|
+
`);
|
|
38
|
+
|
|
39
|
+
const deleteMemoryStmt = db.prepare("DELETE FROM memories WHERE id = ?");
|
|
40
|
+
|
|
41
|
+
const incrementAccessStmt = db.prepare(`
|
|
42
|
+
UPDATE memories
|
|
43
|
+
SET access_count = access_count + 1,
|
|
44
|
+
last_accessed_at = ?
|
|
45
|
+
WHERE id = ?
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
const updateImportanceStmt = db.prepare(`
|
|
49
|
+
UPDATE memories
|
|
50
|
+
SET importance = ?,
|
|
51
|
+
updated_at = ?
|
|
52
|
+
WHERE id = ?
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
const getRecentMemoriesStmt = db.prepare(`
|
|
56
|
+
SELECT
|
|
57
|
+
id, session_id, content, type, category, importance, surprise_score,
|
|
58
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
59
|
+
last_accessed_at, metadata
|
|
60
|
+
FROM memories
|
|
61
|
+
WHERE (session_id = ? OR ? IS NULL)
|
|
62
|
+
ORDER BY created_at DESC
|
|
63
|
+
LIMIT ?
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
const getMemoriesByImportanceStmt = db.prepare(`
|
|
67
|
+
SELECT
|
|
68
|
+
id, session_id, content, type, category, importance, surprise_score,
|
|
69
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
70
|
+
last_accessed_at, metadata
|
|
71
|
+
FROM memories
|
|
72
|
+
WHERE (session_id = ? OR ? IS NULL)
|
|
73
|
+
ORDER BY importance DESC, created_at DESC
|
|
74
|
+
LIMIT ?
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
const getMemoriesBySurpriseStmt = db.prepare(`
|
|
78
|
+
SELECT
|
|
79
|
+
id, session_id, content, type, category, importance, surprise_score,
|
|
80
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
81
|
+
last_accessed_at, metadata
|
|
82
|
+
FROM memories
|
|
83
|
+
WHERE surprise_score >= ?
|
|
84
|
+
ORDER BY surprise_score DESC, created_at DESC
|
|
85
|
+
LIMIT ?
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
const pruneOldMemoriesStmt = db.prepare(`
|
|
89
|
+
DELETE FROM memories
|
|
90
|
+
WHERE created_at < ?
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
const pruneByCountStmt = db.prepare(`
|
|
94
|
+
DELETE FROM memories
|
|
95
|
+
WHERE id NOT IN (
|
|
96
|
+
SELECT id FROM memories
|
|
97
|
+
ORDER BY importance DESC, created_at DESC
|
|
98
|
+
LIMIT ?
|
|
99
|
+
)
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
const countMemoriesStmt = db.prepare("SELECT COUNT(*) as count FROM memories");
|
|
103
|
+
|
|
104
|
+
const getMemoriesByTypeStmt = db.prepare(`
|
|
105
|
+
SELECT
|
|
106
|
+
id, session_id, content, type, category, importance, surprise_score,
|
|
107
|
+
access_count, decay_factor, source_turn_id, created_at, updated_at,
|
|
108
|
+
last_accessed_at, metadata
|
|
109
|
+
FROM memories
|
|
110
|
+
WHERE type = ?
|
|
111
|
+
ORDER BY importance DESC, created_at DESC
|
|
112
|
+
LIMIT ?
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
// Entity tracking
|
|
116
|
+
const upsertEntityStmt = db.prepare(`
|
|
117
|
+
INSERT INTO memory_entities (entity_type, entity_name, first_seen_at, last_seen_at, occurrence_count, properties)
|
|
118
|
+
VALUES (@entity_type, @entity_name, @timestamp, @timestamp, 1, @properties)
|
|
119
|
+
ON CONFLICT(entity_type, entity_name) DO UPDATE SET
|
|
120
|
+
last_seen_at = @timestamp,
|
|
121
|
+
occurrence_count = occurrence_count + 1,
|
|
122
|
+
properties = @properties
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
const getEntityStmt = db.prepare(`
|
|
126
|
+
SELECT id, entity_type, entity_name, first_seen_at, last_seen_at, occurrence_count, properties
|
|
127
|
+
FROM memory_entities
|
|
128
|
+
WHERE entity_type = ? AND entity_name = ?
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
const getAllEntitiesStmt = db.prepare(`
|
|
132
|
+
SELECT id, entity_type, entity_name, first_seen_at, last_seen_at, occurrence_count, properties
|
|
133
|
+
FROM memory_entities
|
|
134
|
+
ORDER BY occurrence_count DESC
|
|
135
|
+
LIMIT ?
|
|
136
|
+
`);
|
|
137
|
+
|
|
138
|
+
// Helper functions
|
|
139
|
+
function parseJSON(value, fallback = null) {
|
|
140
|
+
if (value === null || value === undefined) return fallback;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(value);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
logger.warn({ err }, "Failed to parse JSON from memory store");
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function serialize(value) {
|
|
150
|
+
if (value === undefined || value === null) return null;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.stringify(value);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
logger.warn({ err }, "Failed to serialize JSON for memory store");
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toMemory(row) {
|
|
160
|
+
if (!row) return null;
|
|
161
|
+
return {
|
|
162
|
+
id: row.id,
|
|
163
|
+
sessionId: row.session_id ?? null,
|
|
164
|
+
content: row.content,
|
|
165
|
+
type: row.type,
|
|
166
|
+
category: row.category ?? null,
|
|
167
|
+
importance: row.importance ?? 0.5,
|
|
168
|
+
surpriseScore: row.surprise_score ?? 0.0,
|
|
169
|
+
accessCount: row.access_count ?? 0,
|
|
170
|
+
decayFactor: row.decay_factor ?? 1.0,
|
|
171
|
+
sourceTurnId: row.source_turn_id ?? null,
|
|
172
|
+
createdAt: row.created_at,
|
|
173
|
+
updatedAt: row.updated_at,
|
|
174
|
+
lastAccessedAt: row.last_accessed_at ?? null,
|
|
175
|
+
metadata: parseJSON(row.metadata, {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function toEntity(row) {
|
|
180
|
+
if (!row) return null;
|
|
181
|
+
return {
|
|
182
|
+
id: row.id,
|
|
183
|
+
entityType: row.entity_type,
|
|
184
|
+
entityName: row.entity_name,
|
|
185
|
+
firstSeenAt: row.first_seen_at,
|
|
186
|
+
lastSeenAt: row.last_seen_at,
|
|
187
|
+
occurrenceCount: row.occurrence_count ?? 1,
|
|
188
|
+
properties: parseJSON(row.properties, {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a new memory
|
|
194
|
+
*/
|
|
195
|
+
function createMemory(options) {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
const {
|
|
198
|
+
sessionId = null,
|
|
199
|
+
content,
|
|
200
|
+
type,
|
|
201
|
+
category = null,
|
|
202
|
+
importance = 0.5,
|
|
203
|
+
surpriseScore = 0.0,
|
|
204
|
+
accessCount = 0,
|
|
205
|
+
decayFactor = 1.0,
|
|
206
|
+
sourceTurnId = null,
|
|
207
|
+
metadata = {},
|
|
208
|
+
} = options;
|
|
209
|
+
|
|
210
|
+
if (!content || !type) {
|
|
211
|
+
throw new Error("Memory content and type are required");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = insertMemoryStmt.run({
|
|
215
|
+
session_id: sessionId,
|
|
216
|
+
content,
|
|
217
|
+
type,
|
|
218
|
+
category,
|
|
219
|
+
importance,
|
|
220
|
+
surprise_score: surpriseScore,
|
|
221
|
+
access_count: accessCount,
|
|
222
|
+
decay_factor: decayFactor,
|
|
223
|
+
source_turn_id: sourceTurnId,
|
|
224
|
+
created_at: now,
|
|
225
|
+
updated_at: now,
|
|
226
|
+
last_accessed_at: null,
|
|
227
|
+
metadata: serialize(metadata),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
id: result.lastInsertRowid,
|
|
232
|
+
sessionId,
|
|
233
|
+
content,
|
|
234
|
+
type,
|
|
235
|
+
category,
|
|
236
|
+
importance,
|
|
237
|
+
surpriseScore,
|
|
238
|
+
accessCount,
|
|
239
|
+
decayFactor,
|
|
240
|
+
sourceTurnId,
|
|
241
|
+
createdAt: now,
|
|
242
|
+
updatedAt: now,
|
|
243
|
+
lastAccessedAt: null,
|
|
244
|
+
metadata,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get a memory by ID
|
|
250
|
+
*/
|
|
251
|
+
function getMemory(id) {
|
|
252
|
+
const row = getMemoryStmt.get(id);
|
|
253
|
+
return toMemory(row);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Update a memory
|
|
258
|
+
*/
|
|
259
|
+
function updateMemory(id, updates) {
|
|
260
|
+
const memory = getMemory(id);
|
|
261
|
+
if (!memory) {
|
|
262
|
+
throw new Error(`Memory with ID ${id} not found`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
updateMemoryStmt.run({
|
|
267
|
+
id,
|
|
268
|
+
content: updates.content ?? memory.content,
|
|
269
|
+
type: updates.type ?? memory.type,
|
|
270
|
+
category: updates.category ?? memory.category,
|
|
271
|
+
importance: updates.importance ?? memory.importance,
|
|
272
|
+
surprise_score: updates.surpriseScore ?? memory.surpriseScore,
|
|
273
|
+
decay_factor: updates.decayFactor ?? memory.decayFactor,
|
|
274
|
+
updated_at: now,
|
|
275
|
+
metadata: serialize(updates.metadata ?? memory.metadata),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return getMemory(id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Delete a memory
|
|
283
|
+
*/
|
|
284
|
+
function deleteMemory(id) {
|
|
285
|
+
const result = deleteMemoryStmt.run(id);
|
|
286
|
+
return result.changes > 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Increment access count for a memory
|
|
291
|
+
*/
|
|
292
|
+
function incrementAccessCount(id) {
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
incrementAccessStmt.run(now, id);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Update importance score
|
|
299
|
+
*/
|
|
300
|
+
function updateImportance(id, importance) {
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
updateImportanceStmt.run(importance, now, id);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get recent memories
|
|
307
|
+
*/
|
|
308
|
+
function getRecentMemories(options = {}) {
|
|
309
|
+
const { limit = 10, sessionId = null } = options;
|
|
310
|
+
const rows = getRecentMemoriesStmt.all(sessionId, sessionId, limit);
|
|
311
|
+
return rows.map(toMemory);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get memories by importance
|
|
316
|
+
*/
|
|
317
|
+
function getMemoriesByImportance(options = {}) {
|
|
318
|
+
const { limit = 10, sessionId = null } = options;
|
|
319
|
+
const rows = getMemoriesByImportanceStmt.all(sessionId, sessionId, limit);
|
|
320
|
+
return rows.map(toMemory);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get memories by surprise score
|
|
325
|
+
*/
|
|
326
|
+
function getMemoriesBySurprise(options = {}) {
|
|
327
|
+
const { minScore = 0.3, limit = 10 } = options;
|
|
328
|
+
const rows = getMemoriesBySurpriseStmt.all(minScore, limit);
|
|
329
|
+
return rows.map(toMemory);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get memories by type
|
|
334
|
+
*/
|
|
335
|
+
function getMemoriesByType(type, limit = 10) {
|
|
336
|
+
const rows = getMemoriesByTypeStmt.all(type, limit);
|
|
337
|
+
return rows.map(toMemory);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Prune old memories
|
|
342
|
+
*/
|
|
343
|
+
function pruneOldMemories(olderThanMs) {
|
|
344
|
+
const threshold = Date.now() - olderThanMs;
|
|
345
|
+
const result = pruneOldMemoriesStmt.run(threshold);
|
|
346
|
+
return result.changes;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Prune to keep only top N memories by importance
|
|
351
|
+
*/
|
|
352
|
+
function pruneByCount(maxCount) {
|
|
353
|
+
const result = pruneByCountStmt.run(maxCount);
|
|
354
|
+
return result.changes;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Count total memories
|
|
359
|
+
*/
|
|
360
|
+
function countMemories() {
|
|
361
|
+
const result = countMemoriesStmt.get();
|
|
362
|
+
return result.count;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Track or update an entity
|
|
367
|
+
*/
|
|
368
|
+
function trackEntity(entityType, entityName, properties = {}) {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
upsertEntityStmt.run({
|
|
371
|
+
entity_type: entityType,
|
|
372
|
+
entity_name: entityName,
|
|
373
|
+
timestamp: now,
|
|
374
|
+
properties: serialize(properties),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get an entity
|
|
380
|
+
*/
|
|
381
|
+
function getEntity(entityType, entityName) {
|
|
382
|
+
const row = getEntityStmt.get(entityType, entityName);
|
|
383
|
+
return toEntity(row);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get all entities
|
|
388
|
+
*/
|
|
389
|
+
function getAllEntities(limit = 100) {
|
|
390
|
+
const rows = getAllEntitiesStmt.all(limit);
|
|
391
|
+
return rows.map(toEntity);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
createMemory,
|
|
396
|
+
getMemory,
|
|
397
|
+
updateMemory,
|
|
398
|
+
deleteMemory,
|
|
399
|
+
incrementAccessCount,
|
|
400
|
+
updateImportance,
|
|
401
|
+
getRecentMemories,
|
|
402
|
+
getMemoriesByImportance,
|
|
403
|
+
getMemoriesBySurprise,
|
|
404
|
+
getMemoriesByType,
|
|
405
|
+
pruneOldMemories,
|
|
406
|
+
pruneByCount,
|
|
407
|
+
countMemories,
|
|
408
|
+
trackEntity,
|
|
409
|
+
getEntity,
|
|
410
|
+
getAllEntities,
|
|
411
|
+
};
|