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.
- package/dist/cli/index.js +1266 -181
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +367 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +598 -40
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +486 -49
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +474 -22
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +586 -70
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +537 -27
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +938 -39
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +947 -48
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +475 -22
- package/dist/services/memory-service.js.map +3 -3
- package/dist/ui/app.js +1380 -55
- package/dist/ui/index.html +311 -148
- package/dist/ui/style.css +892 -0
- package/docs/OPERATIONS.md +18 -0
- package/package.json +8 -2
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +110 -58
- package/src/core/sqlite-event-store.ts +444 -6
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +23 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +44 -5
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/index.ts +7 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +43 -7
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/memory-service.ts +137 -5
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1380 -55
- package/src/ui/index.html +311 -148
- package/src/ui/style.css +892 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/.history/package_20260202121115.json +0 -49
- 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:
|
|
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
|
*/
|