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
@@ -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,10 @@ 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;
55
+ /** Lightweight mode for hooks - skip heavy initialization (default: false) */
56
+ lightweightMode?: boolean;
51
57
  }
52
58
 
53
59
  // ============================================================
@@ -165,7 +171,12 @@ export function getSessionProject(sessionId: string): SessionRegistryEntry | nul
165
171
  }
166
172
 
167
173
  export class MemoryService {
168
- private readonly eventStore: EventStore;
174
+ // Primary store: SQLite (WAL mode) - for hooks, always available
175
+ private readonly sqliteStore: SQLiteEventStore;
176
+ // Analytics store: DuckDB - for server reads (optional, synced from SQLite)
177
+ private readonly analyticsStore: EventStore | null;
178
+ private syncWorker: SyncWorker | null = null;
179
+
169
180
  private readonly vectorStore: VectorStore;
170
181
  private readonly embedder: Embedder;
171
182
  private readonly matcher: Matcher;
@@ -191,10 +202,12 @@ export class MemoryService {
191
202
  private projectHash: string | null = null;
192
203
 
193
204
  private readonly readOnly: boolean;
205
+ private readonly lightweightMode: boolean;
194
206
 
195
207
  constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
196
208
  const storagePath = this.expandPath(config.storagePath);
197
209
  this.readOnly = config.readOnly ?? false;
210
+ this.lightweightMode = config.lightweightMode ?? false;
198
211
 
199
212
  // Ensure storage directory exists (only if not read-only)
200
213
  if (!this.readOnly && !fs.existsSync(storagePath)) {
@@ -206,20 +219,52 @@ export class MemoryService {
206
219
  // Default: shared store enabled
207
220
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
208
221
 
209
- // Initialize components
210
- this.eventStore = new EventStore(path.join(storagePath, 'events.duckdb'), { readOnly: this.readOnly });
222
+ // Initialize PRIMARY store: SQLite (WAL mode)
223
+ // This is always used for writes and is the source of truth
224
+ this.sqliteStore = new SQLiteEventStore(
225
+ path.join(storagePath, 'events.sqlite'),
226
+ { readonly: this.readOnly }
227
+ );
228
+
229
+ // Initialize ANALYTICS store: DuckDB (optional, for server reads)
230
+ // Hooks set analyticsEnabled=false to avoid DuckDB lock conflicts
231
+ const analyticsEnabled = config.analyticsEnabled ?? this.readOnly; // Default: enabled only for read-only (server)
232
+
233
+ if (!analyticsEnabled) {
234
+ // Hook mode: skip DuckDB entirely to avoid lock conflicts
235
+ this.analyticsStore = null;
236
+ } else if (this.readOnly) {
237
+ // Server mode: try to use DuckDB for analytics, will fallback to SQLite
238
+ try {
239
+ this.analyticsStore = new EventStore(
240
+ path.join(storagePath, 'analytics.duckdb'),
241
+ { readOnly: true }
242
+ );
243
+ } catch {
244
+ // DuckDB not available, will use SQLite for reads
245
+ this.analyticsStore = null;
246
+ }
247
+ } else {
248
+ // Writer mode with analytics: create DuckDB for sync target
249
+ this.analyticsStore = new EventStore(
250
+ path.join(storagePath, 'analytics.duckdb'),
251
+ { readOnly: false }
252
+ );
253
+ }
254
+
211
255
  this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
212
256
  this.embedder = config.embeddingModel
213
257
  ? new Embedder(config.embeddingModel)
214
258
  : getDefaultEmbedder();
215
259
  this.matcher = getDefaultMatcher();
260
+ // Retriever uses SQLite as primary (always available)
216
261
  this.retriever = createRetriever(
217
- this.eventStore,
262
+ this.sqliteStore as unknown as EventStore, // Interface compatible
218
263
  this.vectorStore,
219
264
  this.embedder,
220
265
  this.matcher
221
266
  );
222
- this.graduation = createGraduationPipeline(this.eventStore);
267
+ this.graduation = createGraduationPipeline(this.sqliteStore as unknown as EventStore);
223
268
  }
224
269
 
225
270
  /**
@@ -228,15 +273,34 @@ export class MemoryService {
228
273
  async initialize(): Promise<void> {
229
274
  if (this.initialized) return;
230
275
 
231
- await this.eventStore.initialize();
276
+ // Initialize PRIMARY store: SQLite (always)
277
+ await this.sqliteStore.initialize();
278
+
279
+ // Lightweight mode: only SQLite, no embedder/vector/workers
280
+ // Used for hooks that just need to store data quickly
281
+ if (this.lightweightMode) {
282
+ this.initialized = true;
283
+ return;
284
+ }
285
+
286
+ // Initialize analytics store if available (DuckDB)
287
+ if (this.analyticsStore) {
288
+ try {
289
+ await this.analyticsStore.initialize();
290
+ } catch (error) {
291
+ console.warn('[MemoryService] Analytics store (DuckDB) initialization failed, using SQLite for reads:', error);
292
+ // Continue without analytics - SQLite will be used for reads
293
+ }
294
+ }
295
+
232
296
  await this.vectorStore.initialize();
233
297
  await this.embedder.initialize();
234
298
 
235
299
  // Skip write-related workers in read-only mode
236
300
  if (!this.readOnly) {
237
- // Start vector worker
301
+ // Start vector worker (uses SQLite as source)
238
302
  this.vectorWorker = createVectorWorker(
239
- this.eventStore,
303
+ this.sqliteStore as unknown as EventStore,
240
304
  this.vectorStore,
241
305
  this.embedder
242
306
  );
@@ -247,13 +311,23 @@ export class MemoryService {
247
311
 
248
312
  // Start graduation worker for automatic level promotion
249
313
  this.graduationWorker = createGraduationWorker(
250
- this.eventStore,
314
+ this.sqliteStore as unknown as EventStore,
251
315
  this.graduation
252
316
  );
253
317
  this.graduationWorker.start();
254
318
 
319
+ // Start sync worker (SQLite -> DuckDB) if analytics store is available
320
+ if (this.analyticsStore) {
321
+ this.syncWorker = new SyncWorker(
322
+ this.sqliteStore,
323
+ this.analyticsStore,
324
+ { intervalMs: 30000, batchSize: 500 }
325
+ );
326
+ this.syncWorker.start();
327
+ }
328
+
255
329
  // Load endless mode setting
256
- const savedMode = await this.eventStore.getEndlessConfig('mode') as MemoryMode | null;
330
+ const savedMode = await this.sqliteStore.getEndlessConfig('mode') as MemoryMode | null;
257
331
  if (savedMode === 'endless') {
258
332
  this.endlessMode = 'endless';
259
333
  await this.initializeEndlessMode();
@@ -309,7 +383,7 @@ export class MemoryService {
309
383
  async startSession(sessionId: string, projectPath?: string): Promise<void> {
310
384
  await this.initialize();
311
385
 
312
- await this.eventStore.upsertSession({
386
+ await this.sqliteStore.upsertSession({
313
387
  id: sessionId,
314
388
  startedAt: new Date(),
315
389
  projectPath
@@ -322,7 +396,7 @@ export class MemoryService {
322
396
  async endSession(sessionId: string, summary?: string): Promise<void> {
323
397
  await this.initialize();
324
398
 
325
- await this.eventStore.upsertSession({
399
+ await this.sqliteStore.upsertSession({
326
400
  id: sessionId,
327
401
  endedAt: new Date(),
328
402
  summary
@@ -339,7 +413,7 @@ export class MemoryService {
339
413
  ): Promise<AppendResult> {
340
414
  await this.initialize();
341
415
 
342
- const result = await this.eventStore.append({
416
+ const result = await this.sqliteStore.append({
343
417
  eventType: 'user_prompt',
344
418
  sessionId,
345
419
  timestamp: new Date(),
@@ -349,7 +423,7 @@ export class MemoryService {
349
423
 
350
424
  // Enqueue for embedding if new
351
425
  if (result.success && !result.isDuplicate) {
352
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
426
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
353
427
  }
354
428
 
355
429
  return result;
@@ -365,7 +439,7 @@ export class MemoryService {
365
439
  ): Promise<AppendResult> {
366
440
  await this.initialize();
367
441
 
368
- const result = await this.eventStore.append({
442
+ const result = await this.sqliteStore.append({
369
443
  eventType: 'agent_response',
370
444
  sessionId,
371
445
  timestamp: new Date(),
@@ -375,7 +449,7 @@ export class MemoryService {
375
449
 
376
450
  // Enqueue for embedding if new
377
451
  if (result.success && !result.isDuplicate) {
378
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
452
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
379
453
  }
380
454
 
381
455
  return result;
@@ -390,7 +464,7 @@ export class MemoryService {
390
464
  ): Promise<AppendResult> {
391
465
  await this.initialize();
392
466
 
393
- const result = await this.eventStore.append({
467
+ const result = await this.sqliteStore.append({
394
468
  eventType: 'session_summary',
395
469
  sessionId,
396
470
  timestamp: new Date(),
@@ -398,7 +472,7 @@ export class MemoryService {
398
472
  });
399
473
 
400
474
  if (result.success && !result.isDuplicate) {
401
- await this.eventStore.enqueueForEmbedding(result.eventId, summary);
475
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
402
476
  }
403
477
 
404
478
  return result;
@@ -416,7 +490,7 @@ export class MemoryService {
416
490
  // Create content for storage (JSON stringified payload)
417
491
  const content = JSON.stringify(payload);
418
492
 
419
- const result = await this.eventStore.append({
493
+ const result = await this.sqliteStore.append({
420
494
  eventType: 'tool_observation',
421
495
  sessionId,
422
496
  timestamp: new Date(),
@@ -434,7 +508,7 @@ export class MemoryService {
434
508
  payload.metadata || {},
435
509
  payload.success
436
510
  );
437
- await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
511
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
438
512
  }
439
513
 
440
514
  return result;
@@ -454,10 +528,8 @@ export class MemoryService {
454
528
  ): Promise<UnifiedRetrievalResult> {
455
529
  await this.initialize();
456
530
 
457
- // Process any pending embeddings first
458
- if (this.vectorWorker) {
459
- await this.vectorWorker.processAll();
460
- }
531
+ // Note: Pending embeddings are processed by the background worker
532
+ // Don't block retrieval - search with whatever vectors are available
461
533
 
462
534
  // Use unified retrieval if shared search is requested
463
535
  if (options?.includeShared && this.sharedStore) {
@@ -471,12 +543,44 @@ export class MemoryService {
471
543
  return this.retriever.retrieve(query, options);
472
544
  }
473
545
 
546
+ /**
547
+ * Fast keyword search using SQLite FTS5
548
+ * Much faster than vector search - no embedding model needed
549
+ */
550
+ async keywordSearch(
551
+ query: string,
552
+ options?: { topK?: number; minScore?: number }
553
+ ): Promise<Array<{event: MemoryEvent; score: number}>> {
554
+ await this.initialize();
555
+
556
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
557
+
558
+ // Normalize FTS5 rank to a score (0-1 range)
559
+ // FTS5 rank is negative (higher is worse), so we convert it
560
+ const maxRank = Math.min(...results.map(r => r.rank), -0.001);
561
+ const minRank = Math.max(...results.map(r => r.rank), -1000);
562
+ const rankRange = maxRank - minRank || 1;
563
+
564
+ return results.map(r => ({
565
+ event: r.event,
566
+ score: 1 - (r.rank - minRank) / rankRange // Normalize to 0-1
567
+ })).filter(r => !options?.minScore || r.score >= options.minScore);
568
+ }
569
+
570
+ /**
571
+ * Rebuild FTS index (call after database upgrade)
572
+ */
573
+ async rebuildFtsIndex(): Promise<number> {
574
+ await this.initialize();
575
+ return this.sqliteStore.rebuildFtsIndex();
576
+ }
577
+
474
578
  /**
475
579
  * Get session history
476
580
  */
477
581
  async getSessionHistory(sessionId: string): Promise<MemoryEvent[]> {
478
582
  await this.initialize();
479
- return this.eventStore.getSessionEvents(sessionId);
583
+ return this.sqliteStore.getSessionEvents(sessionId);
480
584
  }
481
585
 
482
586
  /**
@@ -484,7 +588,7 @@ export class MemoryService {
484
588
  */
485
589
  async getRecentEvents(limit: number = 100): Promise<MemoryEvent[]> {
486
590
  await this.initialize();
487
- return this.eventStore.getRecentEvents(limit);
591
+ return this.sqliteStore.getRecentEvents(limit);
488
592
  }
489
593
 
490
594
  /**
@@ -497,7 +601,7 @@ export class MemoryService {
497
601
  }> {
498
602
  await this.initialize();
499
603
 
500
- const recentEvents = await this.eventStore.getRecentEvents(10000);
604
+ const recentEvents = await this.sqliteStore.getRecentEvents(10000);
501
605
  const vectorCount = await this.vectorStore.count();
502
606
  const levelStats = await this.graduation.getStats();
503
607
 
@@ -523,7 +627,7 @@ export class MemoryService {
523
627
  */
524
628
  async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
525
629
  await this.initialize();
526
- return this.eventStore.getEventsByLevel(level, options);
630
+ return this.sqliteStore.getEventsByLevel(level, options);
527
631
  }
528
632
 
529
633
  /**
@@ -531,7 +635,7 @@ export class MemoryService {
531
635
  */
532
636
  async getEventLevel(eventId: string): Promise<string | null> {
533
637
  await this.initialize();
534
- return this.eventStore.getEventLevel(eventId);
638
+ return this.sqliteStore.getEventLevel(eventId);
535
639
  }
536
640
 
537
641
  /**
@@ -644,14 +748,14 @@ export class MemoryService {
644
748
  async initializeEndlessMode(): Promise<void> {
645
749
  const config = await this.getEndlessConfig();
646
750
 
647
- this.workingSetStore = createWorkingSetStore(this.eventStore, config);
648
- this.consolidatedStore = createConsolidatedStore(this.eventStore);
751
+ this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
752
+ this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
649
753
  this.consolidationWorker = createConsolidationWorker(
650
754
  this.workingSetStore,
651
755
  this.consolidatedStore,
652
756
  config
653
757
  );
654
- this.continuityManager = createContinuityManager(this.eventStore, config);
758
+ this.continuityManager = createContinuityManager(this.sqliteStore, config);
655
759
 
656
760
  // Start consolidation worker
657
761
  this.consolidationWorker.start();
@@ -661,7 +765,7 @@ export class MemoryService {
661
765
  * Get Endless Mode configuration
662
766
  */
663
767
  async getEndlessConfig(): Promise<EndlessModeConfig> {
664
- const savedConfig = await this.eventStore.getEndlessConfig('config') as EndlessModeConfig | null;
768
+ const savedConfig = await this.sqliteStore.getEndlessConfig('config') as EndlessModeConfig | null;
665
769
  return savedConfig || this.getDefaultEndlessConfig();
666
770
  }
667
771
 
@@ -671,7 +775,7 @@ export class MemoryService {
671
775
  async setEndlessConfig(config: Partial<EndlessModeConfig>): Promise<void> {
672
776
  const current = await this.getEndlessConfig();
673
777
  const merged = { ...current, ...config };
674
- await this.eventStore.setEndlessConfig('config', merged);
778
+ await this.sqliteStore.setEndlessConfig('config', merged);
675
779
  }
676
780
 
677
781
  /**
@@ -683,7 +787,7 @@ export class MemoryService {
683
787
  if (mode === this.endlessMode) return;
684
788
 
685
789
  this.endlessMode = mode;
686
- await this.eventStore.setEndlessConfig('mode', mode);
790
+ await this.sqliteStore.setEndlessConfig('mode', mode);
687
791
 
688
792
  if (mode === 'endless') {
689
793
  await this.initializeEndlessMode();
@@ -749,11 +853,56 @@ export class MemoryService {
749
853
  }
750
854
 
751
855
  /**
752
- * Get most accessed consolidated memories
856
+ * Increment access count for memories that were used in prompts
753
857
  */
754
- async getMostAccessedMemories(limit: number = 10): Promise<ConsolidatedMemory[]> {
755
- if (!this.consolidatedStore) return [];
756
- return this.consolidatedStore.getMostAccessed(limit);
858
+ async incrementMemoryAccess(eventIds: string[]): Promise<void> {
859
+ if (eventIds.length === 0) return;
860
+
861
+ // Use SQLite event store if available
862
+ if (this.sqliteStore) {
863
+ await this.sqliteStore.incrementAccessCount(eventIds);
864
+ } else if (this.eventStore) {
865
+ // Fallback to regular event store (which has a stub implementation)
866
+ await this.eventStore.incrementAccessCount(eventIds);
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Get most accessed memories from events
872
+ */
873
+ async getMostAccessedMemories(limit: number = 10): Promise<any[]> {
874
+ console.log('[getMostAccessedMemories] sqliteStore available:', !!this.sqliteStore);
875
+
876
+ // Try to get from SQLite event store if available
877
+ if (this.sqliteStore) {
878
+ const events = await this.sqliteStore.getMostAccessed(limit);
879
+ console.log('[getMostAccessedMemories] Got events from SQLite:', events.length);
880
+ return events.map(event => ({
881
+ memoryId: event.id,
882
+ summary: event.content.substring(0, 200) + (event.content.length > 200 ? '...' : ''),
883
+ topics: [], // Could extract topics from content if needed
884
+ accessCount: (event as any).access_count || 0,
885
+ lastAccessed: (event as any).last_accessed_at || null,
886
+ confidence: 1.0,
887
+ createdAt: event.timestamp
888
+ }));
889
+ }
890
+
891
+ // Fallback to consolidated store if available
892
+ if (this.consolidatedStore) {
893
+ const consolidated = await this.consolidatedStore.getMostAccessed(limit);
894
+ return consolidated.map(m => ({
895
+ memoryId: m.memoryId,
896
+ summary: m.summary,
897
+ topics: m.topics,
898
+ accessCount: m.accessCount,
899
+ lastAccessed: m.accessedAt,
900
+ confidence: m.confidence,
901
+ createdAt: m.createdAt
902
+ }));
903
+ }
904
+
905
+ return [];
757
906
  }
758
907
 
759
908
  /**
@@ -908,12 +1057,23 @@ export class MemoryService {
908
1057
  this.vectorWorker.stop();
909
1058
  }
910
1059
 
1060
+ // Stop sync worker
1061
+ if (this.syncWorker) {
1062
+ this.syncWorker.stop();
1063
+ }
1064
+
911
1065
  // Close shared store
912
1066
  if (this.sharedEventStore) {
913
1067
  await this.sharedEventStore.close();
914
1068
  }
915
1069
 
916
- await this.eventStore.close();
1070
+ // Close primary store (SQLite)
1071
+ await this.sqliteStore.close();
1072
+
1073
+ // Close analytics store (DuckDB)
1074
+ if (this.analyticsStore) {
1075
+ await this.analyticsStore.close();
1076
+ }
917
1077
  }
918
1078
 
919
1079
  /**
@@ -939,11 +1099,14 @@ const GLOBAL_READONLY_KEY = '__global_readonly__';
939
1099
  /**
940
1100
  * Get the global memory service (backward compatibility)
941
1101
  * Use this for operations not tied to a specific project
1102
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
942
1103
  */
943
1104
  export function getDefaultMemoryService(): MemoryService {
944
1105
  if (!serviceCache.has(GLOBAL_KEY)) {
945
1106
  serviceCache.set(GLOBAL_KEY, new MemoryService({
946
- storagePath: '~/.claude-code/memory'
1107
+ storagePath: '~/.claude-code/memory',
1108
+ analyticsEnabled: false, // Hooks don't need DuckDB
1109
+ sharedStoreConfig: { enabled: false } // Shared store uses DuckDB too
947
1110
  }));
948
1111
  }
949
1112
  return serviceCache.get(GLOBAL_KEY)!;
@@ -953,19 +1116,24 @@ export function getDefaultMemoryService(): MemoryService {
953
1116
  * Get a read-only global memory service
954
1117
  * Use this for web server/dashboard that only needs to read data
955
1118
  * Creates a fresh connection each time to avoid blocking the main writer process
1119
+ * Uses SQLite (WAL mode) which supports concurrent readers
956
1120
  */
957
1121
  export function getReadOnlyMemoryService(): MemoryService {
958
1122
  // Don't cache - create fresh instance each time to avoid holding locks
959
1123
  // The connection will be closed when the request completes
1124
+ // Uses SQLite which supports concurrent readers via WAL mode
960
1125
  return new MemoryService({
961
1126
  storagePath: '~/.claude-code/memory',
962
- readOnly: true
1127
+ readOnly: true,
1128
+ analyticsEnabled: false, // Use SQLite for reads (WAL supports concurrent readers)
1129
+ sharedStoreConfig: { enabled: false } // Skip shared store for now
963
1130
  });
964
1131
  }
965
1132
 
966
1133
  /**
967
1134
  * Get memory service for a specific project path
968
1135
  * Creates isolated storage at ~/.claude-code/memory/projects/{hash}/
1136
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
969
1137
  */
970
1138
  export function getMemoryServiceForProject(
971
1139
  projectPath: string,
@@ -978,7 +1146,9 @@ export function getMemoryServiceForProject(
978
1146
  serviceCache.set(hash, new MemoryService({
979
1147
  storagePath,
980
1148
  projectHash: hash,
981
- sharedStoreConfig
1149
+ // Override shared store config - hooks don't need DuckDB
1150
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
1151
+ analyticsEnabled: false // Hooks don't need DuckDB
982
1152
  }));
983
1153
  }
984
1154
 
@@ -1000,6 +1170,32 @@ export function getMemoryServiceForSession(sessionId: string): MemoryService {
1000
1170
  return getDefaultMemoryService();
1001
1171
  }
1002
1172
 
1173
+ /**
1174
+ * Get a lightweight memory service for hooks
1175
+ * Only initializes SQLite - no embedder, no vector store, no workers
1176
+ * This is FAST (<100ms) compared to full initialization (3-5s)
1177
+ */
1178
+ export function getLightweightMemoryService(sessionId: string): MemoryService {
1179
+ const projectInfo = getSessionProject(sessionId);
1180
+ const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : 'lightweight_global';
1181
+
1182
+ if (!serviceCache.has(key)) {
1183
+ const storagePath = projectInfo
1184
+ ? getProjectStoragePath(projectInfo.projectPath)
1185
+ : path.join(os.homedir(), '.claude-code', 'memory');
1186
+
1187
+ serviceCache.set(key, new MemoryService({
1188
+ storagePath,
1189
+ projectHash: projectInfo?.projectHash,
1190
+ lightweightMode: true, // Skip embedder/vector/workers
1191
+ analyticsEnabled: false,
1192
+ sharedStoreConfig: { enabled: false }
1193
+ }));
1194
+ }
1195
+
1196
+ return serviceCache.get(key)!;
1197
+ }
1198
+
1003
1199
  export function createMemoryService(config: MemoryServiceConfig): MemoryService {
1004
1200
  return new MemoryService(config);
1005
1201
  }