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.
Files changed (53) hide show
  1. package/.claude/settings.local.json +10 -1
  2. package/.claude-memory/test.sqlite +0 -0
  3. package/.history/package_20260201192048.json +47 -0
  4. package/.history/package_20260202114053.json +49 -0
  5. package/HANDOFF.md +92 -0
  6. package/dist/cli/index.js +1711 -102
  7. package/dist/cli/index.js.map +4 -4
  8. package/dist/core/index.js +1257 -84
  9. package/dist/core/index.js.map +4 -4
  10. package/dist/hooks/post-tool-use.js +5589 -0
  11. package/dist/hooks/post-tool-use.js.map +7 -0
  12. package/dist/hooks/session-end.js +1382 -85
  13. package/dist/hooks/session-end.js.map +4 -4
  14. package/dist/hooks/session-start.js +1377 -84
  15. package/dist/hooks/session-start.js.map +4 -4
  16. package/dist/hooks/stop.js +1383 -86
  17. package/dist/hooks/stop.js.map +4 -4
  18. package/dist/hooks/user-prompt-submit.js +1412 -84
  19. package/dist/hooks/user-prompt-submit.js.map +4 -4
  20. package/dist/server/api/index.js +1576 -136
  21. package/dist/server/api/index.js.map +4 -4
  22. package/dist/server/index.js +1585 -143
  23. package/dist/server/index.js.map +4 -4
  24. package/dist/services/memory-service.js +1392 -84
  25. package/dist/services/memory-service.js.map +4 -4
  26. package/dist/ui/app.js +304 -0
  27. package/dist/ui/index.html +202 -715
  28. package/dist/ui/style.css +595 -0
  29. package/package.json +4 -1
  30. package/scripts/build.ts +5 -2
  31. package/src/cli/index.ts +226 -0
  32. package/src/core/db-wrapper.ts +8 -1
  33. package/src/core/event-store.ts +70 -3
  34. package/src/core/graduation-worker.ts +171 -0
  35. package/src/core/graduation.ts +15 -2
  36. package/src/core/index.ts +4 -0
  37. package/src/core/retriever.ts +21 -0
  38. package/src/core/sqlite-event-store.ts +849 -0
  39. package/src/core/sqlite-wrapper.ts +108 -0
  40. package/src/core/sync-worker.ts +228 -0
  41. package/src/core/vector-worker.ts +44 -14
  42. package/src/hooks/user-prompt-submit.ts +53 -4
  43. package/src/server/api/citations.ts +7 -3
  44. package/src/server/api/events.ts +7 -3
  45. package/src/server/api/search.ts +7 -3
  46. package/src/server/api/sessions.ts +7 -3
  47. package/src/server/api/stats.ts +159 -12
  48. package/src/server/index.ts +18 -9
  49. package/src/services/memory-service.ts +263 -46
  50. package/src/ui/app.js +304 -0
  51. package/src/ui/index.html +202 -715
  52. package/src/ui/style.css +595 -0
  53. 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';
@@ -42,10 +44,14 @@ import { WorkingSetStore, createWorkingSetStore } from '../core/working-set-stor
42
44
  import { ConsolidatedStore, createConsolidatedStore } from '../core/consolidated-store.js';
43
45
  import { ConsolidationWorker, createConsolidationWorker } from '../core/consolidation-worker.js';
44
46
  import { ContinuityManager, createContinuityManager } from '../core/continuity-manager.js';
47
+ import { GraduationWorker, createGraduationWorker, GraduationRunResult } from '../core/graduation-worker.js';
45
48
 
46
49
  export interface MemoryServiceConfig {
47
50
  storagePath: string;
48
51
  embeddingModel?: string;
52
+ readOnly?: boolean;
53
+ /** Enable DuckDB analytics store (default: true for server, false for hooks) */
54
+ analyticsEnabled?: boolean;
49
55
  }
50
56
 
51
57
  // ============================================================
@@ -163,13 +169,19 @@ export function getSessionProject(sessionId: string): SessionRegistryEntry | nul
163
169
  }
164
170
 
