claude-memory-layer 1.0.8 → 1.0.9
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/.claude/settings.local.json +7 -1
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260202114053.json +49 -0
- package/HANDOFF.md +92 -0
- package/dist/cli/index.js +1150 -71
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1033 -47
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +5589 -0
- package/dist/hooks/post-tool-use.js.map +7 -0
- package/dist/hooks/session-end.js +1117 -64
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1112 -63
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1117 -64
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1151 -67
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1145 -70
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1145 -70
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1122 -65
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +304 -0
- package/dist/ui/index.html +195 -1188
- package/dist/ui/style.css +595 -0
- package/package.json +3 -1
- package/scripts/build.ts +2 -0
- package/src/core/event-store.ts +18 -0
- package/src/core/index.ts +3 -0
- package/src/core/retriever.ts +4 -1
- package/src/core/sqlite-event-store.ts +849 -0
- package/src/core/sqlite-wrapper.ts +108 -0
- package/src/core/sync-worker.ts +228 -0
- package/src/core/vector-worker.ts +44 -14
- package/src/hooks/user-prompt-submit.ts +53 -4
- package/src/server/api/stats.ts +37 -7
- package/src/services/memory-service.ts +168 -39
- package/src/ui/app.js +304 -0
- package/src/ui/index.html +195 -1188
- package/src/ui/style.css +595 -0
- package/test_access.js +49 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-based EventStore implementation
|
|
3
|
+
* Primary store for hooks - WAL mode enables concurrent access
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import {
|
|
8
|
+
MemoryEvent,
|
|
9
|
+
MemoryEventInput,
|
|
10
|
+
Session,
|
|
11
|
+
AppendResult,
|
|
12
|
+
OutboxItem
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import { makeCanonicalKey, makeDedupeKey } from './canonical-key.js';
|
|
15
|
+
import {
|
|
16
|
+
createSQLiteDatabase,
|
|
17
|
+
sqliteRun,
|
|
18
|
+
sqliteAll,
|
|
19
|
+
sqliteGet,
|
|
20
|
+
sqliteClose,
|
|
21
|
+
sqliteExec,
|
|
22
|
+
toDateFromSQLite,
|
|
23
|
+
toSQLiteTimestamp,
|
|
24
|
+
type SQLiteDatabase,
|
|
25
|
+
type SQLiteOptions
|
|
26
|
+
} from './sqlite-wrapper.js';
|
|
27
|
+
|
|
28
|
+
export interface SQLiteEventStoreOptions extends SQLiteOptions {
|
|
29
|
+
// Additional options can be added here
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SQLiteEventStore {
|
|
33
|
+
private db: SQLiteDatabase;
|
|
34
|
+
private initialized = false;
|
|
35
|
+
private readonly readOnly: boolean;
|
|
36
|
+
|
|
37
|
+
constructor(private dbPath: string, options?: SQLiteEventStoreOptions) {
|
|
38
|
+
this.readOnly = options?.readonly ?? false;
|
|
39
|
+
this.db = createSQLiteDatabase(dbPath, {
|
|
40
|
+
readonly: this.readOnly,
|
|
41
|
+
walMode: !this.readOnly
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize database schema
|
|
47
|
+
*/
|
|
48
|
+
async initialize(): Promise<void> {
|
|
49
|
+
if (this.initialized) return;
|
|
50
|
+
|
|
51
|
+
// In read-only mode, skip schema creation
|
|
52
|
+
if (this.readOnly) {
|
|
53
|
+
this.initialized = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create all tables in a single exec for efficiency
|
|
58
|
+
sqliteExec(this.db, `
|
|
59
|
+
-- L0 EventStore: Single Source of Truth (immutable, append-only)
|
|
60
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
event_type TEXT NOT NULL,
|
|
63
|
+
session_id TEXT NOT NULL,
|
|
64
|
+
timestamp TEXT NOT NULL,
|
|
65
|
+
content TEXT NOT NULL,
|
|
66
|
+
canonical_key TEXT NOT NULL,
|
|
67
|
+
dedupe_key TEXT UNIQUE,
|
|
68
|
+
metadata TEXT,
|
|
69
|
+
access_count INTEGER DEFAULT 0,
|
|
70
|
+
last_accessed_at TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- Dedup table for idempotency
|
|
74
|
+
CREATE TABLE IF NOT EXISTS event_dedup (
|
|
75
|
+
dedupe_key TEXT PRIMARY KEY,
|
|
76
|
+
event_id TEXT NOT NULL,
|
|
77
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- Session metadata
|
|
81
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
started_at TEXT NOT NULL,
|
|
84
|
+
ended_at TEXT,
|
|
85
|
+
project_path TEXT,
|
|
86
|
+
summary TEXT,
|
|
87
|
+
tags TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- Insights (derived data, rebuildable)
|
|
91
|
+
CREATE TABLE IF NOT EXISTS insights (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
insight_type TEXT NOT NULL,
|
|
94
|
+
content TEXT NOT NULL,
|
|
95
|
+
canonical_key TEXT NOT NULL,
|
|
96
|
+
confidence REAL,
|
|
97
|
+
source_events TEXT,
|
|
98
|
+
created_at TEXT,
|
|
99
|
+
last_updated TEXT
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
-- Embedding Outbox (Single-Writer Pattern)
|
|
103
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
event_id TEXT NOT NULL,
|
|
106
|
+
content TEXT NOT NULL,
|
|
107
|
+
status TEXT DEFAULT 'pending',
|
|
108
|
+
retry_count INTEGER DEFAULT 0,
|
|
109
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
110
|
+
processed_at TEXT,
|
|
111
|
+
error_message TEXT
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
-- Projection offset tracking
|
|
115
|
+
CREATE TABLE IF NOT EXISTS projection_offsets (
|
|
116
|
+
projection_name TEXT PRIMARY KEY,
|
|
117
|
+
last_event_id TEXT,
|
|
118
|
+
last_timestamp TEXT,
|
|
119
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
-- Memory level tracking
|
|
123
|
+
CREATE TABLE IF NOT EXISTS memory_levels (
|
|
124
|
+
event_id TEXT PRIMARY KEY,
|
|
125
|
+
level TEXT NOT NULL DEFAULT 'L0',
|
|
126
|
+
promoted_at TEXT DEFAULT (datetime('now'))
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
-- Entries (immutable memory units)
|
|
130
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
131
|
+
entry_id TEXT PRIMARY KEY,
|
|
132
|
+
created_ts TEXT NOT NULL,
|
|
133
|
+
entry_type TEXT NOT NULL,
|
|
134
|
+
title TEXT NOT NULL,
|
|
135
|
+
content_json TEXT NOT NULL,
|
|
136
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
137
|
+
status TEXT DEFAULT 'active',
|
|
138
|
+
superseded_by TEXT,
|
|
139
|
+
build_id TEXT,
|
|
140
|
+
evidence_json TEXT,
|
|
141
|
+
canonical_key TEXT,
|
|
142
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
-- Entities (task/condition/artifact)
|
|
146
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
147
|
+
entity_id TEXT PRIMARY KEY,
|
|
148
|
+
entity_type TEXT NOT NULL,
|
|
149
|
+
canonical_key TEXT NOT NULL,
|
|
150
|
+
title TEXT NOT NULL,
|
|
151
|
+
stage TEXT NOT NULL DEFAULT 'raw',
|
|
152
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
153
|
+
current_json TEXT NOT NULL,
|
|
154
|
+
title_norm TEXT,
|
|
155
|
+
search_text TEXT,
|
|
156
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
157
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
-- Entity aliases for canonical key lookup
|
|
161
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
162
|
+
entity_type TEXT NOT NULL,
|
|
163
|
+
canonical_key TEXT NOT NULL,
|
|
164
|
+
entity_id TEXT NOT NULL,
|
|
165
|
+
is_primary INTEGER DEFAULT 0,
|
|
166
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
167
|
+
PRIMARY KEY(entity_type, canonical_key)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
-- Edges (relationships between entries/entities)
|
|
171
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
172
|
+
edge_id TEXT PRIMARY KEY,
|
|
173
|
+
src_type TEXT NOT NULL,
|
|
174
|
+
src_id TEXT NOT NULL,
|
|
175
|
+
rel_type TEXT NOT NULL,
|
|
176
|
+
dst_type TEXT NOT NULL,
|
|
177
|
+
dst_id TEXT NOT NULL,
|
|
178
|
+
meta_json TEXT,
|
|
179
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
-- Vector Outbox V2 Table
|
|
183
|
+
CREATE TABLE IF NOT EXISTS vector_outbox (
|
|
184
|
+
job_id TEXT PRIMARY KEY,
|
|
185
|
+
item_kind TEXT NOT NULL,
|
|
186
|
+
item_id TEXT NOT NULL,
|
|
187
|
+
embedding_version TEXT NOT NULL,
|
|
188
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
189
|
+
retry_count INTEGER DEFAULT 0,
|
|
190
|
+
error TEXT,
|
|
191
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
192
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
193
|
+
UNIQUE(item_kind, item_id, embedding_version)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
-- Build Runs
|
|
197
|
+
CREATE TABLE IF NOT EXISTS build_runs (
|
|
198
|
+
build_id TEXT PRIMARY KEY,
|
|
199
|
+
started_at TEXT NOT NULL,
|
|
200
|
+
finished_at TEXT,
|
|
201
|
+
extractor_model TEXT NOT NULL,
|
|
202
|
+
extractor_prompt_hash TEXT NOT NULL,
|
|
203
|
+
embedder_model TEXT NOT NULL,
|
|
204
|
+
embedding_version TEXT NOT NULL,
|
|
205
|
+
idris_version TEXT NOT NULL,
|
|
206
|
+
schema_version TEXT NOT NULL,
|
|
207
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
208
|
+
error TEXT
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
-- Pipeline Metrics
|
|
212
|
+
CREATE TABLE IF NOT EXISTS pipeline_metrics (
|
|
213
|
+
id TEXT PRIMARY KEY,
|
|
214
|
+
ts TEXT NOT NULL,
|
|
215
|
+
stage TEXT NOT NULL,
|
|
216
|
+
latency_ms REAL NOT NULL,
|
|
217
|
+
success INTEGER NOT NULL,
|
|
218
|
+
error TEXT,
|
|
219
|
+
session_id TEXT
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
-- Working Set table (active memory window)
|
|
223
|
+
CREATE TABLE IF NOT EXISTS working_set (
|
|
224
|
+
id TEXT PRIMARY KEY,
|
|
225
|
+
event_id TEXT NOT NULL,
|
|
226
|
+
added_at TEXT DEFAULT (datetime('now')),
|
|
227
|
+
relevance_score REAL DEFAULT 1.0,
|
|
228
|
+
topics TEXT,
|
|
229
|
+
expires_at TEXT
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
-- Consolidated Memories table (long-term integrated memories)
|
|
233
|
+
CREATE TABLE IF NOT EXISTS consolidated_memories (
|
|
234
|
+
memory_id TEXT PRIMARY KEY,
|
|
235
|
+
summary TEXT NOT NULL,
|
|
236
|
+
topics TEXT,
|
|
237
|
+
source_events TEXT,
|
|
238
|
+
confidence REAL DEFAULT 0.5,
|
|
239
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
240
|
+
accessed_at TEXT,
|
|
241
|
+
access_count INTEGER DEFAULT 0
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
-- Continuity Log table (tracks context transitions)
|
|
245
|
+
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
246
|
+
log_id TEXT PRIMARY KEY,
|
|
247
|
+
from_context_id TEXT,
|
|
248
|
+
to_context_id TEXT,
|
|
249
|
+
continuity_score REAL,
|
|
250
|
+
transition_type TEXT,
|
|
251
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
-- Endless Mode Config table
|
|
255
|
+
CREATE TABLE IF NOT EXISTS endless_config (
|
|
256
|
+
key TEXT PRIMARY KEY,
|
|
257
|
+
value TEXT,
|
|
258
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
262
|
+
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
263
|
+
target_name TEXT PRIMARY KEY,
|
|
264
|
+
last_event_id TEXT,
|
|
265
|
+
last_timestamp TEXT,
|
|
266
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
-- Create indexes
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
271
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
272
|
+
CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type);
|
|
273
|
+
CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage);
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key);
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type);
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
// Migrate existing events table to add access tracking columns if they don't exist
|
|
289
|
+
// Check if columns exist before trying to add them
|
|
290
|
+
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
291
|
+
const columnNames = tableInfo.map((col: any) => col.name);
|
|
292
|
+
|
|
293
|
+
if (!columnNames.includes('access_count')) {
|
|
294
|
+
try {
|
|
295
|
+
sqliteExec(this.db, `
|
|
296
|
+
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
297
|
+
`);
|
|
298
|
+
} catch (err: any) {
|
|
299
|
+
console.error('Error adding access_count column:', err);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!columnNames.includes('last_accessed_at')) {
|
|
304
|
+
try {
|
|
305
|
+
sqliteExec(this.db, `
|
|
306
|
+
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
307
|
+
`);
|
|
308
|
+
} catch (err: any) {
|
|
309
|
+
console.error('Error adding last_accessed_at column:', err);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create indexes for new columns if they don't exist
|
|
314
|
+
try {
|
|
315
|
+
sqliteExec(this.db, `
|
|
316
|
+
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
317
|
+
`);
|
|
318
|
+
} catch (err: any) {
|
|
319
|
+
// Index may already exist, ignore
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
sqliteExec(this.db, `
|
|
324
|
+
CREATE INDEX IF NOT EXISTS idx_events_last_accessed ON events(last_accessed_at DESC);
|
|
325
|
+
`);
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
// Index may already exist, ignore
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
this.initialized = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Append event to store (Append-only, Idempotent)
|
|
335
|
+
*/
|
|
336
|
+
async append(input: MemoryEventInput): Promise<AppendResult> {
|
|
337
|
+
await this.initialize();
|
|
338
|
+
|
|
339
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
340
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
341
|
+
|
|
342
|
+
// Check for duplicate
|
|
343
|
+
const existing = sqliteGet<{ event_id: string }>(
|
|
344
|
+
this.db,
|
|
345
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
346
|
+
[dedupeKey]
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (existing) {
|
|
350
|
+
return {
|
|
351
|
+
success: true,
|
|
352
|
+
eventId: existing.event_id,
|
|
353
|
+
isDuplicate: true
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const id = randomUUID();
|
|
358
|
+
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Use transaction for atomicity
|
|
362
|
+
const insertEvent = this.db.prepare(`
|
|
363
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
364
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
365
|
+
`);
|
|
366
|
+
|
|
367
|
+
const insertDedup = this.db.prepare(`
|
|
368
|
+
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
369
|
+
`);
|
|
370
|
+
|
|
371
|
+
const insertLevel = this.db.prepare(`
|
|
372
|
+
INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')
|
|
373
|
+
`);
|
|
374
|
+
|
|
375
|
+
const transaction = this.db.transaction(() => {
|
|
376
|
+
insertEvent.run(
|
|
377
|
+
id,
|
|
378
|
+
input.eventType,
|
|
379
|
+
input.sessionId,
|
|
380
|
+
timestamp,
|
|
381
|
+
input.content,
|
|
382
|
+
canonicalKey,
|
|
383
|
+
dedupeKey,
|
|
384
|
+
JSON.stringify(input.metadata || {})
|
|
385
|
+
);
|
|
386
|
+
insertDedup.run(dedupeKey, id);
|
|
387
|
+
insertLevel.run(id);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
transaction();
|
|
391
|
+
|
|
392
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return {
|
|
395
|
+
success: false,
|
|
396
|
+
error: error instanceof Error ? error.message : String(error)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get events by session ID
|
|
403
|
+
*/
|
|
404
|
+
async getSessionEvents(sessionId: string): Promise<MemoryEvent[]> {
|
|
405
|
+
await this.initialize();
|
|
406
|
+
|
|
407
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
408
|
+
this.db,
|
|
409
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
410
|
+
[sessionId]
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return rows.map(this.rowToEvent);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get recent events
|
|
418
|
+
*/
|
|
419
|
+
async getRecentEvents(limit: number = 100): Promise<MemoryEvent[]> {
|
|
420
|
+
await this.initialize();
|
|
421
|
+
|
|
422
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
423
|
+
this.db,
|
|
424
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
425
|
+
[limit]
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
return rows.map(this.rowToEvent);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get event by ID
|
|
433
|
+
*/
|
|
434
|
+
async getEvent(id: string): Promise<MemoryEvent | null> {
|
|
435
|
+
await this.initialize();
|
|
436
|
+
|
|
437
|
+
const row = sqliteGet<Record<string, unknown>>(
|
|
438
|
+
this.db,
|
|
439
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
440
|
+
[id]
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (!row) return null;
|
|
444
|
+
return this.rowToEvent(row);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Get events since a timestamp (for sync)
|
|
449
|
+
*/
|
|
450
|
+
async getEventsSince(timestamp: string, limit: number = 1000): Promise<MemoryEvent[]> {
|
|
451
|
+
await this.initialize();
|
|
452
|
+
|
|
453
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
454
|
+
this.db,
|
|
455
|
+
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
456
|
+
[timestamp, limit]
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
return rows.map(this.rowToEvent);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Create or update session
|
|
464
|
+
*/
|
|
465
|
+
async upsertSession(session: Partial<Session> & { id: string }): Promise<void> {
|
|
466
|
+
await this.initialize();
|
|
467
|
+
|
|
468
|
+
const existing = sqliteGet<{ id: string }>(
|
|
469
|
+
this.db,
|
|
470
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
471
|
+
[session.id]
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (!existing) {
|
|
475
|
+
sqliteRun(
|
|
476
|
+
this.db,
|
|
477
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
478
|
+
VALUES (?, ?, ?, ?)`,
|
|
479
|
+
[
|
|
480
|
+
session.id,
|
|
481
|
+
toSQLiteTimestamp(session.startedAt || new Date()),
|
|
482
|
+
session.projectPath || null,
|
|
483
|
+
JSON.stringify(session.tags || [])
|
|
484
|
+
]
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
const updates: string[] = [];
|
|
488
|
+
const values: unknown[] = [];
|
|
489
|
+
|
|
490
|
+
if (session.endedAt) {
|
|
491
|
+
updates.push('ended_at = ?');
|
|
492
|
+
values.push(toSQLiteTimestamp(session.endedAt));
|
|
493
|
+
}
|
|
494
|
+
if (session.summary) {
|
|
495
|
+
updates.push('summary = ?');
|
|
496
|
+
values.push(session.summary);
|
|
497
|
+
}
|
|
498
|
+
if (session.tags) {
|
|
499
|
+
updates.push('tags = ?');
|
|
500
|
+
values.push(JSON.stringify(session.tags));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (updates.length > 0) {
|
|
504
|
+
values.push(session.id);
|
|
505
|
+
sqliteRun(
|
|
506
|
+
this.db,
|
|
507
|
+
`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`,
|
|
508
|
+
values
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get session by ID
|
|
516
|
+
*/
|
|
517
|
+
async getSession(id: string): Promise<Session | null> {
|
|
518
|
+
await this.initialize();
|
|
519
|
+
|
|
520
|
+
const row = sqliteGet<Record<string, unknown>>(
|
|
521
|
+
this.db,
|
|
522
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
523
|
+
[id]
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (!row) return null;
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
id: row.id as string,
|
|
530
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
531
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : undefined,
|
|
532
|
+
projectPath: row.project_path as string | undefined,
|
|
533
|
+
summary: row.summary as string | undefined,
|
|
534
|
+
tags: row.tags ? JSON.parse(row.tags as string) : undefined
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Get all sessions
|
|
540
|
+
*/
|
|
541
|
+
async getAllSessions(): Promise<Session[]> {
|
|
542
|
+
await this.initialize();
|
|
543
|
+
|
|
544
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
545
|
+
this.db,
|
|
546
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
return rows.map(row => ({
|
|
550
|
+
id: row.id as string,
|
|
551
|
+
startedAt: toDateFromSQLite(row.started_at),
|
|
552
|
+
endedAt: row.ended_at ? toDateFromSQLite(row.ended_at) : undefined,
|
|
553
|
+
projectPath: row.project_path as string | undefined,
|
|
554
|
+
summary: row.summary as string | undefined,
|
|
555
|
+
tags: row.tags ? JSON.parse(row.tags as string) : undefined
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Add to embedding outbox
|
|
561
|
+
*/
|
|
562
|
+
async enqueueForEmbedding(eventId: string, content: string): Promise<string> {
|
|
563
|
+
await this.initialize();
|
|
564
|
+
|
|
565
|
+
const id = randomUUID();
|
|
566
|
+
sqliteRun(
|
|
567
|
+
this.db,
|
|
568
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
569
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
570
|
+
[id, eventId, content]
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
return id;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Get pending outbox items
|
|
578
|
+
*/
|
|
579
|
+
async getPendingOutboxItems(limit: number = 32): Promise<OutboxItem[]> {
|
|
580
|
+
await this.initialize();
|
|
581
|
+
|
|
582
|
+
const pending = sqliteAll<Record<string, unknown>>(
|
|
583
|
+
this.db,
|
|
584
|
+
`SELECT * FROM embedding_outbox
|
|
585
|
+
WHERE status = 'pending'
|
|
586
|
+
ORDER BY created_at
|
|
587
|
+
LIMIT ?`,
|
|
588
|
+
[limit]
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
if (pending.length === 0) return [];
|
|
592
|
+
|
|
593
|
+
// Update status to processing
|
|
594
|
+
const ids = pending.map(r => r.id as string);
|
|
595
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
596
|
+
sqliteRun(
|
|
597
|
+
this.db,
|
|
598
|
+
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders})`,
|
|
599
|
+
ids
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
return pending.map(row => ({
|
|
603
|
+
id: row.id as string,
|
|
604
|
+
eventId: row.event_id as string,
|
|
605
|
+
content: row.content as string,
|
|
606
|
+
status: 'processing' as const,
|
|
607
|
+
retryCount: row.retry_count as number,
|
|
608
|
+
createdAt: toDateFromSQLite(row.created_at),
|
|
609
|
+
errorMessage: row.error_message as string | undefined
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Mark outbox items as done
|
|
615
|
+
*/
|
|
616
|
+
async completeOutboxItems(ids: string[]): Promise<void> {
|
|
617
|
+
if (ids.length === 0) return;
|
|
618
|
+
|
|
619
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
620
|
+
sqliteRun(
|
|
621
|
+
this.db,
|
|
622
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
623
|
+
ids
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Mark outbox items as failed
|
|
629
|
+
*/
|
|
630
|
+
async failOutboxItems(ids: string[], error: string): Promise<void> {
|
|
631
|
+
if (ids.length === 0) return;
|
|
632
|
+
|
|
633
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
634
|
+
sqliteRun(
|
|
635
|
+
this.db,
|
|
636
|
+
`UPDATE embedding_outbox
|
|
637
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
638
|
+
retry_count = retry_count + 1,
|
|
639
|
+
error_message = ?
|
|
640
|
+
WHERE id IN (${placeholders})`,
|
|
641
|
+
[error, ...ids]
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Update memory level
|
|
647
|
+
*/
|
|
648
|
+
async updateMemoryLevel(eventId: string, level: string): Promise<void> {
|
|
649
|
+
await this.initialize();
|
|
650
|
+
|
|
651
|
+
sqliteRun(
|
|
652
|
+
this.db,
|
|
653
|
+
`UPDATE memory_levels SET level = ?, promoted_at = datetime('now') WHERE event_id = ?`,
|
|
654
|
+
[level, eventId]
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Get memory level statistics
|
|
660
|
+
*/
|
|
661
|
+
async getLevelStats(): Promise<Array<{ level: string; count: number }>> {
|
|
662
|
+
await this.initialize();
|
|
663
|
+
|
|
664
|
+
const rows = sqliteAll<{ level: string; count: number }>(
|
|
665
|
+
this.db,
|
|
666
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
return rows;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get events by memory level
|
|
674
|
+
*/
|
|
675
|
+
async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
|
|
676
|
+
await this.initialize();
|
|
677
|
+
|
|
678
|
+
const limit = options?.limit || 50;
|
|
679
|
+
const offset = options?.offset || 0;
|
|
680
|
+
|
|
681
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
682
|
+
this.db,
|
|
683
|
+
`SELECT e.* FROM events e
|
|
684
|
+
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
685
|
+
WHERE ml.level = ?
|
|
686
|
+
ORDER BY e.timestamp DESC
|
|
687
|
+
LIMIT ? OFFSET ?`,
|
|
688
|
+
[level, limit, offset]
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return rows.map(row => this.rowToEvent(row));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Get memory level for a specific event
|
|
696
|
+
*/
|
|
697
|
+
async getEventLevel(eventId: string): Promise<string | null> {
|
|
698
|
+
await this.initialize();
|
|
699
|
+
|
|
700
|
+
const row = sqliteGet<{ level: string }>(
|
|
701
|
+
this.db,
|
|
702
|
+
`SELECT level FROM memory_levels WHERE event_id = ?`,
|
|
703
|
+
[eventId]
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
return row ? row.level : null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Get sync position for a target
|
|
711
|
+
*/
|
|
712
|
+
async getSyncPosition(targetName: string): Promise<{ lastEventId: string | null; lastTimestamp: string | null }> {
|
|
713
|
+
await this.initialize();
|
|
714
|
+
|
|
715
|
+
const row = sqliteGet<{ last_event_id: string | null; last_timestamp: string | null }>(
|
|
716
|
+
this.db,
|
|
717
|
+
`SELECT last_event_id, last_timestamp FROM sync_positions WHERE target_name = ?`,
|
|
718
|
+
[targetName]
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
lastEventId: row?.last_event_id ?? null,
|
|
723
|
+
lastTimestamp: row?.last_timestamp ?? null
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Update sync position for a target
|
|
729
|
+
*/
|
|
730
|
+
async updateSyncPosition(targetName: string, lastEventId: string, lastTimestamp: string): Promise<void> {
|
|
731
|
+
await this.initialize();
|
|
732
|
+
|
|
733
|
+
sqliteRun(
|
|
734
|
+
this.db,
|
|
735
|
+
`INSERT OR REPLACE INTO sync_positions (target_name, last_event_id, last_timestamp, updated_at)
|
|
736
|
+
VALUES (?, ?, ?, datetime('now'))`,
|
|
737
|
+
[targetName, lastEventId, lastTimestamp]
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Get config value for endless mode
|
|
743
|
+
*/
|
|
744
|
+
async getEndlessConfig(key: string): Promise<unknown | null> {
|
|
745
|
+
await this.initialize();
|
|
746
|
+
|
|
747
|
+
const row = sqliteGet<{ value: string }>(
|
|
748
|
+
this.db,
|
|
749
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
750
|
+
[key]
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
if (!row) return null;
|
|
754
|
+
return JSON.parse(row.value);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Set config value for endless mode
|
|
759
|
+
*/
|
|
760
|
+
async setEndlessConfig(key: string, value: unknown): Promise<void> {
|
|
761
|
+
await this.initialize();
|
|
762
|
+
|
|
763
|
+
sqliteRun(
|
|
764
|
+
this.db,
|
|
765
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
766
|
+
VALUES (?, ?, datetime('now'))`,
|
|
767
|
+
[key, JSON.stringify(value)]
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Increment access count for events
|
|
773
|
+
*/
|
|
774
|
+
async incrementAccessCount(eventIds: string[]): Promise<void> {
|
|
775
|
+
if (eventIds.length === 0 || this.readOnly) return;
|
|
776
|
+
|
|
777
|
+
await this.initialize();
|
|
778
|
+
|
|
779
|
+
const placeholders = eventIds.map(() => '?').join(',');
|
|
780
|
+
const currentTime = toSQLiteTimestamp(new Date());
|
|
781
|
+
|
|
782
|
+
sqliteRun(
|
|
783
|
+
this.db,
|
|
784
|
+
`UPDATE events
|
|
785
|
+
SET access_count = access_count + 1,
|
|
786
|
+
last_accessed_at = ?
|
|
787
|
+
WHERE id IN (${placeholders})`,
|
|
788
|
+
[currentTime, ...eventIds]
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Get most accessed memories
|
|
794
|
+
*/
|
|
795
|
+
async getMostAccessed(limit: number = 10): Promise<MemoryEvent[]> {
|
|
796
|
+
await this.initialize();
|
|
797
|
+
|
|
798
|
+
const rows = sqliteAll<Record<string, unknown>>(
|
|
799
|
+
this.db,
|
|
800
|
+
`SELECT * FROM events
|
|
801
|
+
WHERE access_count > 0
|
|
802
|
+
ORDER BY access_count DESC, last_accessed_at DESC
|
|
803
|
+
LIMIT ?`,
|
|
804
|
+
[limit]
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
return rows.map(row => this.rowToEvent(row));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get database instance for direct access
|
|
812
|
+
*/
|
|
813
|
+
getDatabase(): SQLiteDatabase {
|
|
814
|
+
return this.db;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Close database connection
|
|
819
|
+
*/
|
|
820
|
+
async close(): Promise<void> {
|
|
821
|
+
sqliteClose(this.db);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Convert database row to MemoryEvent
|
|
826
|
+
*/
|
|
827
|
+
private rowToEvent(row: Record<string, unknown>): MemoryEvent {
|
|
828
|
+
const event: any = {
|
|
829
|
+
id: row.id as string,
|
|
830
|
+
eventType: row.event_type as 'user_prompt' | 'agent_response' | 'session_summary',
|
|
831
|
+
sessionId: row.session_id as string,
|
|
832
|
+
timestamp: toDateFromSQLite(row.timestamp),
|
|
833
|
+
content: row.content as string,
|
|
834
|
+
canonicalKey: row.canonical_key as string,
|
|
835
|
+
dedupeKey: row.dedupe_key as string,
|
|
836
|
+
metadata: row.metadata ? JSON.parse(row.metadata as string) : undefined
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// Include access tracking fields if present
|
|
840
|
+
if (row.access_count !== undefined) {
|
|
841
|
+
event.access_count = row.access_count;
|
|
842
|
+
}
|
|
843
|
+
if (row.last_accessed_at !== undefined) {
|
|
844
|
+
event.last_accessed_at = row.last_accessed_at;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return event;
|
|
848
|
+
}
|
|
849
|
+
}
|