claude-memory-layer 1.0.9 → 1.0.11

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 (73) hide show
  1. package/dist/cli/index.js +1373 -184
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +445 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +705 -43
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +593 -52
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +581 -25
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +693 -73
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +674 -94
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +1045 -42
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +1054 -51
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +599 -25
  20. package/dist/services/memory-service.js.map +3 -3
  21. package/dist/ui/app.js +1380 -55
  22. package/dist/ui/index.html +311 -148
  23. package/dist/ui/style.css +892 -0
  24. package/docs/OPERATIONS.md +18 -0
  25. package/package.json +8 -2
  26. package/scripts/fix-sync-gap.js +32 -0
  27. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  28. package/scripts/report-sync-gap.js +26 -0
  29. package/scripts/review-queue-auto-resolve.js +21 -0
  30. package/scripts/sync-gap-auto-heal.sh +17 -0
  31. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  32. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  33. package/src/cli/index.ts +110 -58
  34. package/src/core/sqlite-event-store.ts +542 -6
  35. package/src/core/sqlite-wrapper.ts +8 -0
  36. package/src/core/turn-state.ts +159 -0
  37. package/src/core/types.ts +23 -8
  38. package/src/core/vector-store.ts +21 -3
  39. package/src/hooks/post-tool-use.ts +68 -23
  40. package/src/hooks/session-end.ts +8 -3
  41. package/src/hooks/stop.ts +96 -25
  42. package/src/hooks/user-prompt-submit.ts +78 -65
  43. package/src/server/api/chat.ts +244 -0
  44. package/src/server/api/citations.ts +3 -3
  45. package/src/server/api/events.ts +30 -5
  46. package/src/server/api/index.ts +7 -1
  47. package/src/server/api/projects.ts +74 -0
  48. package/src/server/api/search.ts +3 -3
  49. package/src/server/api/sessions.ts +3 -3
  50. package/src/server/api/stats.ts +43 -7
  51. package/src/server/api/turns.ts +143 -0
  52. package/src/server/api/utils.ts +46 -0
  53. package/src/services/memory-service.ts +208 -9
  54. package/src/services/session-history-importer.ts +215 -51
  55. package/src/ui/app.js +1380 -55
  56. package/src/ui/index.html +311 -148
  57. package/src/ui/style.css +892 -0
  58. package/.claude/settings.local.json +0 -27
  59. package/.claude-memory/test.sqlite +0 -0
  60. package/.history/package_20260201112328.json +0 -45
  61. package/.history/package_20260201113602.json +0 -45
  62. package/.history/package_20260201113713.json +0 -45
  63. package/.history/package_20260201114110.json +0 -45
  64. package/.history/package_20260201114632.json +0 -46
  65. package/.history/package_20260201133143.json +0 -45
  66. package/.history/package_20260201134319.json +0 -45
  67. package/.history/package_20260201134326.json +0 -45
  68. package/.history/package_20260201134334.json +0 -45
  69. package/.history/package_20260201134912.json +0 -45
  70. package/.history/package_20260201142928.json +0 -46
  71. package/.history/package_20260201192048.json +0 -47
  72. package/.history/package_20260202114053.json +0 -49
  73. package/test_access.js +0 -49
@@ -4,13 +4,14 @@
4
4
  */
5
5
 
6
6
  import { Hono } from 'hono';
7
- import { getReadOnlyMemoryService, getMemoryServiceForProject } from '../../services/memory-service.js';
7
+ import { getMemoryServiceForProject } from '../../services/memory-service.js';
8
+ import { getServiceFromQuery } from './utils.js';
8
9
 
9
10
  export const statsRouter = new Hono();
10
11
 
11
12
  // GET /api/stats/shared - Get shared store statistics
