claude-memory-layer 1.0.0 → 1.0.2
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 +15 -0
- package/.history/package_20260201114632.json +46 -0
- package/dist/cli/index.js +360 -154
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +337 -161
- package/dist/core/index.js.map +3 -3
- package/dist/hooks/session-end.js +320 -130
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +331 -138
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +320 -130
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +320 -130
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/services/memory-service.js +349 -128
- package/dist/services/memory-service.js.map +4 -4
- package/package.json +1 -1
- package/src/cli/index.ts +84 -23
- package/src/core/consolidated-store.ts +33 -18
- package/src/core/continuity-manager.ts +12 -7
- package/src/core/db-wrapper.ts +112 -0
- package/src/core/edge-repo.ts +22 -13
- package/src/core/entity-repo.ts +23 -14
- package/src/core/event-store.ts +98 -72
- package/src/core/task/blocker-resolver.ts +17 -9
- package/src/core/task/task-matcher.ts +8 -6
- package/src/core/task/task-projector.ts +29 -16
- package/src/core/task/task-resolver.ts +17 -9
- package/src/core/vector-outbox.ts +29 -16
- package/src/core/vector-store.ts +23 -12
- package/src/core/vector-worker.ts +7 -4
- package/src/core/working-set-store.ts +31 -18
- package/src/hooks/session-end.ts +3 -2
- package/src/hooks/session-start.ts +12 -8
- package/src/hooks/stop.ts +3 -2
- package/src/hooks/user-prompt-submit.ts +3 -2
- package/src/services/memory-service.ts +158 -6
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Command } from 'commander';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getDefaultMemoryService,
|
|
10
|
+
getMemoryServiceForProject
|
|
11
|
+
} from '../services/memory-service.js';
|
|
9
12
|
import { createSessionHistoryImporter } from '../services/session-history-importer.js';
|
|
10
13
|
|
|
11
14
|
const program = new Command();
|
|
@@ -24,8 +27,10 @@ program
|
|
|
24
27
|
.option('-k, --top-k <number>', 'Number of results', '5')
|
|
25
28
|
.option('-s, --min-score <number>', 'Minimum similarity score', '0.7')
|
|
26
29
|
.option('--session <id>', 'Filter by session ID')
|
|
30
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
27
31
|
.action(async (query: string, options) => {
|
|
28
|
-
const
|
|
32
|
+
const projectPath = options.project || process.cwd();
|
|
33
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
29
34
|
|
|
30
35
|
try {
|
|
31
36
|
const result = await service.retrieveMemories(query, {
|
|
@@ -64,8 +69,10 @@ program
|
|
|
64
69
|
.option('-l, --limit <number>', 'Number of events', '20')
|
|
65
70
|
.option('--session <id>', 'Filter by session ID')
|
|
66
71
|
.option('--type <type>', 'Filter by event type')
|
|
72
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
67
73
|
.action(async (options) => {
|
|
68
|
-
const
|
|
74
|
+
const projectPath = options.project || process.cwd();
|
|
75
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
69
76
|
|
|
70
77
|
try {
|
|
71
78
|
let events;
|
|
@@ -107,8 +114,10 @@ program
|
|
|
107
114
|
program
|
|
108
115
|
.command('stats')
|
|
109
116
|
.description('View memory statistics')
|
|
110
|
-
.
|
|
111
|
-
|
|
117
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
const projectPath = options.project || process.cwd();
|
|
120
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
112
121
|
|
|
113
122
|
try {
|
|
114
123
|
const stats = await service.getStats();
|
|
@@ -139,8 +148,10 @@ program
|
|
|
139
148
|
.option('--session <id>', 'Forget all events from a session')
|
|
140
149
|
.option('--before <date>', 'Forget events before date (YYYY-MM-DD)')
|
|
141
150
|
.option('--confirm', 'Skip confirmation')
|
|
151
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
142
152
|
.action(async (eventId: string | undefined, options) => {
|
|
143
|
-
const
|
|
153
|
+
const projectPath = options.project || process.cwd();
|
|
154
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
144
155
|
|
|
145
156
|
try {
|
|
146
157
|
if (!eventId && !options.session && !options.before) {
|
|
@@ -171,8 +182,10 @@ program
|
|
|
171
182
|
program
|
|
172
183
|
.command('process')
|
|
173
184
|
.description('Process pending embeddings')
|
|
174
|
-
.
|
|
175
|
-
|
|
185
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
186
|
+
.action(async (options) => {
|
|
187
|
+
const projectPath = options.project || process.cwd();
|
|
188
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
176
189
|
|
|
177
190
|
try {
|
|
178
191
|
console.log('⏳ Processing pending embeddings...');
|
|
@@ -198,7 +211,11 @@ program
|
|
|
198
211
|
.option('-l, --limit <number>', 'Limit messages per session')
|
|
199
212
|
.option('-v, --verbose', 'Show detailed progress')
|
|
200
213
|
.action(async (options) => {
|
|
201
|
-
|
|
214
|
+
// Determine target project path for storage
|
|
215
|
+
const targetProjectPath = options.project || process.cwd();
|
|
216
|
+
|
|
217
|
+
// Use project-specific memory service
|
|
218
|
+
const service = getMemoryServiceForProject(targetProjectPath);
|
|
202
219
|
const importer = createSessionHistoryImporter(service);
|
|
203
220
|
|
|
204
221
|
try {
|
|
@@ -208,9 +225,10 @@ program
|
|
|
208
225
|
|
|
209
226
|
if (options.session) {
|
|
210
227
|
// Import specific session file
|
|
211
|
-
console.log(`\n📥 Importing session: ${options.session}
|
|
228
|
+
console.log(`\n📥 Importing session: ${options.session}`);
|
|
229
|
+
console.log(` Target project: ${targetProjectPath}\n`);
|
|
212
230
|
result = await importer.importSessionFile(options.session, {
|
|
213
|
-
projectPath:
|
|
231
|
+
projectPath: targetProjectPath,
|
|
214
232
|
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
215
233
|
verbose: options.verbose
|
|
216
234
|
});
|
|
@@ -223,11 +241,42 @@ program
|
|
|
223
241
|
});
|
|
224
242
|
} else if (options.all) {
|
|
225
243
|
// Import all sessions from all projects
|
|
226
|
-
|
|
227
|
-
|
|
244
|
+
// Note: --all imports to global storage for backward compatibility
|
|
245
|
+
console.log('\n📥 Importing all sessions from all projects');
|
|
246
|
+
console.log(' ⚠️ Using global storage (use -p for project-specific)\n');
|
|
247
|
+
const globalService = getDefaultMemoryService();
|
|
248
|
+
const globalImporter = createSessionHistoryImporter(globalService);
|
|
249
|
+
await globalService.initialize();
|
|
250
|
+
result = await globalImporter.importAll({
|
|
228
251
|
limit: options.limit ? parseInt(options.limit) : undefined,
|
|
229
252
|
verbose: options.verbose
|
|
230
253
|
});
|
|
254
|
+
|
|
255
|
+
// Process embeddings
|
|
256
|
+
console.log('\n⏳ Processing embeddings...');
|
|
257
|
+
const embedCount = await globalService.processPendingEmbeddings();
|
|
258
|
+
|
|
259
|
+
// Show results
|
|
260
|
+
console.log('\n✅ Import Complete\n');
|
|
261
|
+
console.log(`Sessions processed: ${result.totalSessions}`);
|
|
262
|
+
console.log(`Total messages: ${result.totalMessages}`);
|
|
263
|
+
console.log(`Imported prompts: ${result.importedPrompts}`);
|
|
264
|
+
console.log(`Imported responses: ${result.importedResponses}`);
|
|
265
|
+
console.log(`Skipped duplicates: ${result.skippedDuplicates}`);
|
|
266
|
+
console.log(`Embeddings processed: ${embedCount}`);
|
|
267
|
+
|
|
268
|
+
if (result.errors.length > 0) {
|
|
269
|
+
console.log(`\n⚠️ Errors (${result.errors.length}):`);
|
|
270
|
+
for (const error of result.errors.slice(0, 5)) {
|
|
271
|
+
console.log(` - ${error}`);
|
|
272
|
+
}
|
|
273
|
+
if (result.errors.length > 5) {
|
|
274
|
+
console.log(` ... and ${result.errors.length - 5} more`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await globalService.shutdown();
|
|
279
|
+
return;
|
|
231
280
|
} else {
|
|
232
281
|
// Default: import current project
|
|
233
282
|
const cwd = process.cwd();
|
|
@@ -324,8 +373,10 @@ const endlessCmd = program
|
|
|
324
373
|
endlessCmd
|
|
325
374
|
.command('enable')
|
|
326
375
|
.description('Enable Endless Mode')
|
|
327
|
-
.
|
|
328
|
-
|
|
376
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
377
|
+
.action(async (options) => {
|
|
378
|
+
const projectPath = options.project || process.cwd();
|
|
379
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
329
380
|
|
|
330
381
|
try {
|
|
331
382
|
await service.initialize();
|
|
@@ -353,8 +404,10 @@ endlessCmd
|
|
|
353
404
|
endlessCmd
|
|
354
405
|
.command('disable')
|
|
355
406
|
.description('Disable Endless Mode (return to Session Mode)')
|
|
356
|
-
.
|
|
357
|
-
|
|
407
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
408
|
+
.action(async (options) => {
|
|
409
|
+
const projectPath = options.project || process.cwd();
|
|
410
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
358
411
|
|
|
359
412
|
try {
|
|
360
413
|
await service.initialize();
|
|
@@ -377,8 +430,10 @@ endlessCmd
|
|
|
377
430
|
endlessCmd
|
|
378
431
|
.command('status')
|
|
379
432
|
.description('Show Endless Mode status')
|
|
380
|
-
.
|
|
381
|
-
|
|
433
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
434
|
+
.action(async (options) => {
|
|
435
|
+
const projectPath = options.project || process.cwd();
|
|
436
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
382
437
|
|
|
383
438
|
try {
|
|
384
439
|
await service.initialize();
|
|
@@ -423,8 +478,10 @@ endlessCmd
|
|
|
423
478
|
endlessCmd
|
|
424
479
|
.command('consolidate')
|
|
425
480
|
.description('Manually trigger memory consolidation')
|
|
426
|
-
.
|
|
427
|
-
|
|
481
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
482
|
+
.action(async (options) => {
|
|
483
|
+
const projectPath = options.project || process.cwd();
|
|
484
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
428
485
|
|
|
429
486
|
try {
|
|
430
487
|
await service.initialize();
|
|
@@ -460,8 +517,10 @@ endlessCmd
|
|
|
460
517
|
.alias('ws')
|
|
461
518
|
.description('View current working set')
|
|
462
519
|
.option('-l, --limit <number>', 'Number of events to show', '10')
|
|
520
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
463
521
|
.action(async (options) => {
|
|
464
|
-
const
|
|
522
|
+
const projectPath = options.project || process.cwd();
|
|
523
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
465
524
|
|
|
466
525
|
try {
|
|
467
526
|
await service.initialize();
|
|
@@ -519,8 +578,10 @@ endlessCmd
|
|
|
519
578
|
.description('View consolidated memories')
|
|
520
579
|
.option('-l, --limit <number>', 'Number of memories to show', '10')
|
|
521
580
|
.option('-q, --query <text>', 'Search consolidated memories')
|
|
581
|
+
.option('-p, --project <path>', 'Project path (defaults to cwd)')
|
|
522
582
|
.action(async (options) => {
|
|
523
|
-
const
|
|
583
|
+
const projectPath = options.project || process.cwd();
|
|
584
|
+
const service = getMemoryServiceForProject(projectPath);
|
|
524
585
|
|
|
525
586
|
try {
|
|
526
587
|
await service.initialize();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
|
-
import { Database } from '
|
|
8
|
+
import { dbRun, dbAll, toDate, type Database } from './db-wrapper.js';
|
|
9
9
|
import type {
|
|
10
10
|
ConsolidatedMemory,
|
|
11
11
|
ConsolidatedMemoryInput
|
|
@@ -25,7 +25,8 @@ export class ConsolidatedStore {
|
|
|
25
25
|
async create(input: ConsolidatedMemoryInput): Promise<string> {
|
|
26
26
|
const memoryId = randomUUID();
|
|
27
27
|
|
|
28
|
-
await
|
|
28
|
+
await dbRun(
|
|
29
|
+
this.db,
|
|
29
30
|
`INSERT INTO consolidated_memories
|
|
30
31
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
31
32
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
@@ -45,7 +46,8 @@ export class ConsolidatedStore {
|
|
|
45
46
|
* Get a consolidated memory by ID
|
|
46
47
|
*/
|
|
47
48
|
async get(memoryId: string): Promise<ConsolidatedMemory | null> {
|
|
48
|
-
const rows = await
|
|
49
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
50
|
+
this.db,
|
|
49
51
|
`SELECT * FROM consolidated_memories WHERE memory_id = ?`,
|
|
50
52
|
[memoryId]
|
|
51
53
|
);
|
|
@@ -60,7 +62,8 @@ export class ConsolidatedStore {
|
|
|
60
62
|
async search(query: string, options?: { topK?: number }): Promise<ConsolidatedMemory[]> {
|
|
61
63
|
const topK = options?.topK || 5;
|
|
62
64
|
|
|
63
|
-
const rows = await
|
|
65
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
66
|
+
this.db,
|
|
64
67
|
`SELECT * FROM consolidated_memories
|
|
65
68
|
WHERE summary LIKE ?
|
|
66
69
|
ORDER BY confidence DESC
|
|
@@ -81,7 +84,8 @@ export class ConsolidatedStore {
|
|
|
81
84
|
const topicConditions = topics.map(() => `topics LIKE ?`).join(' OR ');
|
|
82
85
|
const topicParams = topics.map(t => `%"${t}"%`);
|
|
83
86
|
|
|
84
|
-
const rows = await
|
|
87
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
88
|
+
this.db,
|
|
85
89
|
`SELECT * FROM consolidated_memories
|
|
86
90
|
WHERE ${topicConditions}
|
|
87
91
|
ORDER BY confidence DESC
|
|
@@ -98,7 +102,8 @@ export class ConsolidatedStore {
|
|
|
98
102
|
async getAll(options?: { limit?: number }): Promise<ConsolidatedMemory[]> {
|
|
99
103
|
const limit = options?.limit || 100;
|
|
100
104
|
|
|
101
|
-
const rows = await
|
|
105
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
106
|
+
this.db,
|
|
102
107
|
`SELECT * FROM consolidated_memories
|
|
103
108
|
ORDER BY confidence DESC, created_at DESC
|
|
104
109
|
LIMIT ?`,
|
|
@@ -115,7 +120,8 @@ export class ConsolidatedStore {
|
|
|
115
120
|
const limit = options?.limit || 10;
|
|
116
121
|
const hours = options?.hours || 24;
|
|
117
122
|
|
|
118
|
-
const rows = await
|
|
123
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
124
|
+
this.db,
|
|
119
125
|
`SELECT * FROM consolidated_memories
|
|
120
126
|
WHERE created_at > datetime('now', '-${hours} hours')
|
|
121
127
|
ORDER BY created_at DESC
|
|
@@ -130,7 +136,8 @@ export class ConsolidatedStore {
|
|
|
130
136
|
* Mark a memory as accessed (tracks usage for importance scoring)
|
|
131
137
|
*/
|
|
132
138
|
async markAccessed(memoryId: string): Promise<void> {
|
|
133
|
-
await
|
|
139
|
+
await dbRun(
|
|
140
|
+
this.db,
|
|
134
141
|
`UPDATE consolidated_memories
|
|
135
142
|
SET accessed_at = CURRENT_TIMESTAMP,
|
|
136
143
|
access_count = access_count + 1
|
|
@@ -143,7 +150,8 @@ export class ConsolidatedStore {
|
|
|
143
150
|
* Update confidence score for a memory
|
|
144
151
|
*/
|
|
145
152
|
async updateConfidence(memoryId: string, confidence: number): Promise<void> {
|
|
146
|
-
await
|
|
153
|
+
await dbRun(
|
|
154
|
+
this.db,
|
|
147
155
|
`UPDATE consolidated_memories
|
|
148
156
|
SET confidence = ?
|
|
149
157
|
WHERE memory_id = ?`,
|
|
@@ -155,7 +163,8 @@ export class ConsolidatedStore {
|
|
|
155
163
|
* Delete a consolidated memory
|
|
156
164
|
*/
|
|
157
165
|
async delete(memoryId: string): Promise<void> {
|
|
158
|
-
await
|
|
166
|
+
await dbRun(
|
|
167
|
+
this.db,
|
|
159
168
|
`DELETE FROM consolidated_memories WHERE memory_id = ?`,
|
|
160
169
|
[memoryId]
|
|
161
170
|
);
|
|
@@ -165,7 +174,8 @@ export class ConsolidatedStore {
|
|
|
165
174
|
* Get count of consolidated memories
|
|
166
175
|
*/
|
|
167
176
|
async count(): Promise<number> {
|
|
168
|
-
const result = await
|
|
177
|
+
const result = await dbAll<{ count: number }>(
|
|
178
|
+
this.db,
|
|
169
179
|
`SELECT COUNT(*) as count FROM consolidated_memories`
|
|
170
180
|
);
|
|
171
181
|
return result[0]?.count || 0;
|
|
@@ -175,7 +185,8 @@ export class ConsolidatedStore {
|
|
|
175
185
|
* Get most accessed memories (for importance scoring)
|
|
176
186
|
*/
|
|
177
187
|
async getMostAccessed(limit: number = 10): Promise<ConsolidatedMemory[]> {
|
|
178
|
-
const rows = await
|
|
188
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
189
|
+
this.db,
|
|
179
190
|
`SELECT * FROM consolidated_memories
|
|
180
191
|
WHERE access_count > 0
|
|
181
192
|
ORDER BY access_count DESC
|
|
@@ -197,12 +208,14 @@ export class ConsolidatedStore {
|
|
|
197
208
|
}> {
|
|
198
209
|
const total = await this.count();
|
|
199
210
|
|
|
200
|
-
const avgResult = await
|
|
211
|
+
const avgResult = await dbAll<{ avg: number | null }>(
|
|
212
|
+
this.db,
|
|
201
213
|
`SELECT AVG(confidence) as avg FROM consolidated_memories`
|
|
202
214
|
);
|
|
203
215
|
const averageConfidence = avgResult[0]?.avg || 0;
|
|
204
216
|
|
|
205
|
-
const recentResult = await
|
|
217
|
+
const recentResult = await dbAll<{ count: number }>(
|
|
218
|
+
this.db,
|
|
206
219
|
`SELECT COUNT(*) as count FROM consolidated_memories
|
|
207
220
|
WHERE created_at > datetime('now', '-24 hours')`
|
|
208
221
|
);
|
|
@@ -230,7 +243,8 @@ export class ConsolidatedStore {
|
|
|
230
243
|
*/
|
|
231
244
|
async isAlreadyConsolidated(eventIds: string[]): Promise<boolean> {
|
|
232
245
|
for (const eventId of eventIds) {
|
|
233
|
-
const result = await
|
|
246
|
+
const result = await dbAll<{ count: number }>(
|
|
247
|
+
this.db,
|
|
234
248
|
`SELECT COUNT(*) as count FROM consolidated_memories
|
|
235
249
|
WHERE source_events LIKE ?`,
|
|
236
250
|
[`%"${eventId}"%`]
|
|
@@ -244,7 +258,8 @@ export class ConsolidatedStore {
|
|
|
244
258
|
* Get the last consolidation time
|
|
245
259
|
*/
|
|
246
260
|
async getLastConsolidationTime(): Promise<Date | null> {
|
|
247
|
-
const result = await
|
|
261
|
+
const result = await dbAll<{ created_at: string }>(
|
|
262
|
+
this.db,
|
|
248
263
|
`SELECT created_at FROM consolidated_memories
|
|
249
264
|
ORDER BY created_at DESC
|
|
250
265
|
LIMIT 1`
|
|
@@ -264,8 +279,8 @@ export class ConsolidatedStore {
|
|
|
264
279
|
topics: JSON.parse(row.topics as string || '[]'),
|
|
265
280
|
sourceEvents: JSON.parse(row.source_events as string || '[]'),
|
|
266
281
|
confidence: row.confidence as number,
|
|
267
|
-
createdAt:
|
|
268
|
-
accessedAt: row.accessed_at ?
|
|
282
|
+
createdAt: toDate(row.created_at),
|
|
283
|
+
accessedAt: row.accessed_at ? toDate(row.accessed_at) : undefined,
|
|
269
284
|
accessCount: row.access_count as number || 0
|
|
270
285
|
};
|
|
271
286
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
|
-
import { Database } from '
|
|
8
|
+
import { dbRun, dbAll, toDate, type Database } from './db-wrapper.js';
|
|
9
9
|
import type {
|
|
10
10
|
EndlessModeConfig,
|
|
11
11
|
ContextSnapshot,
|
|
@@ -107,7 +107,8 @@ export class ContinuityManager {
|
|
|
107
107
|
* Get recent continuity logs
|
|
108
108
|
*/
|
|
109
109
|
async getRecentLogs(limit: number = 10): Promise<ContinuityLog[]> {
|
|
110
|
-
const rows = await
|
|
110
|
+
const rows = await dbAll<Record<string, unknown>>(
|
|
111
|
+
this.db,
|
|
111
112
|
`SELECT * FROM continuity_log
|
|
112
113
|
ORDER BY created_at DESC
|
|
113
114
|
LIMIT ?`,
|
|
@@ -120,7 +121,7 @@ export class ContinuityManager {
|
|
|
120
121
|
toContextId: row.to_context_id as string | undefined,
|
|
121
122
|
continuityScore: row.continuity_score as number,
|
|
122
123
|
transitionType: row.transition_type as TransitionType,
|
|
123
|
-
createdAt:
|
|
124
|
+
createdAt: toDate(row.created_at)
|
|
124
125
|
}));
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -128,7 +129,8 @@ export class ContinuityManager {
|
|
|
128
129
|
* Get average continuity score over time period
|
|
129
130
|
*/
|
|
130
131
|
async getAverageScore(hours: number = 1): Promise<number> {
|
|
131
|
-
const result = await
|
|
132
|
+
const result = await dbAll<{ avg_score: number | null }>(
|
|
133
|
+
this.db,
|
|
132
134
|
`SELECT AVG(continuity_score) as avg_score
|
|
133
135
|
FROM continuity_log
|
|
134
136
|
WHERE created_at > datetime('now', '-${hours} hours')`
|
|
@@ -141,7 +143,8 @@ export class ContinuityManager {
|
|
|
141
143
|
* Get transition type distribution
|
|
142
144
|
*/
|
|
143
145
|
async getTransitionStats(hours: number = 24): Promise<Record<TransitionType, number>> {
|
|
144
|
-
const rows = await
|
|
146
|
+
const rows = await dbAll<{ transition_type: string; count: number }>(
|
|
147
|
+
this.db,
|
|
145
148
|
`SELECT transition_type, COUNT(*) as count
|
|
146
149
|
FROM continuity_log
|
|
147
150
|
WHERE created_at > datetime('now', '-${hours} hours')
|
|
@@ -165,7 +168,8 @@ export class ContinuityManager {
|
|
|
165
168
|
* Clear old continuity logs
|
|
166
169
|
*/
|
|
167
170
|
async cleanup(olderThanDays: number = 7): Promise<number> {
|
|
168
|
-
const result = await
|
|
171
|
+
const result = await dbAll<{ changes: number }>(
|
|
172
|
+
this.db,
|
|
169
173
|
`DELETE FROM continuity_log
|
|
170
174
|
WHERE created_at < datetime('now', '-${olderThanDays} days')
|
|
171
175
|
RETURNING COUNT(*) as changes`
|
|
@@ -211,7 +215,8 @@ export class ContinuityManager {
|
|
|
211
215
|
score: number,
|
|
212
216
|
type: TransitionType
|
|
213
217
|
): Promise<void> {
|
|
214
|
-
await
|
|
218
|
+
await dbRun(
|
|
219
|
+
this.db,
|
|
215
220
|
`INSERT INTO continuity_log
|
|
216
221
|
(log_id, from_context_id, to_context_id, continuity_score, transition_type, created_at)
|
|
217
222
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DuckDB Promise Wrapper
|
|
3
|
+
* Wraps the callback-based DuckDB API with Promise-based async/await interface
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import duckdb from 'duckdb';
|
|
7
|
+
|
|
8
|
+
export type Database = duckdb.Database;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts BigInt values to Number in an object
|
|
12
|
+
* DuckDB returns BigInt for COUNT(*) and other aggregate functions
|
|
13
|
+
*/
|
|
14
|
+
function convertBigInts<T>(obj: T): T {
|
|
15
|
+
if (obj === null || obj === undefined) return obj;
|
|
16
|
+
if (typeof obj === 'bigint') return Number(obj) as unknown as T;
|
|
17
|
+
if (obj instanceof Date) return obj; // Preserve Date objects
|
|
18
|
+
if (Array.isArray(obj)) return obj.map(convertBigInts) as unknown as T;
|
|
19
|
+
if (typeof obj === 'object') {
|
|
20
|
+
const result: Record<string, unknown> = {};
|
|
21
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
22
|
+
result[key] = convertBigInts(value);
|
|
23
|
+
}
|
|
24
|
+
return result as T;
|
|
25
|
+
}
|
|
26
|
+
return obj;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely converts a value to a Date object
|
|
31
|
+
* Handles both Date objects and string timestamps from DuckDB
|
|
32
|
+
*/
|
|
33
|
+
export function toDate(value: unknown): Date {
|
|
34
|
+
if (value instanceof Date) return value;
|
|
35
|
+
if (typeof value === 'string') return new Date(value);
|
|
36
|
+
if (typeof value === 'number') return new Date(value);
|
|
37
|
+
return new Date(String(value));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new DuckDB database with Promise-based API
|
|
42
|
+
*/
|
|
43
|
+
export function createDatabase(path: string): Database {
|
|
44
|
+
return new duckdb.Database(path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Promisified db.run() - executes a statement that doesn't return rows
|
|
49
|
+
*/
|
|
50
|
+
export function dbRun(db: Database, sql: string, params: unknown[] = []): Promise<void> {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (params.length === 0) {
|
|
53
|
+
db.run(sql, (err: Error | null) => {
|
|
54
|
+
if (err) reject(err);
|
|
55
|
+
else resolve();
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
db.run(sql, ...params, (err: Error | null) => {
|
|
59
|
+
if (err) reject(err);
|
|
60
|
+
else resolve();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Promisified db.all() - executes a query and returns all rows
|
|
68
|
+
* Automatically converts BigInt values to Number
|
|
69
|
+
*/
|
|
70
|
+
export function dbAll<T = Record<string, unknown>>(
|
|
71
|
+
db: Database,
|
|
72
|
+
sql: string,
|
|
73
|
+
params: unknown[] = []
|
|
74
|
+
): Promise<T[]> {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
if (params.length === 0) {
|
|
77
|
+
db.all(sql, (err: Error | null, rows: T[]) => {
|
|
78
|
+
if (err) reject(err);
|
|
79
|
+
else resolve(convertBigInts(rows || []));
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
db.all(sql, ...params, (err: Error | null, rows: T[]) => {
|
|
83
|
+
if (err) reject(err);
|
|
84
|
+
else resolve(convertBigInts(rows || []));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Promisified db.close() - closes the database connection
|
|
92
|
+
*/
|
|
93
|
+
export function dbClose(db: Database): Promise<void> {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
db.close((err: Error | null) => {
|
|
96
|
+
if (err) reject(err);
|
|
97
|
+
else resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Promisified db.exec() - executes multiple statements
|
|
104
|
+
*/
|
|
105
|
+
export function dbExec(db: Database, sql: string): Promise<void> {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
db.exec(sql, (err: Error | null) => {
|
|
108
|
+
if (err) reject(err);
|
|
109
|
+
else resolve();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|