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.
- package/dist/config.d.ts +1 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -49
- package/dist/config.js.map +1 -1
- package/dist/dashboard/presets.d.ts +18 -0
- package/dist/dashboard/presets.d.ts.map +1 -0
- package/dist/dashboard/presets.js +78 -0
- package/dist/dashboard/presets.js.map +1 -0
- package/dist/dashboard/routes.d.ts +33 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +283 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard.d.ts +6 -29
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +6 -1158
- package/dist/dashboard.js.map +1 -1
- package/dist/data/file-storage.d.ts +19 -0
- package/dist/data/file-storage.d.ts.map +1 -0
- package/dist/data/file-storage.js +58 -0
- package/dist/data/file-storage.js.map +1 -0
- package/dist/data/memory/bootstrap.d.ts +40 -0
- package/dist/data/memory/bootstrap.d.ts.map +1 -0
- package/dist/data/memory/bootstrap.js +240 -0
- package/dist/data/memory/bootstrap.js.map +1 -0
- package/dist/data/memory/consolidation.d.ts +12 -0
- package/dist/data/memory/consolidation.d.ts.map +1 -0
- package/dist/data/memory/consolidation.js +248 -0
- package/dist/data/memory/consolidation.js.map +1 -0
- package/dist/data/memory/episodes.d.ts +34 -0
- package/dist/data/memory/episodes.d.ts.map +1 -0
- package/dist/data/memory/episodes.js +89 -0
- package/dist/data/memory/episodes.js.map +1 -0
- package/dist/data/memory/index.d.ts +14 -0
- package/dist/data/memory/index.d.ts.map +1 -0
- package/dist/data/memory/index.js +14 -0
- package/dist/data/memory/index.js.map +1 -0
- package/dist/data/memory/procedures.d.ts +42 -0
- package/dist/data/memory/procedures.d.ts.map +1 -0
- package/dist/data/memory/procedures.js +122 -0
- package/dist/data/memory/procedures.js.map +1 -0
- package/dist/data/memory/schema.d.ts +11 -0
- package/dist/data/memory/schema.d.ts.map +1 -0
- package/dist/data/memory/schema.js +327 -0
- package/dist/data/memory/schema.js.map +1 -0
- package/dist/data/memory/semantic.d.ts +94 -0
- package/dist/data/memory/semantic.d.ts.map +1 -0
- package/dist/data/memory/semantic.js +385 -0
- package/dist/data/memory/semantic.js.map +1 -0
- package/dist/data/memory/voice-sig.d.ts +33 -0
- package/dist/data/memory/voice-sig.d.ts.map +1 -0
- package/dist/data/memory/voice-sig.js +48 -0
- package/dist/data/memory/voice-sig.js.map +1 -0
- package/dist/data/templates.d.ts +19 -0
- package/dist/data/templates.d.ts.map +1 -0
- package/dist/data/templates.js +46 -0
- package/dist/data/templates.js.map +1 -0
- package/dist/dispatcher.d.ts +5 -97
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +5 -525
- package/dist/dispatcher.js.map +1 -1
- package/dist/drive.d.ts.map +1 -1
- package/dist/drive.js +3 -1
- package/dist/drive.js.map +1 -1
- package/dist/index.d.ts +4 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -289
- package/dist/index.js.map +1 -1
- package/dist/integrations/openai/chat.d.ts +29 -0
- package/dist/integrations/openai/chat.d.ts.map +1 -0
- package/dist/integrations/openai/chat.js +84 -0
- package/dist/integrations/openai/chat.js.map +1 -0
- package/dist/integrations/openai/index.d.ts +6 -0
- package/dist/integrations/openai/index.d.ts.map +1 -0
- package/dist/integrations/openai/index.js +6 -0
- package/dist/integrations/openai/index.js.map +1 -0
- package/dist/integrations/openai/speech.d.ts +21 -0
- package/dist/integrations/openai/speech.d.ts.map +1 -0
- package/dist/integrations/openai/speech.js +75 -0
- package/dist/integrations/openai/speech.js.map +1 -0
- package/dist/integrations/openai/video.d.ts +15 -0
- package/dist/integrations/openai/video.d.ts.map +1 -0
- package/dist/integrations/openai/video.js +131 -0
- package/dist/integrations/openai/video.js.map +1 -0
- package/dist/integrations/openai/vision.d.ts +23 -0
- package/dist/integrations/openai/vision.d.ts.map +1 -0
- package/dist/integrations/openai/vision.js +116 -0
- package/dist/integrations/openai/vision.js.map +1 -0
- package/dist/integrations/openai/voice-emotion.d.ts +41 -0
- package/dist/integrations/openai/voice-emotion.d.ts.map +1 -0
- package/dist/integrations/openai/voice-emotion.js +50 -0
- package/dist/integrations/openai/voice-emotion.js.map +1 -0
- package/dist/integrations/telegram/types.d.ts +112 -0
- package/dist/integrations/telegram/types.d.ts.map +1 -0
- package/dist/integrations/telegram/types.js +6 -0
- package/dist/integrations/telegram/types.js.map +1 -0
- package/dist/memory.d.ts +6 -205
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -1357
- package/dist/memory.js.map +1 -1
- package/dist/openai.d.ts +11 -102
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +14 -421
- package/dist/openai.js.map +1 -1
- package/dist/response-builders.d.ts +1 -11
- package/dist/response-builders.d.ts.map +1 -1
- package/dist/response-builders.js +2 -38
- package/dist/response-builders.js.map +1 -1
- package/dist/server/factory.d.ts +17 -0
- package/dist/server/factory.d.ts.map +1 -0
- package/dist/server/factory.js +279 -0
- package/dist/server/factory.js.map +1 -0
- package/dist/services/dispatcher/broker.d.ts +83 -0
- package/dist/services/dispatcher/broker.d.ts.map +1 -0
- package/dist/services/dispatcher/broker.js +175 -0
- package/dist/services/dispatcher/broker.js.map +1 -0
- package/dist/services/dispatcher/index.d.ts +7 -0
- package/dist/services/dispatcher/index.d.ts.map +1 -0
- package/dist/services/dispatcher/index.js +7 -0
- package/dist/services/dispatcher/index.js.map +1 -0
- package/dist/services/dispatcher/lock.d.ts +25 -0
- package/dist/services/dispatcher/lock.d.ts.map +1 -0
- package/dist/services/dispatcher/lock.js +111 -0
- package/dist/services/dispatcher/lock.js.map +1 -0
- package/dist/services/dispatcher/poller.d.ts +19 -0
- package/dist/services/dispatcher/poller.d.ts.map +1 -0
- package/dist/services/dispatcher/poller.js +269 -0
- package/dist/services/dispatcher/poller.js.map +1 -0
- package/dist/telegram.d.ts +2 -88
- package/dist/telegram.d.ts.map +1 -1
- package/dist/telegram.js +2 -0
- package/dist/telegram.js.map +1 -1
- package/dist/tool-definitions.d.ts +1 -14
- package/dist/tool-definitions.d.ts.map +1 -1
- package/dist/tool-definitions.js +1 -403
- package/dist/tool-definitions.js.map +1 -1
- package/dist/tools/definitions.d.ts +15 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +404 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +2 -0
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/wait/drive-handler.d.ts +61 -0
- package/dist/tools/wait/drive-handler.d.ts.map +1 -0
- package/dist/tools/wait/drive-handler.js +138 -0
- package/dist/tools/wait/drive-handler.js.map +1 -0
- package/dist/tools/wait/index.d.ts +8 -0
- package/dist/tools/wait/index.d.ts.map +1 -0
- package/dist/tools/wait/index.js +8 -0
- package/dist/tools/wait/index.js.map +1 -0
- package/dist/tools/wait/media-processor.d.ts +52 -0
- package/dist/tools/wait/media-processor.d.ts.map +1 -0
- package/dist/tools/wait/media-processor.js +261 -0
- package/dist/tools/wait/media-processor.js.map +1 -0
- package/dist/tools/wait/message-delivery.d.ts +63 -0
- package/dist/tools/wait/message-delivery.d.ts.map +1 -0
- package/dist/tools/wait/message-delivery.js +281 -0
- package/dist/tools/wait/message-delivery.js.map +1 -0
- package/dist/tools/wait/poll-loop.d.ts +72 -0
- package/dist/tools/wait/poll-loop.d.ts.map +1 -0
- package/dist/tools/wait/poll-loop.js +280 -0
- package/dist/tools/wait/poll-loop.js.map +1 -0
- package/dist/tools/wait/reaction-handler.d.ts +49 -0
- package/dist/tools/wait/reaction-handler.d.ts.map +1 -0
- package/dist/tools/wait/reaction-handler.js +126 -0
- package/dist/tools/wait/reaction-handler.js.map +1 -0
- package/dist/tools/wait/task-handler.d.ts +40 -0
- package/dist/tools/wait/task-handler.d.ts.map +1 -0
- package/dist/tools/wait/task-handler.js +41 -0
- package/dist/tools/wait/task-handler.js.map +1 -0
- package/dist/tools/wait-tool.d.ts +3 -69
- package/dist/tools/wait-tool.d.ts.map +1 -1
- package/dist/tools/wait-tool.js +3 -876
- package/dist/tools/wait-tool.js.map +1 -1
- package/package.json +1 -1
- package/templates/daily-review.default.md +26 -0
- 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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
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
|