psychmem 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +632 -0
- package/dist/adapters/claude-code/index.d.ts +125 -0
- package/dist/adapters/claude-code/index.d.ts.map +1 -0
- package/dist/adapters/claude-code/index.js +398 -0
- package/dist/adapters/claude-code/index.js.map +1 -0
- package/dist/adapters/opencode/index.d.ts +50 -0
- package/dist/adapters/opencode/index.d.ts.map +1 -0
- package/dist/adapters/opencode/index.js +793 -0
- package/dist/adapters/opencode/index.js.map +1 -0
- package/dist/adapters/types.d.ts +226 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +461 -0
- package/dist/cli.js.map +1 -0
- package/dist/hooks/index.d.ts +92 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +304 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/post-tool-use.d.ts +26 -0
- package/dist/hooks/post-tool-use.d.ts.map +1 -0
- package/dist/hooks/post-tool-use.js +69 -0
- package/dist/hooks/post-tool-use.js.map +1 -0
- package/dist/hooks/session-end.d.ts +32 -0
- package/dist/hooks/session-end.d.ts.map +1 -0
- package/dist/hooks/session-end.js +66 -0
- package/dist/hooks/session-end.js.map +1 -0
- package/dist/hooks/session-start.d.ts +55 -0
- package/dist/hooks/session-start.d.ts.map +1 -0
- package/dist/hooks/session-start.js +173 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/stop.d.ts +72 -0
- package/dist/hooks/stop.d.ts.map +1 -0
- package/dist/hooks/stop.js +273 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/context-sweep.d.ts +107 -0
- package/dist/memory/context-sweep.d.ts.map +1 -0
- package/dist/memory/context-sweep.js +557 -0
- package/dist/memory/context-sweep.js.map +1 -0
- package/dist/memory/patterns.d.ts +106 -0
- package/dist/memory/patterns.d.ts.map +1 -0
- package/dist/memory/patterns.js +613 -0
- package/dist/memory/patterns.js.map +1 -0
- package/dist/memory/selective-memory.d.ts +78 -0
- package/dist/memory/selective-memory.d.ts.map +1 -0
- package/dist/memory/selective-memory.js +227 -0
- package/dist/memory/selective-memory.js.map +1 -0
- package/dist/memory/structural-analyzer.d.ts +75 -0
- package/dist/memory/structural-analyzer.d.ts.map +1 -0
- package/dist/memory/structural-analyzer.js +359 -0
- package/dist/memory/structural-analyzer.js.map +1 -0
- package/dist/retrieval/index.d.ts +106 -0
- package/dist/retrieval/index.d.ts.map +1 -0
- package/dist/retrieval/index.js +291 -0
- package/dist/retrieval/index.js.map +1 -0
- package/dist/storage/database.d.ts +138 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +748 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/sqlite-adapter.d.ts +35 -0
- package/dist/storage/sqlite-adapter.d.ts.map +1 -0
- package/dist/storage/sqlite-adapter.js +103 -0
- package/dist/storage/sqlite-adapter.js.map +1 -0
- package/dist/transcript/index.d.ts +8 -0
- package/dist/transcript/index.d.ts.map +1 -0
- package/dist/transcript/index.js +6 -0
- package/dist/transcript/index.js.map +1 -0
- package/dist/transcript/parser.d.ts +93 -0
- package/dist/transcript/parser.d.ts.map +1 -0
- package/dist/transcript/parser.js +373 -0
- package/dist/transcript/parser.js.map +1 -0
- package/dist/transcript/sweep.d.ts +75 -0
- package/dist/transcript/sweep.d.ts.map +1 -0
- package/dist/transcript/sweep.js +202 -0
- package/dist/transcript/sweep.js.map +1 -0
- package/dist/types/index.d.ts +328 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +80 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/paths.d.ts +19 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/paths.js.map +1 -0
- package/hooks/hooks.json +54 -0
- package/package.json +83 -0
- package/plugin.js +45 -0
- package/plugin.json +19 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PsychMem Database Layer
|
|
3
|
+
* SQLite with runtime-agnostic adapter (supports Node.js + Bun)
|
|
4
|
+
*/
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
7
|
+
import { resolveDbPath } from '../utils/paths.js';
|
|
8
|
+
import { createDatabase, isBun } from './sqlite-adapter.js';
|
|
9
|
+
export class MemoryDatabase {
|
|
10
|
+
db;
|
|
11
|
+
config;
|
|
12
|
+
initialized = false;
|
|
13
|
+
_inTransaction = false;
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize database (must be called before use)
|
|
19
|
+
* For sync compatibility, also auto-initializes on first use
|
|
20
|
+
*/
|
|
21
|
+
async init() {
|
|
22
|
+
if (this.initialized)
|
|
23
|
+
return;
|
|
24
|
+
const dbPath = resolveDbPath(this.config.dbPath, this.config.agentType);
|
|
25
|
+
this.db = await createDatabase(dbPath);
|
|
26
|
+
// Set WAL mode
|
|
27
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
28
|
+
this.initializeSchema();
|
|
29
|
+
this.initialized = true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Ensure database is initialized (for backwards compatibility)
|
|
33
|
+
*/
|
|
34
|
+
ensureInit() {
|
|
35
|
+
if (!this.initialized) {
|
|
36
|
+
throw new Error('Database not initialized. Call await db.init() first.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Initialize database schema
|
|
41
|
+
*/
|
|
42
|
+
initializeSchema() {
|
|
43
|
+
// First, create base tables (without project_scope index)
|
|
44
|
+
this.db.exec(`
|
|
45
|
+
-- Schema version table — bump SCHEMA_VERSION constant on breaking changes
|
|
46
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
47
|
+
version INTEGER NOT NULL,
|
|
48
|
+
applied_at TEXT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Sessions table
|
|
52
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
project TEXT NOT NULL,
|
|
55
|
+
started_at TEXT NOT NULL,
|
|
56
|
+
ended_at TEXT,
|
|
57
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
58
|
+
metadata TEXT,
|
|
59
|
+
transcript_path TEXT,
|
|
60
|
+
transcript_watermark INTEGER DEFAULT 0,
|
|
61
|
+
message_watermark INTEGER DEFAULT 0
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- Events table (raw hook events)
|
|
65
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
session_id TEXT NOT NULL,
|
|
68
|
+
hook_type TEXT NOT NULL,
|
|
69
|
+
timestamp TEXT NOT NULL,
|
|
70
|
+
content TEXT NOT NULL,
|
|
71
|
+
tool_name TEXT,
|
|
72
|
+
tool_input TEXT,
|
|
73
|
+
tool_output TEXT,
|
|
74
|
+
metadata TEXT,
|
|
75
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Memory units table (consolidated memories)
|
|
79
|
+
CREATE TABLE IF NOT EXISTS memory_units (
|
|
80
|
+
id TEXT PRIMARY KEY,
|
|
81
|
+
session_id TEXT,
|
|
82
|
+
store TEXT NOT NULL,
|
|
83
|
+
classification TEXT NOT NULL,
|
|
84
|
+
summary TEXT NOT NULL,
|
|
85
|
+
source_event_ids TEXT NOT NULL,
|
|
86
|
+
project_scope TEXT,
|
|
87
|
+
|
|
88
|
+
created_at TEXT NOT NULL,
|
|
89
|
+
updated_at TEXT NOT NULL,
|
|
90
|
+
last_accessed_at TEXT NOT NULL,
|
|
91
|
+
|
|
92
|
+
recency REAL NOT NULL DEFAULT 0,
|
|
93
|
+
frequency INTEGER NOT NULL DEFAULT 1,
|
|
94
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
95
|
+
utility REAL NOT NULL DEFAULT 0.5,
|
|
96
|
+
novelty REAL NOT NULL DEFAULT 0.5,
|
|
97
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
98
|
+
interference REAL NOT NULL DEFAULT 0,
|
|
99
|
+
|
|
100
|
+
strength REAL NOT NULL DEFAULT 0.5,
|
|
101
|
+
decay_rate REAL NOT NULL,
|
|
102
|
+
|
|
103
|
+
tags TEXT,
|
|
104
|
+
associations TEXT,
|
|
105
|
+
|
|
106
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
107
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
108
|
+
|
|
109
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
-- Memory evidence table
|
|
113
|
+
CREATE TABLE IF NOT EXISTS memory_evidence (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
memory_id TEXT NOT NULL,
|
|
116
|
+
event_id TEXT NOT NULL,
|
|
117
|
+
timestamp TEXT NOT NULL,
|
|
118
|
+
contribution TEXT,
|
|
119
|
+
confidence_delta REAL DEFAULT 0,
|
|
120
|
+
FOREIGN KEY (memory_id) REFERENCES memory_units(id),
|
|
121
|
+
FOREIGN KEY (event_id) REFERENCES events(id)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Retrieval logs for feedback learning
|
|
125
|
+
CREATE TABLE IF NOT EXISTS retrieval_logs (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
session_id TEXT NOT NULL,
|
|
128
|
+
memory_id TEXT NOT NULL,
|
|
129
|
+
query TEXT NOT NULL,
|
|
130
|
+
timestamp TEXT NOT NULL,
|
|
131
|
+
was_used INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
user_feedback TEXT,
|
|
133
|
+
relevance_score REAL NOT NULL,
|
|
134
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id),
|
|
135
|
+
FOREIGN KEY (memory_id) REFERENCES memory_units(id)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- User feedback table
|
|
139
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
140
|
+
id TEXT PRIMARY KEY,
|
|
141
|
+
memory_id TEXT,
|
|
142
|
+
type TEXT NOT NULL,
|
|
143
|
+
content TEXT,
|
|
144
|
+
timestamp TEXT NOT NULL,
|
|
145
|
+
FOREIGN KEY (memory_id) REFERENCES memory_units(id)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
-- Indexes for performance (excluding project_scope - added after migration)
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_memory_store ON memory_units(store);
|
|
152
|
+
CREATE INDEX IF NOT EXISTS idx_memory_status ON memory_units(status);
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_memory_strength ON memory_units(strength);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_memory_classification ON memory_units(classification);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_memory_session ON memory_units(session_id);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_session ON retrieval_logs(session_id);
|
|
157
|
+
-- Composite indexes for common query patterns
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_memory_status_strength ON memory_units(status, strength);
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_memory_session_status ON memory_units(session_id, status);
|
|
160
|
+
`);
|
|
161
|
+
// Schema version check — fail loudly on mismatch (prevents silent corruption)
|
|
162
|
+
const SCHEMA_VERSION = 2;
|
|
163
|
+
const row = this.db.prepare('SELECT version FROM schema_version ORDER BY rowid DESC LIMIT 1').get();
|
|
164
|
+
if (!row) {
|
|
165
|
+
// Fresh DB — stamp with current version
|
|
166
|
+
this.db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(SCHEMA_VERSION, new Date().toISOString());
|
|
167
|
+
}
|
|
168
|
+
else if (row.version !== SCHEMA_VERSION) {
|
|
169
|
+
throw new Error(`psychmem DB schema mismatch: expected version ${SCHEMA_VERSION}, found ${row.version}. ` +
|
|
170
|
+
'Delete the database file or run migrations to continue.');
|
|
171
|
+
}
|
|
172
|
+
// Migration: Add project_scope column if it doesn't exist (for existing DBs pre-v1.6)
|
|
173
|
+
// MUST run before creating index on project_scope
|
|
174
|
+
this.migrateProjectScope();
|
|
175
|
+
// Now safe to create index on project_scope (column guaranteed to exist)
|
|
176
|
+
this.db.exec(`
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_memory_project_scope ON memory_units(project_scope);
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Migration: Add project_scope column to existing databases
|
|
182
|
+
*/
|
|
183
|
+
migrateProjectScope() {
|
|
184
|
+
// Check if column exists
|
|
185
|
+
const tableInfo = this.db.prepare(`PRAGMA table_info(memory_units)`).all();
|
|
186
|
+
const hasProjectScope = tableInfo.some(col => col.name === 'project_scope');
|
|
187
|
+
if (!hasProjectScope) {
|
|
188
|
+
this.db.exec(`ALTER TABLE memory_units ADD COLUMN project_scope TEXT`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// ===========================================================================
|
|
192
|
+
// Session Operations
|
|
193
|
+
// ===========================================================================
|
|
194
|
+
createSession(project, metadata, transcriptPath) {
|
|
195
|
+
this.ensureInit();
|
|
196
|
+
const session = {
|
|
197
|
+
id: uuidv4(),
|
|
198
|
+
project,
|
|
199
|
+
startedAt: new Date(),
|
|
200
|
+
status: 'active',
|
|
201
|
+
metadata,
|
|
202
|
+
transcriptPath,
|
|
203
|
+
transcriptWatermark: 0,
|
|
204
|
+
messageWatermark: 0,
|
|
205
|
+
};
|
|
206
|
+
this.db.prepare(`
|
|
207
|
+
INSERT INTO sessions (id, project, started_at, status, metadata, transcript_path, transcript_watermark, message_watermark)
|
|
208
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
209
|
+
`).run(session.id, session.project, session.startedAt.toISOString(), session.status, metadata ? JSON.stringify(metadata) : null, transcriptPath ?? null, 0, 0);
|
|
210
|
+
return session;
|
|
211
|
+
}
|
|
212
|
+
endSession(sessionId, status = 'completed') {
|
|
213
|
+
this.ensureInit();
|
|
214
|
+
this.db.prepare(`
|
|
215
|
+
UPDATE sessions SET ended_at = ?, status = ? WHERE id = ?
|
|
216
|
+
`).run(new Date().toISOString(), status, sessionId);
|
|
217
|
+
}
|
|
218
|
+
getSession(sessionId) {
|
|
219
|
+
this.ensureInit();
|
|
220
|
+
const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(sessionId);
|
|
221
|
+
if (!row)
|
|
222
|
+
return null;
|
|
223
|
+
return this.rowToSession(row);
|
|
224
|
+
}
|
|
225
|
+
getActiveSessions() {
|
|
226
|
+
this.ensureInit();
|
|
227
|
+
const rows = this.db.prepare(`SELECT * FROM sessions WHERE status = 'active'`).all();
|
|
228
|
+
return rows.map(row => this.rowToSession(row));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the current transcript watermark (byte offset) for a session
|
|
232
|
+
*/
|
|
233
|
+
getSessionWatermark(sessionId) {
|
|
234
|
+
this.ensureInit();
|
|
235
|
+
const row = this.db.prepare(`
|
|
236
|
+
SELECT transcript_watermark FROM sessions WHERE id = ?
|
|
237
|
+
`).get(sessionId);
|
|
238
|
+
return row?.transcript_watermark ?? 0;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Update the transcript watermark (byte offset) for a session
|
|
242
|
+
*/
|
|
243
|
+
updateSessionWatermark(sessionId, watermark) {
|
|
244
|
+
this.ensureInit();
|
|
245
|
+
this.db.prepare(`
|
|
246
|
+
UPDATE sessions SET transcript_watermark = ? WHERE id = ?
|
|
247
|
+
`).run(watermark, sessionId);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get the current message watermark (message index) for a session
|
|
251
|
+
* Used by OpenCode adapter to track which messages have been processed
|
|
252
|
+
*/
|
|
253
|
+
getMessageWatermark(sessionId) {
|
|
254
|
+
this.ensureInit();
|
|
255
|
+
const row = this.db.prepare(`
|
|
256
|
+
SELECT message_watermark FROM sessions WHERE id = ?
|
|
257
|
+
`).get(sessionId);
|
|
258
|
+
return row?.message_watermark ?? 0;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Update the message watermark (message index) for a session
|
|
262
|
+
* Used by OpenCode adapter to mark messages as processed
|
|
263
|
+
*/
|
|
264
|
+
updateMessageWatermark(sessionId, watermark) {
|
|
265
|
+
this.ensureInit();
|
|
266
|
+
this.db.prepare(`
|
|
267
|
+
UPDATE sessions SET message_watermark = ? WHERE id = ?
|
|
268
|
+
`).run(watermark, sessionId);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get all memories for a specific session (for deduplication)
|
|
272
|
+
*/
|
|
273
|
+
getSessionMemories(sessionId, status = 'active') {
|
|
274
|
+
this.ensureInit();
|
|
275
|
+
const rows = this.db.prepare(`
|
|
276
|
+
SELECT * FROM memory_units
|
|
277
|
+
WHERE session_id = ? AND status = ?
|
|
278
|
+
ORDER BY created_at DESC
|
|
279
|
+
`).all(sessionId, status);
|
|
280
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
281
|
+
}
|
|
282
|
+
// ===========================================================================
|
|
283
|
+
// Event Operations
|
|
284
|
+
// ===========================================================================
|
|
285
|
+
createEvent(sessionId, hookType, content, options) {
|
|
286
|
+
this.ensureInit();
|
|
287
|
+
const event = {
|
|
288
|
+
id: uuidv4(),
|
|
289
|
+
sessionId,
|
|
290
|
+
hookType,
|
|
291
|
+
timestamp: new Date(),
|
|
292
|
+
content,
|
|
293
|
+
toolName: options?.toolName,
|
|
294
|
+
toolInput: options?.toolInput,
|
|
295
|
+
toolOutput: options?.toolOutput,
|
|
296
|
+
metadata: options?.metadata,
|
|
297
|
+
};
|
|
298
|
+
this.db.prepare(`
|
|
299
|
+
INSERT INTO events (id, session_id, hook_type, timestamp, content, tool_name, tool_input, tool_output, metadata)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
301
|
+
`).run(event.id, event.sessionId, event.hookType, event.timestamp.toISOString(), event.content, event.toolName ?? null, event.toolInput ?? null, event.toolOutput ?? null, event.metadata ? JSON.stringify(event.metadata) : null);
|
|
302
|
+
return event;
|
|
303
|
+
}
|
|
304
|
+
getSessionEvents(sessionId) {
|
|
305
|
+
this.ensureInit();
|
|
306
|
+
const rows = this.db.prepare(`
|
|
307
|
+
SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC
|
|
308
|
+
`).all(sessionId);
|
|
309
|
+
return rows.map(this.rowToEvent);
|
|
310
|
+
}
|
|
311
|
+
getRecentEvents(limit = 100) {
|
|
312
|
+
this.ensureInit();
|
|
313
|
+
const rows = this.db.prepare(`
|
|
314
|
+
SELECT * FROM events ORDER BY timestamp DESC LIMIT ?
|
|
315
|
+
`).all(limit);
|
|
316
|
+
return rows.map(this.rowToEvent);
|
|
317
|
+
}
|
|
318
|
+
// ===========================================================================
|
|
319
|
+
// Memory Unit Operations
|
|
320
|
+
// ===========================================================================
|
|
321
|
+
createMemory(store, classification, summary, sourceEventIds, features = {}) {
|
|
322
|
+
this.ensureInit();
|
|
323
|
+
const now = new Date();
|
|
324
|
+
const decayRate = store === 'stm' ? this.config.stmDecayRate : this.config.ltmDecayRate;
|
|
325
|
+
const memory = {
|
|
326
|
+
id: uuidv4(),
|
|
327
|
+
sessionId: features.sessionId,
|
|
328
|
+
store,
|
|
329
|
+
classification,
|
|
330
|
+
summary,
|
|
331
|
+
sourceEventIds,
|
|
332
|
+
projectScope: features.projectScope,
|
|
333
|
+
createdAt: now,
|
|
334
|
+
updatedAt: now,
|
|
335
|
+
lastAccessedAt: now,
|
|
336
|
+
recency: 0,
|
|
337
|
+
frequency: 1,
|
|
338
|
+
importance: features.importance ?? 0.5,
|
|
339
|
+
utility: features.utility ?? 0.5,
|
|
340
|
+
novelty: features.novelty ?? 0.5,
|
|
341
|
+
confidence: features.confidence ?? 0.5,
|
|
342
|
+
interference: 0,
|
|
343
|
+
strength: this.calculateStrength({
|
|
344
|
+
recency: 0,
|
|
345
|
+
frequency: 1,
|
|
346
|
+
importance: features.importance ?? 0.5,
|
|
347
|
+
utility: features.utility ?? 0.5,
|
|
348
|
+
novelty: features.novelty ?? 0.5,
|
|
349
|
+
confidence: features.confidence ?? 0.5,
|
|
350
|
+
interference: 0,
|
|
351
|
+
}),
|
|
352
|
+
decayRate,
|
|
353
|
+
tags: features.tags ?? [],
|
|
354
|
+
associations: [],
|
|
355
|
+
status: 'active',
|
|
356
|
+
version: 1,
|
|
357
|
+
evidence: [],
|
|
358
|
+
};
|
|
359
|
+
this.db.prepare(`
|
|
360
|
+
INSERT INTO memory_units (
|
|
361
|
+
id, session_id, store, classification, summary, source_event_ids, project_scope,
|
|
362
|
+
created_at, updated_at, last_accessed_at,
|
|
363
|
+
recency, frequency, importance, utility, novelty, confidence, interference,
|
|
364
|
+
strength, decay_rate, tags, associations, status, version
|
|
365
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
366
|
+
`).run(memory.id, memory.sessionId ?? null, memory.store, memory.classification, memory.summary, JSON.stringify(memory.sourceEventIds), memory.projectScope ?? null, memory.createdAt.toISOString(), memory.updatedAt.toISOString(), memory.lastAccessedAt.toISOString(), memory.recency, memory.frequency, memory.importance, memory.utility, memory.novelty, memory.confidence, memory.interference, memory.strength, memory.decayRate, JSON.stringify(memory.tags), JSON.stringify(memory.associations), memory.status, memory.version);
|
|
367
|
+
return memory;
|
|
368
|
+
}
|
|
369
|
+
getMemory(memoryId) {
|
|
370
|
+
this.ensureInit();
|
|
371
|
+
const row = this.db.prepare(`SELECT * FROM memory_units WHERE id = ?`).get(memoryId);
|
|
372
|
+
if (!row)
|
|
373
|
+
return null;
|
|
374
|
+
return this.rowToMemoryUnit(row);
|
|
375
|
+
}
|
|
376
|
+
getMemoriesByStore(store, status = 'active') {
|
|
377
|
+
this.ensureInit();
|
|
378
|
+
const rows = this.db.prepare(`
|
|
379
|
+
SELECT * FROM memory_units
|
|
380
|
+
WHERE store = ? AND status = ?
|
|
381
|
+
ORDER BY strength DESC
|
|
382
|
+
`).all(store, status);
|
|
383
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
384
|
+
}
|
|
385
|
+
getTopMemories(limit = 20, store) {
|
|
386
|
+
this.ensureInit();
|
|
387
|
+
let query = `SELECT * FROM memory_units WHERE status = 'active'`;
|
|
388
|
+
const params = [];
|
|
389
|
+
if (store) {
|
|
390
|
+
query += ` AND store = ?`;
|
|
391
|
+
params.push(store);
|
|
392
|
+
}
|
|
393
|
+
query += ` ORDER BY strength DESC LIMIT ?`;
|
|
394
|
+
params.push(limit);
|
|
395
|
+
const rows = this.db.prepare(query).all(...params);
|
|
396
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get memories filtered by scope for context injection.
|
|
400
|
+
* Returns:
|
|
401
|
+
* - All user-level memories (classification in constraint, preference, learning, procedural)
|
|
402
|
+
* - Project-level memories only if they match the given project
|
|
403
|
+
*
|
|
404
|
+
* @param currentProject - The current project path (used to filter project-level memories)
|
|
405
|
+
* @param limit - Maximum number of memories to return
|
|
406
|
+
* @param store - Optional filter by STM/LTM
|
|
407
|
+
*/
|
|
408
|
+
getMemoriesByScope(currentProject, limit = 20, store) {
|
|
409
|
+
this.ensureInit();
|
|
410
|
+
// User-level classifications (always included)
|
|
411
|
+
const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
|
|
412
|
+
const userClassPlaceholders = userLevelClassifications.map(() => '?').join(', ');
|
|
413
|
+
// Build query: user-level OR (project-level AND matching project)
|
|
414
|
+
let query = `
|
|
415
|
+
SELECT * FROM memory_units
|
|
416
|
+
WHERE status = 'active'
|
|
417
|
+
AND (
|
|
418
|
+
classification IN (${userClassPlaceholders})
|
|
419
|
+
OR (project_scope IS NOT NULL AND project_scope = ?)
|
|
420
|
+
)
|
|
421
|
+
`;
|
|
422
|
+
const params = [...userLevelClassifications, currentProject ?? ''];
|
|
423
|
+
if (store) {
|
|
424
|
+
query += ` AND store = ?`;
|
|
425
|
+
params.push(store);
|
|
426
|
+
}
|
|
427
|
+
query += ` ORDER BY strength DESC LIMIT ?`;
|
|
428
|
+
params.push(limit);
|
|
429
|
+
const rows = this.db.prepare(query).all(...params);
|
|
430
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get only user-level memories (always applicable across all projects)
|
|
434
|
+
*/
|
|
435
|
+
getUserLevelMemories(limit = 20, store) {
|
|
436
|
+
this.ensureInit();
|
|
437
|
+
const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
|
|
438
|
+
const placeholders = userLevelClassifications.map(() => '?').join(', ');
|
|
439
|
+
let query = `
|
|
440
|
+
SELECT * FROM memory_units
|
|
441
|
+
WHERE status = 'active'
|
|
442
|
+
AND classification IN (${placeholders})
|
|
443
|
+
`;
|
|
444
|
+
const params = [...userLevelClassifications];
|
|
445
|
+
if (store) {
|
|
446
|
+
query += ` AND store = ?`;
|
|
447
|
+
params.push(store);
|
|
448
|
+
}
|
|
449
|
+
query += ` ORDER BY strength DESC LIMIT ?`;
|
|
450
|
+
params.push(limit);
|
|
451
|
+
const rows = this.db.prepare(query).all(...params);
|
|
452
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Get project-level memories for a specific project
|
|
456
|
+
*/
|
|
457
|
+
getProjectMemories(project, limit = 20, store) {
|
|
458
|
+
this.ensureInit();
|
|
459
|
+
let query = `
|
|
460
|
+
SELECT * FROM memory_units
|
|
461
|
+
WHERE status = 'active'
|
|
462
|
+
AND project_scope = ?
|
|
463
|
+
`;
|
|
464
|
+
const params = [project];
|
|
465
|
+
if (store) {
|
|
466
|
+
query += ` AND store = ?`;
|
|
467
|
+
params.push(store);
|
|
468
|
+
}
|
|
469
|
+
query += ` ORDER BY strength DESC LIMIT ?`;
|
|
470
|
+
params.push(limit);
|
|
471
|
+
const rows = this.db.prepare(query).all(...params);
|
|
472
|
+
return rows.map(this.rowToMemoryUnit.bind(this));
|
|
473
|
+
}
|
|
474
|
+
updateMemoryStrength(memoryId, strength) {
|
|
475
|
+
this.ensureInit();
|
|
476
|
+
this.db.prepare(`
|
|
477
|
+
UPDATE memory_units
|
|
478
|
+
SET strength = ?, updated_at = ?
|
|
479
|
+
WHERE id = ?
|
|
480
|
+
`).run(strength, new Date().toISOString(), memoryId);
|
|
481
|
+
}
|
|
482
|
+
updateMemoryStatus(memoryId, status) {
|
|
483
|
+
this.ensureInit();
|
|
484
|
+
this.db.prepare(`
|
|
485
|
+
UPDATE memory_units
|
|
486
|
+
SET status = ?, updated_at = ?
|
|
487
|
+
WHERE id = ?
|
|
488
|
+
`).run(status, new Date().toISOString(), memoryId);
|
|
489
|
+
}
|
|
490
|
+
incrementFrequency(memoryId) {
|
|
491
|
+
this.ensureInit();
|
|
492
|
+
const now = new Date().toISOString();
|
|
493
|
+
this.db.prepare(`
|
|
494
|
+
UPDATE memory_units
|
|
495
|
+
SET frequency = frequency + 1, last_accessed_at = ?, updated_at = ?
|
|
496
|
+
WHERE id = ?
|
|
497
|
+
`).run(now, now, memoryId);
|
|
498
|
+
}
|
|
499
|
+
promoteToLtm(memoryId) {
|
|
500
|
+
this.ensureInit();
|
|
501
|
+
this.db.prepare(`
|
|
502
|
+
UPDATE memory_units
|
|
503
|
+
SET store = 'ltm', decay_rate = ?, updated_at = ?
|
|
504
|
+
WHERE id = ?
|
|
505
|
+
`).run(this.config.ltmDecayRate, new Date().toISOString(), memoryId);
|
|
506
|
+
}
|
|
507
|
+
// ===========================================================================
|
|
508
|
+
// Retrieval Log Operations
|
|
509
|
+
// ===========================================================================
|
|
510
|
+
logRetrieval(sessionId, memoryId, query, relevanceScore) {
|
|
511
|
+
this.ensureInit();
|
|
512
|
+
const id = uuidv4();
|
|
513
|
+
this.db.prepare(`
|
|
514
|
+
INSERT INTO retrieval_logs (id, session_id, memory_id, query, timestamp, relevance_score)
|
|
515
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
516
|
+
`).run(id, sessionId, memoryId, query, new Date().toISOString(), relevanceScore);
|
|
517
|
+
return id;
|
|
518
|
+
}
|
|
519
|
+
markRetrievalUsed(logId, wasUsed) {
|
|
520
|
+
this.ensureInit();
|
|
521
|
+
this.db.prepare(`
|
|
522
|
+
UPDATE retrieval_logs SET was_used = ? WHERE id = ?
|
|
523
|
+
`).run(wasUsed ? 1 : 0, logId);
|
|
524
|
+
}
|
|
525
|
+
addRetrievalFeedback(logId, feedback) {
|
|
526
|
+
this.ensureInit();
|
|
527
|
+
this.db.prepare(`
|
|
528
|
+
UPDATE retrieval_logs SET user_feedback = ? WHERE id = ?
|
|
529
|
+
`).run(feedback, logId);
|
|
530
|
+
}
|
|
531
|
+
// ===========================================================================
|
|
532
|
+
// Feedback Operations
|
|
533
|
+
// ===========================================================================
|
|
534
|
+
addFeedback(type, memoryId, content) {
|
|
535
|
+
this.ensureInit();
|
|
536
|
+
this.db.prepare(`
|
|
537
|
+
INSERT INTO feedback (id, memory_id, type, content, timestamp)
|
|
538
|
+
VALUES (?, ?, ?, ?, ?)
|
|
539
|
+
`).run(uuidv4(), memoryId ?? null, type, content ?? null, new Date().toISOString());
|
|
540
|
+
// Apply feedback immediately
|
|
541
|
+
if (memoryId) {
|
|
542
|
+
switch (type) {
|
|
543
|
+
case 'pin':
|
|
544
|
+
this.updateMemoryStatus(memoryId, 'pinned');
|
|
545
|
+
break;
|
|
546
|
+
case 'forget':
|
|
547
|
+
this.updateMemoryStatus(memoryId, 'forgotten');
|
|
548
|
+
break;
|
|
549
|
+
case 'remember':
|
|
550
|
+
// Boost importance and promote to LTM
|
|
551
|
+
this.db.prepare(`
|
|
552
|
+
UPDATE memory_units
|
|
553
|
+
SET importance = MIN(1.0, importance + 0.3), store = 'ltm', decay_rate = ?
|
|
554
|
+
WHERE id = ?
|
|
555
|
+
`).run(this.config.ltmDecayRate, memoryId);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// ===========================================================================
|
|
561
|
+
// Transaction helpers
|
|
562
|
+
// ===========================================================================
|
|
563
|
+
beginTransaction() {
|
|
564
|
+
this.ensureInit();
|
|
565
|
+
if (this._inTransaction)
|
|
566
|
+
return; // already in one — no nested BEGIN
|
|
567
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
568
|
+
this._inTransaction = true;
|
|
569
|
+
}
|
|
570
|
+
commitTransaction() {
|
|
571
|
+
if (!this._inTransaction)
|
|
572
|
+
return;
|
|
573
|
+
this.db.exec('COMMIT');
|
|
574
|
+
this._inTransaction = false;
|
|
575
|
+
}
|
|
576
|
+
rollbackTransaction() {
|
|
577
|
+
if (!this._inTransaction)
|
|
578
|
+
return;
|
|
579
|
+
try {
|
|
580
|
+
this.db.exec('ROLLBACK');
|
|
581
|
+
}
|
|
582
|
+
catch (_) { /* ignore if already rolled back */ }
|
|
583
|
+
this._inTransaction = false;
|
|
584
|
+
}
|
|
585
|
+
// ===========================================================================
|
|
586
|
+
// Decay and Consolidation
|
|
587
|
+
// ===========================================================================
|
|
588
|
+
/**
|
|
589
|
+
* Apply exponential decay to all active memories
|
|
590
|
+
* strength_t = strength_0 * exp(-lambda * dt)
|
|
591
|
+
*/
|
|
592
|
+
applyDecay() {
|
|
593
|
+
this.ensureInit();
|
|
594
|
+
const now = new Date();
|
|
595
|
+
const memories = this.db.prepare(`
|
|
596
|
+
SELECT id, strength, decay_rate, updated_at, status
|
|
597
|
+
FROM memory_units
|
|
598
|
+
WHERE status = 'active'
|
|
599
|
+
`).all();
|
|
600
|
+
let decayedCount = 0;
|
|
601
|
+
const decayThreshold = 0.1; // Below this, mark as decayed
|
|
602
|
+
// Skip starting a new transaction if the caller already opened one
|
|
603
|
+
const ownTransaction = !this._inTransaction;
|
|
604
|
+
if (ownTransaction) {
|
|
605
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
606
|
+
this._inTransaction = true;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
for (const mem of memories) {
|
|
610
|
+
const updatedAt = new Date(mem.updated_at);
|
|
611
|
+
const dtHours = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60);
|
|
612
|
+
const newStrength = mem.strength * Math.exp(-mem.decay_rate * dtHours);
|
|
613
|
+
if (newStrength < decayThreshold) {
|
|
614
|
+
this.updateMemoryStatus(mem.id, 'decayed');
|
|
615
|
+
decayedCount++;
|
|
616
|
+
}
|
|
617
|
+
else if (newStrength !== mem.strength) {
|
|
618
|
+
this.updateMemoryStrength(mem.id, newStrength);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (ownTransaction) {
|
|
622
|
+
this.db.exec('COMMIT');
|
|
623
|
+
this._inTransaction = false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
if (ownTransaction) {
|
|
628
|
+
this.db.exec('ROLLBACK');
|
|
629
|
+
this._inTransaction = false;
|
|
630
|
+
}
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
633
|
+
return decayedCount;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Check and promote eligible STM memories to LTM
|
|
637
|
+
*/
|
|
638
|
+
runConsolidation() {
|
|
639
|
+
this.ensureInit();
|
|
640
|
+
const stmMemories = this.getMemoriesByStore('stm');
|
|
641
|
+
let promotedCount = 0;
|
|
642
|
+
for (const mem of stmMemories) {
|
|
643
|
+
const shouldPromote = mem.strength >= this.config.stmToLtmStrengthThreshold ||
|
|
644
|
+
mem.frequency >= this.config.stmToLtmFrequencyThreshold ||
|
|
645
|
+
this.config.autoPromoteToLtm.includes(mem.classification);
|
|
646
|
+
if (shouldPromote) {
|
|
647
|
+
this.promoteToLtm(mem.id);
|
|
648
|
+
promotedCount++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return promotedCount;
|
|
652
|
+
}
|
|
653
|
+
// ===========================================================================
|
|
654
|
+
// Scoring
|
|
655
|
+
// ===========================================================================
|
|
656
|
+
/**
|
|
657
|
+
* Calculate memory strength from feature vector (rule-based v1)
|
|
658
|
+
*/
|
|
659
|
+
calculateStrength(features) {
|
|
660
|
+
const w = this.config.scoringWeights;
|
|
661
|
+
// Normalize frequency (log scale)
|
|
662
|
+
const normalizedFrequency = Math.min(1, Math.log(features.frequency + 1) / Math.log(10));
|
|
663
|
+
// Recency factor (0 = now, 1 = old)
|
|
664
|
+
const recencyFactor = 1 - Math.min(1, features.recency / 168); // 168 hours = 1 week
|
|
665
|
+
const strength = w.recency * recencyFactor +
|
|
666
|
+
w.frequency * normalizedFrequency +
|
|
667
|
+
w.importance * features.importance +
|
|
668
|
+
w.utility * features.utility +
|
|
669
|
+
w.novelty * features.novelty +
|
|
670
|
+
w.confidence * features.confidence +
|
|
671
|
+
w.interference * features.interference; // Negative weight
|
|
672
|
+
return Math.max(0, Math.min(1, strength));
|
|
673
|
+
}
|
|
674
|
+
// ===========================================================================
|
|
675
|
+
// Helpers
|
|
676
|
+
// ===========================================================================
|
|
677
|
+
rowToSession(row) {
|
|
678
|
+
return {
|
|
679
|
+
id: row.id,
|
|
680
|
+
project: row.project,
|
|
681
|
+
startedAt: new Date(row.started_at),
|
|
682
|
+
endedAt: row.ended_at ? new Date(row.ended_at) : undefined,
|
|
683
|
+
status: row.status,
|
|
684
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
685
|
+
transcriptPath: row.transcript_path ?? undefined,
|
|
686
|
+
transcriptWatermark: row.transcript_watermark ?? 0,
|
|
687
|
+
messageWatermark: row.message_watermark ?? 0,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
rowToEvent(row) {
|
|
691
|
+
return {
|
|
692
|
+
id: row.id,
|
|
693
|
+
sessionId: row.session_id,
|
|
694
|
+
hookType: row.hook_type,
|
|
695
|
+
timestamp: new Date(row.timestamp),
|
|
696
|
+
content: row.content,
|
|
697
|
+
toolName: row.tool_name ?? undefined,
|
|
698
|
+
toolInput: row.tool_input ?? undefined,
|
|
699
|
+
toolOutput: row.tool_output ?? undefined,
|
|
700
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
rowToMemoryUnit(row) {
|
|
704
|
+
return {
|
|
705
|
+
id: row.id,
|
|
706
|
+
sessionId: row.session_id ?? undefined,
|
|
707
|
+
store: row.store,
|
|
708
|
+
classification: row.classification,
|
|
709
|
+
summary: row.summary,
|
|
710
|
+
sourceEventIds: JSON.parse(row.source_event_ids),
|
|
711
|
+
projectScope: row.project_scope ?? undefined,
|
|
712
|
+
createdAt: new Date(row.created_at),
|
|
713
|
+
updatedAt: new Date(row.updated_at),
|
|
714
|
+
lastAccessedAt: new Date(row.last_accessed_at),
|
|
715
|
+
recency: row.recency,
|
|
716
|
+
frequency: row.frequency,
|
|
717
|
+
importance: row.importance,
|
|
718
|
+
utility: row.utility,
|
|
719
|
+
novelty: row.novelty,
|
|
720
|
+
confidence: row.confidence,
|
|
721
|
+
interference: row.interference,
|
|
722
|
+
strength: row.strength,
|
|
723
|
+
decayRate: row.decay_rate,
|
|
724
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
725
|
+
associations: row.associations ? JSON.parse(row.associations) : [],
|
|
726
|
+
status: row.status,
|
|
727
|
+
version: row.version,
|
|
728
|
+
evidence: [], // Loaded separately if needed
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Close database connection
|
|
733
|
+
*/
|
|
734
|
+
close() {
|
|
735
|
+
if (this.db) {
|
|
736
|
+
this.db.close();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Create and initialize a MemoryDatabase instance
|
|
742
|
+
*/
|
|
743
|
+
export async function createMemoryDatabase(config = {}) {
|
|
744
|
+
const db = new MemoryDatabase(config);
|
|
745
|
+
await db.init();
|
|
746
|
+
return db;
|
|
747
|
+
}
|
|
748
|
+
//# sourceMappingURL=database.js.map
|