claude-memory-layer 1.0.7 → 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 +10 -1
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201192048.json +47 -0
- package/.history/package_20260202114053.json +49 -0
- package/HANDOFF.md +92 -0
- package/dist/cli/index.js +1711 -102
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1257 -84
- 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 +1382 -85
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1377 -84
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1383 -86
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1412 -84
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1576 -136
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1585 -143
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1392 -84
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +304 -0
- package/dist/ui/index.html +202 -715
- package/dist/ui/style.css +595 -0
- package/package.json +4 -1
- package/scripts/build.ts +5 -2
- package/src/cli/index.ts +226 -0
- package/src/core/db-wrapper.ts +8 -1
- package/src/core/event-store.ts +70 -3
- package/src/core/graduation-worker.ts +171 -0
- package/src/core/graduation.ts +15 -2
- package/src/core/index.ts +4 -0
- package/src/core/retriever.ts +21 -0
- 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/citations.ts +7 -3
- package/src/server/api/events.ts +7 -3
- package/src/server/api/search.ts +7 -3
- package/src/server/api/sessions.ts +7 -3
- package/src/server/api/stats.ts +159 -12
- package/src/server/index.ts +18 -9
- package/src/services/memory-service.ts +263 -46
- package/src/ui/app.js +304 -0
- package/src/ui/index.html +202 -715
- 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));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadOnlyMemoryService } from '../../services/memory-service.js';
|
|
8
8
|
import { generateCitationId, parseCitationId } from '../../core/citation-generator.js';
|
|
9
9
|
|
|
10
10
|
export const citationsRouter = new Hono();
|
|
@@ -15,9 +15,9 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
15
15
|
|
|
16
16
|
// Support both formats: "a7Bc3x" or "mem:a7Bc3x"
|
|
17
17
|
const citationId = parseCitationId(id) || id;
|
|
18
|
+
const memoryService = getReadOnlyMemoryService();
|
|
18
19
|
|
|
19
20
|
try {
|
|
20
|
-
const memoryService = getDefaultMemoryService();
|
|
21
21
|
await memoryService.initialize();
|
|
22
22
|
|
|
23
23
|
// Search through recent events to find the one matching this citation ID
|
|
@@ -48,6 +48,8 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
48
48
|
});
|
|
49
49
|
} catch (error) {
|
|
50
50
|
return c.json({ error: (error as Error).message }, 500);
|
|
51
|
+
} finally {
|
|
52
|
+
await memoryService.shutdown();
|
|
51
53
|
}
|
|
52
54
|
});
|
|
53
55
|
|
|
@@ -55,9 +57,9 @@ citationsRouter.get('/:id', async (c) => {
|
|
|
55
57
|
citationsRouter.get('/:id/related', async (c) => {
|
|
56
58
|
const { id } = c.req.param();
|
|
57
59
|
const citationId = parseCitationId(id) || id;
|
|
60
|
+
const memoryService = getReadOnlyMemoryService();
|
|
58
61
|
|
|
59
62
|
try {
|
|
60
|
-
const memoryService = getDefaultMemoryService();
|
|
61
63
|
await memoryService.initialize();
|
|
62
64
|
|
|
63
65
|
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
@@ -97,5 +99,7 @@ citationsRouter.get('/:id/related', async (c) => {
|
|
|
97
99
|
});
|
|
98
100
|
} catch (error) {
|
|
99
101
|
return c.json({ error: (error as Error).message }, 500);
|
|
102
|
+
} finally {
|
|
103
|
+
await memoryService.shutdown();
|
|
100
104
|
}
|
|
101
105
|
});
|
package/src/server/api/events.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadOnlyMemoryService } from '../../services/memory-service.js';
|
|
8
8
|
|
|
9
9
|
export const eventsRouter = new Hono();
|
|
10
10
|
|
|
@@ -14,9 +14,9 @@ eventsRouter.get('/', async (c) => {
|
|
|
14
14
|
const eventType = c.req.query('type');
|
|
15
15
|
const limit = parseInt(c.req.query('limit') || '100', 10);
|
|
16
16
|
const offset = parseInt(c.req.query('offset') || '0', 10);
|
|
17
|
+
const memoryService = getReadOnlyMemoryService();
|
|
17
18
|
|
|
18
19
|
try {
|
|
19
|
-
const memoryService = getDefaultMemoryService();
|
|
20
20
|
await memoryService.initialize();
|
|
21
21
|
|
|
22
22
|
let events = await memoryService.getRecentEvents(limit + offset + 1000);
|
|
@@ -51,15 +51,17 @@ eventsRouter.get('/', async (c) => {
|
|
|
51
51
|
});
|
|
52
52
|
} catch (error) {
|
|
53
53
|
return c.json({ error: (error as Error).message }, 500);
|
|
54
|
+
} finally {
|
|
55
|
+
await memoryService.shutdown();
|
|
54
56
|
}
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
// GET /api/events/:id - Get event details
|
|
58
60
|
eventsRouter.get('/:id', async (c) => {
|
|
59
61
|
const { id } = c.req.param();
|
|
62
|
+
const memoryService = getReadOnlyMemoryService();
|
|
60
63
|
|
|
61
64
|
try {
|
|
62
|
-
const memoryService = getDefaultMemoryService();
|
|
63
65
|
await memoryService.initialize();
|
|
64
66
|
|
|
65
67
|
const recentEvents = await memoryService.getRecentEvents(10000);
|
|
@@ -97,5 +99,7 @@ eventsRouter.get('/:id', async (c) => {
|
|
|
97
99
|
});
|
|
98
100
|
} catch (error) {
|
|
99
101
|
return c.json({ error: (error as Error).message }, 500);
|
|
102
|
+
} finally {
|
|
103
|
+
await memoryService.shutdown();
|
|
100
104
|
}
|
|
101
105
|
});
|
package/src/server/api/search.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { getReadOnlyMemoryService } from '../../services/memory-service.js';
|
|
8
8
|
|
|
9
9
|
export const searchRouter = new Hono();
|
|
10
10
|
|
|
@@ -20,6 +20,7 @@ interface SearchRequest {
|
|
|
20
20
|
|
|
21
21
|
// POST /api/search - Search memories
|
|
22
22
|
searchRouter.post('/', async (c) => {
|
|
23
|
+
const memoryService = getReadOnlyMemoryService();
|
|
23
24
|
try {
|
|
24
25
|
const body = await c.req.json<SearchRequest>();
|
|
25
26
|
|
|
@@ -27,7 +28,6 @@ searchRouter.post('/', async (c) => {
|
|
|
27
28
|
return c.json({ error: 'Query is required' }, 400);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const memoryService = getDefaultMemoryService();
|
|
31
31
|
await memoryService.initialize();
|
|
32
32
|
|
|
33
33
|
const startTime = Date.now();
|
|
@@ -60,6 +60,8 @@ searchRouter.post('/', async (c) => {
|
|
|
60
60
|
});
|
|
61
61
|
} catch (error) {
|
|
62
62
|
return c.json({ error: (error as Error).message }, 500);
|
|
63
|
+
} finally {
|
|
64
|
+
await memoryService.shutdown();
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
|
|
@@ -72,9 +74,9 @@ searchRouter.get('/', async (c) => {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
const topK = parseInt(c.req.query('topK') || '5', 10);
|
|
77
|
+
const memoryService = getReadOnlyMemoryService();
|
|
75
78
|
|
|
76
79
|
try {
|
|
77
|
-
const memoryService = getDefaultMemoryService();
|
|
78
80
|
await memoryService.initialize();
|
|
79
81
|
|
|
80
82
|
const result = await memoryService.retrieveMemories(query, { topK });
|
|
@@ -94,5 +96,7 @@ searchRouter.get('/', async (c) => {
|
|
|
94
96
|
});
|
|
95
97
|
} catch (error) {
|
|
96
98
|
return c.json({ error: (error as Error).message }, 500);
|
|
99
|
+
} finally {
|
|
100
|
+
await memoryService.shutdown();
|
|
97
101
|
}
|
|
98
102
|
});
|