sensorium-mcp 2.16.28 → 2.16.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/dist/config.d.ts +1 -11
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +3 -49
  4. package/dist/config.js.map +1 -1
  5. package/dist/dashboard/presets.d.ts +18 -0
  6. package/dist/dashboard/presets.d.ts.map +1 -0
  7. package/dist/dashboard/presets.js +78 -0
  8. package/dist/dashboard/presets.js.map +1 -0
  9. package/dist/dashboard/routes.d.ts +33 -0
  10. package/dist/dashboard/routes.d.ts.map +1 -0
  11. package/dist/dashboard/routes.js +283 -0
  12. package/dist/dashboard/routes.js.map +1 -0
  13. package/dist/dashboard.d.ts +6 -29
  14. package/dist/dashboard.d.ts.map +1 -1
  15. package/dist/dashboard.js +6 -1158
  16. package/dist/dashboard.js.map +1 -1
  17. package/dist/data/file-storage.d.ts +19 -0
  18. package/dist/data/file-storage.d.ts.map +1 -0
  19. package/dist/data/file-storage.js +58 -0
  20. package/dist/data/file-storage.js.map +1 -0
  21. package/dist/data/memory/bootstrap.d.ts +40 -0
  22. package/dist/data/memory/bootstrap.d.ts.map +1 -0
  23. package/dist/data/memory/bootstrap.js +240 -0
  24. package/dist/data/memory/bootstrap.js.map +1 -0
  25. package/dist/data/memory/consolidation.d.ts +12 -0
  26. package/dist/data/memory/consolidation.d.ts.map +1 -0
  27. package/dist/data/memory/consolidation.js +248 -0
  28. package/dist/data/memory/consolidation.js.map +1 -0
  29. package/dist/data/memory/episodes.d.ts +34 -0
  30. package/dist/data/memory/episodes.d.ts.map +1 -0
  31. package/dist/data/memory/episodes.js +89 -0
  32. package/dist/data/memory/episodes.js.map +1 -0
  33. package/dist/data/memory/index.d.ts +14 -0
  34. package/dist/data/memory/index.d.ts.map +1 -0
  35. package/dist/data/memory/index.js +14 -0
  36. package/dist/data/memory/index.js.map +1 -0
  37. package/dist/data/memory/procedures.d.ts +42 -0
  38. package/dist/data/memory/procedures.d.ts.map +1 -0
  39. package/dist/data/memory/procedures.js +122 -0
  40. package/dist/data/memory/procedures.js.map +1 -0
  41. package/dist/data/memory/schema.d.ts +11 -0
  42. package/dist/data/memory/schema.d.ts.map +1 -0
  43. package/dist/data/memory/schema.js +327 -0
  44. package/dist/data/memory/schema.js.map +1 -0
  45. package/dist/data/memory/semantic.d.ts +94 -0
  46. package/dist/data/memory/semantic.d.ts.map +1 -0
  47. package/dist/data/memory/semantic.js +385 -0
  48. package/dist/data/memory/semantic.js.map +1 -0
  49. package/dist/data/memory/voice-sig.d.ts +33 -0
  50. package/dist/data/memory/voice-sig.d.ts.map +1 -0
  51. package/dist/data/memory/voice-sig.js +48 -0
  52. package/dist/data/memory/voice-sig.js.map +1 -0
  53. package/dist/data/templates.d.ts +19 -0
  54. package/dist/data/templates.d.ts.map +1 -0
  55. package/dist/data/templates.js +46 -0
  56. package/dist/data/templates.js.map +1 -0
  57. package/dist/dispatcher.d.ts +5 -97
  58. package/dist/dispatcher.d.ts.map +1 -1
  59. package/dist/dispatcher.js +5 -525
  60. package/dist/dispatcher.js.map +1 -1
  61. package/dist/drive.d.ts.map +1 -1
  62. package/dist/drive.js +3 -1
  63. package/dist/drive.js.map +1 -1
  64. package/dist/index.d.ts +4 -23
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +11 -289
  67. package/dist/index.js.map +1 -1
  68. package/dist/integrations/openai/chat.d.ts +29 -0
  69. package/dist/integrations/openai/chat.d.ts.map +1 -0
  70. package/dist/integrations/openai/chat.js +84 -0
  71. package/dist/integrations/openai/chat.js.map +1 -0
  72. package/dist/integrations/openai/index.d.ts +6 -0
  73. package/dist/integrations/openai/index.d.ts.map +1 -0
  74. package/dist/integrations/openai/index.js +6 -0
  75. package/dist/integrations/openai/index.js.map +1 -0
  76. package/dist/integrations/openai/speech.d.ts +21 -0
  77. package/dist/integrations/openai/speech.d.ts.map +1 -0
  78. package/dist/integrations/openai/speech.js +75 -0
  79. package/dist/integrations/openai/speech.js.map +1 -0
  80. package/dist/integrations/openai/video.d.ts +15 -0
  81. package/dist/integrations/openai/video.d.ts.map +1 -0
  82. package/dist/integrations/openai/video.js +131 -0
  83. package/dist/integrations/openai/video.js.map +1 -0
  84. package/dist/integrations/openai/vision.d.ts +23 -0
  85. package/dist/integrations/openai/vision.d.ts.map +1 -0
  86. package/dist/integrations/openai/vision.js +116 -0
  87. package/dist/integrations/openai/vision.js.map +1 -0
  88. package/dist/integrations/openai/voice-emotion.d.ts +41 -0
  89. package/dist/integrations/openai/voice-emotion.d.ts.map +1 -0
  90. package/dist/integrations/openai/voice-emotion.js +50 -0
  91. package/dist/integrations/openai/voice-emotion.js.map +1 -0
  92. package/dist/integrations/telegram/types.d.ts +112 -0
  93. package/dist/integrations/telegram/types.d.ts.map +1 -0
  94. package/dist/integrations/telegram/types.js +6 -0
  95. package/dist/integrations/telegram/types.js.map +1 -0
  96. package/dist/memory.d.ts +6 -205
  97. package/dist/memory.d.ts.map +1 -1
  98. package/dist/memory.js +6 -1357
  99. package/dist/memory.js.map +1 -1
  100. package/dist/openai.d.ts +11 -102
  101. package/dist/openai.d.ts.map +1 -1
  102. package/dist/openai.js +14 -421
  103. package/dist/openai.js.map +1 -1
  104. package/dist/response-builders.d.ts +1 -11
  105. package/dist/response-builders.d.ts.map +1 -1
  106. package/dist/response-builders.js +2 -38
  107. package/dist/response-builders.js.map +1 -1
  108. package/dist/server/factory.d.ts +17 -0
  109. package/dist/server/factory.d.ts.map +1 -0
  110. package/dist/server/factory.js +279 -0
  111. package/dist/server/factory.js.map +1 -0
  112. package/dist/services/dispatcher/broker.d.ts +83 -0
  113. package/dist/services/dispatcher/broker.d.ts.map +1 -0
  114. package/dist/services/dispatcher/broker.js +175 -0
  115. package/dist/services/dispatcher/broker.js.map +1 -0
  116. package/dist/services/dispatcher/index.d.ts +7 -0
  117. package/dist/services/dispatcher/index.d.ts.map +1 -0
  118. package/dist/services/dispatcher/index.js +7 -0
  119. package/dist/services/dispatcher/index.js.map +1 -0
  120. package/dist/services/dispatcher/lock.d.ts +25 -0
  121. package/dist/services/dispatcher/lock.d.ts.map +1 -0
  122. package/dist/services/dispatcher/lock.js +111 -0
  123. package/dist/services/dispatcher/lock.js.map +1 -0
  124. package/dist/services/dispatcher/poller.d.ts +19 -0
  125. package/dist/services/dispatcher/poller.d.ts.map +1 -0
  126. package/dist/services/dispatcher/poller.js +269 -0
  127. package/dist/services/dispatcher/poller.js.map +1 -0
  128. package/dist/telegram.d.ts +2 -88
  129. package/dist/telegram.d.ts.map +1 -1
  130. package/dist/telegram.js +2 -0
  131. package/dist/telegram.js.map +1 -1
  132. package/dist/tool-definitions.d.ts +1 -14
  133. package/dist/tool-definitions.d.ts.map +1 -1
  134. package/dist/tool-definitions.js +1 -403
  135. package/dist/tool-definitions.js.map +1 -1
  136. package/dist/tools/definitions.d.ts +15 -0
  137. package/dist/tools/definitions.d.ts.map +1 -0
  138. package/dist/tools/definitions.js +404 -0
  139. package/dist/tools/definitions.js.map +1 -0
  140. package/dist/tools/start-session-tool.d.ts.map +1 -1
  141. package/dist/tools/start-session-tool.js +2 -0
  142. package/dist/tools/start-session-tool.js.map +1 -1
  143. package/dist/tools/wait/drive-handler.d.ts +61 -0
  144. package/dist/tools/wait/drive-handler.d.ts.map +1 -0
  145. package/dist/tools/wait/drive-handler.js +138 -0
  146. package/dist/tools/wait/drive-handler.js.map +1 -0
  147. package/dist/tools/wait/index.d.ts +8 -0
  148. package/dist/tools/wait/index.d.ts.map +1 -0
  149. package/dist/tools/wait/index.js +8 -0
  150. package/dist/tools/wait/index.js.map +1 -0
  151. package/dist/tools/wait/media-processor.d.ts +52 -0
  152. package/dist/tools/wait/media-processor.d.ts.map +1 -0
  153. package/dist/tools/wait/media-processor.js +261 -0
  154. package/dist/tools/wait/media-processor.js.map +1 -0
  155. package/dist/tools/wait/message-delivery.d.ts +63 -0
  156. package/dist/tools/wait/message-delivery.d.ts.map +1 -0
  157. package/dist/tools/wait/message-delivery.js +281 -0
  158. package/dist/tools/wait/message-delivery.js.map +1 -0
  159. package/dist/tools/wait/poll-loop.d.ts +72 -0
  160. package/dist/tools/wait/poll-loop.d.ts.map +1 -0
  161. package/dist/tools/wait/poll-loop.js +280 -0
  162. package/dist/tools/wait/poll-loop.js.map +1 -0
  163. package/dist/tools/wait/reaction-handler.d.ts +49 -0
  164. package/dist/tools/wait/reaction-handler.d.ts.map +1 -0
  165. package/dist/tools/wait/reaction-handler.js +126 -0
  166. package/dist/tools/wait/reaction-handler.js.map +1 -0
  167. package/dist/tools/wait/task-handler.d.ts +40 -0
  168. package/dist/tools/wait/task-handler.d.ts.map +1 -0
  169. package/dist/tools/wait/task-handler.js +41 -0
  170. package/dist/tools/wait/task-handler.js.map +1 -0
  171. package/dist/tools/wait-tool.d.ts +3 -69
  172. package/dist/tools/wait-tool.d.ts.map +1 -1
  173. package/dist/tools/wait-tool.js +3 -876
  174. package/dist/tools/wait-tool.js.map +1 -1
  175. package/package.json +1 -1
  176. package/templates/daily-review.default.md +26 -0
  177. package/templates/drive-dispatcher.default.md +2 -0
