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.
Files changed (43) 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/HANDOFF.md +92 -0
  5. package/dist/cli/index.js +1150 -71
  6. package/dist/cli/index.js.map +4 -4
  7. package/dist/core/index.js +1033 -47
  8. package/dist/core/index.js.map +4 -4
  9. package/dist/hooks/post-tool-use.js +5589 -0
  10. package/dist/hooks/post-tool-use.js.map +7 -0
  11. package/dist/hooks/session-end.js +1117 -64
  12. package/dist/hooks/session-end.js.map +4 -4
  13. package/dist/hooks/session-start.js +1112 -63
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +1117 -64
  16. package/dist/hooks/stop.js.map +4 -4
  17. package/dist/hooks/user-prompt-submit.js +1151 -67
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +1145 -70
  20. package/dist/server/api/index.js.map +4 -4
  21. package/dist/server/index.js +1145 -70
  22. package/dist/server/index.js.map +4 -4
  23. package/dist/services/memory-service.js +1122 -65
  24. package/dist/services/memory-service.js.map +4 -4
  25. package/dist/ui/app.js +304 -0
  26. package/dist/ui/index.html +195 -1188
  27. package/dist/ui/style.css +595 -0
  28. package/package.json +3 -1
  29. package/scripts/build.ts +2 -0
  30. package/src/core/event-store.ts +18 -0
  31. package/src/core/index.ts +3 -0
  32. package/src/core/retriever.ts +4 -1
  33. package/src/core/sqlite-event-store.ts +849 -0
  34. package/src/core/sqlite-wrapper.ts +108 -0
  35. package/src/core/sync-worker.ts +228 -0
  36. package/src/core/vector-worker.ts +44 -14
  37. package/src/hooks/user-prompt-submit.ts +53 -4
  38. package/src/server/api/stats.ts +37 -7
  39. package/src/services/memory-service.ts +168 -39
  40. package/src/ui/app.js +304 -0
  41. package/src/ui/index.html +195 -1188
  42. package/src/ui/style.css +595 -0
  43. package/test_access.js +49 -0
@@ -9,6 +9,8 @@ import * as fs from 'fs';
9
9
  import * as crypto from 'crypto';
10
10
 
11
11
  import { EventStore } from '../core/event-store.js';
12
+ import { SQLiteEventStore } from '../core/sqlite-event-store.js';
13
+ import { SyncWorker } from '../core/sync-worker.js';
12
14
  import { VectorStore } from '../core/vector-store.js';
13
15
  import { Embedder, getDefaultEmbedder } from '../core/embedder.js';
14
16
  import { VectorWorker, createVectorWorker } from '../core/vector-worker.js';
@@ -48,6 +50,8 @@ export interface MemoryServiceConfig {
48
50
  storagePath: string;
49
51
  embeddingModel?: string;
50
52
  readOnly?: boolean;
53
+ /** Enable DuckDB analytics store (default: true for server, false for hooks) */
54
+ analyticsEnabled?: boolean;
51
55
  }
52
56
 
53
57
  // ============================================================
@@ -165,7 +169,12 @@ export function getSessionProject(sessionId: string): SessionRegistryEntry | nul
165
169
  }
166
170
 
