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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Wrapper with WAL Mode Support
|
|
3
|
+
* Primary store for hooks - always available, no lock conflicts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
|
|
8
|
+
export type SQLiteDatabase = Database.Database;
|
|
9
|
+
|
|
10
|
+
export interface SQLiteOptions {
|
|
11
|
+
readonly?: boolean;
|
|
12
|
+
walMode?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new SQLite database with WAL mode
|
|
17
|
+
*/
|
|
18
|
+
export function createSQLiteDatabase(path: string, options?: SQLiteOptions): SQLiteDatabase {
|
|
19
|
+
const db = new Database(path, {
|
|
20
|
+
readonly: options?.readonly ?? false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Enable WAL mode for concurrent access (unless read-only)
|
|
24
|
+
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
25
|
+
db.pragma('journal_mode = WAL');
|
|
26
|
+
db.pragma('synchronous = NORMAL');
|
|
27
|
+
db.pragma('busy_timeout = 5000');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute a statement that doesn't return rows (INSERT, UPDATE, DELETE)
|
|
35
|
+
*/
|
|
36
|
+
export function sqliteRun(
|
|
37
|
+
db: SQLiteDatabase,
|
|
38
|
+
sql: string,
|
|
39
|
+
params: unknown[] = []
|
|
40
|
+
): Database.RunResult {
|
|
41
|
+
const stmt = db.prepare(sql);
|
|
42
|
+
return stmt.run(...params);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute a query and return all rows
|
|
47
|
+
*/
|
|
48
|
+
export function sqliteAll<T = Record<string, unknown>>(
|
|
49
|
+
db: SQLiteDatabase,
|
|
50
|
+
sql: string,
|
|
51
|
+
params: unknown[] = []
|
|
52
|
+
): T[] {
|
|
53
|
+
const stmt = db.prepare(sql);
|
|
54
|
+
return stmt.all(...params) as T[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Execute a query and return first row
|
|
59
|
+
*/
|
|
60
|
+
export function sqliteGet<T = Record<string, unknown>>(
|
|
61
|
+
db: SQLiteDatabase,
|
|
62
|
+
sql: string,
|
|
63
|
+
params: unknown[] = []
|
|
64
|
+
): T | undefined {
|
|
65
|
+
const stmt = db.prepare(sql);
|
|
66
|
+
return stmt.get(...params) as T | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute multiple statements (for schema creation)
|
|
71
|
+
*/
|
|
72
|
+
export function sqliteExec(db: SQLiteDatabase, sql: string): void {
|
|
73
|
+
db.exec(sql);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Close database connection
|
|
78
|
+
*/
|
|
79
|
+
export function sqliteClose(db: SQLiteDatabase): void {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run multiple statements in a transaction
|
|
85
|
+
*/
|
|
86
|
+
export function sqliteTransaction<T>(
|
|
87
|
+
db: SQLiteDatabase,
|
|
88
|
+
fn: () => T
|
|
89
|
+
): T {
|
|
90
|
+
return db.transaction(fn)();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Safely converts a value to a Date object
|
|
95
|
+
*/
|
|
96
|
+
export function toDateFromSQLite(value: unknown): Date {
|
|
97
|
+
if (value instanceof Date) return value;
|
|
98
|
+
if (typeof value === 'string') return new Date(value);
|
|
99
|
+
if (typeof value === 'number') return new Date(value);
|
|
100
|
+
return new Date(String(value));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert Date to ISO string for SQLite storage
|
|
105
|
+
*/
|
|
106
|
+
export function toSQLiteTimestamp(date: Date): string {
|
|
107
|
+
return date.toISOString();
|
|
108
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Worker - SQLite to DuckDB synchronization
|
|
3
|
+
* Runs periodically to sync primary store (SQLite) to analytics store (DuckDB)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SQLiteEventStore } from './sqlite-event-store.js';
|
|
7
|
+
import { EventStore } from './event-store.js';
|
|
8
|
+
import { MemoryEvent } from './types.js';
|
|
9
|
+
|
|
10
|
+
export interface SyncWorkerConfig {
|
|
11
|
+
intervalMs: number; // Sync interval (default: 30000 = 30 seconds)
|
|
12
|
+
batchSize: number; // Events per batch (default: 500)
|
|
13
|
+
maxRetries: number; // Max retries on failure (default: 3)
|
|
14
|
+
retryDelayMs: number; // Delay between retries (default: 5000)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG: SyncWorkerConfig = {
|
|
18
|
+
intervalMs: 30000,
|
|
19
|
+
batchSize: 500,
|
|
20
|
+
maxRetries: 3,
|
|
21
|
+
retryDelayMs: 5000
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface SyncStats {
|
|
25
|
+
lastSyncAt: Date | null;
|
|
26
|
+
eventsSynced: number;
|
|
27
|
+
sessionsSynced: number;
|
|
28
|
+
errors: number;
|
|
29
|
+
status: 'idle' | 'syncing' | 'error' | 'stopped';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SyncWorker {
|
|
33
|
+
private config: SyncWorkerConfig;
|
|
34
|
+
private intervalHandle: NodeJS.Timeout | null = null;
|
|
35
|
+
private running = false;
|
|
36
|
+
private stats: SyncStats = {
|
|
37
|
+
lastSyncAt: null,
|
|
38
|
+
eventsSynced: 0,
|
|
39
|
+
sessionsSynced: 0,
|
|
40
|
+
errors: 0,
|
|
41
|
+
status: 'idle'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private sqliteStore: SQLiteEventStore,
|
|
46
|
+
private duckdbStore: EventStore,
|
|
47
|
+
config?: Partial<SyncWorkerConfig>
|
|
48
|
+
) {
|
|
49
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Start the sync worker
|
|
54
|
+
*/
|
|
55
|
+
start(): void {
|
|
56
|
+
if (this.running) return;
|
|
57
|
+
|
|
58
|
+
this.running = true;
|
|
59
|
+
this.stats.status = 'idle';
|
|
60
|
+
|
|
61
|
+
// Run initial sync
|
|
62
|
+
this.syncNow().catch(err => {
|
|
63
|
+
console.error('[SyncWorker] Initial sync failed:', err);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Schedule periodic sync
|
|
67
|
+
this.intervalHandle = setInterval(() => {
|
|
68
|
+
this.syncNow().catch(err => {
|
|
69
|
+
console.error('[SyncWorker] Periodic sync failed:', err);
|
|
70
|
+
});
|
|
71
|
+
}, this.config.intervalMs);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop the sync worker
|
|
76
|
+
*/
|
|
77
|
+
stop(): void {
|
|
78
|
+
this.running = false;
|
|
79
|
+
this.stats.status = 'stopped';
|
|
80
|
+
|
|
81
|
+
if (this.intervalHandle) {
|
|
82
|
+
clearInterval(this.intervalHandle);
|
|
83
|
+
this.intervalHandle = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Trigger immediate sync
|
|
89
|
+
*/
|
|
90
|
+
async syncNow(): Promise<void> {
|
|
91
|
+
if (this.stats.status === 'syncing') {
|
|
92
|
+
return; // Already syncing
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.stats.status = 'syncing';
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await this.syncEvents();
|
|
99
|
+
await this.syncSessions();
|
|
100
|
+
this.stats.lastSyncAt = new Date();
|
|
101
|
+
this.stats.status = 'idle';
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this.stats.errors++;
|
|
104
|
+
this.stats.status = 'error';
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sync events from SQLite to DuckDB
|
|
111
|
+
*/
|
|
112
|
+
private async syncEvents(): Promise<void> {
|
|
113
|
+
const targetName = 'duckdb_analytics';
|
|
114
|
+
|
|
115
|
+
// Get last sync position from SQLite
|
|
116
|
+
const position = await this.sqliteStore.getSyncPosition(targetName);
|
|
117
|
+
const lastTimestamp = position.lastTimestamp || '1970-01-01T00:00:00.000Z';
|
|
118
|
+
|
|
119
|
+
let hasMore = true;
|
|
120
|
+
let totalSynced = 0;
|
|
121
|
+
|
|
122
|
+
while (hasMore) {
|
|
123
|
+
// Get batch of events since last sync
|
|
124
|
+
const events = await this.sqliteStore.getEventsSince(lastTimestamp, this.config.batchSize);
|
|
125
|
+
|
|
126
|
+
if (events.length === 0) {
|
|
127
|
+
hasMore = false;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Insert into DuckDB with retry
|
|
132
|
+
await this.retryWithBackoff(async () => {
|
|
133
|
+
for (const event of events) {
|
|
134
|
+
await this.insertEventToDuckDB(event);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
totalSynced += events.length;
|
|
139
|
+
|
|
140
|
+
// Update sync position
|
|
141
|
+
const lastEvent = events[events.length - 1];
|
|
142
|
+
await this.sqliteStore.updateSyncPosition(
|
|
143
|
+
targetName,
|
|
144
|
+
lastEvent.id,
|
|
145
|
+
lastEvent.timestamp.toISOString()
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Check if we got a full batch (more to sync)
|
|
149
|
+
hasMore = events.length === this.config.batchSize;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.stats.eventsSynced += totalSynced;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sync sessions from SQLite to DuckDB
|
|
157
|
+
*/
|
|
158
|
+
private async syncSessions(): Promise<void> {
|
|
159
|
+
// Get all sessions from SQLite
|
|
160
|
+
const sessions = await this.sqliteStore.getAllSessions();
|
|
161
|
+
|
|
162
|
+
// Upsert each session to DuckDB
|
|
163
|
+
for (const session of sessions) {
|
|
164
|
+
await this.retryWithBackoff(async () => {
|
|
165
|
+
await this.duckdbStore.upsertSession(session);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.stats.sessionsSynced = sessions.length;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Insert a single event into DuckDB
|
|
174
|
+
*/
|
|
175
|
+
private async insertEventToDuckDB(event: MemoryEvent): Promise<void> {
|
|
176
|
+
// Use append which handles deduplication
|
|
177
|
+
await this.duckdbStore.append({
|
|
178
|
+
eventType: event.eventType,
|
|
179
|
+
sessionId: event.sessionId,
|
|
180
|
+
timestamp: event.timestamp,
|
|
181
|
+
content: event.content,
|
|
182
|
+
metadata: event.metadata
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Retry operation with exponential backoff
|
|
188
|
+
*/
|
|
189
|
+
private async retryWithBackoff<T>(fn: () => Promise<T>): Promise<T> {
|
|
190
|
+
let lastError: Error | null = null;
|
|
191
|
+
|
|
192
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
193
|
+
try {
|
|
194
|
+
return await fn();
|
|
195
|
+
} catch (error) {
|
|
196
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
197
|
+
|
|
198
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
199
|
+
const delay = this.config.retryDelayMs * Math.pow(2, attempt);
|
|
200
|
+
await this.sleep(delay);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw lastError;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sleep utility
|
|
210
|
+
*/
|
|
211
|
+
private sleep(ms: number): Promise<void> {
|
|
212
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get sync statistics
|
|
217
|
+
*/
|
|
218
|
+
getStats(): SyncStats {
|
|
219
|
+
return { ...this.stats };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if worker is running
|
|
224
|
+
*/
|
|
225
|
+
isRunning(): boolean {
|
|
226
|
+
return this.running;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -26,6 +26,7 @@ export class VectorWorker {
|
|
|
26
26
|
private readonly embedder: Embedder;
|
|
27
27
|
private readonly config: WorkerConfig;
|
|
28
28
|
private running = false;
|
|
29
|
+
private stopping = false;
|
|
29
30
|
private pollTimeout: NodeJS.Timeout | null = null;
|
|
30
31
|
|
|
31
32
|
constructor(
|
|
@@ -46,6 +47,7 @@ export class VectorWorker {
|
|
|
46
47
|
start(): void {
|
|
47
48
|
if (this.running) return;
|
|
48
49
|
this.running = true;
|
|
50
|
+
this.stopping = false;
|
|
49
51
|
this.poll();
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -54,6 +56,7 @@ export class VectorWorker {
|
|
|
54
56
|
*/
|
|
55
57
|
stop(): void {
|
|
56
58
|
this.running = false;
|
|
59
|
+
this.stopping = true;
|
|
57
60
|
if (this.pollTimeout) {
|
|
58
61
|
clearTimeout(this.pollTimeout);
|
|
59
62
|
this.pollTimeout = null;
|
|
@@ -122,10 +125,17 @@ export class VectorWorker {
|
|
|
122
125
|
|
|
123
126
|
return successful.length;
|
|
124
127
|
} catch (error) {
|
|
125
|
-
// Mark all items as failed
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
// Mark all items as failed, but only if not stopping (DB might be closed)
|
|
129
|
+
if (!this.stopping) {
|
|
130
|
+
try {
|
|
131
|
+
const allIds = items.map(i => i.id);
|
|
132
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
133
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
134
|
+
} catch (failError) {
|
|
135
|
+
// Database might be closed during shutdown, ignore
|
|
136
|
+
console.warn('Could not mark outbox items as failed (database may be closed)');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
129
139
|
throw error;
|
|
130
140
|
}
|
|
131
141
|
}
|
|
@@ -134,16 +144,21 @@ export class VectorWorker {
|
|
|
134
144
|
* Poll for new items
|
|
135
145
|
*/
|
|
136
146
|
private async poll(): Promise<void> {
|
|
137
|
-
if (!this.running) return;
|
|
147
|
+
if (!this.running || this.stopping) return;
|
|
138
148
|
|
|
139
149
|
try {
|
|
140
150
|
await this.processBatch();
|
|
141
151
|
} catch (error) {
|
|
142
|
-
|
|
152
|
+
// Only log if not stopping (error during shutdown is expected)
|
|
153
|
+
if (!this.stopping) {
|
|
154
|
+
console.error('Vector worker error:', error);
|
|
155
|
+
}
|
|
143
156
|
}
|
|
144
157
|
|
|
145
|
-
// Schedule next poll
|
|
146
|
-
this.
|
|
158
|
+
// Schedule next poll only if still running
|
|
159
|
+
if (this.running && !this.stopping) {
|
|
160
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
161
|
+
}
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
/**
|
|
@@ -319,6 +334,7 @@ export class VectorWorkerV2 {
|
|
|
319
334
|
private readonly contentProvider: ContentProvider;
|
|
320
335
|
private readonly config: WorkerConfigV2;
|
|
321
336
|
private running = false;
|
|
337
|
+
private stopping = false;
|
|
322
338
|
private pollTimeout: NodeJS.Timeout | null = null;
|
|
323
339
|
|
|
324
340
|
constructor(
|
|
@@ -344,6 +360,7 @@ export class VectorWorkerV2 {
|
|
|
344
360
|
start(): void {
|
|
345
361
|
if (this.running) return;
|
|
346
362
|
this.running = true;
|
|
363
|
+
this.stopping = false;
|
|
347
364
|
this.poll();
|
|
348
365
|
}
|
|
349
366
|
|
|
@@ -352,6 +369,7 @@ export class VectorWorkerV2 {
|
|
|
352
369
|
*/
|
|
353
370
|
stop(): void {
|
|
354
371
|
this.running = false;
|
|
372
|
+
this.stopping = true;
|
|
355
373
|
if (this.pollTimeout) {
|
|
356
374
|
clearTimeout(this.pollTimeout);
|
|
357
375
|
this.pollTimeout = null;
|
|
@@ -376,8 +394,15 @@ export class VectorWorkerV2 {
|
|
|
376
394
|
await this.outbox.markDone(job.jobId);
|
|
377
395
|
successCount++;
|
|
378
396
|
} catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
397
|
+
// Only try to mark as failed if not stopping (DB might be closed)
|
|
398
|
+
if (!this.stopping) {
|
|
399
|
+
try {
|
|
400
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
401
|
+
await this.outbox.markFailed(job.jobId, errorMessage);
|
|
402
|
+
} catch {
|
|
403
|
+
// Database might be closed during shutdown, ignore
|
|
404
|
+
}
|
|
405
|
+
}
|
|
381
406
|
}
|
|
382
407
|
}
|
|
383
408
|
|
|
@@ -422,16 +447,21 @@ export class VectorWorkerV2 {
|
|
|
422
447
|
* Poll for new jobs
|
|
423
448
|
*/
|
|
424
449
|
private async poll(): Promise<void> {
|
|
425
|
-
if (!this.running) return;
|
|
450
|
+
if (!this.running || this.stopping) return;
|
|
426
451
|
|
|
427
452
|
try {
|
|
428
453
|
await this.processBatch();
|
|
429
454
|
} catch (error) {
|
|
430
|
-
|
|
455
|
+
// Only log if not stopping (error during shutdown is expected)
|
|
456
|
+
if (!this.stopping) {
|
|
457
|
+
console.error('Vector worker V2 error:', error);
|
|
458
|
+
}
|
|
431
459
|
}
|
|
432
460
|
|
|
433
|
-
// Schedule next poll
|
|
434
|
-
this.
|
|
461
|
+
// Schedule next poll only if still running
|
|
462
|
+
if (this.running && !this.stopping) {
|
|
463
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
464
|
+
}
|
|
435
465
|
}
|
|
436
466
|
|
|
437
467
|
/**
|
|
@@ -15,14 +15,24 @@ async function main(): Promise<void> {
|
|
|
15
15
|
// Get project-specific memory service via session lookup
|
|
16
16
|
const memoryService = getMemoryServiceForSession(input.session_id);
|
|
17
17
|
|
|
18
|
+
// Configuration from environment variables
|
|
19
|
+
const config = {
|
|
20
|
+
candidateCount: parseInt(process.env.CLAUDE_MEMORY_CANDIDATES || '10'),
|
|
21
|
+
minScore: parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.7'),
|
|
22
|
+
maxMemories: parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5'),
|
|
23
|
+
dynamicThresholdRatio: parseFloat(process.env.CLAUDE_MEMORY_THRESHOLD_RATIO || '0.85'),
|
|
24
|
+
strictMinScore: parseFloat(process.env.CLAUDE_MEMORY_STRICT_MIN || '0.75')
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
try {
|
|
19
28
|
// Check if shared store is enabled
|
|
20
29
|
const includeShared = memoryService.isSharedStoreEnabled();
|
|
21
30
|
|
|
22
31
|
// Retrieve relevant memories for the prompt (including shared if enabled)
|
|
32
|
+
// First, get more candidates to filter from
|
|
23
33
|
const retrievalResult = await memoryService.retrieveMemories(input.prompt, {
|
|
24
|
-
topK:
|
|
25
|
-
minScore:
|
|
34
|
+
topK: config.candidateCount, // Get more candidates initially
|
|
35
|
+
minScore: config.minScore,
|
|
26
36
|
includeShared
|
|
27
37
|
});
|
|
28
38
|
|
|
@@ -32,8 +42,47 @@ async function main(): Promise<void> {
|
|
|
32
42
|
input.prompt
|
|
33
43
|
);
|
|
34
44
|
|
|
35
|
-
//
|
|
36
|
-
|
|
45
|
+
// Filter memories based on relevance and confidence
|
|
46
|
+
let relevantMemories = [];
|
|
47
|
+
if (retrievalResult.memories && retrievalResult.memories.length > 0) {
|
|
48
|
+
// Dynamic threshold based on the best score
|
|
49
|
+
const bestScore = Math.max(...retrievalResult.memories.map(m => m.score));
|
|
50
|
+
const dynamicThreshold = Math.max(config.strictMinScore, bestScore * config.dynamicThresholdRatio);
|
|
51
|
+
|
|
52
|
+
// Filter memories that meet the dynamic threshold
|
|
53
|
+
relevantMemories = retrievalResult.memories.filter(m => m.score >= dynamicThreshold);
|
|
54
|
+
|
|
55
|
+
// Limit to configured max memories
|
|
56
|
+
relevantMemories = relevantMemories.slice(0, config.maxMemories);
|
|
57
|
+
|
|
58
|
+
// Check if we have enough highly relevant memories
|
|
59
|
+
if (relevantMemories.length === 0) {
|
|
60
|
+
// If no memories meet the high threshold, take top 3 with relaxed threshold
|
|
61
|
+
const fallbackCount = Math.min(3, config.maxMemories);
|
|
62
|
+
relevantMemories = retrievalResult.memories
|
|
63
|
+
.filter(m => m.score >= config.minScore)
|
|
64
|
+
.slice(0, fallbackCount);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Log filtering statistics for debugging
|
|
68
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
69
|
+
console.error(`Memory filtering: ${retrievalResult.memories.length} candidates -> ${relevantMemories.length} selected`);
|
|
70
|
+
console.error(`Threshold: ${dynamicThreshold.toFixed(3)}, Best score: ${bestScore.toFixed(3)}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Increment access count only for memories that will actually be included
|
|
74
|
+
if (relevantMemories.length > 0) {
|
|
75
|
+
const eventIds = relevantMemories.map(m => m.event.id);
|
|
76
|
+
await memoryService.incrementMemoryAccess(eventIds);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Format context for Claude with only the relevant memories
|
|
81
|
+
const filteredResult = {
|
|
82
|
+
...retrievalResult,
|
|
83
|
+
memories: relevantMemories
|
|
84
|
+
};
|
|
85
|
+
const context = memoryService.formatAsContext(filteredResult);
|
|
37
86
|
|
|
38
87
|
const output: UserPromptSubmitOutput = { context };
|
|
39
88
|
console.log(JSON.stringify(output));
|
package/src/server/api/stats.ts
CHANGED
|
@@ -66,6 +66,7 @@ statsRouter.get('/levels/:level', async (c) => {
|
|
|
66
66
|
const { level } = c.req.param();
|
|
67
67
|
const limit = parseInt(c.req.query('limit') || '20', 10);
|
|
68
68
|
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
69
|
+
const sort = c.req.query('sort') || 'recent';
|
|
69
70
|
|
|
70
71
|
// Validate level
|
|
71
72
|
const validLevels = ['L0', 'L1', 'L2', 'L3', 'L4'];
|
|
@@ -76,19 +77,45 @@ statsRouter.get('/levels/:level', async (c) => {
|
|
|
76
77
|
const memoryService = getReadOnlyMemoryService();
|
|
77
78
|
try {
|
|
78
79
|
await memoryService.initialize();
|
|
79
|
-
|
|
80
|
+
let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
|
|
80
81
|
const stats = await memoryService.getStats();
|
|
81
82
|
const levelStat = stats.levelStats.find(s => s.level === level);
|
|
82
83
|
|
|
84
|
+
// Apply sorting
|
|
85
|
+
if (sort === 'accessed') {
|
|
86
|
+
// Sort by access count (will need to get from SQLite)
|
|
87
|
+
// For now, add access count from SQLite if available
|
|
88
|
+
const sqliteStore = (memoryService as any).sqliteEventStore;
|
|
89
|
+
if (sqliteStore) {
|
|
90
|
+
const eventIds = events.map(e => e.id);
|
|
91
|
+
const accessedEvents = await sqliteStore.getMostAccessed(1000);
|
|
92
|
+
const accessMap = new Map(accessedEvents.map((e: any) => [e.id, e.access_count || 0]));
|
|
93
|
+
events = events.map((e: any) => ({
|
|
94
|
+
...e,
|
|
95
|
+
accessCount: accessMap.get(e.id) || 0
|
|
96
|
+
}));
|
|
97
|
+
events.sort((a: any, b: any) => b.accessCount - a.accessCount);
|
|
98
|
+
}
|
|
99
|
+
} else if (sort === 'oldest') {
|
|
100
|
+
events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
101
|
+
} else {
|
|
102
|
+
// 'recent' - default sorting (newest first)
|
|
103
|
+
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Apply limit after sorting
|
|
107
|
+
events = events.slice(0, limit);
|
|
108
|
+
|
|
83
109
|
return c.json({
|
|
84
110
|
level,
|
|
85
|
-
events: events.map(e => ({
|
|
111
|
+
events: events.map((e: any) => ({
|
|
86
112
|
id: e.id,
|
|
87
113
|
eventType: e.eventType,
|
|
88
114
|
sessionId: e.sessionId,
|
|
89
115
|
timestamp: e.timestamp.toISOString(),
|
|
90
116
|
content: e.content.slice(0, 500) + (e.content.length > 500 ? '...' : ''),
|
|
91
|
-
metadata: e.metadata
|
|
117
|
+
metadata: e.metadata,
|
|
118
|
+
accessCount: e.accessCount || 0
|
|
92
119
|
})),
|
|
93
120
|
total: levelStat?.count || 0,
|
|
94
121
|
limit,
|
|
@@ -159,12 +186,14 @@ statsRouter.get('/', async (c) => {
|
|
|
159
186
|
// GET /api/stats/most-accessed - Get most accessed memories
|
|
160
187
|
statsRouter.get('/most-accessed', async (c) => {
|
|
161
188
|
const limit = parseInt(c.req.query('limit') || '10', 10);
|
|
162
|
-
|
|
163
|
-
const memoryService =
|
|
189
|
+
// Use the same read-only service that other stats endpoints use
|
|
190
|
+
const memoryService = getReadOnlyMemoryService();
|
|
164
191
|
|
|
165
192
|
try {
|
|
166
193
|
await memoryService.initialize();
|
|
194
|
+
console.log('[most-accessed] Fetching most accessed memories, limit:', limit);
|
|
167
195
|
const memories = await memoryService.getMostAccessedMemories(limit);
|
|
196
|
+
console.log('[most-accessed] Got memories:', memories.length);
|
|
168
197
|
|
|
169
198
|
return c.json({
|
|
170
199
|
memories: memories.map(m => ({
|
|
@@ -172,13 +201,14 @@ statsRouter.get('/most-accessed', async (c) => {
|
|
|
172
201
|
summary: m.summary,
|
|
173
202
|
topics: m.topics,
|
|
174
203
|
accessCount: m.accessCount,
|
|
175
|
-
lastAccessed: m.
|
|
204
|
+
lastAccessed: m.lastAccessed || null,
|
|
176
205
|
confidence: m.confidence,
|
|
177
|
-
createdAt: m.createdAt.toISOString()
|
|
206
|
+
createdAt: m.createdAt instanceof Date ? m.createdAt.toISOString() : m.createdAt
|
|
178
207
|
})),
|
|
179
208
|
total: memories.length
|
|
180
209
|
});
|
|
181
210
|
} catch (error) {
|
|
211
|
+
console.error('[most-accessed] Error:', error);
|
|
182
212
|
return c.json({
|
|
183
213
|
memories: [],
|
|
184
214
|
total: 0,
|