claude-memory-layer 1.0.8 → 1.0.10

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.
Files changed (44) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.claude-memory/test.sqlite +0 -0
  3. package/.history/package_20260202114053.json +49 -0
  4. package/.history/package_20260202121115.json +49 -0
  5. package/HANDOFF.md +92 -0
  6. package/dist/cli/index.js +1257 -74
  7. package/dist/cli/index.js.map +4 -4
  8. package/dist/core/index.js +1111 -47
  9. package/dist/core/index.js.map +4 -4
  10. package/dist/hooks/post-tool-use.js +5693 -0
  11. package/dist/hooks/post-tool-use.js.map +7 -0
  12. package/dist/hooks/session-end.js +1224 -67
  13. package/dist/hooks/session-end.js.map +4 -4
  14. package/dist/hooks/session-start.js +1219 -66
  15. package/dist/hooks/session-start.js.map +4 -4
  16. package/dist/hooks/stop.js +1224 -67
  17. package/dist/hooks/stop.js.map +4 -4
  18. package/dist/hooks/user-prompt-submit.js +1252 -98
  19. package/dist/hooks/user-prompt-submit.js.map +4 -4
  20. package/dist/server/api/index.js +1252 -73
  21. package/dist/server/api/index.js.map +4 -4
  22. package/dist/server/index.js +1252 -73
  23. package/dist/server/index.js.map +4 -4
  24. package/dist/services/memory-service.js +1246 -68
  25. package/dist/services/memory-service.js.map +4 -4
  26. package/dist/ui/app.js +304 -0
  27. package/dist/ui/index.html +195 -1188
  28. package/dist/ui/style.css +595 -0
  29. package/package.json +3 -1
  30. package/scripts/build.ts +2 -0
  31. package/src/core/event-store.ts +18 -0
  32. package/src/core/index.ts +3 -0
  33. package/src/core/retriever.ts +4 -1
  34. package/src/core/sqlite-event-store.ts +947 -0
  35. package/src/core/sqlite-wrapper.ts +108 -0
  36. package/src/core/sync-worker.ts +228 -0
  37. package/src/core/vector-worker.ts +44 -14
  38. package/src/hooks/user-prompt-submit.ts +40 -17
  39. package/src/server/api/stats.ts +37 -7
  40. package/src/services/memory-service.ts +239 -43
  41. package/src/ui/app.js +304 -0
  42. package/src/ui/index.html +195 -1188
  43. package/src/ui/style.css +595 -0
  44. 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
- const allIds = items.map(i => i.id);
127
- const errorMessage = error instanceof Error ? error.message : String(error);
128
- await this.eventStore.failOutboxItems(allIds, errorMessage);
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
- console.error('Vector worker error:', error);
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.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
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
- const errorMessage = error instanceof Error ? error.message : String(error);
380
- await this.outbox.markFailed(job.jobId, errorMessage);
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
- console.error('Vector worker V2 error:', error);
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.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
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
  /**
@@ -1,44 +1,67 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * User Prompt Submit Hook
4
- * Called when user submits a prompt - retrieves relevant memories
4
+ * Called when user submits a prompt - retrieves relevant memories using fast keyword search
5
+ *
6
+ * Uses SQLite FTS5 for fast keyword-based search (no ML model needed)
7
+ * Much faster than vector search (~100ms vs 3-5s)
5
8
  */
6
9
 
7
- import { getMemoryServiceForSession } from '../services/memory-service.js';
10
+ import { getLightweightMemoryService } from '../services/memory-service.js';
8
11
  import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
9
12
 
13
+ // Configuration
14
+ const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
15
+ const MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.3');
16
+ const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
17
+
10
18
  async function main(): Promise<void> {
11
19
  // Read input from stdin
12
20
  const inputData = await readStdin();
13
21
  const input: UserPromptSubmitInput = JSON.parse(inputData);
14
22
 
15
- // Get project-specific memory service via session lookup
16
- const memoryService = getMemoryServiceForSession(input.session_id);
23
+ // Use lightweight service (SQLite only, no embedder/vector - FAST!)
24
+ const memoryService = getLightweightMemoryService(input.session_id);
17
25
 
18
26
  try {
19
- // Check if shared store is enabled
20
- const includeShared = memoryService.isSharedStoreEnabled();
21
-
22
- // Retrieve relevant memories for the prompt (including shared if enabled)
23
- const retrievalResult = await memoryService.retrieveMemories(input.prompt, {
24
- topK: 5,
25
- minScore: 0.7,
26
- includeShared
27
- });
28
-
29
27
  // Store the user prompt for future retrieval
30
28
  await memoryService.storeUserPrompt(
31
29
  input.session_id,
32
30
  input.prompt
33
31
  );
34
32
 
35
- // Format context for Claude
36
- const context = memoryService.formatAsContext(retrievalResult);
33
+ let context = '';
34
+
35
+ // Fast keyword search if enabled
36
+ if (ENABLE_SEARCH && input.prompt.length > 10) {
37
+ const results = await memoryService.keywordSearch(input.prompt, {
38
+ topK: MAX_MEMORIES,
39
+ minScore: MIN_SCORE
40
+ });
41
+
42
+ if (results.length > 0) {
43
+ // Increment access count for found memories
44
+ const eventIds = results.map(r => r.event.id);
45
+ await memoryService.incrementMemoryAccess(eventIds);
46
+
47
+ // Format context
48
+ const memories = results.map(r => {
49
+ const preview = r.event.content.length > 300
50
+ ? r.event.content.substring(0, 300) + '...'
51
+ : r.event.content;
52
+ return `- [${r.event.eventType}] ${preview}`;
53
+ });
54
+
55
+ context = `💡 **Related memories found:**\n\n${memories.join('\n\n')}`;
56
+ }
57
+ }
37
58
 
38
59
  const output: UserPromptSubmitOutput = { context };
39
60
  console.log(JSON.stringify(output));
40
61
  } catch (error) {
41
- console.error('Memory hook error:', error);
62
+ if (process.env.CLAUDE_MEMORY_DEBUG) {
63
+ console.error('Memory hook error:', error);
64
+ }
42
65
  console.log(JSON.stringify({ context: '' }));
43
66
  }
44
67
  }
@@ -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
- const events = await memoryService.getEventsByLevel(level, { limit, offset });
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
- const projectPath = c.req.query('project') || process.cwd();
163
- const memoryService = getMemoryServiceForProject(projectPath);
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.accessedAt?.toISOString() || null,
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,