167
171
  export class MemoryService {
168
- private readonly eventStore: EventStore;
172
+ // Primary store: SQLite (WAL mode) - for hooks, always available
173
+ private readonly sqliteStore: SQLiteEventStore;
174
+ // Analytics store: DuckDB - for server reads (optional, synced from SQLite)
175
+ private readonly analyticsStore: EventStore | null;
176
+ private syncWorker: SyncWorker | null = null;
177
+
169
178
  private readonly vectorStore: VectorStore;
170
179
  private readonly embedder: Embedder;
171
180
  private readonly matcher: Matcher;
@@ -206,20 +215,52 @@ export class MemoryService {
206
215
  // Default: shared store enabled
207
216
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
208
217
 
209
- // Initialize components
210
- this.eventStore = new EventStore(path.join(storagePath, 'events.duckdb'), { readOnly: this.readOnly });
218
+ // Initialize PRIMARY store: SQLite (WAL mode)
219
+ // This is always used for writes and is the source of truth
220
+ this.sqliteStore = new SQLiteEventStore(
221
+ path.join(storagePath, 'events.sqlite'),
222
+ { readonly: this.readOnly }
223
+ );
224
+
225
+ // Initialize ANALYTICS store: DuckDB (optional, for server reads)
226
+ // Hooks set analyticsEnabled=false to avoid DuckDB lock conflicts
227
+ const analyticsEnabled = config.analyticsEnabled ?? this.readOnly; // Default: enabled only for read-only (server)
228
+
229
+ if (!analyticsEnabled) {
230
+ // Hook mode: skip DuckDB entirely to avoid lock conflicts
231
+ this.analyticsStore = null;
232
+ } else if (this.readOnly) {
233
+ // Server mode: try to use DuckDB for analytics, will fallback to SQLite
234
+ try {
235
+ this.analyticsStore = new EventStore(
236
+ path.join(storagePath, 'analytics.duckdb'),
237
+ { readOnly: true }
238
+ );
239
+ } catch {
240
+ // DuckDB not available, will use SQLite for reads
241
+ this.analyticsStore = null;
242
+ }
243
+ } else {
244
+ // Writer mode with analytics: create DuckDB for sync target
245
+ this.analyticsStore = new EventStore(
246
+ path.join(storagePath, 'analytics.duckdb'),
247
+ { readOnly: false }
248
+ );
249
+ }
250
+
211
251
  this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
212
252
  this.embedder = config.embeddingModel
213
253
  ? new Embedder(config.embeddingModel)
214
254
  : getDefaultEmbedder();
215
255
  this.matcher = getDefaultMatcher();
256
+ // Retriever uses SQLite as primary (always available)
216
257
  this.retriever = createRetriever(
217
- this.eventStore,
258
+ this.sqliteStore as unknown as EventStore, // Interface compatible
218
259
  this.vectorStore,
219
260
  this.embedder,
220
261
  this.matcher
221
262
  );
222
- this.graduation = createGraduationPipeline(this.eventStore);
263
+ this.graduation = createGraduationPipeline(this.sqliteStore as unknown as EventStore);
223
264
  }
224
265
 
225
266
  /**
@@ -228,15 +269,27 @@ export class MemoryService {
228
269
  async initialize(): Promise<void> {
229
270
  if (this.initialized) return;
230
271
 
231
- await this.eventStore.initialize();
272
+ // Initialize PRIMARY store: SQLite (always)
273
+ await this.sqliteStore.initialize();
274
+
275
+ // Initialize analytics store if available (DuckDB)
276
+ if (this.analyticsStore) {
277
+ try {
278
+ await this.analyticsStore.initialize();
279
+ } catch (error) {
280
+ console.warn('[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:', error);
281
+ // Continue without analytics - SQLite will be used for reads
282
+ }
283
+ }
284
+
232
285
  await this.vectorStore.initialize();
233
286
  await this.embedder.initialize();
234
287
 
235
288
  // Skip write-related workers in read-only mode
236
289
  if (!this.readOnly) {
237
- // Start vector worker
290
+ // Start vector worker (uses SQLite as source)
238
291
  this.vectorWorker = createVectorWorker(
239
- this.eventStore,
292
+ this.sqliteStore as unknown as EventStore,
240
293
  this.vectorStore,
241
294
  this.embedder
242
295
  );
@@ -247,13 +300,23 @@ export class MemoryService {
247
300
 
248
301
  // Start graduation worker for automatic level promotion
249
302
  this.graduationWorker = createGraduationWorker(
250
- this.eventStore,
303
+ this.sqliteStore as unknown as EventStore,
251
304
  this.graduation
252
305
  );
253
306
  this.graduationWorker.start();
254
307
 
308
+ // Start sync worker (SQLite -> DuckDB) if analytics store is available
309
+ if (this.analyticsStore) {
310
+ this.syncWorker = new SyncWorker(
311
+ this.sqliteStore,
312
+ this.analyticsStore,
313
+ { intervalMs: 30000, batchSize: 500 }
314
+ );
315
+ this.syncWorker.start();
316
+ }
317
+
255
318
  // Load endless mode setting
256
- const savedMode = await this.eventStore.getEndlessConfig('mode') as MemoryMode | null;
319
+ const savedMode = await this.sqliteStore.getEndlessConfig('mode') as MemoryMode | null;
257
320
  if (savedMode === 'endless') {
258
321
  this.endlessMode = 'endless';
259
322
  await this.initializeEndlessMode();
@@ -309,7 +372,7 @@ export class MemoryService {
309
372
  async startSession(sessionId: string, projectPath?: string): Promise<void> {
310
373
  await this.initialize();
311
374
 
312
- await this.eventStore.upsertSession({
375
+ await this.sqliteStore.upsertSession({
313
376
  id: sessionId,
314
377
  startedAt: new Date(),
315
378
  projectPath
@@ -322,7 +385,7 @@ export class MemoryService {
322
385
  async endSession(sessionId: string, summary?: string): Promise<void> {
323
386
  await this.initialize();
324
387
 
325
- await this.eventStore.upsertSession({
388
+ await this.sqliteStore.upsertSession({
326
389
  id: sessionId,
327
390
  endedAt: new Date(),
328
391
  summary
@@ -339,7 +402,7 @@ export class MemoryService {
339
402
  ): Promise<AppendResult> {
340
403
  await this.initialize();
341
404
 
342
- const result = await this.eventStore.append({
405
+ const result = await this.sqliteStore.append({
343
406
  eventType: 'user_prompt',
344
407
  sessionId,
345
408
  timestamp: new Date(),
@@ -349,7 +412,7 @@ export class MemoryService {
349
412
 
350
413
  // Enqueue for embedding if new
351
414
  if (result.success && !result.isDuplicate) {
352
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
415
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
353
416
  }
354
417
 
355
418
  return result;
@@ -365,7 +428,7 @@ export class MemoryService {
365
428
  ): Promise<AppendResult> {
366
429
  await this.initialize();
367
430
 
368
- const result = await this.eventStore.append({
431
+ const result = await this.sqliteStore.append({
369
432
  eventType: 'agent_response',
370
433
  sessionId,
371
434
  timestamp: new Date(),
@@ -375,7 +438,7 @@ export class MemoryService {
375
438
 
376
439
  // Enqueue for embedding if new
377
440
  if (result.success && !result.isDuplicate) {
378
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
441
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
379
442
  }
380
443
 
381
444
  return result;
@@ -390,7 +453,7 @@ export class MemoryService {
390
453
  ): Promise<AppendResult> {
391
454
  await this.initialize();
392
455
 
393
- const result = await this.eventStore.append({
456
+ const result = await this.sqliteStore.append({
394
457
  eventType: 'session_summary',
395
458
  sessionId,
396
459
  timestamp: new Date(),
@@ -398,7 +461,7 @@ export class MemoryService {
398
461
  });
399
462
 
400
463
  if (result.success && !result.isDuplicate) {
401
- await this.eventStore.enqueueForEmbedding(result.eventId, summary);
464
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
402
465
  }
403
466
 
404
467
  return result;
@@ -416,7 +479,7 @@ export class MemoryService {
416
479
  // Create content for storage (JSON stringified payload)
417
480
  const content = JSON.stringify(payload);
418
481
 
419
- const result = await this.eventStore.append({
482
+ const result = await this.sqliteStore.append({
420
483
  eventType: 'tool_observation',
421
484
  sessionId,
422
485
  timestamp: new Date(),
@@ -434,7 +497,7 @@ export class MemoryService {
434
497
  payload.metadata || {},
435
498
  payload.success
436
499
  );
437
- await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
500
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
438
501
  }
439
502
 
440
503
  return result;
@@ -476,7 +539,7 @@ export class MemoryService {
476
539
  */
477
540
  async getSessionHistory(sessionId: string): Promise<MemoryEvent[]> {
478
541
  await this.initialize();
479
- return this.eventStore.getSessionEvents(sessionId);
542
+ return this.sqliteStore.getSessionEvents(sessionId);
480
543
  }
481
544
 
482
545
  /**
@@ -484,7 +547,7 @@ export class MemoryService {
484
547
  */
485
548
  async getRecentEvents(limit: number = 100): Promise<MemoryEvent[]> {
486
549
  await this.initialize();
487
- return this.eventStore.getRecentEvents(limit);
550
+ return this.sqliteStore.getRecentEvents(limit);
488
551
  }
489
552
 
490
553
  /**
@@ -497,7 +560,7 @@ export class MemoryService {
497
560
  }> {
498
561
  await this.initialize();
499
562
 
500
- const recentEvents = await this.eventStore.getRecentEvents(10000);
563
+ const recentEvents = await this.sqliteStore.getRecentEvents(10000);
501
564
  const vectorCount = await this.vectorStore.count();
502
565
  const levelStats = await this.graduation.getStats();
503
566
 
@@ -523,7 +586,7 @@ export class MemoryService {
523
586
  */
524
587
  async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
525
588
  await this.initialize();
526
- return this.eventStore.getEventsByLevel(level, options);
589
+ return this.sqliteStore.getEventsByLevel(level, options);
527
590
  }
