claude-memory-layer 1.0.23 → 1.0.25
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/.claude/settings.local.json +25 -0
- package/README.md +2 -0
- package/dist/cli/index.js +229 -978
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +59 -71
- package/dist/core/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +287 -976
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +6520 -0
- package/dist/hooks/semantic-daemon.js.map +7 -0
- package/dist/hooks/session-end.js +209 -973
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +293 -978
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +247 -975
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +406 -1036
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +209 -973
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +209 -973
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +209 -973
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +48 -1
- package/dist/ui/index.html +11 -3
- package/memory/_index.md +1 -0
- package/memory/agent_response/uncategorized/2026-03-04.md +1314 -1
- package/memory/session_summary/uncategorized/2026-03-04.md +50 -0
- package/memory/tool_observation/uncategorized/2026-03-04.md +969 -1
- package/memory/user_prompt/uncategorized/2026-03-04.md +555 -1
- package/package.json +1 -2
- package/scripts/build.ts +2 -1
- package/specs/memory-utilization-improvements/context.md +145 -0
- package/specs/memory-utilization-improvements/plan.md +361 -0
- package/specs/memory-utilization-improvements/spec.md +308 -0
- package/specs/optional-duckdb/context.md +77 -0
- package/specs/optional-duckdb/plan.md +142 -0
- package/specs/optional-duckdb/spec.md +35 -0
- package/specs/selective-tool-observation/context.md +100 -0
- package/specs/selective-tool-observation/plan.md +158 -0
- package/specs/selective-tool-observation/spec.md +127 -0
- package/src/cli/index.ts +1 -0
- package/src/core/db-wrapper.ts +18 -73
- package/src/core/embedder.ts +13 -4
- package/src/core/sqlite-event-store.ts +40 -0
- package/src/core/turn-state.ts +48 -0
- package/src/core/types.ts +1 -0
- package/src/hooks/post-tool-use.ts +72 -2
- package/src/hooks/semantic-daemon-client.ts +208 -0
- package/src/hooks/semantic-daemon.ts +276 -0
- package/src/hooks/session-start.ts +11 -0
- package/src/hooks/stop.ts +33 -4
- package/src/hooks/user-prompt-submit.ts +48 -40
- package/src/services/memory-service.ts +112 -65
- package/src/services/session-history-importer.ts +18 -0
- package/src/ui/app.js +48 -1
- package/src/ui/index.html +11 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as net from 'net';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
interface SemanticRequest {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
prompt: string;
|
|
10
|
+
topK: number;
|
|
11
|
+
minScore: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SemanticMemory {
|
|
15
|
+
type: string;
|
|
16
|
+
content: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
score?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SemanticDaemonRequest {
|
|
22
|
+
type: 'retrieve';
|
|
23
|
+
sessionId: string;
|
|
24
|
+
prompt: string;
|
|
25
|
+
topK: number;
|
|
26
|
+
minScore: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SemanticDaemonResponse {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
memories?: SemanticMemory[];
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SOCKET_PATH = path.join(
|
|
36
|
+
os.homedir(),
|
|
37
|
+
'.claude-code',
|
|
38
|
+
'memory',
|
|
39
|
+
'semantic-daemon.sock'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const DAEMON_SOCKET_PATH = process.env.CLAUDE_MEMORY_SEMANTIC_SOCKET || DEFAULT_SOCKET_PATH;
|
|
43
|
+
const DAEMON_START_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_DAEMON_START_MS || '1500');
|
|
44
|
+
|
|
45
|
+
let daemonStartPromise: Promise<void> | null = null;
|
|
46
|
+
|
|
47
|
+
export async function retrieveSemanticMemories(
|
|
48
|
+
request: SemanticRequest,
|
|
49
|
+
timeoutMs: number
|
|
50
|
+
): Promise<SemanticMemory[]> {
|
|
51
|
+
const payload: SemanticDaemonRequest = {
|
|
52
|
+
type: 'retrieve',
|
|
53
|
+
sessionId: request.sessionId,
|
|
54
|
+
prompt: request.prompt,
|
|
55
|
+
topK: request.topK,
|
|
56
|
+
minScore: request.minScore
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return await requestFromDaemon(payload, timeoutMs);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!isConnectionError(error)) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await ensureDaemonRunning();
|
|
67
|
+
return requestFromDaemon(payload, timeoutMs).catch((retryError) => {
|
|
68
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
69
|
+
console.error('[semantic-client] retry failed after daemon start:', retryError);
|
|
70
|
+
}
|
|
71
|
+
throw retryError;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requestFromDaemon(
|
|
77
|
+
payload: SemanticDaemonRequest,
|
|
78
|
+
timeoutMs: number
|
|
79
|
+
): Promise<SemanticMemory[]> {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const client = net.createConnection(DAEMON_SOCKET_PATH);
|
|
82
|
+
client.setEncoding('utf8');
|
|
83
|
+
|
|
84
|
+
let settled = false;
|
|
85
|
+
let responseRaw = '';
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
const timeoutError = new Error(`semantic daemon timeout (${timeoutMs}ms)`);
|
|
88
|
+
(timeoutError as NodeJS.ErrnoException).code = 'ETIMEDOUT';
|
|
89
|
+
settle(timeoutError);
|
|
90
|
+
client.destroy();
|
|
91
|
+
}, timeoutMs);
|
|
92
|
+
|
|
93
|
+
const settle = (error?: Error, memories?: SemanticMemory[]) => {
|
|
94
|
+
if (settled) return;
|
|
95
|
+
settled = true;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
if (error) {
|
|
98
|
+
reject(error);
|
|
99
|
+
} else {
|
|
100
|
+
resolve(memories || []);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
client.on('connect', () => {
|
|
105
|
+
client.end(JSON.stringify(payload));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
client.on('data', (chunk) => {
|
|
109
|
+
responseRaw += chunk;
|
|
110
|
+
if (responseRaw.length > 4 * 1024 * 1024) {
|
|
111
|
+
settle(new Error('semantic daemon response too large'));
|
|
112
|
+
client.destroy();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
client.on('end', () => {
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(responseRaw || '{}') as SemanticDaemonResponse;
|
|
119
|
+
if (!parsed.ok) {
|
|
120
|
+
settle(new Error(parsed.error || 'semantic daemon error'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
settle(undefined, parsed.memories || []);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
settle(error as Error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
client.on('error', (error) => {
|
|
130
|
+
settle(error as Error);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function ensureDaemonRunning(): Promise<void> {
|
|
136
|
+
if (daemonStartPromise) {
|
|
137
|
+
return daemonStartPromise;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
daemonStartPromise = (async () => {
|
|
141
|
+
if (await canConnect()) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const daemonScriptPath = getDaemonScriptPath();
|
|
146
|
+
if (!fs.existsSync(daemonScriptPath)) {
|
|
147
|
+
throw new Error(`semantic daemon script not found: ${daemonScriptPath}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const daemonDir = path.dirname(DAEMON_SOCKET_PATH);
|
|
151
|
+
if (!fs.existsSync(daemonDir)) {
|
|
152
|
+
fs.mkdirSync(daemonDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const child = spawn(process.execPath, [daemonScriptPath], {
|
|
156
|
+
detached: true,
|
|
157
|
+
stdio: 'ignore',
|
|
158
|
+
env: process.env
|
|
159
|
+
});
|
|
160
|
+
child.unref();
|
|
161
|
+
|
|
162
|
+
const startDeadline = Date.now() + DAEMON_START_TIMEOUT_MS;
|
|
163
|
+
while (Date.now() < startDeadline) {
|
|
164
|
+
if (await canConnect()) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
await sleep(60);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(`semantic daemon start timeout (${DAEMON_START_TIMEOUT_MS}ms)`);
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await daemonStartPromise;
|
|
175
|
+
} finally {
|
|
176
|
+
daemonStartPromise = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getDaemonScriptPath(): string {
|
|
181
|
+
return path.join(path.dirname(new URL(import.meta.url).pathname), 'semantic-daemon.js');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function canConnect(): Promise<boolean> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
let settled = false;
|
|
187
|
+
const client = net.createConnection(DAEMON_SOCKET_PATH);
|
|
188
|
+
const finalize = (ok: boolean) => {
|
|
189
|
+
if (settled) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
client.destroy();
|
|
192
|
+
resolve(ok);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
client.on('connect', () => finalize(true));
|
|
196
|
+
client.on('error', () => finalize(false));
|
|
197
|
+
setTimeout(() => finalize(false), 120).unref();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isConnectionError(error: unknown): boolean {
|
|
202
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
203
|
+
return code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'EPIPE' || code === 'ECONNRESET';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sleep(ms: number): Promise<void> {
|
|
207
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
208
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as net from 'net';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { MemoryService, getProjectStoragePath, getSessionProject } from '../services/memory-service.js';
|
|
8
|
+
|
|
9
|
+
interface SemanticDaemonRequest {
|
|
10
|
+
type?: 'retrieve';
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
prompt?: string;
|
|
13
|
+
topK?: number;
|
|
14
|
+
minScore?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SemanticMemory {
|
|
18
|
+
type: string;
|
|
19
|
+
content: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
score?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SemanticDaemonResponse {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
memories?: SemanticMemory[];
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SOCKET_PATH = process.env.CLAUDE_MEMORY_SEMANTIC_SOCKET || path.join(
|
|
31
|
+
os.homedir(),
|
|
32
|
+
'.claude-code',
|
|
33
|
+
'memory',
|
|
34
|
+
'semantic-daemon.sock'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const IDLE_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_DAEMON_IDLE_MS || '600000');
|
|
38
|
+
const serviceCache = new Map<string, MemoryService>();
|
|
39
|
+
|
|
40
|
+
let server: net.Server | null = null;
|
|
41
|
+
let idleTimer: NodeJS.Timeout | null = null;
|
|
42
|
+
let shuttingDown = false;
|
|
43
|
+
|
|
44
|
+
function scheduleIdleShutdown(): void {
|
|
45
|
+
if (idleTimer) {
|
|
46
|
+
clearTimeout(idleTimer);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
idleTimer = setTimeout(() => {
|
|
50
|
+
shutdown(0).catch(() => {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
}, IDLE_TIMEOUT_MS);
|
|
54
|
+
idleTimer.unref();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseRequest(raw: string): SemanticDaemonRequest {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(raw) as SemanticDaemonRequest;
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidRequest(input: SemanticDaemonRequest): input is Required<SemanticDaemonRequest> {
|
|
66
|
+
return input.type === 'retrieve'
|
|
67
|
+
&& typeof input.sessionId === 'string'
|
|
68
|
+
&& input.sessionId.length > 0
|
|
69
|
+
&& typeof input.prompt === 'string'
|
|
70
|
+
&& input.prompt.length > 0
|
|
71
|
+
&& Number.isFinite(input.topK)
|
|
72
|
+
&& Number.isFinite(input.minScore);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeErrorResponse(error: unknown): SemanticDaemonResponse {
|
|
76
|
+
return { ok: false, error: error instanceof Error ? error.message : 'unknown daemon error' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isVectorSessionFilterError(error: unknown): boolean {
|
|
80
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
81
|
+
return message.includes('no field named sessionid');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getServiceForSession(sessionId: string): MemoryService {
|
|
85
|
+
const projectInfo = getSessionProject(sessionId);
|
|
86
|
+
const key = projectInfo?.projectHash || '__global__';
|
|
87
|
+
|
|
88
|
+
if (serviceCache.has(key)) {
|
|
89
|
+
return serviceCache.get(key)!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const service = new MemoryService({
|
|
93
|
+
storagePath: projectInfo
|
|
94
|
+
? getProjectStoragePath(projectInfo.projectPath)
|
|
95
|
+
: path.join(os.homedir(), '.claude-code', 'memory'),
|
|
96
|
+
projectHash: projectInfo?.projectHash,
|
|
97
|
+
projectPath: projectInfo?.projectPath,
|
|
98
|
+
readOnly: false,
|
|
99
|
+
embeddingOnly: true,
|
|
100
|
+
analyticsEnabled: false,
|
|
101
|
+
sharedStoreConfig: { enabled: false }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
serviceCache.set(key, service);
|
|
105
|
+
return service;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handleRequest(raw: string): Promise<SemanticDaemonResponse> {
|
|
109
|
+
const input = parseRequest(raw);
|
|
110
|
+
if (!isValidRequest(input)) {
|
|
111
|
+
return { ok: false, error: 'invalid request' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const service = getServiceForSession(input.sessionId);
|
|
116
|
+
let result;
|
|
117
|
+
try {
|
|
118
|
+
result = await service.retrieveMemories(input.prompt, {
|
|
119
|
+
topK: input.topK,
|
|
120
|
+
minScore: input.minScore,
|
|
121
|
+
sessionId: input.sessionId,
|
|
122
|
+
intentRewrite: true,
|
|
123
|
+
adaptiveRerank: true,
|
|
124
|
+
projectScopeMode: 'strict'
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!isVectorSessionFilterError(error)) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// LanceDB field-case mismatch can fail sessionId filtering.
|
|
132
|
+
// Retry without session filter and keep project strict scoping.
|
|
133
|
+
result = await service.retrieveMemories(input.prompt, {
|
|
134
|
+
topK: input.topK,
|
|
135
|
+
minScore: input.minScore,
|
|
136
|
+
intentRewrite: true,
|
|
137
|
+
adaptiveRerank: true,
|
|
138
|
+
projectScopeMode: 'strict'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const memories = result.memories.map((m) => ({
|
|
143
|
+
type: m.event.eventType,
|
|
144
|
+
content: m.event.content,
|
|
145
|
+
id: m.event.id,
|
|
146
|
+
score: m.score
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
return { ok: true, memories };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return makeErrorResponse(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createServer(): net.Server {
|
|
156
|
+
return net.createServer({ allowHalfOpen: true }, (socket) => {
|
|
157
|
+
scheduleIdleShutdown();
|
|
158
|
+
socket.setEncoding('utf8');
|
|
159
|
+
|
|
160
|
+
let requestRaw = '';
|
|
161
|
+
|
|
162
|
+
socket.on('data', (chunk) => {
|
|
163
|
+
requestRaw += chunk;
|
|
164
|
+
if (requestRaw.length > 1024 * 1024) {
|
|
165
|
+
socket.end(JSON.stringify({ ok: false, error: 'request too large' }));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
socket.on('end', async () => {
|
|
170
|
+
const response = await handleRequest(requestRaw);
|
|
171
|
+
socket.end(JSON.stringify(response));
|
|
172
|
+
scheduleIdleShutdown();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
socket.on('error', () => {
|
|
176
|
+
// Ignore per-socket errors to keep daemon process alive.
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function socketInUse(p: string): Promise<boolean> {
|
|
182
|
+
if (!fs.existsSync(p)) return false;
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
let settled = false;
|
|
185
|
+
const client = net.createConnection(p);
|
|
186
|
+
const done = (alive: boolean) => {
|
|
187
|
+
if (settled) return;
|
|
188
|
+
settled = true;
|
|
189
|
+
client.destroy();
|
|
190
|
+
resolve(alive);
|
|
191
|
+
};
|
|
192
|
+
client.on('connect', () => done(true));
|
|
193
|
+
client.on('error', () => done(false));
|
|
194
|
+
setTimeout(() => done(false), 120).unref();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function listenServer(): Promise<void> {
|
|
199
|
+
const socketDir = path.dirname(SOCKET_PATH);
|
|
200
|
+
if (!fs.existsSync(socketDir)) {
|
|
201
|
+
fs.mkdirSync(socketDir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (await socketInUse(SOCKET_PATH)) {
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
209
|
+
try {
|
|
210
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore stale socket unlink failures.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
server = createServer();
|
|
217
|
+
|
|
218
|
+
await new Promise<void>((resolve, reject) => {
|
|
219
|
+
if (!server) {
|
|
220
|
+
reject(new Error('daemon server not initialized'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
server.once('error', reject);
|
|
225
|
+
server.listen(SOCKET_PATH, () => {
|
|
226
|
+
server?.off('error', reject);
|
|
227
|
+
resolve();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function shutdown(code: number): Promise<void> {
|
|
233
|
+
if (shuttingDown) return;
|
|
234
|
+
shuttingDown = true;
|
|
235
|
+
|
|
236
|
+
if (idleTimer) {
|
|
237
|
+
clearTimeout(idleTimer);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const closePromises: Promise<void>[] = [];
|
|
241
|
+
for (const service of serviceCache.values()) {
|
|
242
|
+
closePromises.push(service.shutdown().catch(() => undefined));
|
|
243
|
+
}
|
|
244
|
+
await Promise.all(closePromises);
|
|
245
|
+
serviceCache.clear();
|
|
246
|
+
|
|
247
|
+
if (server) {
|
|
248
|
+
await new Promise<void>((resolve) => {
|
|
249
|
+
server?.close(() => resolve());
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
256
|
+
} catch {
|
|
257
|
+
// Ignore socket cleanup failure.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
process.exit(code);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function main(): Promise<void> {
|
|
265
|
+
await listenServer();
|
|
266
|
+
scheduleIdleShutdown();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.on('SIGINT', () => { shutdown(0).catch(() => process.exit(0)); });
|
|
270
|
+
process.on('SIGTERM', () => { shutdown(0).catch(() => process.exit(0)); });
|
|
271
|
+
process.on('uncaughtException', () => { shutdown(1).catch(() => process.exit(1)); });
|
|
272
|
+
process.on('unhandledRejection', () => { shutdown(1).catch(() => process.exit(1)); });
|
|
273
|
+
|
|
274
|
+
main().catch(() => {
|
|
275
|
+
process.exit(1);
|
|
276
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getLightweightMemoryService,
|
|
9
9
|
registerSession
|
|
10
10
|
} from '../services/memory-service.js';
|
|
11
|
+
import { ensureDaemonRunning } from './semantic-daemon-client.js';
|
|
11
12
|
import type { SessionStartInput, SessionStartOutput } from '../core/types.js';
|
|
12
13
|
|
|
13
14
|
async function main(): Promise<void> {
|
|
@@ -18,6 +19,12 @@ async function main(): Promise<void> {
|
|
|
18
19
|
// Register session with project path for other hooks to find
|
|
19
20
|
registerSession(input.session_id, input.cwd);
|
|
20
21
|
|
|
22
|
+
// Start semantic daemon in the background (non-blocking) so VectorWorker
|
|
23
|
+
// can process any pending embedding_outbox items immediately.
|
|
24
|
+
ensureDaemonRunning().catch(() => {
|
|
25
|
+
// Ignore - daemon will start on first prompt if needed
|
|
26
|
+
});
|
|
27
|
+
|
|
21
28
|
// Use lightweight service to avoid starting background workers in hook process
|
|
22
29
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
23
30
|
|
|
@@ -25,6 +32,10 @@ async function main(): Promise<void> {
|
|
|
25
32
|
// Start session in memory service
|
|
26
33
|
await memoryService.startSession(input.session_id, input.cwd);
|
|
27
34
|
|
|
35
|
+
// Backfill session summaries for recent sessions that ended without Stop hook
|
|
36
|
+
// (crash, force-close, etc.). Run in background - non-blocking.
|
|
37
|
+
memoryService.backfillMissingSummaries(input.session_id, 5).catch(() => {});
|
|
38
|
+
|
|
28
39
|
// Get recent context for this project (now automatically scoped)
|
|
29
40
|
const recentEvents = await memoryService.getRecentEvents(10);
|
|
30
41
|
|
package/src/hooks/stop.ts
CHANGED
|
@@ -17,7 +17,7 @@ import * as fs from 'fs';
|
|
|
17
17
|
import * as readline from 'readline';
|
|
18
18
|
import { getLightweightMemoryService } from '../services/memory-service.js';
|
|
19
19
|
import { applyPrivacyFilter } from '../core/privacy/index.js';
|
|
20
|
-
import { readTurnState, clearTurnState } from '../core/turn-state.js';
|
|
20
|
+
import { readTurnState, clearTurnState, writeLastAssistantSnippet } from '../core/turn-state.js';
|
|
21
21
|
import type { StopInput, Config } from '../core/types.js';
|
|
22
22
|
|
|
23
23
|
// Default privacy config
|
|
@@ -94,8 +94,16 @@ async function main(): Promise<void> {
|
|
|
94
94
|
// Read assistant messages from transcript
|
|
95
95
|
const assistantMessages = await extractAssistantMessages(input.transcript_path);
|
|
96
96
|
|
|
97
|
+
const MIN_AGENT_RESPONSE_LEN = parseInt(
|
|
98
|
+
process.env.CLAUDE_MEMORY_AGENT_RESPONSE_MIN_LEN || '150'
|
|
99
|
+
);
|
|
100
|
+
const lastIdx = assistantMessages.length - 1;
|
|
101
|
+
|
|
97
102
|
// Store each assistant response
|
|
98
|
-
for (
|
|
103
|
+
for (let i = 0; i < assistantMessages.length; i++) {
|
|
104
|
+
const text = assistantMessages[i];
|
|
105
|
+
const isLast = i === lastIdx;
|
|
106
|
+
|
|
99
107
|
// Apply privacy filter
|
|
100
108
|
const filterResult = applyPrivacyFilter(text, DEFAULT_PRIVACY_CONFIG);
|
|
101
109
|
let content = filterResult.content;
|
|
@@ -105,8 +113,9 @@ async function main(): Promise<void> {
|
|
|
105
113
|
content = content.slice(0, 5000) + '...[truncated]';
|
|
106
114
|
}
|
|
107
115
|
|
|
108
|
-
// Skip very short responses (likely just tool calls)
|
|
109
|
-
|
|
116
|
+
// Skip very short responses (likely just tool calls or transition messages)
|
|
117
|
+
// Always store the last message (may be the final answer)
|
|
118
|
+
if (!isLast && content.trim().length < MIN_AGENT_RESPONSE_LEN) continue;
|
|
110
119
|
|
|
111
120
|
await memoryService.storeAgentResponse(
|
|
112
121
|
input.session_id,
|
|
@@ -118,9 +127,29 @@ async function main(): Promise<void> {
|
|
|
118
127
|
);
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
// Save last assistant response snippet for next-turn retrieval context enrichment
|
|
131
|
+
if (assistantMessages.length > 0) {
|
|
132
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
133
|
+
writeLastAssistantSnippet(input.session_id, lastMessage);
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
// Clean up turn state file after processing
|
|
122
137
|
clearTurnState(input.session_id);
|
|
123
138
|
|
|
139
|
+
// Evaluate helpfulness of retrieved memories for this session
|
|
140
|
+
try {
|
|
141
|
+
await memoryService.evaluateSessionHelpfulness(input.session_id);
|
|
142
|
+
} catch {
|
|
143
|
+
// non-critical
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generate session summary from recent events (rule-based, no LLM needed)
|
|
147
|
+
try {
|
|
148
|
+
await memoryService.generateSessionSummary(input.session_id);
|
|
149
|
+
} catch {
|
|
150
|
+
// non-critical
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
// Embeddings enqueued in SQLite - will be processed by vector worker when server runs
|
|
125
154
|
await memoryService.processPendingEmbeddings();
|
|
126
155
|
|