12
13
  statsRouter.get('/shared', async (c) => {
13
- const memoryService = getReadOnlyMemoryService();
14
+ const memoryService = getServiceFromQuery(c);
14
15
  try {
15
16
  await memoryService.initialize();
16
17
  const sharedStats = await memoryService.getSharedStoreStats();
@@ -74,7 +75,7 @@ statsRouter.get('/levels/:level', async (c) => {
74
75
  return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(', ')}` }, 400);
75
76
  }
76
77
 
77
- const memoryService = getReadOnlyMemoryService();
78
+ const memoryService = getServiceFromQuery(c);
78
79
  try {
79
80
  await memoryService.initialize();
80
81
  let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
@@ -131,7 +132,7 @@ statsRouter.get('/levels/:level', async (c) => {
131
132
 
132
133
  // GET /api/stats - Get overall statistics
133
134
  statsRouter.get('/', async (c) => {
134
- const memoryService = getReadOnlyMemoryService();
135
+ const memoryService = getServiceFromQuery(c);
135
136
  try {
136
137
  await memoryService.initialize();
137
138
  const stats = await memoryService.getStats();
@@ -187,7 +188,7 @@ statsRouter.get('/', async (c) => {
187
188
  statsRouter.get('/most-accessed', async (c) => {
188
189
  const limit = parseInt(c.req.query('limit') || '10', 10);
189
190
  // Use the same read-only service that other stats endpoints use
190
- const memoryService = getReadOnlyMemoryService();
191
+ const memoryService = getServiceFromQuery(c);
191
192
 
192
193
  try {
193
194
  await memoryService.initialize();
@@ -222,7 +223,7 @@ statsRouter.get('/most-accessed', async (c) => {
222
223
  // GET /api/stats/timeline - Get activity timeline
223
224
  statsRouter.get('/timeline', async (c) => {
224
225
  const days = parseInt(c.req.query('days') || '7', 10);
225
- const memoryService = getReadOnlyMemoryService();
226
+ const memoryService = getServiceFromQuery(c);
226
227
 
227
228
  try {
228
229
  await memoryService.initialize();
@@ -255,9 +256,44 @@ statsRouter.get('/timeline', async (c) => {
255
256
  }
256
257
  });
257
258
 
259
+ // GET /api/stats/helpfulness - Get helpfulness statistics and top helpful memories
260
+ statsRouter.get('/helpfulness', async (c) => {
261
+ const limit = parseInt(c.req.query('limit') || '10', 10);
262
+ const memoryService = getServiceFromQuery(c);
263
+
264
+ try {
265
+ await memoryService.initialize();
266
+ const stats = await memoryService.getHelpfulnessStats();
267
+ const topMemories = await memoryService.getHelpfulMemories(limit);
268
+
269
+ return c.json({
270
+ ...stats,
271
+ topMemories: topMemories.map(m => ({
272
+ eventId: m.eventId,
273
+ summary: m.summary,
274
+ helpfulnessScore: m.helpfulnessScore,
275
+ accessCount: m.accessCount,
276
+ evaluationCount: m.evaluationCount
277
+ }))
278
+ });
279
+ } catch (error) {
280
+ return c.json({
281
+ avgScore: 0,
282
+ totalEvaluated: 0,
283
+ totalRetrievals: 0,
284
+ helpful: 0,
285
+ neutral: 0,
286
+ unhelpful: 0,
287
+ topMemories: []
288
+ });
289
+ } finally {
290
+ await memoryService.shutdown();
291
+ }
292
+ });
293
+
258
294
  // POST /api/stats/graduation/run - Force graduation evaluation
259
295
  statsRouter.post('/graduation/run', async (c) => {
260
- const memoryService = getReadOnlyMemoryService();
296
+ const memoryService = getServiceFromQuery(c);
261
297
  try {
262
298
  await memoryService.initialize();
263
299
  const result = await memoryService.forceGraduation();
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Turns API
3
+ * Endpoints for viewing events grouped by conversation turn
4
+ *
5
+ * A "turn" groups a user_prompt with its associated tool_observations
6
+ * and the final agent_response into a single logical unit.
7
+ */
8
+
9
+ import { Hono } from 'hono';
10
+ import { getServiceFromQuery } from './utils.js';
11
+
12
+ export const turnsRouter = new Hono();
13
+
14
+ // GET /api/turns?sessionId=xxx - List turns for a session
15
+ turnsRouter.get('/', async (c) => {
16
+ const sessionId = c.req.query('sessionId');
17
+ const limit = parseInt(c.req.query('limit') || '20', 10);
18
+ const offset = parseInt(c.req.query('offset') || '0', 10);
19
+
20
+ if (!sessionId) {
21
+ return c.json({ error: 'sessionId is required' }, 400);
22
+ }
23
+
24
+ const memoryService = getServiceFromQuery(c);
25
+
26
+ try {
27
+ await memoryService.initialize();
28
+
29
+ const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
30
+ const totalTurns = await memoryService.countSessionTurns(sessionId);
31
+
32
+ return c.json({
33
+ turns: turns.map(t => ({
34
+ turnId: t.turnId,
35
+ startedAt: t.startedAt.toISOString(),
36
+ promptPreview: t.promptPreview,
37
+ eventCount: t.eventCount,
38
+ toolCount: t.toolCount,
39
+ hasResponse: t.hasResponse,
40
+ events: t.events.map(e => ({
41
+ id: e.id,
42
+ eventType: e.eventType,
43
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
44
+ preview: e.content.slice(0, 300) + (e.content.length > 300 ? '...' : ''),
45
+ contentLength: e.content.length
46
+ }))
47
+ })),
48
+ total: totalTurns,
49
+ limit,
50
+ offset,
51
+ hasMore: offset + limit < totalTurns
52
+ });
53
+ } catch (error) {
54
+ return c.json({ error: (error as Error).message }, 500);
55
+ } finally {
56
+ await memoryService.shutdown();
57
+ }
58
+ });
59
+
60
+ // GET /api/turns/:turnId - Get full turn details
61
+ turnsRouter.get('/:turnId', async (c) => {
62
+ const { turnId } = c.req.param();
63
+ const memoryService = getServiceFromQuery(c);
64
+
65
+ try {
66
+ await memoryService.initialize();
67
+
68
+ const events = await memoryService.getEventsByTurn(turnId);
69
+
70
+ if (events.length === 0) {
71
+ return c.json({ error: 'Turn not found' }, 404);
72
+ }
73
+
74
+ const promptEvent = events.find(e => e.eventType === 'user_prompt');
75
+ const toolEvents = events.filter(e => e.eventType === 'tool_observation');
76
+ const responseEvents = events.filter(e => e.eventType === 'agent_response');
77
+
78
+ return c.json({
79
+ turnId,
80
+ sessionId: events[0].sessionId,
81
+ startedAt: events[0].timestamp instanceof Date
82
+ ? events[0].timestamp.toISOString()
83
+ : events[0].timestamp,
84
+ prompt: promptEvent ? {
85
+ id: promptEvent.id,
86
+ content: promptEvent.content,
87
+ timestamp: promptEvent.timestamp instanceof Date
88
+ ? promptEvent.timestamp.toISOString()
89
+ : promptEvent.timestamp
90
+ } : null,
91
+ tools: toolEvents.map(e => {
92
+ let toolName = '';
93
+ let success = true;
94
+ try {
95
+ const parsed = JSON.parse(e.content);
96
+ toolName = parsed.toolName || '';
97
+ success = parsed.success !== false;
98
+ } catch { /* ignore */ }
99
+
100
+ return {
101
+ id: e.id,
102
+ toolName,
103
+ success,
104
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
105
+ preview: e.content.slice(0, 500) + (e.content.length > 500 ? '...' : '')
106
+ };
107
+ }),
108
+ responses: responseEvents.map(e => ({
109
+ id: e.id,
110
+ content: e.content,
111
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
112
+ })),
113
+ totalEvents: events.length
114
+ });
115
+ } catch (error) {
116
+ return c.json({ error: (error as Error).message }, 500);
117
+ } finally {
118
+ await memoryService.shutdown();
119
+ }
120
+ });
121
+
122
+ // POST /api/turns/backfill - Backfill turn_ids from metadata
123
+ turnsRouter.post('/backfill', async (c) => {
124
+ const memoryService = getServiceFromQuery(c);
125
+
126
+ try {
127
+ await memoryService.initialize();
128
+ const updated = await memoryService.backfillTurnIds();
129
+
130
+ return c.json({
131
+ success: true,
132
+ updated,
133
+ message: `Backfilled turn_id for ${updated} events`
134
+ });
135
+ } catch (error) {
136
+ return c.json({
137
+ success: false,
138
+ error: (error as Error).message
139
+ }, 500);
140
+ } finally {
141
+ await memoryService.shutdown();
142
+ }
143
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * API Utilities
3
+ * Shared helpers for API endpoints
4
+ */
5
+
6
+ import type { Context } from 'hono';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { getReadOnlyMemoryService } from '../../services/memory-service.js';
10
+ import { MemoryService } from '../../services/memory-service.js';
11
+
12
+ /**
13
+ * Get the appropriate MemoryService based on the ?project= query parameter.
14
+ * - If ?project=<hash> is set (8 hex chars), resolves directly to project storage
15
+ * - If ?project=<path> is set, computes hash from path
16
+ * - Otherwise, returns the global read-only service
17
+ *
18
+ * Always creates read-only services for the dashboard API to avoid
19
+ * VectorWorker lifecycle issues with per-request services.
20
+ */
21
+ export function getServiceFromQuery(c: Context): MemoryService {
22
+ const project = c.req.query('project');
23
+ if (project) {
24
+ // Check if it's a hash (8 hex chars) or a path
25
+ const isHash = /^[a-f0-9]{8}$/.test(project);
26
+ let storagePath: string;
27
+
28
+ if (isHash) {
29
+ storagePath = path.join(os.homedir(), '.claude-code', 'memory', 'projects', project);
30
+ } else {
31
+ // Import hashProjectPath dynamically to compute the hash from path
32
+ const crypto = require('crypto');
33
+ const normalized = project.replace(/\/+$/, '') || '/';
34
+ const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
35
+ storagePath = path.join(os.homedir(), '.claude-code', 'memory', 'projects', hash);
36
+ }
37
+
38
+ return new MemoryService({
39
+ storagePath,
40
+ readOnly: true,
41
+ analyticsEnabled: false,
42
+ sharedStoreConfig: { enabled: false }
43
+ });
44
+ }
45
+ return getReadOnlyMemoryService();
46
+ }
@@ -52,6 +52,8 @@ export interface MemoryServiceConfig {
52
52
  readOnly?: boolean;
53
53
  /** Enable DuckDB analytics store (default: true for server, false for hooks) */
54
54
  analyticsEnabled?: boolean;
55
+ /** Lightweight mode for hooks - skip heavy initialization (default: false) */
56
+ lightweightMode?: boolean;
55
57
  }
56
58
 
57
59
  // ============================================================
@@ -101,18 +103,18 @@ export function getProjectStoragePath(projectPath: string): string {
101
103
  const REGISTRY_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'session-registry.json');
102
104
  const SHARED_STORAGE_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'shared');
103
105
 
104
- interface SessionRegistryEntry {
106
+ export interface SessionRegistryEntry {
105
107
  projectPath: string;
106
108
  projectHash: string;
107
109
  registeredAt: string;
108
110
  }
109
111
 
110
- interface SessionRegistry {
112
+ export interface SessionRegistry {
111
113
  version: number;
112
114
  sessions: Record<string, SessionRegistryEntry>;
113
115
  }
114
116
 
115
- function loadSessionRegistry(): SessionRegistry {
117
+ export function loadSessionRegistry(): SessionRegistry {
116
118
  try {
117
119
  if (fs.existsSync(REGISTRY_PATH)) {
118
120
  const data = fs.readFileSync(REGISTRY_PATH, 'utf-8');
@@ -200,10 +202,12 @@ export class MemoryService {
200
202
  private projectHash: string | null = null;
201
203
 
202
204
  private readonly readOnly: boolean;
205
+ private readonly lightweightMode: boolean;
203
206
 
204
207
  constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
205
208
  const storagePath = this.expandPath(config.storagePath);
206
209
  this.readOnly = config.readOnly ?? false;
210
+ this.lightweightMode = config.lightweightMode ?? false;
207
211
 
208
212
  // Ensure storage directory exists (only if not read-only)
209
213
  if (!this.readOnly && !fs.existsSync(storagePath)) {
@@ -272,6 +276,13 @@ export class MemoryService {
272
276
  // Initialize PRIMARY store: SQLite (always)
273
277
  await this.sqliteStore.initialize();
274
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
+
275
286
  // Initialize analytics store if available (DuckDB)
276
287
  if (this.analyticsStore) {
277
288
  try {
@@ -479,6 +490,9 @@ export class MemoryService {
479
490
  // Create content for storage (JSON stringified payload)
480
491
  const content = JSON.stringify(payload);
481
492
 
493
+ // Extract turnId from payload metadata if present (set by PostToolUse hook)
494
+ const turnId = payload.metadata?.turnId;
495
+
482
496
  const result = await this.sqliteStore.append({
483
497
  eventType: 'tool_observation',
484
498
  sessionId,
@@ -486,7 +500,8 @@ export class MemoryService {
486
500
  content,
487
501
  metadata: {
488
502
  toolName: payload.toolName,
489
- success: payload.success
503
+ success: payload.success,
504
+ ...(turnId ? { turnId } : {})
490
505
  }
491
506
  });
492
507
 
@@ -517,10 +532,8 @@ export class MemoryService {
517
532
  ): Promise<UnifiedRetrievalResult> {
518
533
  await this.initialize();
519
534
 
520
- // Process any pending embeddings first
521
- if (this.vectorWorker) {
522
- await this.vectorWorker.processAll();
523
- }
535
+ // Note: Pending embeddings are processed by the background worker
536
+ // Don't block retrieval - search with whatever vectors are available
524
537
 
525
538
  // Use unified retrieval if shared search is requested
526
539
  if (options?.includeShared && this.sharedStore) {
@@ -534,6 +547,38 @@ export class MemoryService {
534
547
  return this.retriever.retrieve(query, options);
535
548
  }
536
549
 
550
+ /**
551
+ * Fast keyword search using SQLite FTS5
552
+ * Much faster than vector search - no embedding model needed
553
+ */
554
+ async keywordSearch(
555
+ query: string,
556
+ options?: { topK?: number; minScore?: number }
557
+ ): Promise<Array<{event: MemoryEvent; score: number}>> {
558
+ await this.initialize();
559
+
560
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
561
+
562
+ // Normalize FTS5 rank to a score (0-1 range)
563
+ // FTS5 rank is negative (higher is worse), so we convert it
564
+ const maxRank = Math.min(...results.map(r => r.rank), -0.001);
565
+ const minRank = Math.max(...results.map(r => r.rank), -1000);
566
+ const rankRange = maxRank - minRank || 1;
567
+
568
+ return results.map(r => ({
569
+ event: r.event,
570
+ score: 1 - (r.rank - minRank) / rankRange // Normalize to 0-1
571
+ })).filter(r => !options?.minScore || r.score >= options.minScore);
572
+ }
573
+
574
+ /**
575
+ * Rebuild FTS index (call after database upgrade)
576
+ */
577
+ async rebuildFtsIndex(): Promise<number> {
578
+ await this.initialize();
579
+ return this.sqliteStore.rebuildFtsIndex();
580
+ }
581
+
537
582
  /**
538
583
  * Get session history
539
584
  */
@@ -811,6 +856,37 @@ export class MemoryService {
811
856
  return this.consolidatedStore.getAll({ limit });
812
857
  }
813
858
 
859
+ /**
860
+ * Extract topic keywords from event content (markdown headings and key terms)
861
+ */
862
+ private extractTopicsFromContent(content: string): string[] {
863
+ const topics: Set<string> = new Set();
864
+
865
+ // Extract markdown headings (## heading)
866
+ const headings = content.match(/^#{1,3}\s+(.+)$/gm);
867
+ if (headings) {
868
+ for (const h of headings.slice(0, 5)) {
869
+ const text = h.replace(/^#+\s+/, '').replace(/[*_`#]/g, '').trim();
870
+ if (text.length > 2 && text.length < 50) {
871
+ topics.add(text);
872
+ }
873
+ }
874
+ }
875
+
876
+ // Extract bold terms (**term**)
877
+ const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
878
+ if (boldTerms) {
879
+ for (const b of boldTerms.slice(0, 5)) {
880
+ const text = b.replace(/\*\*/g, '').trim();
881
+ if (text.length > 2 && text.length < 30) {
882
+ topics.add(text);
883
+ }
884
+ }
885
+ }
886
+
887
+ return Array.from(topics).slice(0, 5);
888
+ }
889
+
814
890
  /**
815
891
  * Increment access count for memories that were used in prompts
816
892
  */
@@ -839,7 +915,7 @@ export class MemoryService {
839
915
  return events.map(event => ({
840
916
  memoryId: event.id,
841
917
  summary: event.content.substring(0, 200) + (event.content.length > 200 ? '...' : ''),
842
- topics: [], // Could extract topics from content if needed
918
+ topics: this.extractTopicsFromContent(event.content),
843
919
  accessCount: (event as any).access_count || 0,
844
920
  lastAccessed: (event as any).last_accessed_at || null,
845
921
  confidence: 1.0,
@@ -864,6 +940,51 @@ export class MemoryService {
864
940
  return [];
865
941
  }
866
942
 
943
+ /**
944
+ * Record a memory retrieval for helpfulness tracking
945
+ */
946
+ async recordRetrieval(eventId: string, sessionId: string, score: number, query: string): Promise<void> {
947
+ await this.initialize();
948
+ await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
949
+ }
950
+
951
+ /**
952
+ * Evaluate helpfulness of retrievals in a session (called at session end)
953
+ */
954
+ async evaluateSessionHelpfulness(sessionId: string): Promise<void> {
955
+ await this.initialize();
956
+ await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
957
+ }
958
+
959
+ /**
960
+ * Get most helpful memories ranked by helpfulness score
961
+ */
962
+ async getHelpfulMemories(limit: number = 10): Promise<Array<{
963
+ eventId: string;
964
+ summary: string;
965
+ helpfulnessScore: number;
966
+ accessCount: number;
967
+ evaluationCount: number;
968
+ }>> {
969
+ await this.initialize();
970
+ return this.sqliteStore.getHelpfulMemories(limit);
971
+ }
972
+
973
+ /**
974
+ * Get helpfulness statistics for dashboard
975
+ */
976
+ async getHelpfulnessStats(): Promise<{
977
+ avgScore: number;
978
+ totalEvaluated: number;
979
+ totalRetrievals: number;
980
+ helpful: number;
981
+ neutral: number;
982
+ unhelpful: number;
983
+ }> {
984
+ await this.initialize();
985
+ return this.sqliteStore.getHelpfulnessStats();
986
+ }
987
+
867
988
  /**
868
989
  * Mark a consolidated memory as accessed
869
990
  */
@@ -938,6 +1059,58 @@ export class MemoryService {
938
1059
  };
939
1060
  }
940
1061
 
1062
+ // ============================================================
1063
+ // Turn Grouping Methods
1064
+ // ============================================================
1065
+
1066
+ /**
1067
+ * Get events grouped by turn for a session
1068
+ */
1069
+ async getSessionTurns(sessionId: string, options?: { limit?: number; offset?: number }): Promise<Array<{
1070
+ turnId: string;
1071
+ events: MemoryEvent[];
1072
+ startedAt: Date;
1073
+ promptPreview: string;
1074
+ eventCount: number;
1075
+ toolCount: number;
1076
+ hasResponse: boolean;
1077
+ }>> {
1078
+ await this.initialize();
1079
+ return this.sqliteStore.getSessionTurns(sessionId, options);
1080
+ }
1081
+
1082
+ /**
1083
+ * Get all events for a specific turn
1084
+ */
1085
+ async getEventsByTurn(turnId: string): Promise<MemoryEvent[]> {
1086
+ await this.initialize();
1087
+ return this.sqliteStore.getEventsByTurn(turnId);
1088
+ }
1089
+
1090
+ /**
1091
+ * Count total turns for a session
1092
+ */
1093
+ async countSessionTurns(sessionId: string): Promise<number> {
1094
+ await this.initialize();
1095
+ return this.sqliteStore.countSessionTurns(sessionId);
1096
+ }
1097
+
1098
+ /**
1099
+ * Backfill turn_ids from metadata for events stored before the migration
1100
+ */
1101
+ async backfillTurnIds(): Promise<number> {
1102
+ await this.initialize();
1103
+ return this.sqliteStore.backfillTurnIds();
1104
+ }
1105
+
1106
+ /**
1107
+ * Delete all events for a session (for force reimport)
1108
+ */
1109
+ async deleteSessionEvents(sessionId: string): Promise<number> {
1110
+ await this.initialize();
1111
+ return this.sqliteStore.deleteSessionEvents(sessionId);
1112
+ }
1113
+
941
1114
  /**
942
1115
  * Format Endless Mode context for Claude
943
1116
  */
@@ -1129,6 +1302,32 @@ export function getMemoryServiceForSession(sessionId: string): MemoryService {
1129
1302
  return getDefaultMemoryService();
1130
1303
  }
1131
1304
 
1305
+ /**
1306
+ * Get a lightweight memory service for hooks
1307
+ * Only initializes SQLite - no embedder, no vector store, no workers
1308
+ * This is FAST (<100ms) compared to full initialization (3-5s)
1309
+ */
1310
+ export function getLightweightMemoryService(sessionId: string): MemoryService {
1311
+ const projectInfo = getSessionProject(sessionId);
1312
+ const key = projectInfo ? `lightweight_${projectInfo.projectHash}` : 'lightweight_global';
1313
+
1314
+ if (!serviceCache.has(key)) {
1315
+ const storagePath = projectInfo
1316
+ ? getProjectStoragePath(projectInfo.projectPath)
1317
+ : path.join(os.homedir(), '.claude-code', 'memory');
1318
+
1319
+ serviceCache.set(key, new MemoryService({
1320
+ storagePath,
1321
+ projectHash: projectInfo?.projectHash,
1322
+ lightweightMode: true, // Skip embedder/vector/workers
1323
+ analyticsEnabled: false,
1324
+ sharedStoreConfig: { enabled: false }
1325
+ }));
1326
+ }
1327
+
1328
+ return serviceCache.get(key)!;
1329
+ }
1330
+
1132
1331
  export function createMemoryService(config: MemoryServiceConfig): MemoryService {
1133
1332
  return new MemoryService(config);
1134
1333
  }