528
591
 
529
592
  /**
@@ -531,7 +594,7 @@ export class MemoryService {
531
594
  */
532
595
  async getEventLevel(eventId: string): Promise<string | null> {
533
596
  await this.initialize();
534
- return this.eventStore.getEventLevel(eventId);
597
+ return this.sqliteStore.getEventLevel(eventId);
535
598
  }
536
599
 
537
600
  /**
@@ -644,14 +707,14 @@ export class MemoryService {
644
707
  async initializeEndlessMode(): Promise<void> {
645
708
  const config = await this.getEndlessConfig();
646
709
 
647
- this.workingSetStore = createWorkingSetStore(this.eventStore, config);
648
- this.consolidatedStore = createConsolidatedStore(this.eventStore);
710
+ this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
711
+ this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
649
712
  this.consolidationWorker = createConsolidationWorker(
650
713
  this.workingSetStore,
651
714
  this.consolidatedStore,
652
715
  config
653
716
  );
654
- this.continuityManager = createContinuityManager(this.eventStore, config);
717
+ this.continuityManager = createContinuityManager(this.sqliteStore, config);
655
718
 
656
719
  // Start consolidation worker
657
720
  this.consolidationWorker.start();
@@ -661,7 +724,7 @@ export class MemoryService {
661
724
  * Get Endless Mode configuration
662
725
  */
