claude-memory-layer 1.0.10 → 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 (74) hide show
  1. package/dist/cli/index.js +1266 -181
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +367 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +598 -40
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +486 -49
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +474 -22
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +586 -70
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +537 -27
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +938 -39
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +947 -48
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +475 -22
  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 +444 -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 +44 -5
  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 +137 -5
  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/.history/package_20260202121115.json +0 -49
  74. package/test_access.js +0 -49
@@ -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
+ }
@@ -103,18 +103,18 @@ export function getProjectStoragePath(projectPath: string): string {
103
103
  const REGISTRY_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'session-registry.json');
104
104
  const SHARED_STORAGE_PATH = path.join(os.homedir(), '.claude-code', 'memory', 'shared');
105
105
 
106
- interface SessionRegistryEntry {
106
+ export interface SessionRegistryEntry {
107
107
  projectPath: string;
108
108
  projectHash: string;
109
109
  registeredAt: string;
110
110
  }
111
111
 
112
- interface SessionRegistry {
112
+ export interface SessionRegistry {
113
113
  version: number;
114
114
  sessions: Record<string, SessionRegistryEntry>;
115
115
  }
116
116
 
117
- function loadSessionRegistry(): SessionRegistry {
117
+ export function loadSessionRegistry(): SessionRegistry {
118
118
  try {
119
119
  if (fs.existsSync(REGISTRY_PATH)) {
120
120
  const data = fs.readFileSync(REGISTRY_PATH, 'utf-8');
@@ -490,6 +490,9 @@ export class MemoryService {
490
490
  // Create content for storage (JSON stringified payload)
491
491
  const content = JSON.stringify(payload);
492
492
 
493
+ // Extract turnId from payload metadata if present (set by PostToolUse hook)
494
+ const turnId = payload.metadata?.turnId;
495
+
493
496
  const result = await this.sqliteStore.append({
494
497
  eventType: 'tool_observation',
495
498
  sessionId,
@@ -497,7 +500,8 @@ export class MemoryService {
497
500
  content,
498
501
  metadata: {
499
502
  toolName: payload.toolName,
500
- success: payload.success
503
+ success: payload.success,
504
+ ...(turnId ? { turnId } : {})
501
505
  }
502
506
  });
503
507
 
@@ -852,6 +856,37 @@ export class MemoryService {
852
856
  return this.consolidatedStore.getAll({ limit });
853
857
  }
854
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
+
855
890
  /**
856
891
  * Increment access count for memories that were used in prompts
857
892
  */
@@ -880,7 +915,7 @@ export class MemoryService {
880
915
  return events.map(event => ({
881
916
  memoryId: event.id,
882
917
  summary: event.content.substring(0, 200) + (event.content.length > 200 ? '...' : ''),
883
- topics: [], // Could extract topics from content if needed
918
+ topics: this.extractTopicsFromContent(event.content),
884
919
  accessCount: (event as any).access_count || 0,
885
920
  lastAccessed: (event as any).last_accessed_at || null,
886
921
  confidence: 1.0,
@@ -905,6 +940,51 @@ export class MemoryService {
905
940
  return [];
906
941
  }
907
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
+
908
988
  /**
909
989
  * Mark a consolidated memory as accessed
910
990
  */
@@ -979,6 +1059,58 @@ export class MemoryService {
979
1059
  };
980
1060
  }
981
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
+
982
1114
  /**
983
1115
  * Format Endless Mode context for Claude
984
1116
  */