165
171
  export class MemoryService {
166
- 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
+
167
178
  private readonly vectorStore: VectorStore;
168
179
  private readonly embedder: Embedder;
169
180
  private readonly matcher: Matcher;
170
181
  private readonly retriever: Retriever;
171
182
  private readonly graduation: GraduationPipeline;
172
183
  private vectorWorker: VectorWorker | null = null;
184
+ private graduationWorker: GraduationWorker | null = null;
173
185
  private initialized = false;
174
186
 
175
187
  // Endless Mode components
@@ -187,11 +199,14 @@ export class MemoryService {
187
199
  private sharedStoreConfig: SharedStoreConfig | null = null;
188
200
  private projectHash: string | null = null;
189
201
 
202
+ private readonly readOnly: boolean;
203
+
190
204
  constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
191
205
  const storagePath = this.expandPath(config.storagePath);
206
+ this.readOnly = config.readOnly ?? false;
192
207
 
193
- // Ensure storage directory exists
194
- if (!fs.existsSync(storagePath)) {
208
+ // Ensure storage directory exists (only if not read-only)
209
+ if (!this.readOnly && !fs.existsSync(storagePath)) {
195
210
  fs.mkdirSync(storagePath, { recursive: true });
196
211
  }
197
212
 
@@ -200,20 +215,52 @@ export class MemoryService {
200
215
  // Default: shared store enabled
201
216
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
202
217
 
203
- // Initialize components
204
- this.eventStore = new EventStore(path.join(storagePath, 'events.duckdb'));
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
+
205
251
  this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
206
252
  this.embedder = config.embeddingModel
207
253
  ? new Embedder(config.embeddingModel)
208
254
  : getDefaultEmbedder();
209
255
  this.matcher = getDefaultMatcher();
256
+ // Retriever uses SQLite as primary (always available)
210
257
  this.retriever = createRetriever(
211
- this.eventStore,
258
+ this.sqliteStore as unknown as EventStore, // Interface compatible
212
259
  this.vectorStore,
213
260
  this.embedder,
214
261
  this.matcher
215
262
  );
216
- this.graduation = createGraduationPipeline(this.eventStore);
263
+ this.graduation = createGraduationPipeline(this.sqliteStore as unknown as EventStore);
217
264
  }
218
265
 
219
266
  /**
@@ -222,28 +269,63 @@ export class MemoryService {
222
269
  async initialize(): Promise<void> {
223
270
  if (this.initialized) return;
224
271
 
225
- 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
+
226
285
  await this.vectorStore.initialize();
227
286
  await this.embedder.initialize();
228
287
 
229
- // Start vector worker
230
- this.vectorWorker = createVectorWorker(
231
- this.eventStore,
232
- this.vectorStore,
233
- this.embedder
234
- );
235
- this.vectorWorker.start();
288
+ // Skip write-related workers in read-only mode
289
+ if (!this.readOnly) {
290
+ // Start vector worker (uses SQLite as source)
291
+ this.vectorWorker = createVectorWorker(
292
+ this.sqliteStore as unknown as EventStore,
293
+ this.vectorStore,
294
+ this.embedder
295
+ );
296
+ this.vectorWorker.start();
236
297
 
237
- // Load endless mode setting
238
- const savedMode = await this.eventStore.getEndlessConfig('mode') as MemoryMode | null;
239
- if (savedMode === 'endless') {
240
- this.endlessMode = 'endless';
241
- await this.initializeEndlessMode();
242
- }
298
+ // Connect graduation pipeline to retriever for access tracking
299
+ this.retriever.setGraduationPipeline(this.graduation);
243
300
 
244
- // Initialize shared store (enabled by default)
245
- if (this.sharedStoreConfig?.enabled !== false) {
246
- await this.initializeSharedStore();
301
+ // Start graduation worker for automatic level promotion
302
+ this.graduationWorker = createGraduationWorker(
303
+ this.sqliteStore as unknown as EventStore,
304
+ this.graduation
305
+ );
306
+ this.graduationWorker.start();
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
+
318
+ // Load endless mode setting
319
+ const savedMode = await this.sqliteStore.getEndlessConfig('mode') as MemoryMode | null;
320
+ if (savedMode === 'endless') {
321
+ this.endlessMode = 'endless';
322
+ await this.initializeEndlessMode();
323
+ }
324
+
325
+ // Initialize shared store (enabled by default)
326
+ if (this.sharedStoreConfig?.enabled !== false) {
327
+ await this.initializeSharedStore();
328
+ }
247
329
  }
248
330
 
249
331
  this.initialized = true;
@@ -290,7 +372,7 @@ export class MemoryService {
290
372
  async startSession(sessionId: string, projectPath?: string): Promise<void> {
291
373
  await this.initialize();
292
374
 
293
- await this.eventStore.upsertSession({
375
+ await this.sqliteStore.upsertSession({
294
376
  id: sessionId,
295
377
  startedAt: new Date(),
296
378
  projectPath
@@ -303,7 +385,7 @@ export class MemoryService {
303
385
  async endSession(sessionId: string, summary?: string): Promise<void> {
304
386
  await this.initialize();
305
387
 
306
- await this.eventStore.upsertSession({
388
+ await this.sqliteStore.upsertSession({
307
389
  id: sessionId,
308
390
  endedAt: new Date(),
309
391
  summary
@@ -320,7 +402,7 @@ export class MemoryService {
320
402
  ): Promise<AppendResult> {
321
403
  await this.initialize();
322
404
 
323
- const result = await this.eventStore.append({
405
+ const result = await this.sqliteStore.append({
324
406
  eventType: 'user_prompt',
325
407
  sessionId,
326
408
  timestamp: new Date(),
@@ -330,7 +412,7 @@ export class MemoryService {
330
412
 
331
413
  // Enqueue for embedding if new
332
414
  if (result.success && !result.isDuplicate) {
333
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
415
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
334
416
  }
335
417
 
336
418
  return result;
@@ -346,7 +428,7 @@ export class MemoryService {
346
428
  ): Promise<AppendResult> {
347
429
  await this.initialize();
348
430
 
349
- const result = await this.eventStore.append({
431
+ const result = await this.sqliteStore.append({
350
432
  eventType: 'agent_response',
351
433
  sessionId,
352
434
  timestamp: new Date(),
@@ -356,7 +438,7 @@ export class MemoryService {
356
438
 
357
439
  // Enqueue for embedding if new
358
440
  if (result.success && !result.isDuplicate) {
359
- await this.eventStore.enqueueForEmbedding(result.eventId, content);
441
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, content);
360
442
  }
361
443
 
362
444
  return result;
@@ -371,7 +453,7 @@ export class MemoryService {
371
453
  ): Promise<AppendResult> {
372
454
  await this.initialize();
373
455
 
374
- const result = await this.eventStore.append({
456
+ const result = await this.sqliteStore.append({
375
457
  eventType: 'session_summary',
376
458
  sessionId,
377
459
  timestamp: new Date(),
@@ -379,7 +461,7 @@ export class MemoryService {
379
461
  });
380
462
 
381
463
  if (result.success && !result.isDuplicate) {
382
- await this.eventStore.enqueueForEmbedding(result.eventId, summary);
464
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, summary);
383
465
  }
384
466
 
385
467
  return result;
@@ -397,7 +479,7 @@ export class MemoryService {
397
479
  // Create content for storage (JSON stringified payload)
398
480
  const content = JSON.stringify(payload);
399
481
 
400
- const result = await this.eventStore.append({
482
+ const result = await this.sqliteStore.append({
401
483
  eventType: 'tool_observation',
402
484
  sessionId,
403
485
  timestamp: new Date(),
@@ -415,7 +497,7 @@ export class MemoryService {
415
497
  payload.metadata || {},
416
498
  payload.success
417
499
  );
418
- await this.eventStore.enqueueForEmbedding(result.eventId, embeddingContent);
500
+ await this.sqliteStore.enqueueForEmbedding(result.eventId, embeddingContent);
419
501
  }
420
502
 
421
503
  return result;
@@ -457,7 +539,7 @@ export class MemoryService {
457
539
  */
458
540
  async getSessionHistory(sessionId: string): Promise<MemoryEvent[]> {
459
541
  await this.initialize();
460
- return this.eventStore.getSessionEvents(sessionId);
542
+ return this.sqliteStore.getSessionEvents(sessionId);
461
543
  }
462
544
 
463
545
  /**
@@ -465,7 +547,7 @@ export class MemoryService {
465
547
  */
466
548
  async getRecentEvents(limit: number = 100): Promise<MemoryEvent[]> {
467
549
  await this.initialize();
468
- return this.eventStore.getRecentEvents(limit);
550
+ return this.sqliteStore.getRecentEvents(limit);
469
551
  }
470
552
 
471
553
  /**
@@ -478,7 +560,7 @@ export class MemoryService {
478
560
  }> {
479
561
  await this.initialize();
480
562
 
481
- const recentEvents = await this.eventStore.getRecentEvents(10000);
563
+ const recentEvents = await this.sqliteStore.getRecentEvents(10000);
482
564
  const vectorCount = await this.vectorStore.count();
483
565
  const levelStats = await this.graduation.getStats();
484
566
 
@@ -499,6 +581,22 @@ export class MemoryService {
499
581
  return 0;
500
582
  }
501
583
 
584
+ /**
585
+ * Get events by memory level
586
+ */
587
+ async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
588
+ await this.initialize();
589
+ return this.sqliteStore.getEventsByLevel(level, options);
590
+ }
591
+
592
+ /**
593
+ * Get memory level for a specific event
594
+ */
595
+ async getEventLevel(eventId: string): Promise<string | null> {
596
+ await this.initialize();
597
+ return this.sqliteStore.getEventLevel(eventId);
598
+ }
599
+
502
600
  /**
503
601
  * Format retrieval results as context for Claude
504
602
  */
@@ -609,14 +707,14 @@ export class MemoryService {
609
707
  async initializeEndlessMode(): Promise<void> {
610
708
  const config = await this.getEndlessConfig();
611
709
 
612
- this.workingSetStore = createWorkingSetStore(this.eventStore, config);
613
- this.consolidatedStore = createConsolidatedStore(this.eventStore);
710
+ this.workingSetStore = createWorkingSetStore(this.sqliteStore, config);
711
+ this.consolidatedStore = createConsolidatedStore(this.sqliteStore);
614
712
  this.consolidationWorker = createConsolidationWorker(
615
713
  this.workingSetStore,
616
714
  this.consolidatedStore,
617
715
  config
618
716
  );
619
- this.continuityManager = createContinuityManager(this.eventStore, config);
717
+ this.continuityManager = createContinuityManager(this.sqliteStore, config);
620
718
 
621
719
  // Start consolidation worker
622
720
  this.consolidationWorker.start();
@@ -626,7 +724,7 @@ export class MemoryService {
626
724
  * Get Endless Mode configuration
627
725
  */
628
726
  async getEndlessConfig(): Promise<EndlessModeConfig> {
629
- const savedConfig = await this.eventStore.getEndlessConfig('config') as EndlessModeConfig | null;
727
+ const savedConfig = await this.sqliteStore.getEndlessConfig('config') as EndlessModeConfig | null;
630
728
  return savedConfig || this.getDefaultEndlessConfig();
631
729
  }
632
730
 
@@ -636,7 +734,7 @@ export class MemoryService {
636
734
  async setEndlessConfig(config: Partial<EndlessModeConfig>): Promise<void> {
637
735
  const current = await this.getEndlessConfig();
638
736
  const merged = { ...current, ...config };
639
- await this.eventStore.setEndlessConfig('config', merged);
737
+ await this.sqliteStore.setEndlessConfig('config', merged);
640
738
  }
641
739
 
642
740
  /**
@@ -648,7 +746,7 @@ export class MemoryService {
648
746
  if (mode === this.endlessMode) return;
649
747
 
650
748
  this.endlessMode = mode;
651
- await this.eventStore.setEndlessConfig('mode', mode);
749
+ await this.sqliteStore.setEndlessConfig('mode', mode);
652
750
 
653
751
  if (mode === 'endless') {
654
752
  await this.initializeEndlessMode();
@@ -713,6 +811,67 @@ export class MemoryService {
713
811
  return this.consolidatedStore.getAll({ limit });
714
812
  }
715
813
 
814
+ /**
815
+ * Increment access count for memories that were used in prompts
816
+ */
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 [];
865
+ }
866
+
867
+ /**
868
+ * Mark a consolidated memory as accessed
869
+ */
870
+ async markMemoryAccessed(memoryId: string): Promise<void> {
871
+ if (!this.consolidatedStore) return;
872
+ await this.consolidatedStore.markAccessed(memoryId);
873
+ }
874
+
716
875
  /**
717
876
  * Calculate continuity score for current context
718
877
  */
@@ -822,10 +981,32 @@ export class MemoryService {
822
981
  return parts.join('\n');
823
982
  }
824
983
 
984
+ /**
985
+ * Force a graduation evaluation run
986
+ */
987
+ async forceGraduation(): Promise<GraduationRunResult> {
988
+ if (!this.graduationWorker) {
989
+ return { evaluated: 0, graduated: 0, byLevel: {} };
990
+ }
991
+ return this.graduationWorker.forceRun();
992
+ }
993
+
994
+ /**
995
+ * Record access to a memory event (for graduation scoring)
996
+ */
997
+ recordMemoryAccess(eventId: string, sessionId: string, confidence: number = 1.0): void {
998
+ this.graduation.recordAccess(eventId, sessionId, confidence);
999
+ }
1000
+
825
1001
  /**
826
1002
  * Shutdown service
827
1003
  */
828
1004
  async shutdown(): Promise<void> {
1005
+ // Stop graduation worker
1006
+ if (this.graduationWorker) {
1007
+ this.graduationWorker.stop();
1008
+ }
1009
+
829
1010
  // Stop endless mode components
830
1011
  if (this.consolidationWorker) {
831
1012
  this.consolidationWorker.stop();
@@ -835,12 +1016,23 @@ export class MemoryService {
835
1016
  this.vectorWorker.stop();
836
1017
  }
837
1018
 
1019
+ // Stop sync worker
1020
+ if (this.syncWorker) {
1021
+ this.syncWorker.stop();
1022
+ }
1023
+
838
1024
  // Close shared store
839
1025
  if (this.sharedEventStore) {
840
1026
  await this.sharedEventStore.close();
841
1027
  }
842
1028
 
843
- 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
+ }
844
1036
  }
845
1037
 
846
1038
  /**
@@ -861,23 +1053,46 @@ export class MemoryService {
861
1053
  // Instance cache: Map from project hash (or '__global__') to MemoryService
862
1054
  const serviceCache = new Map<string, MemoryService>();
863
1055
  const GLOBAL_KEY = '__global__';
1056
+ const GLOBAL_READONLY_KEY = '__global_readonly__';
864
1057
 
865
1058
  /**
866
1059
  * Get the global memory service (backward compatibility)
867
1060
  * Use this for operations not tied to a specific project
1061
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
868
1062
  */
869
1063
  export function getDefaultMemoryService(): MemoryService {
870
1064
  if (!serviceCache.has(GLOBAL_KEY)) {
871
1065
  serviceCache.set(GLOBAL_KEY, new MemoryService({
872
- 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
873
1069
  }));
874
1070
  }
875
1071
  return serviceCache.get(GLOBAL_KEY)!;
876
1072
  }
877
1073
 
1074
+ /**
1075
+ * Get a read-only global memory service
1076
+ * Use this for web server/dashboard that only needs to read data
1077
+ * Creates a fresh connection each time to avoid blocking the main writer process
1078
+ * Uses SQLite (WAL mode) which supports concurrent readers
1079
+ */
1080
+ export function getReadOnlyMemoryService(): MemoryService {
1081
+ // Don't cache - create fresh instance each time to avoid holding locks
1082
+ // The connection will be closed when the request completes
1083
+ // Uses SQLite which supports concurrent readers via WAL mode
1084
+ return new MemoryService({
1085
+ storagePath: '~/.claude-code/memory',
1086
+ readOnly: true,
1087
+ analyticsEnabled: false, // Use SQLite for reads (WAL supports concurrent readers)
1088
+ sharedStoreConfig: { enabled: false } // Skip shared store for now
1089
+ });
1090
+ }
1091
+
878
1092
  /**
879
1093
  * Get memory service for a specific project path
880
1094
  * Creates isolated storage at ~/.claude-code/memory/projects/{hash}/
1095
+ * Note: analyticsEnabled=false and sharedStore disabled to avoid DuckDB lock conflicts
881
1096
  */
882
1097
  export function getMemoryServiceForProject(
883
1098
  projectPath: string,
@@ -890,7 +1105,9 @@ export function getMemoryServiceForProject(
890
1105
  serviceCache.set(hash, new MemoryService({
891
1106
  storagePath,
892
1107
  projectHash: hash,
893
- sharedStoreConfig
1108
+ // Override shared store config - hooks don't need DuckDB
1109
+ sharedStoreConfig: sharedStoreConfig ?? { enabled: false },
1110
+ analyticsEnabled: false // Hooks don't need DuckDB
894
1111
  }));
895
1112
  }
896
1113