663
726
  async getEndlessConfig(): Promise<EndlessModeConfig> {
664
- const savedConfig = await this.eventStore.getEndlessConfig('config') as EndlessModeConfig | null;
727
+ const savedConfig = await this.sqliteStore.getEndlessConfig('config') as EndlessModeConfig | null;
665
728
  return savedConfig || this.getDefaultEndlessConfig();
666
729
  }
667
730
 
@@ -671,7 +734,7 @@ export class MemoryService {
671
734
  async setEndlessConfig(config: Partial<EndlessModeConfig>): Promise<void> {
672
735
  const current = await this.getEndlessConfig();
673
736
  const merged = { ...current, ...config };
674
- await this.eventStore.setEndlessConfig('config', merged);
737
+ await this.sqliteStore.setEndlessConfig('config', merged);
675
738
  }
676
739
 
677
740
  /**
@@ -683,7 +746,7 @@ export class MemoryService {
683
746
  if (mode === this.endlessMode) return;
684
747
 
685
748
  this.endlessMode = mode;
686
- await this.eventStore.setEndlessConfig('mode', mode);
749
+ await this.sqliteStore.setEndlessConfig('mode', mode);
687
750
 
688
751
  if (mode === 'endless') {
689
752
  await this.initializeEndlessMode();
@@ -749,11 +812,56 @@ export class MemoryService {
749
812
  }
750
813
 
751
814
  /**
752
- * Get most accessed consolidated memories
815
+ * Increment access count for memories that were used in prompts
753
816
  */
754
- async getMostAccessedMemories(limit: number = 10): Promise<ConsolidatedMemory[]> {
755
- if (!this.consolidatedStore) return [];
756
- return this.consolidatedStore.getMostAccessed(limit);
817
+ async incrementMemoryAccess(eventIds: string[]): Promise<void> {
818
+ if (eventIds.length === 0) return;
819
+
820
+ // Use SQLite event store if available
821
+ if (this.sqliteStore) {
822
+ await this.sqliteStore.incrementAccessCount(eventIds);
823
+ } else if (this.eventStore) {
824
+ // Fallback to regular event store (which has a stub implementation)
825
+ await this.eventStore.incrementAccessCount(eventIds);
826
+ }
827
+ }
828
+
829
+ /**
830
+ * Get most accessed memories from events
831
+ */
832
+ async getMostAccessedMemories(limit: number = 10): Promise<any[]> {
833
+ console.log('[getMostAccessedMemories] sqliteStore available:', !!this.sqliteStore);
834
+
835
+ // Try to get from SQLite event store if available
836
+ if (this.sqliteStore) {
837
+ const events = await this.sqliteStore.getMostAccessed(limit);
838
+ console.log('[getMostAccessedMemories] Got events from SQLite:', events.length);
839
+ return events.map(event => ({
840
+ memoryId: event.id,
841
+ summary: event.content.substring(0, 200) + (event.content.length > 200 ? '...' : ''),
842
+ topics: [], // Could extract topics from content if needed
843
+ accessCount: (event as any).access_count || 0,
844
+ lastAccessed: (event as any).last_accessed_at || null,
845
+ confidence: 1.0,
846
+ createdAt: event.timestamp
847
+ }));
848
+ }
849
+
850
+ // Fallback to consolidated store if available
851
+ if (this.consolidatedStore) {
852
+ const consolidated = await this.consolidatedStore.getMostAccessed(limit);
853
+ return consolidated.map(m => ({
854
+ memoryId: m.memoryId,
855
+ summary: m.summary,
856
+ topics: m.topics,
857
+ accessCount: m.accessCount,
858
+ lastAccessed: m.accessedAt,
859
+ confidence: m.confidence,
860
+ createdAt: m.createdAt
861
+ }));
862
+ }
863
+
864
+ return [];
757
865
  }
758
866
 
759
867
  /**
@@ -908,12 +1016,23 @@ export class MemoryService {
908
1016
  this.vectorWorker.stop();
909
1017
  }
910
1018
 
1019
+ // Stop sync worker
1020
+ if (this.syncWorker) {
1021
+ this.syncWorker.stop();
1022
+ }
1023
+
911
1024
  // Close shared store
912
1025
  if (this.sharedEventStore) {
913
1026
  await this.sharedEventStore.close();
914
1027
  }
915
1028
 
916
- await this.eventStore.close();
1029
+ // Close primary store (SQLite)
1030
+ await this.sqliteStore.close();
1031
+
1032
+ // Close analytics store (DuckDB)
1033
+ if (this.analyticsStore) {
1034
+ await this.analyticsStore.close();
1035
+ }
917
1036
  }
918
1037
 
919
1038
  /**
@@ -939,11 +1058,14 @@ const GLOBAL_READONLY_KEY = '__global_readonly__';
939
1058
  /**
940
1059
  * Get the global memory service (backward compatibility)
941
1060
  * Use this for operations not tied to a specific project
1061
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
942
1062
  */
943
1063
  export function getDefaultMemoryService(): MemoryService {
944
1064
  if (!serviceCache.has(GLOBAL_KEY)) {
945
1065
  serviceCache.set(GLOBAL_KEY, new MemoryService({
946
- storagePath: '~/.claude-code/memory'
1066
+ storagePath: '~/.claude-code/memory',
1067
+ analyticsEnabled: false, // Hooks don't need DuckDB
1068
+ sharedStoreConfig: { enabled: false } // Shared store uses DuckDB too
947
1069
  }));
948
1070
  }
949
1071
  return serviceCache.get(GLOBAL_KEY)!;
@@ -953,19 +1075,24 @@ export function getDefaultMemoryService(): MemoryService {
953
1075
  * Get a read-only global memory service
954
1076
  * Use this for web server/dashboard that only needs to read data
955
1077
  * Creates a fresh connection each time to avoid blocking the main writer process
1078
+ * Uses SQLite (WAL mode) which supports concurrent readers
956
1079
  */
957
1080
  export function getReadOnlyMemoryService(): MemoryService {
958
1081
  // Don't cache - create fresh instance each time to avoid holding locks
959
1082
  // The connection will be closed when the request completes
1083
+ // Uses SQLite which supports concurrent readers via WAL mode
960
1084
  return new MemoryService({
961
1085
  storagePath: '~/.claude-code/memory',
962
- readOnly: true
1086
+ readOnly: true,
1087
+ analyticsEnabled: false, // Use SQLite for reads (WAL supports concurrent readers)
1088
+ sharedStoreConfig: { enabled: false } // Skip shared store for now
963
1089
  });
964
1090
  }
965
1091
 
966
1092
  /**
967
1093
  * Get memory service for a specific project path
968
1094
  * Creates isolated storage at ~/.claude-code/memory/projects/{hash}/
1095
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
969
1096
  */
970
1097
  export function getMemoryServiceForProject(
971
1098
  projectPath: string,
@@ -978,7 +1105,9 @@ export function getMemoryServiceForProject(
978
1105
  serviceCache.set(hash, new MemoryService({
979
1106
  storagePath,
980
1107
  projectHash: hash,
981
- sharedStoreConfig
1108
+ // Override shared store config - hooks don't need DuckDB
1109
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
1110
+ analyticsEnabled: false // Hooks don't need DuckDB
982
1111
  }));
983
1112
  }
984
1113