sensorium-mcp 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +596 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +180 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +680 -0
- package/dist/memory.js.map +1 -0
- package/package.json +3 -1
package/dist/memory.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { mkdirSync, statSync } from "fs";
|
|
6
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
7
|
+
function generateId(prefix) {
|
|
8
|
+
return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
9
|
+
}
|
|
10
|
+
function nowISO() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
function jsonOrNull(val) {
|
|
14
|
+
if (val === undefined || val === null)
|
|
15
|
+
return null;
|
|
16
|
+
return JSON.stringify(val);
|
|
17
|
+
}
|
|
18
|
+
function parseJsonArray(val) {
|
|
19
|
+
if (!val)
|
|
20
|
+
return [];
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(val);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseJsonObject(val) {
|
|
29
|
+
if (!val)
|
|
30
|
+
return {};
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(val);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ─── Database Initialization ─────────────────────────────────────────────────
|
|
39
|
+
const SCHEMA_VERSION = 1;
|
|
40
|
+
const SCHEMA_SQL = `
|
|
41
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
42
|
+
episode_id TEXT PRIMARY KEY,
|
|
43
|
+
session_id TEXT NOT NULL,
|
|
44
|
+
thread_id INTEGER NOT NULL,
|
|
45
|
+
timestamp TEXT NOT NULL,
|
|
46
|
+
type TEXT NOT NULL CHECK(type IN ('operator_message','agent_action','system_event')),
|
|
47
|
+
modality TEXT NOT NULL CHECK(modality IN ('text','voice','photo','video_note','document','mixed')),
|
|
48
|
+
content TEXT NOT NULL,
|
|
49
|
+
topic_tags TEXT,
|
|
50
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
51
|
+
consolidated INTEGER DEFAULT 0,
|
|
52
|
+
accessed_count INTEGER DEFAULT 0,
|
|
53
|
+
last_accessed TEXT,
|
|
54
|
+
created_at TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_ep_thread_time ON episodes(thread_id, timestamp DESC);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_ep_importance ON episodes(importance DESC);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_ep_uncons ON episodes(consolidated) WHERE consolidated = 0;
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS semantic_notes (
|
|
62
|
+
note_id TEXT PRIMARY KEY,
|
|
63
|
+
type TEXT NOT NULL CHECK(type IN ('fact','preference','pattern','entity','relationship')),
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
keywords TEXT NOT NULL,
|
|
66
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
67
|
+
source_episodes TEXT,
|
|
68
|
+
linked_notes TEXT,
|
|
69
|
+
link_reasons TEXT,
|
|
70
|
+
valid_from TEXT NOT NULL,
|
|
71
|
+
valid_to TEXT,
|
|
72
|
+
superseded_by TEXT,
|
|
73
|
+
access_count INTEGER DEFAULT 0,
|
|
74
|
+
last_accessed TEXT,
|
|
75
|
+
created_at TEXT NOT NULL,
|
|
76
|
+
updated_at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_sem_type ON semantic_notes(type);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_sem_conf ON semantic_notes(confidence DESC);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_sem_valid ON semantic_notes(valid_to) WHERE valid_to IS NULL;
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS procedures (
|
|
84
|
+
procedure_id TEXT PRIMARY KEY,
|
|
85
|
+
name TEXT NOT NULL,
|
|
86
|
+
type TEXT NOT NULL CHECK(type IN ('workflow','habit','tool_pattern','template')),
|
|
87
|
+
description TEXT NOT NULL,
|
|
88
|
+
steps TEXT,
|
|
89
|
+
trigger_conditions TEXT,
|
|
90
|
+
success_rate REAL DEFAULT 0.5,
|
|
91
|
+
times_executed INTEGER DEFAULT 0,
|
|
92
|
+
last_executed_at TEXT,
|
|
93
|
+
learned_from TEXT,
|
|
94
|
+
corrections TEXT,
|
|
95
|
+
related_procedures TEXT,
|
|
96
|
+
confidence REAL DEFAULT 0.5,
|
|
97
|
+
created_at TEXT NOT NULL,
|
|
98
|
+
updated_at TEXT NOT NULL
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS meta_topic_index (
|
|
102
|
+
topic TEXT PRIMARY KEY,
|
|
103
|
+
semantic_count INTEGER DEFAULT 0,
|
|
104
|
+
procedural_count INTEGER DEFAULT 0,
|
|
105
|
+
last_updated TEXT,
|
|
106
|
+
avg_confidence REAL DEFAULT 0.5,
|
|
107
|
+
total_accesses INTEGER DEFAULT 0
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS meta_consolidation_log (
|
|
111
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
|
+
run_at TEXT NOT NULL,
|
|
113
|
+
episodes_processed INTEGER,
|
|
114
|
+
notes_created INTEGER,
|
|
115
|
+
notes_merged INTEGER,
|
|
116
|
+
notes_superseded INTEGER,
|
|
117
|
+
procedures_updated INTEGER,
|
|
118
|
+
duration_ms INTEGER
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS voice_signatures (
|
|
122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
|
+
episode_id TEXT NOT NULL,
|
|
124
|
+
emotion TEXT,
|
|
125
|
+
arousal REAL,
|
|
126
|
+
dominance REAL,
|
|
127
|
+
valence REAL,
|
|
128
|
+
speech_rate REAL,
|
|
129
|
+
mean_pitch_hz REAL,
|
|
130
|
+
pitch_std_hz REAL,
|
|
131
|
+
jitter REAL,
|
|
132
|
+
shimmer REAL,
|
|
133
|
+
hnr_db REAL,
|
|
134
|
+
audio_events TEXT,
|
|
135
|
+
duration_sec REAL,
|
|
136
|
+
created_at TEXT NOT NULL
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_voice_ep ON voice_signatures(episode_id);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_voice_time ON voice_signatures(created_at DESC);
|
|
141
|
+
|
|
142
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
143
|
+
version INTEGER PRIMARY KEY,
|
|
144
|
+
applied_at TEXT NOT NULL
|
|
145
|
+
);
|
|
146
|
+
`;
|
|
147
|
+
export function initMemoryDb() {
|
|
148
|
+
const dbDir = join(homedir(), ".remote-copilot-mcp");
|
|
149
|
+
mkdirSync(dbDir, { recursive: true });
|
|
150
|
+
const dbPath = join(dbDir, "memory.db");
|
|
151
|
+
const db = new BetterSqlite3(dbPath);
|
|
152
|
+
db.pragma("journal_mode = WAL");
|
|
153
|
+
db.pragma("foreign_keys = ON");
|
|
154
|
+
// Create all tables
|
|
155
|
+
db.exec(SCHEMA_SQL);
|
|
156
|
+
// Record schema version if not yet recorded
|
|
157
|
+
const existing = db.prepare("SELECT version FROM schema_version WHERE version = ?").get(SCHEMA_VERSION);
|
|
158
|
+
if (!existing) {
|
|
159
|
+
db.prepare("INSERT OR IGNORE INTO schema_version (version, applied_at) VALUES (?, ?)").run(SCHEMA_VERSION, nowISO());
|
|
160
|
+
}
|
|
161
|
+
return db;
|
|
162
|
+
}
|
|
163
|
+
// ─── Row → Interface mappers ─────────────────────────────────────────────────
|
|
164
|
+
function rowToEpisode(row) {
|
|
165
|
+
return {
|
|
166
|
+
episodeId: row.episode_id,
|
|
167
|
+
sessionId: row.session_id,
|
|
168
|
+
threadId: row.thread_id,
|
|
169
|
+
timestamp: row.timestamp,
|
|
170
|
+
type: row.type,
|
|
171
|
+
modality: row.modality,
|
|
172
|
+
content: parseJsonObject(row.content),
|
|
173
|
+
topicTags: parseJsonArray(row.topic_tags),
|
|
174
|
+
importance: row.importance,
|
|
175
|
+
consolidated: row.consolidated === 1,
|
|
176
|
+
accessedCount: row.accessed_count,
|
|
177
|
+
lastAccessed: row.last_accessed ?? null,
|
|
178
|
+
createdAt: row.created_at,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function rowToSemanticNote(row) {
|
|
182
|
+
return {
|
|
183
|
+
noteId: row.note_id,
|
|
184
|
+
type: row.type,
|
|
185
|
+
content: row.content,
|
|
186
|
+
keywords: parseJsonArray(row.keywords),
|
|
187
|
+
confidence: row.confidence,
|
|
188
|
+
sourceEpisodes: parseJsonArray(row.source_episodes),
|
|
189
|
+
linkedNotes: parseJsonArray(row.linked_notes),
|
|
190
|
+
linkReasons: parseJsonObject(row.link_reasons),
|
|
191
|
+
validFrom: row.valid_from,
|
|
192
|
+
validTo: row.valid_to ?? null,
|
|
193
|
+
supersededBy: row.superseded_by ?? null,
|
|
194
|
+
accessCount: row.access_count,
|
|
195
|
+
lastAccessed: row.last_accessed ?? null,
|
|
196
|
+
createdAt: row.created_at,
|
|
197
|
+
updatedAt: row.updated_at,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function rowToProcedure(row) {
|
|
201
|
+
return {
|
|
202
|
+
procedureId: row.procedure_id,
|
|
203
|
+
name: row.name,
|
|
204
|
+
type: row.type,
|
|
205
|
+
description: row.description,
|
|
206
|
+
steps: parseJsonArray(row.steps),
|
|
207
|
+
triggerConditions: parseJsonArray(row.trigger_conditions),
|
|
208
|
+
successRate: row.success_rate,
|
|
209
|
+
timesExecuted: row.times_executed,
|
|
210
|
+
lastExecutedAt: row.last_executed_at ?? null,
|
|
211
|
+
learnedFrom: parseJsonArray(row.learned_from),
|
|
212
|
+
corrections: parseJsonArray(row.corrections),
|
|
213
|
+
relatedProcedures: parseJsonArray(row.related_procedures),
|
|
214
|
+
confidence: row.confidence,
|
|
215
|
+
createdAt: row.created_at,
|
|
216
|
+
updatedAt: row.updated_at,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function rowToTopicEntry(row) {
|
|
220
|
+
return {
|
|
221
|
+
topic: row.topic,
|
|
222
|
+
semanticCount: row.semantic_count,
|
|
223
|
+
proceduralCount: row.procedural_count,
|
|
224
|
+
lastUpdated: row.last_updated ?? null,
|
|
225
|
+
avgConfidence: row.avg_confidence,
|
|
226
|
+
totalAccesses: row.total_accesses,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// ─── Episodic Memory ─────────────────────────────────────────────────────────
|
|
230
|
+
export function saveEpisode(db, episode) {
|
|
231
|
+
const id = generateId("ep");
|
|
232
|
+
const now = nowISO();
|
|
233
|
+
db.prepare(`INSERT INTO episodes
|
|
234
|
+
(episode_id, session_id, thread_id, timestamp, type, modality, content, topic_tags, importance, consolidated, accessed_count, created_at)
|
|
235
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?)`).run(id, episode.sessionId, episode.threadId, now, episode.type, episode.modality, JSON.stringify(episode.content), jsonOrNull(episode.topicTags), episode.importance ?? 0.5, now);
|
|
236
|
+
return id;
|
|
237
|
+
}
|
|
238
|
+
export function getRecentEpisodes(db, threadId, limit = 20) {
|
|
239
|
+
const rows = db
|
|
240
|
+
.prepare(`SELECT * FROM episodes WHERE thread_id = ? ORDER BY timestamp DESC LIMIT ?`)
|
|
241
|
+
.all(threadId, limit);
|
|
242
|
+
return rows.map(rowToEpisode);
|
|
243
|
+
}
|
|
244
|
+
export function getUnconsolidatedEpisodes(db, threadId, limit = 50) {
|
|
245
|
+
const rows = db
|
|
246
|
+
.prepare(`SELECT * FROM episodes WHERE thread_id = ? AND consolidated = 0 ORDER BY timestamp ASC LIMIT ?`)
|
|
247
|
+
.all(threadId, limit);
|
|
248
|
+
return rows.map(rowToEpisode);
|
|
249
|
+
}
|
|
250
|
+
export function markConsolidated(db, episodeIds) {
|
|
251
|
+
if (episodeIds.length === 0)
|
|
252
|
+
return;
|
|
253
|
+
const stmt = db.prepare(`UPDATE episodes SET consolidated = 1 WHERE episode_id = ?`);
|
|
254
|
+
const txn = db.transaction(() => {
|
|
255
|
+
for (const id of episodeIds) {
|
|
256
|
+
stmt.run(id);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
txn();
|
|
260
|
+
}
|
|
261
|
+
// ─── Semantic Memory ─────────────────────────────────────────────────────────
|
|
262
|
+
export function saveSemanticNote(db, note) {
|
|
263
|
+
const id = generateId("sn");
|
|
264
|
+
const now = nowISO();
|
|
265
|
+
db.prepare(`INSERT INTO semantic_notes
|
|
266
|
+
(note_id, type, content, keywords, confidence, source_episodes, linked_notes, link_reasons, valid_from, access_count, created_at, updated_at)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`).run(id, note.type, note.content, JSON.stringify(note.keywords), Math.max(0, Math.min(1, note.confidence ?? 0.5)), jsonOrNull(note.sourceEpisodes), null, null, now, now, now);
|
|
268
|
+
// Update topic index for each keyword
|
|
269
|
+
updateTopicIndexForKeywords(db, note.keywords, "semantic");
|
|
270
|
+
return id;
|
|
271
|
+
}
|
|
272
|
+
export function searchSemanticNotes(db, query, options) {
|
|
273
|
+
const maxResults = options?.maxResults ?? 10;
|
|
274
|
+
const terms = query
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.split(/\s+/)
|
|
277
|
+
.filter((t) => t.length > 1);
|
|
278
|
+
if (terms.length === 0)
|
|
279
|
+
return [];
|
|
280
|
+
// Build LIKE conditions: each term must match content OR keywords
|
|
281
|
+
const conditions = [];
|
|
282
|
+
const params = [];
|
|
283
|
+
for (const term of terms) {
|
|
284
|
+
conditions.push(`(LOWER(content) LIKE ? OR LOWER(keywords) LIKE ?)`);
|
|
285
|
+
params.push(`%${term}%`, `%${term}%`);
|
|
286
|
+
}
|
|
287
|
+
let sql = `SELECT * FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL AND (${conditions.join(" OR ")})`;
|
|
288
|
+
if (options?.types && options.types.length > 0) {
|
|
289
|
+
const placeholders = options.types.map(() => "?").join(",");
|
|
290
|
+
sql += ` AND type IN (${placeholders})`;
|
|
291
|
+
params.push(...options.types);
|
|
292
|
+
}
|
|
293
|
+
sql += ` ORDER BY confidence DESC, access_count DESC LIMIT ?`;
|
|
294
|
+
params.push(maxResults);
|
|
295
|
+
const rows = db.prepare(sql).all(...params);
|
|
296
|
+
// Update access counts
|
|
297
|
+
const now = nowISO();
|
|
298
|
+
const updateStmt = db.prepare(`UPDATE semantic_notes SET access_count = access_count + 1, last_accessed = ? WHERE note_id = ?`);
|
|
299
|
+
const txn = db.transaction(() => {
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
updateStmt.run(now, row.note_id);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
txn();
|
|
305
|
+
return rows.map(rowToSemanticNote);
|
|
306
|
+
}
|
|
307
|
+
export function getTopSemanticNotes(db, options) {
|
|
308
|
+
const limit = options?.limit ?? 10;
|
|
309
|
+
const sortBy = options?.sortBy ?? "confidence";
|
|
310
|
+
const validSortColumns = {
|
|
311
|
+
confidence: "confidence DESC",
|
|
312
|
+
access_count: "access_count DESC",
|
|
313
|
+
created_at: "created_at DESC",
|
|
314
|
+
};
|
|
315
|
+
const orderClause = validSortColumns[sortBy] ?? "confidence DESC";
|
|
316
|
+
let sql = `SELECT * FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL`;
|
|
317
|
+
const params = [];
|
|
318
|
+
if (options?.type) {
|
|
319
|
+
sql += ` AND type = ?`;
|
|
320
|
+
params.push(options.type);
|
|
321
|
+
}
|
|
322
|
+
sql += ` ORDER BY ${orderClause} LIMIT ?`;
|
|
323
|
+
params.push(limit);
|
|
324
|
+
const rows = db.prepare(sql).all(...params);
|
|
325
|
+
return rows.map(rowToSemanticNote);
|
|
326
|
+
}
|
|
327
|
+
export function updateSemanticNote(db, noteId, updates) {
|
|
328
|
+
const now = nowISO();
|
|
329
|
+
const setClauses = ["updated_at = ?"];
|
|
330
|
+
const params = [now];
|
|
331
|
+
if (updates.content !== undefined) {
|
|
332
|
+
setClauses.push("content = ?");
|
|
333
|
+
params.push(updates.content);
|
|
334
|
+
}
|
|
335
|
+
if (updates.confidence !== undefined) {
|
|
336
|
+
setClauses.push("confidence = ?");
|
|
337
|
+
params.push(updates.confidence);
|
|
338
|
+
}
|
|
339
|
+
if (updates.keywords !== undefined) {
|
|
340
|
+
setClauses.push("keywords = ?");
|
|
341
|
+
params.push(JSON.stringify(updates.keywords));
|
|
342
|
+
}
|
|
343
|
+
if (updates.linkedNotes !== undefined) {
|
|
344
|
+
setClauses.push("linked_notes = ?");
|
|
345
|
+
params.push(JSON.stringify(updates.linkedNotes));
|
|
346
|
+
}
|
|
347
|
+
if (updates.linkReasons !== undefined) {
|
|
348
|
+
setClauses.push("link_reasons = ?");
|
|
349
|
+
params.push(JSON.stringify(updates.linkReasons));
|
|
350
|
+
}
|
|
351
|
+
params.push(noteId);
|
|
352
|
+
db.prepare(`UPDATE semantic_notes SET ${setClauses.join(", ")} WHERE note_id = ?`).run(...params);
|
|
353
|
+
}
|
|
354
|
+
export function supersedeNote(db, oldNoteId, newNote) {
|
|
355
|
+
const newId = saveSemanticNote(db, {
|
|
356
|
+
type: newNote.type,
|
|
357
|
+
content: newNote.content,
|
|
358
|
+
keywords: newNote.keywords,
|
|
359
|
+
confidence: newNote.confidence,
|
|
360
|
+
sourceEpisodes: newNote.sourceEpisodes,
|
|
361
|
+
});
|
|
362
|
+
const now = nowISO();
|
|
363
|
+
db.prepare(`UPDATE semantic_notes SET superseded_by = ?, valid_to = ?, updated_at = ? WHERE note_id = ?`).run(newId, now, now, oldNoteId);
|
|
364
|
+
return newId;
|
|
365
|
+
}
|
|
366
|
+
export function expireNote(db, noteId) {
|
|
367
|
+
const now = nowISO();
|
|
368
|
+
db.prepare(`UPDATE semantic_notes SET valid_to = ?, updated_at = ? WHERE note_id = ?`).run(now, now, noteId);
|
|
369
|
+
}
|
|
370
|
+
// ─── Procedural Memory ──────────────────────────────────────────────────────
|
|
371
|
+
export function saveProcedure(db, proc) {
|
|
372
|
+
const id = generateId("pr");
|
|
373
|
+
const now = nowISO();
|
|
374
|
+
db.prepare(`INSERT INTO procedures
|
|
375
|
+
(procedure_id, name, type, description, steps, trigger_conditions, success_rate, times_executed, learned_from, corrections, related_procedures, confidence, created_at, updated_at)
|
|
376
|
+
VALUES (?, ?, ?, ?, ?, ?, 0.5, 0, ?, ?, ?, 0.5, ?, ?)`).run(id, proc.name, proc.type, proc.description, jsonOrNull(proc.steps), jsonOrNull(proc.triggerConditions), null, // learned_from
|
|
377
|
+
null, // corrections
|
|
378
|
+
null, // related_procedures
|
|
379
|
+
now, now);
|
|
380
|
+
// Update topic index based on procedure name words
|
|
381
|
+
const keywords = proc.name
|
|
382
|
+
.toLowerCase()
|
|
383
|
+
.split(/\s+/)
|
|
384
|
+
.filter((w) => w.length > 2);
|
|
385
|
+
updateTopicIndexForKeywords(db, keywords, "procedural");
|
|
386
|
+
return id;
|
|
387
|
+
}
|
|
388
|
+
export function searchProcedures(db, query, maxResults = 10) {
|
|
389
|
+
const terms = query
|
|
390
|
+
.toLowerCase()
|
|
391
|
+
.split(/\s+/)
|
|
392
|
+
.filter((t) => t.length > 1);
|
|
393
|
+
if (terms.length === 0)
|
|
394
|
+
return [];
|
|
395
|
+
const conditions = [];
|
|
396
|
+
const params = [];
|
|
397
|
+
for (const term of terms) {
|
|
398
|
+
conditions.push(`(LOWER(name) LIKE ? OR LOWER(description) LIKE ? OR LOWER(steps) LIKE ? OR LOWER(trigger_conditions) LIKE ?)`);
|
|
399
|
+
params.push(`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`);
|
|
400
|
+
}
|
|
401
|
+
const sql = `SELECT * FROM procedures WHERE ${conditions.join(" OR ")} ORDER BY confidence DESC, success_rate DESC LIMIT ?`;
|
|
402
|
+
params.push(maxResults);
|
|
403
|
+
const rows = db.prepare(sql).all(...params);
|
|
404
|
+
return rows.map(rowToProcedure);
|
|
405
|
+
}
|
|
406
|
+
export function updateProcedure(db, procedureId, updates) {
|
|
407
|
+
const now = nowISO();
|
|
408
|
+
const setClauses = ["updated_at = ?"];
|
|
409
|
+
const params = [now];
|
|
410
|
+
if (updates.description !== undefined) {
|
|
411
|
+
setClauses.push("description = ?");
|
|
412
|
+
params.push(updates.description);
|
|
413
|
+
}
|
|
414
|
+
if (updates.steps !== undefined) {
|
|
415
|
+
setClauses.push("steps = ?");
|
|
416
|
+
params.push(JSON.stringify(updates.steps));
|
|
417
|
+
}
|
|
418
|
+
if (updates.triggerConditions !== undefined) {
|
|
419
|
+
setClauses.push("trigger_conditions = ?");
|
|
420
|
+
params.push(JSON.stringify(updates.triggerConditions));
|
|
421
|
+
}
|
|
422
|
+
if (updates.successRate !== undefined) {
|
|
423
|
+
setClauses.push("success_rate = ?");
|
|
424
|
+
params.push(updates.successRate);
|
|
425
|
+
}
|
|
426
|
+
if (updates.timesExecuted !== undefined) {
|
|
427
|
+
setClauses.push("times_executed = ?");
|
|
428
|
+
params.push(updates.timesExecuted);
|
|
429
|
+
}
|
|
430
|
+
if (updates.corrections !== undefined) {
|
|
431
|
+
setClauses.push("corrections = ?");
|
|
432
|
+
params.push(JSON.stringify(updates.corrections));
|
|
433
|
+
}
|
|
434
|
+
if (updates.confidence !== undefined) {
|
|
435
|
+
setClauses.push("confidence = ?");
|
|
436
|
+
params.push(updates.confidence);
|
|
437
|
+
}
|
|
438
|
+
params.push(procedureId);
|
|
439
|
+
db.prepare(`UPDATE procedures SET ${setClauses.join(", ")} WHERE procedure_id = ?`).run(...params);
|
|
440
|
+
}
|
|
441
|
+
export function recordProcedureExecution(db, procedureId, success) {
|
|
442
|
+
const now = nowISO();
|
|
443
|
+
// Get current stats
|
|
444
|
+
const row = db.prepare(`SELECT times_executed, success_rate FROM procedures WHERE procedure_id = ?`).get(procedureId);
|
|
445
|
+
if (!row)
|
|
446
|
+
return;
|
|
447
|
+
const newCount = row.times_executed + 1;
|
|
448
|
+
// Weighted moving average for success rate
|
|
449
|
+
const newRate = (row.success_rate * row.times_executed + (success ? 1 : 0)) / newCount;
|
|
450
|
+
db.prepare(`UPDATE procedures SET times_executed = ?, success_rate = ?, last_executed_at = ?, updated_at = ? WHERE procedure_id = ?`).run(newCount, newRate, now, now, procedureId);
|
|
451
|
+
}
|
|
452
|
+
// ─── Meta Memory ─────────────────────────────────────────────────────────────
|
|
453
|
+
function updateTopicIndexForKeywords(db, keywords, layer) {
|
|
454
|
+
const now = nowISO();
|
|
455
|
+
const col = layer === "semantic" ? "semantic_count" : "procedural_count";
|
|
456
|
+
const upsertStmt = db.prepare(`INSERT INTO meta_topic_index (topic, ${col}, last_updated)
|
|
457
|
+
VALUES (?, 1, ?)
|
|
458
|
+
ON CONFLICT(topic) DO UPDATE SET
|
|
459
|
+
${col} = ${col} + 1,
|
|
460
|
+
last_updated = ?,
|
|
461
|
+
total_accesses = total_accesses + 1`);
|
|
462
|
+
const txn = db.transaction(() => {
|
|
463
|
+
for (const kw of keywords) {
|
|
464
|
+
const normalised = kw.toLowerCase().trim();
|
|
465
|
+
if (normalised.length > 1) {
|
|
466
|
+
upsertStmt.run(normalised, now, now);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
txn();
|
|
471
|
+
}
|
|
472
|
+
export function getMemoryStatus(db, threadId) {
|
|
473
|
+
const totalEpisodes = db.prepare(`SELECT COUNT(*) as cnt FROM episodes WHERE thread_id = ?`).get(threadId).cnt;
|
|
474
|
+
const unconsolidatedEpisodes = db
|
|
475
|
+
.prepare(`SELECT COUNT(*) as cnt FROM episodes WHERE thread_id = ? AND consolidated = 0`)
|
|
476
|
+
.get(threadId).cnt;
|
|
477
|
+
const totalSemanticNotes = db.prepare(`SELECT COUNT(*) as cnt FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL`).get().cnt;
|
|
478
|
+
const totalProcedures = db.prepare(`SELECT COUNT(*) as cnt FROM procedures`).get().cnt;
|
|
479
|
+
const totalVoiceSignatures = db.prepare(`SELECT COUNT(*) as cnt FROM voice_signatures`).get().cnt;
|
|
480
|
+
const lastConsolidationRow = db
|
|
481
|
+
.prepare(`SELECT run_at FROM meta_consolidation_log ORDER BY run_at DESC LIMIT 1`)
|
|
482
|
+
.get();
|
|
483
|
+
const topTopics = getTopicIndex(db).slice(0, 5);
|
|
484
|
+
// Database file size
|
|
485
|
+
const dbPath = join(homedir(), ".remote-copilot-mcp", "memory.db");
|
|
486
|
+
let dbSizeBytes = 0;
|
|
487
|
+
try {
|
|
488
|
+
dbSizeBytes = statSync(dbPath).size;
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// file might not exist yet or be inaccessible
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
totalEpisodes,
|
|
495
|
+
unconsolidatedEpisodes,
|
|
496
|
+
totalSemanticNotes,
|
|
497
|
+
totalProcedures,
|
|
498
|
+
totalVoiceSignatures,
|
|
499
|
+
lastConsolidation: lastConsolidationRow?.run_at ?? null,
|
|
500
|
+
topTopics,
|
|
501
|
+
dbSizeBytes,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
export function getTopicIndex(db) {
|
|
505
|
+
const rows = db
|
|
506
|
+
.prepare(`SELECT * FROM meta_topic_index ORDER BY total_accesses DESC, semantic_count DESC LIMIT 50`)
|
|
507
|
+
.all();
|
|
508
|
+
return rows.map(rowToTopicEntry);
|
|
509
|
+
}
|
|
510
|
+
export function logConsolidation(db, log) {
|
|
511
|
+
db.prepare(`INSERT INTO meta_consolidation_log
|
|
512
|
+
(run_at, episodes_processed, notes_created, notes_merged, notes_superseded, procedures_updated, duration_ms)
|
|
513
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(nowISO(), log.episodesProcessed, log.notesCreated, log.notesMerged, log.notesSuperseded, log.proceduresUpdated, log.durationMs);
|
|
514
|
+
}
|
|
515
|
+
// ─── Voice Signatures ────────────────────────────────────────────────────────
|
|
516
|
+
export function saveVoiceSignature(db, sig) {
|
|
517
|
+
db.prepare(`INSERT INTO voice_signatures
|
|
518
|
+
(episode_id, emotion, arousal, dominance, valence, speech_rate, mean_pitch_hz, pitch_std_hz, jitter, shimmer, hnr_db, audio_events, duration_sec, created_at)
|
|
519
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(sig.episodeId, sig.emotion ?? null, sig.arousal ?? null, sig.dominance ?? null, sig.valence ?? null, sig.speechRate ?? null, sig.meanPitchHz ?? null, sig.pitchStdHz ?? null, sig.jitter ?? null, sig.shimmer ?? null, sig.hnrDb ?? null, jsonOrNull(sig.audioEvents), sig.durationSec ?? null, nowISO());
|
|
520
|
+
}
|
|
521
|
+
export function getVoiceBaseline(db, dayRange = 30) {
|
|
522
|
+
const cutoff = new Date(Date.now() - dayRange * 24 * 60 * 60 * 1000).toISOString();
|
|
523
|
+
const row = db
|
|
524
|
+
.prepare(`SELECT
|
|
525
|
+
AVG(arousal) AS avg_arousal,
|
|
526
|
+
AVG(dominance) AS avg_dominance,
|
|
527
|
+
AVG(valence) AS avg_valence,
|
|
528
|
+
AVG(speech_rate) AS avg_speech_rate,
|
|
529
|
+
AVG(mean_pitch_hz) AS avg_mean_pitch_hz,
|
|
530
|
+
AVG(pitch_std_hz) AS avg_pitch_std_hz,
|
|
531
|
+
AVG(jitter) AS avg_jitter,
|
|
532
|
+
AVG(shimmer) AS avg_shimmer,
|
|
533
|
+
AVG(hnr_db) AS avg_hnr_db,
|
|
534
|
+
COUNT(*) AS sample_count
|
|
535
|
+
FROM voice_signatures
|
|
536
|
+
WHERE created_at >= ?`)
|
|
537
|
+
.get(cutoff);
|
|
538
|
+
if (!row || row.sample_count === 0)
|
|
539
|
+
return null;
|
|
540
|
+
return {
|
|
541
|
+
avgArousal: row.avg_arousal,
|
|
542
|
+
avgDominance: row.avg_dominance,
|
|
543
|
+
avgValence: row.avg_valence,
|
|
544
|
+
avgSpeechRate: row.avg_speech_rate,
|
|
545
|
+
avgMeanPitchHz: row.avg_mean_pitch_hz,
|
|
546
|
+
avgPitchStdHz: row.avg_pitch_std_hz,
|
|
547
|
+
avgJitter: row.avg_jitter,
|
|
548
|
+
avgShimmer: row.avg_shimmer,
|
|
549
|
+
avgHnrDb: row.avg_hnr_db,
|
|
550
|
+
sampleCount: row.sample_count,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// ─── Bootstrap ───────────────────────────────────────────────────────────────
|
|
554
|
+
export function assembleBootstrap(db, threadId) {
|
|
555
|
+
const status = getMemoryStatus(db, threadId);
|
|
556
|
+
const recentEpisodes = getRecentEpisodes(db, threadId, 5);
|
|
557
|
+
const topNotes = getTopSemanticNotes(db, { limit: 10, sortBy: "access_count" });
|
|
558
|
+
// Preferences first
|
|
559
|
+
const preferences = topNotes.filter((n) => n.type === "preference");
|
|
560
|
+
const otherNotes = topNotes.filter((n) => n.type !== "preference");
|
|
561
|
+
const sortedNotes = [...preferences, ...otherNotes].slice(0, 10);
|
|
562
|
+
const activeProcedures = db
|
|
563
|
+
.prepare(`SELECT * FROM procedures ORDER BY times_executed DESC, confidence DESC LIMIT 5`)
|
|
564
|
+
.all();
|
|
565
|
+
const procedures = activeProcedures.map(rowToProcedure);
|
|
566
|
+
const baseline = getVoiceBaseline(db);
|
|
567
|
+
const lines = [];
|
|
568
|
+
lines.push("# Memory Briefing");
|
|
569
|
+
lines.push("");
|
|
570
|
+
// Status
|
|
571
|
+
lines.push("## Status");
|
|
572
|
+
lines.push(`- Episodes: ${status.totalEpisodes} (${status.unconsolidatedEpisodes} unconsolidated)`);
|
|
573
|
+
lines.push(`- Semantic notes: ${status.totalSemanticNotes}`);
|
|
574
|
+
lines.push(`- Procedures: ${status.totalProcedures}`);
|
|
575
|
+
lines.push(`- Voice signatures: ${status.totalVoiceSignatures}`);
|
|
576
|
+
if (status.lastConsolidation) {
|
|
577
|
+
lines.push(`- Last consolidation: ${status.lastConsolidation}`);
|
|
578
|
+
}
|
|
579
|
+
lines.push(`- DB size: ${(status.dbSizeBytes / 1024).toFixed(1)} KB`);
|
|
580
|
+
lines.push("");
|
|
581
|
+
// Recent episodes
|
|
582
|
+
if (recentEpisodes.length > 0) {
|
|
583
|
+
lines.push("## Recent Episodes");
|
|
584
|
+
for (const ep of recentEpisodes) {
|
|
585
|
+
const summary = typeof ep.content === "object" && ep.content !== null
|
|
586
|
+
? ep.content.text ?? ep.content.caption ?? JSON.stringify(ep.content).slice(0, 120)
|
|
587
|
+
: String(ep.content).slice(0, 120);
|
|
588
|
+
lines.push(`- [${ep.type}/${ep.modality}] ${summary} (${ep.timestamp})`);
|
|
589
|
+
}
|
|
590
|
+
lines.push("");
|
|
591
|
+
}
|
|
592
|
+
// Key knowledge
|
|
593
|
+
if (sortedNotes.length > 0) {
|
|
594
|
+
lines.push("## Key Knowledge");
|
|
595
|
+
for (const note of sortedNotes) {
|
|
596
|
+
lines.push(`- **[${note.type}]** ${note.content} (conf: ${note.confidence.toFixed(2)}, accessed: ${note.accessCount}x)`);
|
|
597
|
+
}
|
|
598
|
+
lines.push("");
|
|
599
|
+
}
|
|
600
|
+
// Active procedures
|
|
601
|
+
if (procedures.length > 0) {
|
|
602
|
+
lines.push("## Active Procedures");
|
|
603
|
+
for (const proc of procedures) {
|
|
604
|
+
lines.push(`- **${proc.name}** (${proc.type}) — success: ${(proc.successRate * 100).toFixed(0)}%, used ${proc.timesExecuted}x`);
|
|
605
|
+
if (proc.steps.length > 0) {
|
|
606
|
+
lines.push(` Steps: ${proc.steps.join(" → ")}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
lines.push("");
|
|
610
|
+
}
|
|
611
|
+
// Voice baseline
|
|
612
|
+
if (baseline && baseline.sampleCount > 0) {
|
|
613
|
+
lines.push("## Voice Baseline (30d)");
|
|
614
|
+
lines.push(`- Samples: ${baseline.sampleCount}`);
|
|
615
|
+
if (baseline.avgValence !== null)
|
|
616
|
+
lines.push(`- Avg valence: ${baseline.avgValence.toFixed(2)}`);
|
|
617
|
+
if (baseline.avgArousal !== null)
|
|
618
|
+
lines.push(`- Avg arousal: ${baseline.avgArousal.toFixed(2)}`);
|
|
619
|
+
if (baseline.avgSpeechRate !== null)
|
|
620
|
+
lines.push(`- Avg speech rate: ${baseline.avgSpeechRate.toFixed(1)}`);
|
|
621
|
+
if (baseline.avgMeanPitchHz !== null)
|
|
622
|
+
lines.push(`- Avg pitch: ${baseline.avgMeanPitchHz.toFixed(1)} Hz`);
|
|
623
|
+
lines.push("");
|
|
624
|
+
}
|
|
625
|
+
// Topics
|
|
626
|
+
if (status.topTopics.length > 0) {
|
|
627
|
+
lines.push("## Top Topics");
|
|
628
|
+
for (const t of status.topTopics) {
|
|
629
|
+
lines.push(`- ${t.topic} (semantic: ${t.semanticCount}, procedural: ${t.proceduralCount})`);
|
|
630
|
+
}
|
|
631
|
+
lines.push("");
|
|
632
|
+
}
|
|
633
|
+
return lines.join("\n");
|
|
634
|
+
}
|
|
635
|
+
// ─── Forget ──────────────────────────────────────────────────────────────────
|
|
636
|
+
export function forgetMemory(db, memoryId, reason) {
|
|
637
|
+
// Determine layer by prefix
|
|
638
|
+
if (memoryId.startsWith("ep_")) {
|
|
639
|
+
const existing = db.prepare(`SELECT episode_id FROM episodes WHERE episode_id = ?`).get(memoryId);
|
|
640
|
+
if (!existing)
|
|
641
|
+
return { layer: "episodic", deleted: false };
|
|
642
|
+
db.prepare(`DELETE FROM episodes WHERE episode_id = ?`).run(memoryId);
|
|
643
|
+
// Also delete associated voice signature
|
|
644
|
+
db.prepare(`DELETE FROM voice_signatures WHERE episode_id = ?`).run(memoryId);
|
|
645
|
+
return { layer: "episodic", deleted: true };
|
|
646
|
+
}
|
|
647
|
+
if (memoryId.startsWith("sn_")) {
|
|
648
|
+
const existing = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ?`).get(memoryId);
|
|
649
|
+
if (!existing)
|
|
650
|
+
return { layer: "semantic", deleted: false };
|
|
651
|
+
db.prepare(`DELETE FROM semantic_notes WHERE note_id = ?`).run(memoryId);
|
|
652
|
+
return { layer: "semantic", deleted: true };
|
|
653
|
+
}
|
|
654
|
+
if (memoryId.startsWith("pr_")) {
|
|
655
|
+
const existing = db.prepare(`SELECT procedure_id FROM procedures WHERE procedure_id = ?`).get(memoryId);
|
|
656
|
+
if (!existing)
|
|
657
|
+
return { layer: "procedural", deleted: false };
|
|
658
|
+
db.prepare(`DELETE FROM procedures WHERE procedure_id = ?`).run(memoryId);
|
|
659
|
+
return { layer: "procedural", deleted: true };
|
|
660
|
+
}
|
|
661
|
+
// Unknown prefix — try all layers
|
|
662
|
+
let row = db.prepare(`SELECT episode_id FROM episodes WHERE episode_id = ?`).get(memoryId);
|
|
663
|
+
if (row) {
|
|
664
|
+
db.prepare(`DELETE FROM episodes WHERE episode_id = ?`).run(memoryId);
|
|
665
|
+
db.prepare(`DELETE FROM voice_signatures WHERE episode_id = ?`).run(memoryId);
|
|
666
|
+
return { layer: "episodic", deleted: true };
|
|
667
|
+
}
|
|
668
|
+
row = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ?`).get(memoryId);
|
|
669
|
+
if (row) {
|
|
670
|
+
db.prepare(`DELETE FROM semantic_notes WHERE note_id = ?`).run(memoryId);
|
|
671
|
+
return { layer: "semantic", deleted: true };
|
|
672
|
+
}
|
|
673
|
+
row = db.prepare(`SELECT procedure_id FROM procedures WHERE procedure_id = ?`).get(memoryId);
|
|
674
|
+
if (row) {
|
|
675
|
+
db.prepare(`DELETE FROM procedures WHERE procedure_id = ?`).run(memoryId);
|
|
676
|
+
return { layer: "procedural", deleted: true };
|
|
677
|
+
}
|
|
678
|
+
return { layer: "unknown", deleted: false };
|
|
679
|
+
}
|
|
680
|
+
//# sourceMappingURL=memory.js.map
|