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.
Files changed (58) hide show
  1. package/.claude/settings.local.json +25 -0
  2. package/README.md +2 -0
  3. package/dist/cli/index.js +229 -978
  4. package/dist/cli/index.js.map +4 -4
  5. package/dist/core/index.js +59 -71
  6. package/dist/core/index.js.map +3 -3
  7. package/dist/hooks/post-tool-use.js +287 -976
  8. package/dist/hooks/post-tool-use.js.map +4 -4
  9. package/dist/hooks/semantic-daemon.js +6520 -0
  10. package/dist/hooks/semantic-daemon.js.map +7 -0
  11. package/dist/hooks/session-end.js +209 -973
  12. package/dist/hooks/session-end.js.map +4 -4
  13. package/dist/hooks/session-start.js +293 -978
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +247 -975
  16. package/dist/hooks/stop.js.map +4 -4
  17. package/dist/hooks/user-prompt-submit.js +406 -1036
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +209 -973
  20. package/dist/server/api/index.js.map +4 -4
  21. package/dist/server/index.js +209 -973
  22. package/dist/server/index.js.map +4 -4
  23. package/dist/services/memory-service.js +209 -973
  24. package/dist/services/memory-service.js.map +4 -4
  25. package/dist/ui/app.js +48 -1
  26. package/dist/ui/index.html +11 -3
  27. package/memory/_index.md +1 -0
  28. package/memory/agent_response/uncategorized/2026-03-04.md +1314 -1
  29. package/memory/session_summary/uncategorized/2026-03-04.md +50 -0
  30. package/memory/tool_observation/uncategorized/2026-03-04.md +969 -1
  31. package/memory/user_prompt/uncategorized/2026-03-04.md +555 -1
  32. package/package.json +1 -2
  33. package/scripts/build.ts +2 -1
  34. package/specs/memory-utilization-improvements/context.md +145 -0
  35. package/specs/memory-utilization-improvements/plan.md +361 -0
  36. package/specs/memory-utilization-improvements/spec.md +308 -0
  37. package/specs/optional-duckdb/context.md +77 -0
  38. package/specs/optional-duckdb/plan.md +142 -0
  39. package/specs/optional-duckdb/spec.md +35 -0
  40. package/specs/selective-tool-observation/context.md +100 -0
  41. package/specs/selective-tool-observation/plan.md +158 -0
  42. package/specs/selective-tool-observation/spec.md +127 -0
  43. package/src/cli/index.ts +1 -0
  44. package/src/core/db-wrapper.ts +18 -73
  45. package/src/core/embedder.ts +13 -4
  46. package/src/core/sqlite-event-store.ts +40 -0
  47. package/src/core/turn-state.ts +48 -0
  48. package/src/core/types.ts +1 -0
  49. package/src/hooks/post-tool-use.ts +72 -2
  50. package/src/hooks/semantic-daemon-client.ts +208 -0
  51. package/src/hooks/semantic-daemon.ts +276 -0
  52. package/src/hooks/session-start.ts +11 -0
  53. package/src/hooks/stop.ts +33 -4
  54. package/src/hooks/user-prompt-submit.ts +48 -40
  55. package/src/services/memory-service.ts +112 -65
  56. package/src/services/session-history-importer.ts +18 -0
  57. package/src/ui/app.js +48 -1
  58. 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 (const text of assistantMessages) {
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
- if (content.trim().length < 10) continue;
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