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
package/src/core/types.ts
CHANGED
|
@@ -256,25 +256,40 @@ export interface UserPromptSubmitOutput {
|
|
|
256
256
|
context?: string;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// Stop Hook Input (matches actual Claude Code hook format)
|
|
259
260
|
export interface StopInput {
|
|
260
261
|
session_id: string;
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
transcript_path: string;
|
|
263
|
+
cwd: string;
|
|
264
|
+
permission_mode: string;
|
|
265
|
+
hook_event_name: string;
|
|
266
|
+
stop_hook_active: boolean;
|
|
263
267
|
}
|
|
264
268
|
|
|
265
269
|
export interface SessionEndInput {
|
|
266
270
|
session_id: string;
|
|
267
271
|
}
|
|
268
272
|
|
|
269
|
-
// PostToolUse Hook Input
|
|
273
|
+
// PostToolUse Hook Input (matches actual Claude Code hook format)
|
|
270
274
|
export interface PostToolUseInput {
|
|
275
|
+
session_id: string;
|
|
276
|
+
hook_event_name: string;
|
|
271
277
|
tool_name: string;
|
|
272
278
|
tool_input: Record<string, unknown>;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
tool_use_id: string;
|
|
280
|
+
// Claude Code sends tool_response as an object, not tool_output as string
|
|
281
|
+
tool_response: {
|
|
282
|
+
stdout?: string;
|
|
283
|
+
stderr?: string;
|
|
284
|
+
content?: string;
|
|
285
|
+
interrupted?: boolean;
|
|
286
|
+
isImage?: boolean;
|
|
287
|
+
// For non-Bash tools, response may be a plain string or other format
|
|
288
|
+
[key: string]: unknown;
|
|
289
|
+
};
|
|
290
|
+
cwd: string;
|
|
291
|
+
transcript_path: string;
|
|
292
|
+
permission_mode: string;
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
// ============================================================
|
package/src/core/vector-store.ts
CHANGED
|
@@ -65,8 +65,17 @@ export class VectorStore {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
if (!this.table) {
|
|
68
|
-
// Create table with first record
|
|
69
|
-
|
|
68
|
+
// Create table with first record (handle race condition)
|
|
69
|
+
try {
|
|
70
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
71
|
+
} catch (e: any) {
|
|
72
|
+
if (e?.message?.includes('already exists')) {
|
|
73
|
+
this.table = await this.db.openTable(this.tableName);
|
|
74
|
+
await this.table.add([data]);
|
|
75
|
+
} else {
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
70
79
|
} else {
|
|
71
80
|
await this.table.add([data]);
|
|
72
81
|
}
|
|
@@ -96,7 +105,16 @@ export class VectorStore {
|
|
|
96
105
|
}));
|
|
97
106
|
|
|
98
107
|
if (!this.table) {
|
|
99
|
-
|
|
108
|
+
try {
|
|
109
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
if (e?.message?.includes('already exists')) {
|
|
112
|
+
this.table = await this.db.openTable(this.tableName);
|
|
113
|
+
await this.table.add(data);
|
|
114
|
+
} else {
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
100
118
|
} else {
|
|
101
119
|
await this.table.add(data);
|
|
102
120
|
}
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PostToolUse Hook
|
|
4
4
|
* Called after each tool execution - stores tool observations
|
|
5
|
+
*
|
|
6
|
+
* Actual Claude Code input format:
|
|
7
|
+
* {
|
|
8
|
+
* session_id, tool_name, tool_input, tool_use_id,
|
|
9
|
+
* tool_response: { stdout?, stderr?, content?, interrupted?, isImage? },
|
|
10
|
+
* cwd, transcript_path, permission_mode, hook_event_name
|
|
11
|
+
* }
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
|
-
import {
|
|
14
|
+
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
8
15
|
import { applyPrivacyFilter, maskSensitiveInput, truncateOutput } from '../core/privacy/index.js';
|
|
9
|
-
import { extractMetadata
|
|
16
|
+
import { extractMetadata } from '../core/metadata-extractor.js';
|
|
17
|
+
import { readTurnState } from '../core/turn-state.js';
|
|
10
18
|
import type { PostToolUseInput, ToolObservationPayload, Config } from '../core/types.js';
|
|
11
19
|
|
|
12
|
-
// Default config
|
|
20
|
+
// Default config
|
|
13
21
|
const DEFAULT_CONFIG: Config['toolObservation'] = {
|
|
14
22
|
enabled: true,
|
|
15
23
|
excludedTools: ['TodoWrite', 'TodoRead'],
|
|
@@ -30,12 +38,38 @@ const DEFAULT_PRIVACY_CONFIG: Config['privacy'] = {
|
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
|
-
*
|
|
41
|
+
* Extract text output from tool_response object
|
|
34
42
|
*/
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
function extractToolOutput(response: PostToolUseInput['tool_response']): string {
|
|
44
|
+
if (!response) return '';
|
|
45
|
+
|
|
46
|
+
// Bash tools: stdout + stderr
|
|
47
|
+
if (response.stdout !== undefined) {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
if (response.stdout) parts.push(response.stdout);
|
|
50
|
+
if (response.stderr) parts.push(`[stderr] ${response.stderr}`);
|
|
51
|
+
return parts.join('\n') || '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Other tools may have content field
|
|
55
|
+
if (response.content !== undefined) {
|
|
56
|
+
return typeof response.content === 'string'
|
|
57
|
+
? response.content
|
|
58
|
+
: JSON.stringify(response.content);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: stringify the whole response
|
|
62
|
+
return JSON.stringify(response);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determine if the tool execution was successful
|
|
67
|
+
*/
|
|
68
|
+
function isToolSuccess(response: PostToolUseInput['tool_response']): boolean {
|
|
69
|
+
if (!response) return false;
|
|
70
|
+
if (response.interrupted) return false;
|
|
71
|
+
// If stderr has content but stdout also has content, still consider success
|
|
72
|
+
return true;
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
async function main(): Promise<void> {
|
|
@@ -58,55 +92,66 @@ async function main(): Promise<void> {
|
|
|
58
92
|
return;
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
// 3.
|
|
62
|
-
const
|
|
95
|
+
// 3. Extract output from tool_response object
|
|
96
|
+
const toolOutput = extractToolOutput(input.tool_response);
|
|
97
|
+
const success = isToolSuccess(input.tool_response);
|
|
98
|
+
|
|
99
|
+
// 4. Check success filter
|
|
63
100
|
if (!success && config.storeOnlyOnSuccess) {
|
|
64
101
|
console.log(JSON.stringify({}));
|
|
65
102
|
return;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
try {
|
|
69
|
-
const memoryService =
|
|
106
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
70
107
|
|
|
71
|
-
//
|
|
108
|
+
// 5. Mask sensitive data in input
|
|
72
109
|
const maskedInput = maskSensitiveInput(input.tool_input);
|
|
73
110
|
|
|
74
|
-
//
|
|
75
|
-
const filterResult = applyPrivacyFilter(
|
|
111
|
+
// 6. Apply privacy filter to output
|
|
112
|
+
const filterResult = applyPrivacyFilter(toolOutput, privacyConfig);
|
|
76
113
|
const maskedOutput = filterResult.content;
|
|
77
114
|
|
|
78
|
-
//
|
|
115
|
+
// 7. Truncate output
|
|
79
116
|
const truncatedOutput = truncateOutput(maskedOutput, {
|
|
80
117
|
maxLength: config.maxOutputLength,
|
|
81
118
|
maxLines: config.maxOutputLines
|
|
82
119
|
});
|
|
83
120
|
|
|
84
|
-
//
|
|
121
|
+
// 8. Extract metadata
|
|
85
122
|
const metadata = extractMetadata(
|
|
86
123
|
input.tool_name,
|
|
87
124
|
maskedInput,
|
|
88
|
-
|
|
125
|
+
toolOutput,
|
|
89
126
|
success
|
|
90
127
|
);
|
|
91
128
|
|
|
92
|
-
// 8.
|
|
129
|
+
// 8.5. Read current turn_id from state file
|
|
130
|
+
const turnId = readTurnState(input.session_id);
|
|
131
|
+
|
|
132
|
+
// 9. Create payload (include turnId in metadata for grouping)
|
|
93
133
|
const payload: ToolObservationPayload = {
|
|
94
134
|
toolName: input.tool_name,
|
|
95
135
|
toolInput: maskedInput,
|
|
96
136
|
toolOutput: truncatedOutput,
|
|
97
|
-
durationMs:
|
|
137
|
+
durationMs: 0, // Claude Code doesn't provide timing info
|
|
98
138
|
success,
|
|
99
|
-
errorMessage: input.
|
|
100
|
-
metadata
|
|
139
|
+
errorMessage: input.tool_response?.stderr || undefined,
|
|
140
|
+
metadata: {
|
|
141
|
+
...metadata,
|
|
142
|
+
...(turnId ? { turnId } : {})
|
|
143
|
+
}
|
|
101
144
|
};
|
|
102
145
|
|
|
103
|
-
//
|
|
146
|
+
// 10. Store observation
|
|
104
147
|
await memoryService.storeToolObservation(input.session_id, payload);
|
|
105
148
|
|
|
106
149
|
// Output empty (hook doesn't return context)
|
|
107
150
|
console.log(JSON.stringify({}));
|
|
108
151
|
} catch (error) {
|
|
109
|
-
|
|
152
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
153
|
+
console.error('PostToolUse hook error:', error);
|
|
154
|
+
}
|
|
110
155
|
console.log(JSON.stringify({}));
|
|
111
156
|
}
|
|
112
157
|
}
|
package/src/hooks/session-end.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Called when session ends - generates and stores session summary
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
8
8
|
import type { SessionEndInput } from '../core/types.js';
|
|
9
9
|
|
|
10
10
|
async function main(): Promise<void> {
|
|
@@ -12,8 +12,8 @@ async function main(): Promise<void> {
|
|
|
12
12
|
const inputData = await readStdin();
|
|
13
13
|
const input: SessionEndInput = JSON.parse(inputData);
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
const memoryService =
|
|
15
|
+
// Use lightweight service (SQLite only, no embedder/vector - FAST!)
|
|
16
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
// Get session history
|
|
@@ -29,6 +29,11 @@ async function main(): Promise<void> {
|
|
|
29
29
|
// End session with summary
|
|
30
30
|
await memoryService.endSession(input.session_id, summary);
|
|
31
31
|
|
|
32
|
+
// Evaluate helpfulness of memory retrievals in this session
|
|
33
|
+
try {
|
|
34
|
+
await memoryService.evaluateSessionHelpfulness(input.session_id);
|
|
35
|
+
} catch { /* non-critical */ }
|
|
36
|
+
|
|
32
37
|
// Process any pending embeddings
|
|
33
38
|
await memoryService.processPendingEmbeddings();
|
|
34
39
|
}
|
package/src/hooks/stop.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Stop Hook
|
|
4
|
-
* Called when agent stops - stores
|
|
4
|
+
* Called when agent stops - reads transcript and stores assistant responses
|
|
5
|
+
*
|
|
6
|
+
* Actual Claude Code input format:
|
|
7
|
+
* {
|
|
8
|
+
* session_id, transcript_path, cwd, permission_mode,
|
|
9
|
+
* hook_event_name: "Stop", stop_hook_active
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* NOTE: Claude Code does NOT send messages in the Stop hook.
|
|
13
|
+
* We read them from the transcript JSONL file instead.
|
|
5
14
|
*/
|
|
6
15
|
|
|
7
|
-
import
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as readline from 'readline';
|
|
18
|
+
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
8
19
|
import { applyPrivacyFilter } from '../core/privacy/index.js';
|
|
20
|
+
import { readTurnState, clearTurnState } from '../core/turn-state.js';
|
|
9
21
|
import type { StopInput, Config } from '../core/types.js';
|
|
10
22
|
|
|
11
23
|
// Default privacy config
|
|
@@ -20,45 +32,104 @@ const DEFAULT_PRIVACY_CONFIG: Config['privacy'] = {
|
|
|
20
32
|
}
|
|
21
33
|
};
|
|
22
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Extract assistant text messages from transcript JSONL.
|
|
37
|
+
* Only reads the last N lines to avoid processing entire transcript.
|
|
38
|
+
*/
|
|
39
|
+
async function extractAssistantMessages(transcriptPath: string): Promise<string[]> {
|
|
40
|
+
if (!fs.existsSync(transcriptPath)) return [];
|
|
41
|
+
|
|
42
|
+
const messages: string[] = [];
|
|
43
|
+
|
|
44
|
+
// Read last portion of file (last ~200KB should cover recent messages)
|
|
45
|
+
const stats = fs.statSync(transcriptPath);
|
|
46
|
+
const readStart = Math.max(0, stats.size - 200 * 1024);
|
|
47
|
+
|
|
48
|
+
const stream = fs.createReadStream(transcriptPath, {
|
|
49
|
+
start: readStart,
|
|
50
|
+
encoding: 'utf8'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
54
|
+
|
|
55
|
+
for await (const line of rl) {
|
|
56
|
+
try {
|
|
57
|
+
const entry = JSON.parse(line);
|
|
58
|
+
|
|
59
|
+
// Only process assistant messages with text content
|
|
60
|
+
if (entry.type !== 'assistant') continue;
|
|
61
|
+
|
|
62
|
+
const content = entry.message?.content;
|
|
63
|
+
if (!Array.isArray(content)) continue;
|
|
64
|
+
|
|
65
|
+
// Extract text blocks from content array
|
|
66
|
+
const textParts = content
|
|
67
|
+
.filter((c: { type: string }) => c.type === 'text')
|
|
68
|
+
.map((c: { text: string }) => c.text)
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
|
|
71
|
+
if (textParts.length > 0) {
|
|
72
|
+
messages.push(textParts.join('\n'));
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Skip malformed lines (e.g., partial first line from readStart offset)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return messages;
|
|
80
|
+
}
|
|
81
|
+
|
|
23
82
|
async function main(): Promise<void> {
|
|
24
83
|
// Read input from stdin
|
|
25
84
|
const inputData = await readStdin();
|
|
26
85
|
const input: StopInput = JSON.parse(inputData);
|
|
27
86
|
|
|
28
|
-
//
|
|
29
|
-
const memoryService =
|
|
87
|
+
// Use lightweight service (SQLite only, no embedder/vector - FAST!)
|
|
88
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
30
89
|
|
|
31
90
|
try {
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let content = filterResult.content;
|
|
38
|
-
|
|
39
|
-
// Truncate very long responses
|
|
40
|
-
if (content.length > 5000) {
|
|
41
|
-
content = content.slice(0, 5000) + '...[truncated]';
|
|
42
|
-
}
|
|
91
|
+
// Read current turn_id from state file
|
|
92
|
+
const turnId = readTurnState(input.session_id);
|
|
93
|
+
|
|
94
|
+
// Read assistant messages from transcript
|
|
95
|
+
const assistantMessages = await extractAssistantMessages(input.transcript_path);
|
|
43
96
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
// Store each assistant response
|
|
98
|
+
for (const text of assistantMessages) {
|
|
99
|
+
// Apply privacy filter
|
|
100
|
+
const filterResult = applyPrivacyFilter(text, DEFAULT_PRIVACY_CONFIG);
|
|
101
|
+
let content = filterResult.content;
|
|
102
|
+
|
|
103
|
+
// Truncate very long responses
|
|
104
|
+
if (content.length > 5000) {
|
|
105
|
+
content = content.slice(0, 5000) + '...[truncated]';
|
|
52
106
|
}
|
|
107
|
+
|
|
108
|
+
// Skip very short responses (likely just tool calls)
|
|
109
|
+
if (content.trim().length < 10) continue;
|
|
110
|
+
|
|
111
|
+
await memoryService.storeAgentResponse(
|
|
112
|
+
input.session_id,
|
|
113
|
+
content,
|
|
114
|
+
{
|
|
115
|
+
privacy: filterResult.metadata,
|
|
116
|
+
...(turnId ? { turnId } : {})
|
|
117
|
+
}
|
|
118
|
+
);
|
|
53
119
|
}
|
|
54
120
|
|
|
55
|
-
//
|
|
121
|
+
// Clean up turn state file after processing
|
|
122
|
+
clearTurnState(input.session_id);
|
|
123
|
+
|
|
124
|
+
// Embeddings enqueued in SQLite - will be processed by vector worker when server runs
|
|
56
125
|
await memoryService.processPendingEmbeddings();
|
|
57
126
|
|
|
58
127
|
// Output empty (stop hook doesn't return context)
|
|
59
128
|
console.log(JSON.stringify({}));
|
|
60
129
|
} catch (error) {
|
|
61
|
-
|
|
130
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
131
|
+
console.error('Stop hook error:', error);
|
|
132
|
+
}
|
|
62
133
|
console.log(JSON.stringify({}));
|
|
63
134
|
}
|
|
64
135
|
}
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses SQLite FTS5 for fast keyword-based search (no ML model needed)
|
|
7
7
|
* Much faster than vector search (~100ms vs 3-5s)
|
|
8
|
+
*
|
|
9
|
+
* Turn Grouping: Generates a turn_id and persists it to a state file
|
|
10
|
+
* so PostToolUse and Stop hooks can associate their events with this turn.
|
|
8
11
|
*/
|
|
9
12
|
|
|
13
|
+
import { randomUUID } from 'crypto';
|
|
10
14
|
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
15
|
+
import { writeTurnState } from '../core/turn-state.js';
|
|
11
16
|
import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
|
|
12
17
|
|
|
13
18
|
// Configuration
|
|
@@ -15,20 +20,42 @@ const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
|
|
|
15
20
|
const MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.3');
|
|
16
21
|
const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Determine if a prompt is worth storing as a memory.
|
|
25
|
+
* Filters slash commands, very short inputs, and trivial patterns.
|
|
26
|
+
*/
|
|
27
|
+
function shouldStorePrompt(prompt: string): boolean {
|
|
28
|
+
const trimmed = prompt.trim();
|
|
29
|
+
if (trimmed.startsWith('/')) return false;
|
|
30
|
+
if (trimmed.length < 15) return false;
|
|
31
|
+
if (!/[a-zA-Z가-힣]{2,}/.test(trimmed)) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
async function main(): Promise<void> {
|
|
19
36
|
// Read input from stdin
|
|
20
37
|
const inputData = await readStdin();
|
|
21
38
|
const input: UserPromptSubmitInput = JSON.parse(inputData);
|
|
22
39
|
|
|
40
|
+
// Generate a new turn_id for this user prompt
|
|
41
|
+
// This groups the prompt with subsequent tool calls and the final agent response
|
|
42
|
+
const turnId = randomUUID();
|
|
43
|
+
|
|
44
|
+
// Persist turn state so PostToolUse and Stop hooks can read it
|
|
45
|
+
writeTurnState(input.session_id, turnId);
|
|
46
|
+
|
|
23
47
|
// Use lightweight service (SQLite only, no embedder/vector - FAST!)
|
|
24
48
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
25
49
|
|
|
26
50
|
try {
|
|
27
|
-
// Store
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
51
|
+
// Store only non-trivial prompts (skip /commands, short inputs)
|
|
52
|
+
if (shouldStorePrompt(input.prompt)) {
|
|
53
|
+
await memoryService.storeUserPrompt(
|
|
54
|
+
input.session_id,
|
|
55
|
+
input.prompt,
|
|
56
|
+
{ turnId }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
32
59
|
|
|
33
60
|
let context = '';
|
|
34
61
|
|
|
@@ -44,6 +71,18 @@ async function main(): Promise<void> {
|
|
|
44
71
|
const eventIds = results.map(r => r.event.id);
|
|
45
72
|
await memoryService.incrementMemoryAccess(eventIds);
|
|
46
73
|
|
|
74
|
+
// Record each retrieval for helpfulness tracking
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
try {
|
|
77
|
+
await memoryService.recordRetrieval(
|
|
78
|
+
r.event.id,
|
|
79
|
+
input.session_id,
|
|
80
|
+
r.score,
|
|
81
|
+
input.prompt
|
|
82
|
+
);
|
|
83
|
+
} catch { /* non-critical */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
// Format context
|
|
48
87
|
const memories = results.map(r => {
|
|
49
88
|
const preview = r.event.content.length > 300
|