package/dist/memory.js CHANGED
@@ -1,1360 +1,9 @@
1
- import BetterSqlite3 from "better-sqlite3";
2
- import { randomUUID } from "crypto";
3
- import { mkdirSync, statSync } from "fs";
4
- import { homedir } from "os";
5
- import { join } from "path";
6
- import { log } from "./logger.js";
7
- import { cosineSimilarity } from "./openai.js";
8
- // ─── Helpers ─────────────────────────────────────────────────────────────────
9
- function generateId(prefix) {
10
- return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
11
- }
12
- function nowISO() {
13
- return new Date().toISOString();
14
- }
15
- function jsonOrNull(val) {
16
- if (val === undefined || val === null)
17
- return null;
18
- return JSON.stringify(val);
19
- }
20
- function parseJsonArray(val) {
21
- if (!val)
22
- return [];
23
- try {
24
- return JSON.parse(val);
25
- }
26
- catch {
27
- return [];
28
- }
29
- }
30
- function parseJsonObject(val) {
31
- if (!val)
32
- return {};
33
- try {
34
- return JSON.parse(val);
35
- }
36
- catch {
37
- return {};
38
- }
39
- }
40
- // ─── Database Initialization ─────────────────────────────────────────────────
41
- const SCHEMA_VERSION = 5;
42
- // ─── Migrations ──────────────────────────────────────────────────────────────
43
1
  /**
44
- * Migration functions keyed by target schema version.
45
- * Each migration upgrades from version (key - 1) to version (key).
46
- * Add new migrations here when SCHEMA_VERSION is bumped.
2
+ * Memory subsystem barrel re-export.
3
+ *
4
+ * All memory functionality lives in src/data/memory/ modules.
5
+ * This file re-exports everything so existing `import … from "./memory.js"`
6
+ * statements continue to work unchanged.
47
7
  */
