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/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