48
- const MIGRATIONS = {
49
- 2: (db) => {
50
- db.exec(`
51
- CREATE TABLE IF NOT EXISTS note_embeddings (
52
- note_id TEXT PRIMARY KEY,
53
- embedding BLOB NOT NULL,
54
- model TEXT NOT NULL DEFAULT 'text-embedding-3-small',
55
- created_at TEXT NOT NULL
56
- );
57
- CREATE INDEX IF NOT EXISTS idx_emb_note ON note_embeddings(note_id);
58
- `);
59
- },
60
- 3: (db) => {
61
- // Add priority column: 0=normal, 1=notable, 2=high importance
62
- // Use try/catch because new databases already have the column in SCHEMA_SQL
63
- try {
64
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN priority INTEGER NOT NULL DEFAULT 0`);
65
- }
66
- catch {
67
- // Column already exists — safe to ignore
68
- }
69
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL`);
70
- },
71
- 4: (db) => {
72
- // Add thread_id column: NULL = global, number = thread-scoped
73
- try {
74
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN thread_id INTEGER`);
75
- }
76
- catch {
77
- // Column already exists — safe to ignore
78
- }
79
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL`);
80
- // Backfill thread_id from source episodes
81
- const notes = db.prepare(`SELECT note_id, source_episodes FROM semantic_notes WHERE thread_id IS NULL`).all();
82
- const update = db.prepare(`UPDATE semantic_notes SET thread_id = ? WHERE note_id = ?`);
83
- let backfilled = 0;
84
- for (const note of notes) {
85
- let episodeIds = [];
86
- try {
87
- episodeIds = JSON.parse(note.source_episodes ?? "[]");
88
- }
89
- catch { /* ignore */ }
90
- if (episodeIds.length === 0)
91
- continue;
92
- const placeholders = episodeIds.map(() => "?").join(",");
93
- const rows = db.prepare(`SELECT thread_id, COUNT(*) as cnt FROM episodes WHERE episode_id IN (${placeholders}) GROUP BY thread_id ORDER BY cnt DESC LIMIT 1`).all(...episodeIds);
94
- if (rows.length > 0 && rows[0].thread_id != null) {
95
- update.run(rows[0].thread_id, note.note_id);
96
- backfilled++;
97
- }
98
- }
99
- if (backfilled > 0) {
100
- log.info(`[migration-4] Backfilled thread_id on ${backfilled}/${notes.length} existing notes.`);
101
- }
102
- },
103
- 5: (db) => {
104
- // Widen CHECK constraints on episodes table to include 'operator_reaction'
105
- // type and 'reaction' modality. SQLite does not support ALTER COLUMN, so we
106
- // must recreate the table.
107
- db.exec(`
108
- CREATE TABLE IF NOT EXISTS episodes_new (
109
- episode_id TEXT PRIMARY KEY,
110
- session_id TEXT NOT NULL,
111
- thread_id INTEGER NOT NULL,
112
- timestamp TEXT NOT NULL,
113
- type TEXT NOT NULL CHECK(type IN ('operator_message','agent_action','system_event','operator_reaction')),
114
- modality TEXT NOT NULL CHECK(modality IN ('text','voice','photo','video_note','document','mixed','reaction')),
115
- content TEXT NOT NULL,
116
- topic_tags TEXT,
117
- importance REAL NOT NULL DEFAULT 0.5,
118
- consolidated INTEGER DEFAULT 0,
119
- accessed_count INTEGER DEFAULT 0,
120
- last_accessed TEXT,
121
- created_at TEXT NOT NULL
122
- );
123
- INSERT INTO episodes_new SELECT * FROM episodes;
124
- DROP TABLE episodes;
125
- ALTER TABLE episodes_new RENAME TO episodes;
126
- CREATE INDEX IF NOT EXISTS idx_ep_thread_time ON episodes(thread_id, timestamp DESC);
127
- CREATE INDEX IF NOT EXISTS idx_ep_importance ON episodes(importance DESC);
128
- CREATE INDEX IF NOT EXISTS idx_ep_uncons ON episodes(consolidated) WHERE consolidated = 0;
129
- `);
130
- },
131
- };
132
- /**
133
- * Read the current schema version from the database.
134
- * Returns 1 if no version is recorded (initial schema).
135
- */
136
- function getCurrentSchemaVersion(db) {
137
- try {
138
- const row = db
139
- .prepare("SELECT MAX(version) as v FROM schema_version")
140
- .get();
141
- return row?.v ?? 1;
142
- }
143
- catch {
144
- // Table may not exist yet on first run
145
- return 0;
146
- }
147
- }
148
- /**
149
- * Run any pending migrations sequentially from the current stored version
150
- * up to SCHEMA_VERSION. Each migration runs inside a transaction.
151
- */
152
- function runMigrations(db) {
153
- const currentVersion = getCurrentSchemaVersion(db);
154
- log.info(`[memory] Current schema version: ${currentVersion}, target: ${SCHEMA_VERSION}`);
155
- for (let v = currentVersion + 1; v <= SCHEMA_VERSION; v++) {
156
- const migration = MIGRATIONS[v];
157
- if (migration) {
158
- try {
159
- // Run DDL migrations outside transactions — SQLite DDL + transactions
160
- // can have subtle issues in WAL mode with better-sqlite3.
161
- migration(db);
162
- db.prepare("INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (?, ?)").run(v, nowISO());
163
- log.info(`[memory] Migrated schema to version ${v}`);
164
- }
165
- catch (err) {
166
- log.error(`[memory] Migration ${v} FAILED: ${err instanceof Error ? err.message : String(err)}`);
167
- throw err;
168
- }
169
- }
170
- }
171
- }
172
- const SCHEMA_SQL = `
173
- CREATE TABLE IF NOT EXISTS episodes (
174
- episode_id TEXT PRIMARY KEY,
175
- session_id TEXT NOT NULL,
176
- thread_id INTEGER NOT NULL,
177
- timestamp TEXT NOT NULL,
178
- type TEXT NOT NULL CHECK(type IN ('operator_message','agent_action','system_event','operator_reaction')),
179
- modality TEXT NOT NULL CHECK(modality IN ('text','voice','photo','video_note','document','mixed','reaction')),
180
- content TEXT NOT NULL,
181
- topic_tags TEXT,
182
- importance REAL NOT NULL DEFAULT 0.5,
183
- consolidated INTEGER DEFAULT 0,
184
- accessed_count INTEGER DEFAULT 0,
185
- last_accessed TEXT,
186
- created_at TEXT NOT NULL
187
- );
188
-
189
- CREATE INDEX IF NOT EXISTS idx_ep_thread_time ON episodes(thread_id, timestamp DESC);
190
- CREATE INDEX IF NOT EXISTS idx_ep_importance ON episodes(importance DESC);
191
- CREATE INDEX IF NOT EXISTS idx_ep_uncons ON episodes(consolidated) WHERE consolidated = 0;
192
-
193
- CREATE TABLE IF NOT EXISTS semantic_notes (
194
- note_id TEXT PRIMARY KEY,
195
- type TEXT NOT NULL CHECK(type IN ('fact','preference','pattern','entity','relationship')),
196
- content TEXT NOT NULL,
197
- keywords TEXT NOT NULL,
198
- confidence REAL NOT NULL DEFAULT 0.5,
199
- source_episodes TEXT,
200
- linked_notes TEXT,
201
- link_reasons TEXT,
202
- valid_from TEXT NOT NULL,
203
- valid_to TEXT,
204
- superseded_by TEXT,
205
- access_count INTEGER DEFAULT 0,
206
- last_accessed TEXT,
207
- priority INTEGER NOT NULL DEFAULT 0,
208
- thread_id INTEGER,
209
- created_at TEXT NOT NULL,
210
- updated_at TEXT NOT NULL
211
- );
212
-
213
- CREATE INDEX IF NOT EXISTS idx_sem_type ON semantic_notes(type);
214
- CREATE INDEX IF NOT EXISTS idx_sem_conf ON semantic_notes(confidence DESC);
215
- CREATE INDEX IF NOT EXISTS idx_sem_valid ON semantic_notes(valid_to) WHERE valid_to IS NULL;
216
- CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL;
217
- CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL;
218
-
219
- CREATE TABLE IF NOT EXISTS procedures (
220
- procedure_id TEXT PRIMARY KEY,
221
- name TEXT NOT NULL,
222
- type TEXT NOT NULL CHECK(type IN ('workflow','habit','tool_pattern','template')),
223
- description TEXT NOT NULL,
224
- steps TEXT,
225
- trigger_conditions TEXT,
226
- success_rate REAL DEFAULT 0.5,
227
- times_executed INTEGER DEFAULT 0,
228
- last_executed_at TEXT,
229
- learned_from TEXT,
230
- corrections TEXT,
231
- related_procedures TEXT,
232
- confidence REAL DEFAULT 0.5,
233
- created_at TEXT NOT NULL,
234
- updated_at TEXT NOT NULL
235
- );
236
-
237
- CREATE INDEX IF NOT EXISTS idx_proc_name ON procedures(name);
238
- CREATE INDEX IF NOT EXISTS idx_proc_type ON procedures(type);
239
-
240
- CREATE TABLE IF NOT EXISTS meta_topic_index (
241
- topic TEXT PRIMARY KEY,
242
- semantic_count INTEGER DEFAULT 0,
243
- procedural_count INTEGER DEFAULT 0,
244
- last_updated TEXT,
245
- avg_confidence REAL DEFAULT 0.5,
246
- total_accesses INTEGER DEFAULT 0
247
- );
248
-
249
- CREATE TABLE IF NOT EXISTS meta_consolidation_log (
250
- id INTEGER PRIMARY KEY AUTOINCREMENT,
251
- run_at TEXT NOT NULL,
252
- episodes_processed INTEGER,
253
- notes_created INTEGER,
254
- duration_ms INTEGER
255
- );
256
-
257
- CREATE TABLE IF NOT EXISTS voice_signatures (
258
- id INTEGER PRIMARY KEY AUTOINCREMENT,
259
- episode_id TEXT NOT NULL,
260
- emotion TEXT,
261
- arousal REAL,
262
- dominance REAL,
263
- valence REAL,
264
- speech_rate REAL,
265
- mean_pitch_hz REAL,
266
- pitch_std_hz REAL,
267
- jitter REAL,
268
- shimmer REAL,
269
- hnr_db REAL,
270
- audio_events TEXT,
271
- duration_sec REAL,
272
- created_at TEXT NOT NULL
273
- );
274
-
275
- CREATE INDEX IF NOT EXISTS idx_voice_ep ON voice_signatures(episode_id);
276
- CREATE INDEX IF NOT EXISTS idx_voice_time ON voice_signatures(created_at DESC);
277
-
278
- CREATE TABLE IF NOT EXISTS note_embeddings (
279
- note_id TEXT PRIMARY KEY,
280
- embedding BLOB NOT NULL,
281
- model TEXT NOT NULL DEFAULT 'text-embedding-3-small',
282
- created_at TEXT NOT NULL
283
- );
284
-
285
- CREATE INDEX IF NOT EXISTS idx_emb_note ON note_embeddings(note_id);
286
-
287
- CREATE TABLE IF NOT EXISTS schema_version (
288
- version INTEGER PRIMARY KEY,
289
- applied_at TEXT NOT NULL
290
- );
291
- `;
292
- export function initMemoryDb() {
293
- const dbDir = join(homedir(), ".remote-copilot-mcp");
294
- mkdirSync(dbDir, { recursive: true });
295
- const dbPath = join(dbDir, "memory.db");
296
- const db = new BetterSqlite3(dbPath);
297
- db.pragma("journal_mode = WAL");
298
- db.pragma("foreign_keys = ON");
299
- // Create all tables
300
- db.exec(SCHEMA_SQL);
301
- // Record base schema version for brand-new databases only
302
- const versionCount = db.prepare("SELECT COUNT(*) as cnt FROM schema_version").get().cnt;
303
- if (versionCount === 0) {
304
- // New database — record version 1 as the base, then run all migrations up to SCHEMA_VERSION
305
- db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (1, ?)").run(nowISO());
306
- }
307
- else {
308
- // Repair: older code may have recorded SCHEMA_VERSION prematurely without running migrations.
309
- // Detect by checking if version 3 was recorded but the priority column is missing.
310
- const hasV3 = db.prepare("SELECT version FROM schema_version WHERE version = 3").get();
311
- if (hasV3) {
312
- const cols = db.prepare("PRAGMA table_info(semantic_notes)").all();
313
- const hasPriority = cols.some(c => c.name === "priority");
314
- if (!hasPriority) {
315
- // Version 3 was recorded but migration never ran — reset to version 2
316
- db.prepare("DELETE FROM schema_version WHERE version >= 3").run();
317
- log.warn("[memory] Repaired: schema_version was ahead of actual migrations, reset to v2");
318
- }
319
- }
320
- const hasV4 = db.prepare("SELECT version FROM schema_version WHERE version = 4").get();
321
- if (hasV4) {
322
- const cols = db.prepare("PRAGMA table_info(semantic_notes)").all();
323
- const hasThreadId = cols.some(c => c.name === "thread_id");
324
- if (!hasThreadId) {
325
- db.prepare("DELETE FROM schema_version WHERE version >= 4").run();
326
- log.warn("[memory] Repaired: schema_version was ahead of actual migrations, reset to v3");
327
- }
328
- }
329
- }
330
- // Run any pending migrations (will upgrade from stored version to SCHEMA_VERSION)
331
- runMigrations(db);
332
- // Direct repair: ensure priority column exists regardless of migration state.
333
- // This handles edge cases where migrations fail silently or the migration system
334
- // recorded a version without actually applying the schema change.
335
- {
336
- const cols = db.prepare("PRAGMA table_info(semantic_notes)").all();
337
- if (!cols.some(c => c.name === "priority")) {
338
- log.info("[memory] Direct repair: adding missing priority column");
339
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN priority INTEGER NOT NULL DEFAULT 0`);
340
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_priority ON semantic_notes(priority DESC) WHERE valid_to IS NULL`);
341
- }
342
- if (!cols.some(c => c.name === "thread_id")) {
343
- log.info("[memory] Direct repair: adding missing thread_id column");
344
- db.exec(`ALTER TABLE semantic_notes ADD COLUMN thread_id INTEGER`);
345
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sem_thread ON semantic_notes(thread_id) WHERE valid_to IS NULL`);
346
- }
347
- }
348
- return db;
349
- }
350
- // ─── Row → Interface mappers ─────────────────────────────────────────────────
351
- function rowToEpisode(row) {
352
- return {
353
- episodeId: row.episode_id,
354
- sessionId: row.session_id,
355
- threadId: row.thread_id,
356
- timestamp: row.timestamp,
357
- type: row.type,
358
- modality: row.modality,
359
- content: parseJsonObject(row.content),
360
- topicTags: parseJsonArray(row.topic_tags),
361
- importance: row.importance,
362
- consolidated: row.consolidated === 1,
363
- accessedCount: row.accessed_count,
364
- lastAccessed: row.last_accessed ?? null,
365
- createdAt: row.created_at,
366
- };
367
- }
368
- function rowToSemanticNote(row) {
369
- return {
370
- noteId: row.note_id,
371
- type: row.type,
372
- content: row.content,
373
- keywords: parseJsonArray(row.keywords),
374
- confidence: row.confidence,
375
- priority: row.priority ?? 0,
376
- threadId: row.thread_id ?? null,
377
- sourceEpisodes: parseJsonArray(row.source_episodes),
378
- linkedNotes: parseJsonArray(row.linked_notes),
379
- linkReasons: parseJsonObject(row.link_reasons),
380
- validFrom: row.valid_from,
381
- validTo: row.valid_to ?? null,
382
- supersededBy: row.superseded_by ?? null,
383
- accessCount: row.access_count,
384
- lastAccessed: row.last_accessed ?? null,
385
- createdAt: row.created_at,
386
- updatedAt: row.updated_at,
387
- };
388
- }
389
- function rowToProcedure(row) {
390
- return {
391
- procedureId: row.procedure_id,
392
- name: row.name,
393
- type: row.type,
394
- description: row.description,
395
- steps: parseJsonArray(row.steps),
396
- triggerConditions: parseJsonArray(row.trigger_conditions),
397
- successRate: row.success_rate,
398
- timesExecuted: row.times_executed,
399
- lastExecutedAt: row.last_executed_at ?? null,
400
- learnedFrom: parseJsonArray(row.learned_from),
401
- corrections: parseJsonArray(row.corrections),
402
- relatedProcedures: parseJsonArray(row.related_procedures),
403
- confidence: row.confidence,
404
- createdAt: row.created_at,
405
- updatedAt: row.updated_at,
406
- };
407
- }
408
- function rowToTopicEntry(row) {
409
- return {
410
- topic: row.topic,
411
- semanticCount: row.semantic_count,
412
- proceduralCount: row.procedural_count,
413
- lastUpdated: row.last_updated ?? null,
414
- avgConfidence: row.avg_confidence,
415
- totalAccesses: row.total_accesses,
416
- };
417
- }
418
- // ─── Episodic Memory ─────────────────────────────────────────────────────────
419
- export function saveEpisode(db, episode) {
420
- const id = generateId("ep");
421
- const now = nowISO();
422
- db.prepare(`INSERT INTO episodes
423
- (episode_id, session_id, thread_id, timestamp, type, modality, content, topic_tags, importance, consolidated, accessed_count, created_at)
424
- 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);
425
- return id;
426
- }
427
- export function getRecentEpisodes(db, threadId, limit = 20) {
428
- const rows = db
429
- .prepare(`SELECT * FROM episodes WHERE thread_id = ? ORDER BY timestamp DESC LIMIT ?`)
430
- .all(threadId, limit);
431
- return rows.map(rowToEpisode);
432
- }
433
- function getUnconsolidatedEpisodes(db, threadId, limit = 50) {
434
- const rows = db
435
- .prepare(`SELECT * FROM episodes WHERE thread_id = ? AND consolidated = 0 ORDER BY timestamp ASC LIMIT ?`)
436
- .all(threadId, limit);
437
- return rows.map(rowToEpisode);
438
- }
439
- function markConsolidated(db, episodeIds) {
440
- if (episodeIds.length === 0)
441
- return;
442
- const stmt = db.prepare(`UPDATE episodes SET consolidated = 1 WHERE episode_id = ?`);
443
- const txn = db.transaction(() => {
444
- for (const id of episodeIds) {
445
- stmt.run(id);
446
- }
447
- });
448
- txn();
449
- }
450
- // ─── Semantic Memory ─────────────────────────────────────────────────────────
451
- export function saveSemanticNote(db, note) {
452
- const id = generateId("sn");
453
- const now = nowISO();
454
- db.prepare(`INSERT INTO semantic_notes
455
- (note_id, type, content, keywords, confidence, priority, thread_id, source_episodes, linked_notes, link_reasons, valid_from, access_count, created_at, updated_at)
456
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`).run(id, note.type, note.content, JSON.stringify(note.keywords), Math.max(0, Math.min(1, note.confidence ?? 0.5)), Math.max(0, Math.min(2, note.priority ?? 0)), note.threadId ?? null, jsonOrNull(note.sourceEpisodes), null, null, now, now, now);
457
- // Update topic index for each keyword
458
- updateTopicIndexForKeywords(db, note.keywords, "semantic");
459
- return id;
460
- }
461
- export function searchSemanticNotes(db, query, options) {
462
- const maxResults = options?.maxResults ?? 10;
463
- const terms = query
464
- .toLowerCase()
465
- .split(/\s+/)
466
- .filter((t) => t.length > 1);
467
- if (terms.length === 0)
468
- return [];
469
- // Build LIKE conditions: each term must match content OR keywords
470
- // Escape SQL LIKE wildcards in search terms
471
- const conditions = [];
472
- const params = [];
473
- for (const term of terms) {
474
- const escaped = term.replace(/%/g, "\\%").replace(/_/g, "\\_");
475
- conditions.push(`(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(keywords) LIKE ? ESCAPE '\\')`);
476
- params.push(`%${escaped}%`, `%${escaped}%`);
477
- }
478
- let sql = `SELECT * FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL AND (${conditions.join(" AND ")})`;
479
- if (options?.types && options.types.length > 0) {
480
- const placeholders = options.types.map(() => "?").join(",");
481
- sql += ` AND type IN (${placeholders})`;
482
- params.push(...options.types);
483
- }
484
- sql += ` ORDER BY confidence DESC, access_count DESC LIMIT ?`;
485
- params.push(maxResults);
486
- const rows = db.prepare(sql).all(...params);
487
- // Update access counts
488
- if (!options?.skipAccessTracking) {
489
- const now = nowISO();
490
- const updateStmt = db.prepare(`UPDATE semantic_notes SET access_count = access_count + 1, last_accessed = ? WHERE note_id = ?`);
491
- const txn = db.transaction(() => {
492
- for (const row of rows) {
493
- updateStmt.run(now, row.note_id);
494
- }
495
- });
496
- txn();
497
- }
498
- return rows.map(rowToSemanticNote);
499
- }
500
- export function searchSemanticNotesRanked(db, query, options) {
501
- const maxResults = options?.maxResults ?? 10;
502
- const minMatchRatio = options?.minMatchRatio ?? 0.4; // require at least 40% of terms to match
503
- const terms = query.toLowerCase().split(/\\s+/).filter(t => t.length > 1);
504
- if (terms.length === 0)
505
- return [];
506
- // Use OR to get broad recall
507
- const conditions = [];
508
- const params = [];
509
- for (const term of terms) {
510
- const escaped = term.replace(/%/g, "\\\\%").replace(/_/g, "\\\\_");
511
- conditions.push(`(LOWER(content) LIKE ? ESCAPE '\\\\' OR LOWER(keywords) LIKE ? ESCAPE '\\\\')`);
512
- params.push(`%${escaped}%`, `%${escaped}%`);
513
- }
514
- let sql = `SELECT * FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL AND (${conditions.join(" OR ")})`;
515
- // Thread filtering: show notes from this thread + global notes
516
- if (options?.threadId !== undefined) {
517
- sql += ` AND (thread_id IS NULL OR thread_id = ?)`;
518
- params.push(options.threadId);
519
- }
520
- if (options?.types && options.types.length > 0) {
521
- const placeholders = options.types.map(() => "?").join(",");
522
- sql += ` AND type IN (${placeholders})`;
523
- params.push(...options.types);
524
- }
525
- sql += ` ORDER BY confidence DESC, access_count DESC LIMIT ?`;
526
- params.push(maxResults * 3); // fetch more to allow scoring/filtering
527
- const rows = db.prepare(sql).all(...params);
528
- const allNotes = rows.map(rowToSemanticNote);
529
- // Score by how many terms match
530
- const minMatches = Math.max(2, Math.ceil(terms.length * minMatchRatio));
531
- const scored = allNotes.map(n => {
532
- const text = (n.content + " " + n.keywords.join(" ")).toLowerCase();
533
- let matchCount = 0;
534
- for (const term of terms) {
535
- if (text.includes(term))
536
- matchCount++;
537
- }
538
- return { ...n, _matchCount: matchCount };
539
- })
540
- .filter(n => n._matchCount >= minMatches)
541
- .sort((a, b) => {
542
- if (b._matchCount !== a._matchCount)
543
- return b._matchCount - a._matchCount;
544
- return b.confidence - a.confidence;
545
- })
546
- .slice(0, maxResults);
547
- const notes = scored;
548
- // Update access counts
549
- if (!options?.skipAccessTracking) {
550
- const now = nowISO();
551
- const updateStmt = db.prepare(`UPDATE semantic_notes SET access_count = access_count + 1, last_accessed = ? WHERE note_id = ?`);
552
- db.transaction(() => {
553
- for (const note of notes)
554
- updateStmt.run(now, note.noteId);
555
- })();
556
- }
557
- return notes;
558
- }
559
- export function getTopSemanticNotes(db, options) {
560
- const limit = options?.limit ?? 10;
561
- const sortBy = options?.sortBy ?? "confidence";
562
- const validSortColumns = {
563
- confidence: "confidence DESC",
564
- access_count: "access_count DESC",
565
- created_at: "created_at DESC",
566
- };
567
- const orderClause = validSortColumns[sortBy] ?? "confidence DESC";
568
- let sql = `SELECT * FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL`;
569
- const params = [];
570
- if (options?.type) {
571
- sql += ` AND type = ?`;
572
- params.push(options.type);
573
- }
574
- sql += ` ORDER BY ${orderClause} LIMIT ?`;
575
- params.push(limit);
576
- const rows = db.prepare(sql).all(...params);
577
- return rows.map(rowToSemanticNote);
578
- }
579
- export function updateSemanticNote(db, noteId, updates) {
580
- const now = nowISO();
581
- const setClauses = ["updated_at = ?"];
582
- const params = [now];
583
- if (updates.content !== undefined) {
584
- setClauses.push("content = ?");
585
- params.push(updates.content);
586
- }
587
- if (updates.confidence !== undefined) {
588
- setClauses.push("confidence = ?");
589
- params.push(updates.confidence);
590
- }
591
- if (updates.priority !== undefined) {
592
- setClauses.push("priority = ?");
593
- params.push(Math.max(0, Math.min(2, updates.priority)));
594
- }
595
- if (updates.keywords !== undefined) {
596
- setClauses.push("keywords = ?");
597
- params.push(JSON.stringify(updates.keywords));
598
- }
599
- if (updates.linkedNotes !== undefined) {
600
- setClauses.push("linked_notes = ?");
601
- params.push(JSON.stringify(updates.linkedNotes));
602
- }
603
- if (updates.linkReasons !== undefined) {
604
- setClauses.push("link_reasons = ?");
605
- params.push(JSON.stringify(updates.linkReasons));
606
- }
607
- params.push(noteId);
608
- db.prepare(`UPDATE semantic_notes SET ${setClauses.join(", ")} WHERE note_id = ?`).run(...params);
609
- }
610
- export function supersedeNote(db, oldNoteId, newNote) {
611
- // Inherit thread_id from the old note being superseded
612
- const oldRow = db.prepare(`SELECT thread_id FROM semantic_notes WHERE note_id = ?`).get(oldNoteId);
613
- const newId = saveSemanticNote(db, {
614
- type: newNote.type,
615
- content: newNote.content,
616
- keywords: newNote.keywords,
617
- confidence: newNote.confidence,
618
- priority: newNote.priority,
619
- threadId: oldRow?.thread_id ?? null,
620
- sourceEpisodes: newNote.sourceEpisodes,
621
- });
622
- const now = nowISO();
623
- db.prepare(`UPDATE semantic_notes SET superseded_by = ?, valid_to = ?, updated_at = ? WHERE note_id = ?`).run(newId, now, now, oldNoteId);
624
- // Create bidirectional link: add old note to new note's linked_notes
625
- const newRow = db.prepare(`SELECT linked_notes, link_reasons FROM semantic_notes WHERE note_id = ?`).get(newId);
626
- const currentLinked = parseJsonArray(newRow?.linked_notes);
627
- const currentReasons = parseJsonObject(newRow?.link_reasons);
628
- if (!currentLinked.includes(oldNoteId)) {
629
- currentLinked.push(oldNoteId);
630
- }
631
- currentReasons[oldNoteId] = "supersedes";
632
- updateSemanticNote(db, newId, {
633
- linkedNotes: currentLinked,
634
- linkReasons: currentReasons,
635
- });
636
- return newId;
637
- }
638
- /**
639
- * Find existing active notes that potentially conflict with a newly saved note.
640
- * Matches notes with the same type and at least 2 keywords in common.
641
- * Returns matching notes (excluding the note itself) for the agent to review.
642
- */
643
- export function findPotentialConflicts(db, noteId) {
644
- const row = db.prepare(`SELECT note_id, type, keywords FROM semantic_notes WHERE note_id = ?`).get(noteId);
645
- if (!row)
646
- return [];
647
- const noteKeywords = row.keywords ? JSON.parse(row.keywords) : [];
648
- if (noteKeywords.length < 2)
649
- return [];
650
- // Fetch all active notes of the same type (excluding this note and superseded ones)
651
- const candidates = db.prepare(`SELECT * FROM semantic_notes
652
- WHERE type = ? AND note_id != ? AND valid_to IS NULL AND superseded_by IS NULL`).all(row.type, noteId);
653
- const lowerKeywords = new Set(noteKeywords.map(k => k.toLowerCase()));
654
- return candidates
655
- .filter(c => {
656
- const cKeywords = c.keywords ? JSON.parse(c.keywords) : [];
657
- const overlap = cKeywords.filter(k => lowerKeywords.has(k.toLowerCase())).length;
658
- return overlap >= 2;
659
- })
660
- .map(rowToSemanticNote);
661
- }
662
- // ─── Procedural Memory ──────────────────────────────────────────────────────
663
- export function saveProcedure(db, proc) {
664
- const id = generateId("pr");
665
- const now = nowISO();
666
- db.prepare(`INSERT INTO procedures
667
- (procedure_id, name, type, description, steps, trigger_conditions, success_rate, times_executed, learned_from, corrections, related_procedures, confidence, created_at, updated_at)
668
- VALUES (?, ?, ?, ?, ?, ?, 0.5, 0, ?, ?, ?, 0.5, ?, ?)`).run(id, proc.name, proc.type, proc.description, jsonOrNull(proc.steps), jsonOrNull(proc.triggerConditions), null, // learned_from
669
- null, // corrections
670
- null, // related_procedures
671
- now, now);
672
- // Update topic index based on procedure name words
673
- const keywords = proc.name
674
- .toLowerCase()
675
- .split(/\s+/)
676
- .filter((w) => w.length > 2);
677
- updateTopicIndexForKeywords(db, keywords, "procedural");
678
- return id;
679
- }
680
- export function searchProcedures(db, query, maxResults = 10) {
681
- const terms = query
682
- .toLowerCase()
683
- .split(/\s+/)
684
- .filter((t) => t.length > 1);
685
- if (terms.length === 0)
686
- return [];
687
- const conditions = [];
688
- const params = [];
689
- for (const term of terms) {
690
- const escaped = term.replace(/%/g, "\\%").replace(/_/g, "\\_");
691
- conditions.push(`(LOWER(name) LIKE ? ESCAPE '\\' OR LOWER(description) LIKE ? ESCAPE '\\' OR LOWER(steps) LIKE ? ESCAPE '\\' OR LOWER(trigger_conditions) LIKE ? ESCAPE '\\')`);
692
- params.push(`%${escaped}%`, `%${escaped}%`, `%${escaped}%`, `%${escaped}%`);
693
- }
694
- const sql = `SELECT * FROM procedures WHERE ${conditions.join(" OR ")} ORDER BY confidence DESC, success_rate DESC LIMIT ?`;
695
- params.push(maxResults);
696
- const rows = db.prepare(sql).all(...params);
697
- return rows.map(rowToProcedure);
698
- }
699
- export function updateProcedure(db, procedureId, updates) {
700
- const now = nowISO();
701
- const setClauses = ["updated_at = ?"];
702
- const params = [now];
703
- if (updates.description !== undefined) {
704
- setClauses.push("description = ?");
705
- params.push(updates.description);
706
- }
707
- if (updates.steps !== undefined) {
708
- setClauses.push("steps = ?");
709
- params.push(JSON.stringify(updates.steps));
710
- }
711
- if (updates.triggerConditions !== undefined) {
712
- setClauses.push("trigger_conditions = ?");
713
- params.push(JSON.stringify(updates.triggerConditions));
714
- }
715
- if (updates.successRate !== undefined) {
716
- setClauses.push("success_rate = ?");
717
- params.push(updates.successRate);
718
- }
719
- if (updates.timesExecuted !== undefined) {
720
- setClauses.push("times_executed = ?");
721
- params.push(updates.timesExecuted);
722
- }
723
- if (updates.corrections !== undefined) {
724
- setClauses.push("corrections = ?");
725
- params.push(JSON.stringify(updates.corrections));
726
- }
727
- if (updates.confidence !== undefined) {
728
- setClauses.push("confidence = ?");
729
- params.push(updates.confidence);
730
- }
731
- params.push(procedureId);
732
- db.prepare(`UPDATE procedures SET ${setClauses.join(", ")} WHERE procedure_id = ?`).run(...params);
733
- }
734
- // ─── Meta Memory ─────────────────────────────────────────────────────────────
735
- function updateTopicIndexForKeywords(db, keywords, layer) {
736
- const now = nowISO();
737
- const col = layer === "semantic" ? "semantic_count" : "procedural_count";
738
- const upsertStmt = db.prepare(`INSERT INTO meta_topic_index (topic, ${col}, last_updated)
739
- VALUES (?, 1, ?)
740
- ON CONFLICT(topic) DO UPDATE SET
741
- ${col} = ${col} + 1,
742
- last_updated = ?,
743
- total_accesses = total_accesses + 1`);
744
- const txn = db.transaction(() => {
745
- for (const kw of keywords) {
746
- const normalised = kw.toLowerCase().trim();
747
- if (normalised.length > 1) {
748
- upsertStmt.run(normalised, now, now);
749
- }
750
- }
751
- });
752
- txn();
753
- }
754
- function decrementTopicIndexForKeywords(db, keywords, layer) {
755
- const col = layer === "semantic" ? "semantic_count" : "procedural_count";
756
- const decrementStmt = db.prepare(`UPDATE meta_topic_index SET ${col} = MAX(${col} - 1, 0) WHERE topic = ?`);
757
- const deleteStmt = db.prepare(`DELETE FROM meta_topic_index WHERE topic = ? AND semantic_count <= 0 AND procedural_count <= 0`);
758
- const txn = db.transaction(() => {
759
- for (const kw of keywords) {
760
- const normalised = kw.toLowerCase().trim();
761
- if (normalised.length > 1) {
762
- decrementStmt.run(normalised);
763
- deleteStmt.run(normalised);
764
- }
765
- }
766
- });
767
- txn();
768
- }
769
- export function getMemoryStatus(db, threadId) {
770
- const totalEpisodes = db.prepare(`SELECT COUNT(*) as cnt FROM episodes WHERE thread_id = ?`).get(threadId).cnt;
771
- const unconsolidatedEpisodes = db
772
- .prepare(`SELECT COUNT(*) as cnt FROM episodes WHERE thread_id = ? AND consolidated = 0`)
773
- .get(threadId).cnt;
774
- const totalSemanticNotes = db.prepare(`SELECT COUNT(*) as cnt FROM semantic_notes WHERE valid_to IS NULL AND superseded_by IS NULL`).get().cnt;
775
- const totalProcedures = db.prepare(`SELECT COUNT(*) as cnt FROM procedures`).get().cnt;
776
- const totalVoiceSignatures = db.prepare(`SELECT COUNT(*) as cnt FROM voice_signatures`).get().cnt;
777
- const lastConsolidationRow = db
778
- .prepare(`SELECT run_at FROM meta_consolidation_log ORDER BY run_at DESC LIMIT 1`)
779
- .get();
780
- const topTopics = getTopicIndex(db).slice(0, 5);
781
- // Database file size
782
- const dbPath = join(homedir(), ".remote-copilot-mcp", "memory.db");
783
- let dbSizeBytes = 0;
784
- try {
785
- dbSizeBytes = statSync(dbPath).size;
786
- }
787
- catch {
788
- // file might not exist yet or be inaccessible
789
- }
790
- return {
791
- totalEpisodes,
792
- unconsolidatedEpisodes,
793
- totalSemanticNotes,
794
- totalProcedures,
795
- totalVoiceSignatures,
796
- lastConsolidation: lastConsolidationRow?.run_at ?? null,
797
- topTopics,
798
- dbSizeBytes,
799
- };
800
- }
801
- export function getTopicIndex(db) {
802
- const rows = db
803
- .prepare(`SELECT * FROM meta_topic_index ORDER BY total_accesses DESC, semantic_count DESC LIMIT 50`)
804
- .all();
805
- return rows.map(rowToTopicEntry);
806
- }
807
- function logConsolidation(db, log) {
808
- db.prepare(`INSERT INTO meta_consolidation_log
809
- (run_at, episodes_processed, notes_created, duration_ms)
810
- VALUES (?, ?, ?, ?)`).run(nowISO(), log.episodesProcessed, log.notesCreated, log.durationMs);
811
- }
812
- // ─── Voice Signatures ────────────────────────────────────────────────────────
813
- export function saveVoiceSignature(db, sig) {
814
- db.prepare(`INSERT INTO voice_signatures
815
- (episode_id, emotion, arousal, dominance, valence, speech_rate, mean_pitch_hz, pitch_std_hz, jitter, shimmer, hnr_db, audio_events, duration_sec, created_at)
816
- 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());
817
- }
818
- function getVoiceBaseline(db, dayRange = 30) {
819
- const cutoff = new Date(Date.now() - dayRange * 24 * 60 * 60 * 1000).toISOString();
820
- const row = db
821
- .prepare(`SELECT
822
- AVG(arousal) AS avg_arousal,
823
- AVG(dominance) AS avg_dominance,
824
- AVG(valence) AS avg_valence,
825
- AVG(speech_rate) AS avg_speech_rate,
826
- AVG(mean_pitch_hz) AS avg_mean_pitch_hz,
827
- AVG(pitch_std_hz) AS avg_pitch_std_hz,
828
- AVG(jitter) AS avg_jitter,
829
- AVG(shimmer) AS avg_shimmer,
830
- AVG(hnr_db) AS avg_hnr_db,
831
- COUNT(*) AS sample_count
832
- FROM voice_signatures
833
- WHERE created_at >= ?`)
834
- .get(cutoff);
835
- if (!row || row.sample_count === 0)
836
- return null;
837
- return {
838
- avgArousal: row.avg_arousal,
839
- avgDominance: row.avg_dominance,
840
- avgValence: row.avg_valence,
841
- avgSpeechRate: row.avg_speech_rate,
842
- avgMeanPitchHz: row.avg_mean_pitch_hz,
843
- avgPitchStdHz: row.avg_pitch_std_hz,
844
- avgJitter: row.avg_jitter,
845
- avgShimmer: row.avg_shimmer,
846
- avgHnrDb: row.avg_hnr_db,
847
- sampleCount: row.sample_count,
848
- };
849
- }
850
- // ─── Bootstrap ───────────────────────────────────────────────────────────────
851
- export function assembleBootstrap(db, threadId) {
852
- const status = getMemoryStatus(db, threadId ?? 0);
853
- const recentEpisodes = threadId ? getRecentEpisodes(db, threadId, 5) : [];
854
- // Thread-aware Key Knowledge:
855
- // 1. Thread-scoped notes (up to 6) for the current thread
856
- // 2. Global profile notes (thread_id IS NULL, preference/pattern) up to 4
857
- // If no threadId, show up to 10 null-thread notes (preferences/patterns first)
858
- let sortedNotes;
859
- if (threadId) {
860
- const threadNotes = db
861
- .prepare(`SELECT * FROM semantic_notes
862
- WHERE valid_to IS NULL AND superseded_by IS NULL
863
- AND thread_id = ?
864
- ORDER BY access_count DESC, confidence DESC
865
- LIMIT 6`)
866
- .all(threadId);
867
- const remaining = 10 - threadNotes.length;
868
- const globalNotes = remaining > 0
869
- ? db
870
- .prepare(`SELECT * FROM semantic_notes
871
- WHERE valid_to IS NULL AND superseded_by IS NULL
872
- AND thread_id IS NULL
873
- AND type IN ('preference', 'pattern')
874
- ORDER BY access_count DESC, confidence DESC
875
- LIMIT ?`)
876
- .all(remaining)
877
- : [];
878
- sortedNotes = [...threadNotes, ...globalNotes].map(rowToSemanticNote);
879
- }
880
- else {
881
- const nullThreadNotes = db
882
- .prepare(`SELECT * FROM semantic_notes
883
- WHERE valid_to IS NULL AND superseded_by IS NULL
884
- AND thread_id IS NULL
885
- ORDER BY
886
- CASE WHEN type IN ('preference', 'pattern') THEN 0 ELSE 1 END,
887
- access_count DESC, confidence DESC
888
- LIMIT 10`)
889
- .all();
890
- sortedNotes = nullThreadNotes.map(rowToSemanticNote);
891
- }
892
- const activeProcedures = db
893
- .prepare(`SELECT * FROM procedures ORDER BY times_executed DESC, confidence DESC LIMIT 5`)
894
- .all();
895
- const procedures = activeProcedures.map(rowToProcedure);
896
- const baseline = getVoiceBaseline(db);
897
- const lines = [];
898
- lines.push("# Memory Briefing");
899
- lines.push("");
900
- // Status
901
- lines.push("## Status");
902
- lines.push(`- Episodes: ${status.totalEpisodes} (${status.unconsolidatedEpisodes} unconsolidated)`);
903
- lines.push(`- Semantic notes: ${status.totalSemanticNotes}`);
904
- lines.push(`- Procedures: ${status.totalProcedures}`);
905
- lines.push(`- Voice signatures: ${status.totalVoiceSignatures}`);
906
- if (status.lastConsolidation) {
907
- lines.push(`- Last consolidation: ${status.lastConsolidation}`);
908
- }
909
- lines.push(`- DB size: ${(status.dbSizeBytes / 1024).toFixed(1)} KB`);
910
- lines.push("");
911
- // Recent episodes
912
- if (recentEpisodes.length > 0) {
913
- lines.push("## Recent Episodes");
914
- for (const ep of recentEpisodes) {
915
- const summary = typeof ep.content === "object" && ep.content !== null
916
- ? ep.content.text ?? ep.content.caption ?? JSON.stringify(ep.content).slice(0, 120)
917
- : String(ep.content).slice(0, 120);
918
- lines.push(`- [${ep.type}/${ep.modality}] ${summary} (${ep.timestamp})`);
919
- }
920
- lines.push("");
921
- }
922
- // Key knowledge
923
- if (sortedNotes.length > 0) {
924
- lines.push("## Key Knowledge");
925
- for (const note of sortedNotes) {
926
- const savedDate = note.createdAt ? note.createdAt.slice(0, 10) : 'unknown';
927
- lines.push(`- **[${note.type}]** ${note.content} (conf: ${note.confidence.toFixed(2)}, accessed: ${note.accessCount}x, saved: ${savedDate})`);
928
- }
929
- lines.push("");
930
- }
931
- // Active procedures
932
- if (procedures.length > 0) {
933
- lines.push("## Active Procedures");
934
- for (const proc of procedures) {
935
- lines.push(`- **${proc.name}** (${proc.type}) — success: ${(proc.successRate * 100).toFixed(0)}%, used ${proc.timesExecuted}x`);
936
- if (proc.steps.length > 0) {
937
- lines.push(` Steps: ${proc.steps.join(" → ")}`);
938
- }
939
- }
940
- lines.push("");
941
- }
942
- // Voice baseline
943
- if (baseline && baseline.sampleCount > 0) {
944
- lines.push("## Voice Baseline (30d)");
945
- lines.push(`- Samples: ${baseline.sampleCount}`);
946
- if (baseline.avgValence !== null)
947
- lines.push(`- Avg valence: ${baseline.avgValence.toFixed(2)}`);
948
- if (baseline.avgArousal !== null)
949
- lines.push(`- Avg arousal: ${baseline.avgArousal.toFixed(2)}`);
950
- if (baseline.avgSpeechRate !== null)
951
- lines.push(`- Avg speech rate: ${baseline.avgSpeechRate.toFixed(1)}`);
952
- if (baseline.avgMeanPitchHz !== null)
953
- lines.push(`- Avg pitch: ${baseline.avgMeanPitchHz.toFixed(1)} Hz`);
954
- lines.push("");
955
- }
956
- // Topics
957
- if (status.topTopics.length > 0) {
958
- lines.push("## Top Topics");
959
- for (const t of status.topTopics) {
960
- lines.push(`- ${t.topic} (semantic: ${t.semanticCount}, procedural: ${t.proceduralCount})`);
961
- }
962
- lines.push("");
963
- }
964
- return lines.join("\n");
965
- }
966
- /**
967
- * Compact memory refresh — a condensed briefing for injection during long sessions.
968
- * Much shorter than full bootstrap. Designed to re-ground the agent after context compaction.
969
- */
970
- export function assembleCompactRefresh(db, threadId) {
971
- const topNotes = getTopSemanticNotes(db, { limit: 6, sortBy: "access_count" });
972
- if (topNotes.length === 0)
973
- return "";
974
- const lines = [];
975
- lines.push("## Memory Refresh");
976
- for (const note of topNotes) {
977
- lines.push(`- **[${note.type}]** ${note.content}`);
978
- }
979
- return lines.join("\n");
980
- }
981
- // ─── Intelligent Consolidation ───────────────────────────────────────────────
982
- // PRIVACY NOTE: This function sends conversation episode excerpts to OpenAI's
983
- // API for knowledge extraction and consolidation. Operators can disable this
984
- // by setting the environment variable CONSOLIDATION_ENABLED=false (or "0").
985
- export async function runIntelligentConsolidation(db, threadId, options) {
986
- // Opt-out: allow operators to disable consolidation for privacy reasons
987
- const consolidationEnabled = process.env.CONSOLIDATION_ENABLED;
988
- if (consolidationEnabled === "false" || consolidationEnabled === "0") {
989
- return {
990
- episodesProcessed: 0,
991
- notesCreated: 0,
992
- durationMs: 0,
993
- details: ["Consolidation disabled via CONSOLIDATION_ENABLED env var."],
994
- };
995
- }
996
- const startMs = Date.now();
997
- const maxEpisodes = options?.maxEpisodes ?? 30;
998
- const dryRun = options?.dryRun ?? false;
999
- const episodes = getUnconsolidatedEpisodes(db, threadId, maxEpisodes);
1000
- if (episodes.length === 0) {
1001
- return {
1002
- episodesProcessed: 0,
1003
- notesCreated: 0,
1004
- durationMs: Date.now() - startMs,
1005
- details: ["Nothing to consolidate."],
1006
- };
1007
- }
1008
- // Format episodes for the prompt
1009
- const episodesText = episodes
1010
- .map((ep, i) => {
1011
- const content = typeof ep.content === "object" && ep.content !== null
1012
- ? ep.content.text ?? ep.content.caption ?? JSON.stringify(ep.content)
1013
- : String(ep.content);
1014
- return `[${i + 1}] (${ep.type}/${ep.modality}, ${ep.timestamp}) ${content}`;
1015
- })
1016
- .join("\n");
1017
- // ── Contradiction detection: find existing notes related to these episodes ──
1018
- // Extract keywords from episodes to search for potentially conflicting notes
1019
- const episodeWords = episodesText.toLowerCase()
1020
- .replace(/[^a-z0-9\s]/g, " ")
1021
- .split(/\s+/)
1022
- .filter(w => w.length > 3);
1023
- const wordFreq = new Map();
1024
- const stopWords = new Set(["this", "that", "with", "from", "have", "been", "will", "would", "could", "should", "about", "there", "their", "which", "when", "what", "were", "they", "than", "then", "also", "just", "more", "some", "into", "over", "after", "before", "other", "very", "your", "here"]);
1025
- for (const w of episodeWords) {
1026
- if (!stopWords.has(w))
1027
- wordFreq.set(w, (wordFreq.get(w) ?? 0) + 1);
1028
- }
1029
- const topKeywords = [...wordFreq.entries()]
1030
- .sort((a, b) => b[1] - a[1])
1031
- .slice(0, 12)
1032
- .map(([w]) => w);
1033
- let existingNotesSection = "";
1034
- if (topKeywords.length > 0) {
1035
- try {
1036
- const related = searchSemanticNotesRanked(db, topKeywords.join(" "), {
1037
- maxResults: 15,
1038
- skipAccessTracking: true,
1039
- minMatchRatio: 0.2, // broader recall for contradiction scan
1040
- });
1041
- if (related.length > 0) {
1042
- existingNotesSection = `\n\nExisting memory notes (potentially related):
1043
- ${related.map(n => `[${n.noteId}] (${n.type}, conf: ${n.confidence}) ${n.content}`).join("\n")}`;
1044
- }
1045
- }
1046
- catch (_) { /* non-fatal — proceed without existing notes */ }
1047
- }
1048
- const systemPrompt = `You are a memory consolidation agent. Analyze these conversation episodes and extract knowledge that should be remembered across sessions.
1049
-
1050
- Episodes:
1051
- ${episodesText}${existingNotesSection}
1052
-
1053
- Output a JSON object with:
1054
- {
1055
- "notes": [
1056
- {
1057
- "type": "fact" | "preference" | "pattern" | "entity" | "relationship",
1058
- "content": "One clear sentence describing the knowledge",
1059
- "keywords": ["keyword1", "keyword2", "keyword3"],
1060
- "confidence": 0.0-1.0,
1061
- "priority": 0 | 1 | 2
1062
- }
1063
- ],
1064
- "supersede": [
1065
- {
1066
- "oldNoteId": "sn_xxx",
1067
- "reason": "Why the old note is outdated/contradicted",
1068
- "newContent": "Updated version of the knowledge",
1069
- "type": "fact",
1070
- "keywords": ["keyword1", "keyword2"],
1071
- "confidence": 0.8,
1072
- "priority": 0 | 1 | 2
1073
- }
1074
- ]
1075
- }
1076
-
1077
- Rules:
1078
- - Only extract information that would be useful in future sessions
1079
- - Preferences are stronger signals than facts (confidence: 0.9)
1080
- - Do not extract trivial/transient information
1081
- - If the operator corrected the agent, extract the correction as a preference
1082
- - Focus on: operator name, preferences, communication style, technical choices, project context
1083
- - CRITICAL: Check existing notes for CONTRADICTIONS. If a new episode contradicts or updates an existing note, add a "supersede" entry. The new episodes represent MORE RECENT information.
1084
- - Common contradictions: decisions changed, projects completed/abandoned, preferences updated, tools/tech switched
1085
- - PRIORITY DETECTION: Infer priority from the operator's language and emotional investment:
1086
- - priority 2 (high importance): operator says "important", "crucial", "I really need", "don't forget", shows strong emotional investment, repeated emphasis
1087
- - priority 1 (notable): operator says "would be nice", "I'd like", "should", mentions something multiple times across conversations
1088
- - priority 0 (normal): default for routine facts, observations, patterns
1089
- - Return {"notes": [], "supersede": []} if nothing notable`;
1090
- let notesCreated = 0;
1091
- const details = [];
1092
- try {
1093
- const apiKey = process.env.OPENAI_API_KEY;
1094
- if (!apiKey) {
1095
- throw new Error("OPENAI_API_KEY not set");
1096
- }
1097
- const controller = new AbortController();
1098
- const timer = setTimeout(() => controller.abort(), 60_000);
1099
- try {
1100
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
1101
- method: "POST",
1102
- headers: {
1103
- "Content-Type": "application/json",
1104
- Authorization: `Bearer ${apiKey}`,
1105
- },
1106
- body: JSON.stringify({
1107
- model: process.env.CONSOLIDATION_MODEL ?? "gpt-4o-mini",
1108
- messages: [
1109
- { role: "system", content: systemPrompt },
1110
- { role: "user", content: "Extract knowledge from the episodes above." },
1111
- ],
1112
- response_format: { type: "json_object" },
1113
- }),
1114
- signal: controller.signal,
1115
- });
1116
- clearTimeout(timer);
1117
- if (!response.ok) {
1118
- const errText = await response.text().catch(() => response.statusText);
1119
- throw new Error(`OpenAI API error: ${response.status} ${errText}`);
1120
- }
1121
- const result = (await response.json());
1122
- const raw = result.choices?.[0]?.message?.content ?? "{}";
1123
- const parsed = JSON.parse(raw);
1124
- const extractedNotes = parsed.notes ?? [];
1125
- const supersedeActions = parsed.supersede ?? [];
1126
- const episodeIds = episodes.map((ep) => ep.episodeId);
1127
- if (!dryRun) {
1128
- for (const note of extractedNotes) {
1129
- const validTypes = ["fact", "preference", "pattern", "entity", "relationship"];
1130
- const noteType = validTypes.includes(note.type)
1131
- ? note.type
1132
- : "fact";
1133
- saveSemanticNote(db, {
1134
- type: noteType,
1135
- content: note.content,
1136
- keywords: Array.isArray(note.keywords) ? note.keywords : [],
1137
- confidence: Math.max(0, Math.min(1, note.confidence ?? 0.5)),
1138
- priority: Math.max(0, Math.min(2, note.priority ?? 0)),
1139
- threadId: threadId,
1140
- sourceEpisodes: episodeIds,
1141
- });
1142
- notesCreated++;
1143
- details.push(`[${noteType}] ${note.content}`);
1144
- }
1145
- // Execute supersede actions — resolve contradictions with existing notes
1146
- let supersededCount = 0;
1147
- for (const action of supersedeActions) {
1148
- if (!action.oldNoteId || !action.newContent)
1149
- continue;
1150
- // Verify old note exists and is still active
1151
- const oldNote = db.prepare(`SELECT note_id FROM semantic_notes WHERE note_id = ? AND valid_to IS NULL AND superseded_by IS NULL`).get(action.oldNoteId);
1152
- if (!oldNote) {
1153
- details.push(`[skip-supersede] ${action.oldNoteId} not found or already superseded`);
1154
- continue;
1155
- }
1156
- try {
1157
- const validTypes = ["fact", "preference", "pattern", "entity", "relationship"];
1158
- const noteType = validTypes.includes(action.type) ? action.type : "fact";
1159
- const newId = supersedeNote(db, action.oldNoteId, {
1160
- type: noteType,
1161
- content: action.newContent,
1162
- keywords: Array.isArray(action.keywords) ? action.keywords : [],
1163
- confidence: Math.max(0, Math.min(1, action.confidence ?? 0.8)),
1164
- priority: Math.max(0, Math.min(2, action.priority ?? 0)),
1165
- sourceEpisodes: episodeIds,
1166
- });
1167
- supersededCount++;
1168
- details.push(`[supersede] ${action.oldNoteId} → ${newId}: ${action.reason}`);
1169
- }
1170
- catch (err) {
1171
- details.push(`[supersede-error] ${action.oldNoteId}: ${err instanceof Error ? err.message : String(err)}`);
1172
- }
1173
- }
1174
- if (supersededCount > 0) {
1175
- log.info(`[memory] Contradiction resolution: superseded ${supersededCount} outdated note(s)`);
1176
- }
1177
- // Mark episodes as consolidated
1178
- markConsolidated(db, episodeIds);
1179
- // Log the consolidation
1180
- logConsolidation(db, {
1181
- episodesProcessed: episodes.length,
1182
- notesCreated: notesCreated + supersededCount,
1183
- durationMs: Date.now() - startMs,
1184
- });
1185
- }
1186
- else {
1187
- for (const note of extractedNotes) {
1188
- details.push(`[dry-run] [${note.type}] ${note.content}`);
1189
- notesCreated++;
1190
- }
1191
- for (const action of supersedeActions) {
1192
- details.push(`[dry-run] [supersede] ${action.oldNoteId} → ${action.reason}`);
1193
- }
1194
- }
1195
- }
1196
- finally {
1197
- clearTimeout(timer);
1198
- }
1199
- }
1200
- catch (err) {
1201
- // Do NOT mark episodes as consolidated on failure — they should be
1202
- // retried on the next consolidation run. Previously this was a silent
1203
- // data-loss bug: a transient OpenAI outage would permanently lose the
1204
- // episodes' knowledge without extracting anything.
1205
- const msg = err instanceof Error ? err.message : String(err);
1206
- log.error(`[memory] Intelligent consolidation failed (episodes NOT marked): ${msg}`);
1207
- details.push(`Consolidation failed (will retry): ${msg}`);
1208
- }
1209
- return {
1210
- episodesProcessed: episodes.length,
1211
- notesCreated,
1212
- durationMs: Date.now() - startMs,
1213
- details,
1214
- };
1215
- }
1216
- // ─── Forget ──────────────────────────────────────────────────────────────────
1217
- export function forgetMemory(db, memoryId, reason) {
1218
- // Determine layer by prefix
1219
- if (memoryId.startsWith("ep_")) {
1220
- const existing = db.prepare(`SELECT episode_id FROM episodes WHERE episode_id = ?`).get(memoryId);
1221
- if (!existing)
1222
- return { layer: "episodic", deleted: false };
1223
- db.transaction(() => {
1224
- db.prepare(`DELETE FROM episodes WHERE episode_id = ?`).run(memoryId);
1225
- // Also delete associated voice signature
1226
- db.prepare(`DELETE FROM voice_signatures WHERE episode_id = ?`).run(memoryId);
1227
- })();
1228
- return { layer: "episodic", deleted: true };
1229
- }
1230
- if (memoryId.startsWith("sn_")) {
1231
- const existing = db.prepare(`SELECT note_id, keywords FROM semantic_notes WHERE note_id = ?`).get(memoryId);
1232
- if (!existing)
1233
- return { layer: "semantic", deleted: false };
1234
- const kws = parseJsonArray(existing.keywords);
1235
- db.transaction(() => {
1236
- db.prepare(`DELETE FROM semantic_notes WHERE note_id = ?`).run(memoryId);
1237
- db.prepare(`DELETE FROM note_embeddings WHERE note_id = ?`).run(memoryId);
1238
- decrementTopicIndexForKeywords(db, kws, "semantic");
1239
- })();
1240
- return { layer: "semantic", deleted: true };
1241
- }
1242
- if (memoryId.startsWith("pr_")) {
1243
- const existing = db.prepare(`SELECT procedure_id, name FROM procedures WHERE procedure_id = ?`).get(memoryId);
1244
- if (!existing)
1245
- return { layer: "procedural", deleted: false };
1246
- const kws = existing.name.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
1247
- db.transaction(() => {
1248
- db.prepare(`DELETE FROM procedures WHERE procedure_id = ?`).run(memoryId);
1249
- decrementTopicIndexForKeywords(db, kws, "procedural");
1250
- })();
1251
- return { layer: "procedural", deleted: true };
1252
- }
1253
- // Unknown prefix — try all layers
1254
- let row = db.prepare(`SELECT episode_id FROM episodes WHERE episode_id = ?`).get(memoryId);
1255
- if (row) {
1256
- db.transaction(() => {
1257
- db.prepare(`DELETE FROM episodes WHERE episode_id = ?`).run(memoryId);
1258
- db.prepare(`DELETE FROM voice_signatures WHERE episode_id = ?`).run(memoryId);
1259
- })();
1260
- return { layer: "episodic", deleted: true };
1261
- }
1262
- row = db.prepare(`SELECT note_id, keywords FROM semantic_notes WHERE note_id = ?`).get(memoryId);
1263
- if (row) {
1264
- const kws = parseJsonArray(row.keywords);
1265
- db.transaction(() => {
1266
- db.prepare(`DELETE FROM semantic_notes WHERE note_id = ?`).run(memoryId);
1267
- db.prepare(`DELETE FROM note_embeddings WHERE note_id = ?`).run(memoryId);
1268
- decrementTopicIndexForKeywords(db, kws, "semantic");
1269
- })();
1270
- return { layer: "semantic", deleted: true };
1271
- }
1272
- row = db.prepare(`SELECT procedure_id, name FROM procedures WHERE procedure_id = ?`).get(memoryId);
1273
- if (row) {
1274
- const kws = (row.name).toLowerCase().split(/\s+/).filter((w) => w.length > 2);
1275
- db.transaction(() => {
1276
- db.prepare(`DELETE FROM procedures WHERE procedure_id = ?`).run(memoryId);
1277
- decrementTopicIndexForKeywords(db, kws, "procedural");
1278
- })();
1279
- return { layer: "procedural", deleted: true };
1280
- }
1281
- return { layer: "unknown", deleted: false };
1282
- }
1283
- // ─── Embedding-based Semantic Search ─────────────────────────────────────────
1284
- /** Store a pre-computed embedding vector for a semantic note. */
1285
- export function saveNoteEmbedding(db, noteId, embedding) {
1286
- const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
1287
- db.prepare(`INSERT OR REPLACE INTO note_embeddings (note_id, embedding, model, created_at) VALUES (?, ?, ?, ?)`).run(noteId, buf, "text-embedding-3-small", nowISO());
1288
- }
1289
- /** Load all note embeddings into memory for cosine similarity search. */
1290
- function loadAllEmbeddings(db, threadId) {
1291
- // When threadId is provided, return embeddings for notes in that thread OR global notes (thread_id IS NULL)
1292
- let sql = `SELECT ne.note_id, ne.embedding FROM note_embeddings ne
1293
- JOIN semantic_notes sn ON sn.note_id = ne.note_id
1294
- WHERE sn.valid_to IS NULL AND sn.superseded_by IS NULL`;
1295
- const params = [];
1296
- if (threadId !== undefined) {
1297
- sql += ` AND (sn.thread_id IS NULL OR sn.thread_id = ?)`;
1298
- params.push(threadId);
1299
- }
1300
- const rows = db.prepare(sql).all(...params);
1301
- const map = new Map();
1302
- for (const row of rows) {
1303
- map.set(row.note_id, new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4));
1304
- }
1305
- return map;
1306
- }
1307
- /**
1308
- * Search semantic notes using embedding cosine similarity.
1309
- * Returns notes sorted by similarity score, filtered by minimum threshold.
1310
- */
1311
- export function searchByEmbedding(db, queryEmbedding, options) {
1312
- const maxResults = options?.maxResults ?? 5;
1313
- const minSimilarity = options?.minSimilarity ?? 0.3;
1314
- // Load embeddings — filtered by thread when provided
1315
- const embeddings = loadAllEmbeddings(db, options?.threadId);
1316
- // Compute similarities
1317
- const scores = [];
1318
- for (const [noteId, emb] of embeddings) {
1319
- const sim = cosineSimilarity(queryEmbedding, emb);
1320
- if (sim >= minSimilarity) {
1321
- scores.push({ noteId, similarity: sim });
1322
- }
1323
- }
1324
- // Sort by similarity descending
1325
- scores.sort((a, b) => b.similarity - a.similarity);
1326
- const topIds = scores.slice(0, maxResults);
1327
- if (topIds.length === 0)
1328
- return [];
1329
- // Fetch full notes
1330
- const placeholders = topIds.map(() => "?").join(",");
1331
- const rows = db.prepare(`SELECT * FROM semantic_notes WHERE note_id IN (${placeholders})`).all(...topIds.map(s => s.noteId));
1332
- const noteMap = new Map();
1333
- for (const row of rows) {
1334
- const note = rowToSemanticNote(row);
1335
- noteMap.set(note.noteId, note);
1336
- }
1337
- // Update access counts
1338
- if (!options?.skipAccessTracking) {
1339
- const now = nowISO();
1340
- const updateStmt = db.prepare(`UPDATE semantic_notes SET access_count = access_count + 1, last_accessed = ? WHERE note_id = ?`);
1341
- db.transaction(() => {
1342
- for (const s of topIds)
1343
- updateStmt.run(now, s.noteId);
1344
- })();
1345
- }
1346
- // Return in similarity order
1347
- return topIds
1348
- .map(s => {
1349
- const note = noteMap.get(s.noteId);
1350
- return note ? { ...note, similarity: s.similarity } : null;
1351
- })
1352
- .filter((n) => n !== null);
1353
- }
1354
- /** Get note IDs that don't have embeddings yet (for backfill). */
1355
- export function getNotesWithoutEmbeddings(db) {
1356
- return db.prepare(`SELECT sn.note_id as noteId, sn.content FROM semantic_notes sn
1357
- LEFT JOIN note_embeddings ne ON ne.note_id = sn.note_id
1358
- WHERE ne.note_id IS NULL AND sn.valid_to IS NULL AND sn.superseded_by IS NULL`).all();
1359
- }
8
+ export * from "./data/memory/index.js";
1360
9
  //# sourceMappingURL=memory.js.map