@aitytech/agentkits-memory 1.0.1 → 2.0.0

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 (105) hide show
  1. package/README.md +54 -5
  2. package/dist/better-sqlite3-backend.d.ts +192 -0
  3. package/dist/better-sqlite3-backend.d.ts.map +1 -0
  4. package/dist/better-sqlite3-backend.js +801 -0
  5. package/dist/better-sqlite3-backend.js.map +1 -0
  6. package/dist/cli/save.js +0 -0
  7. package/dist/cli/setup.d.ts +6 -2
  8. package/dist/cli/setup.d.ts.map +1 -1
  9. package/dist/cli/setup.js +289 -42
  10. package/dist/cli/setup.js.map +1 -1
  11. package/dist/cli/viewer.js +25 -56
  12. package/dist/cli/viewer.js.map +1 -1
  13. package/dist/cli/web-viewer.d.ts +2 -1
  14. package/dist/cli/web-viewer.d.ts.map +1 -1
  15. package/dist/cli/web-viewer.js +791 -141
  16. package/dist/cli/web-viewer.js.map +1 -1
  17. package/dist/embeddings/embedding-cache.d.ts +131 -0
  18. package/dist/embeddings/embedding-cache.d.ts.map +1 -0
  19. package/dist/embeddings/embedding-cache.js +217 -0
  20. package/dist/embeddings/embedding-cache.js.map +1 -0
  21. package/dist/embeddings/index.d.ts +11 -0
  22. package/dist/embeddings/index.d.ts.map +1 -0
  23. package/dist/embeddings/index.js +11 -0
  24. package/dist/embeddings/index.js.map +1 -0
  25. package/dist/embeddings/local-embeddings.d.ts +140 -0
  26. package/dist/embeddings/local-embeddings.d.ts.map +1 -0
  27. package/dist/embeddings/local-embeddings.js +293 -0
  28. package/dist/embeddings/local-embeddings.js.map +1 -0
  29. package/dist/hooks/context.d.ts +6 -1
  30. package/dist/hooks/context.d.ts.map +1 -1
  31. package/dist/hooks/context.js +12 -2
  32. package/dist/hooks/context.js.map +1 -1
  33. package/dist/hooks/observation.d.ts +6 -1
  34. package/dist/hooks/observation.d.ts.map +1 -1
  35. package/dist/hooks/observation.js +12 -2
  36. package/dist/hooks/observation.js.map +1 -1
  37. package/dist/hooks/service.d.ts +1 -6
  38. package/dist/hooks/service.d.ts.map +1 -1
  39. package/dist/hooks/service.js +33 -85
  40. package/dist/hooks/service.js.map +1 -1
  41. package/dist/hooks/session-init.d.ts +6 -1
  42. package/dist/hooks/session-init.d.ts.map +1 -1
  43. package/dist/hooks/session-init.js +12 -2
  44. package/dist/hooks/session-init.js.map +1 -1
  45. package/dist/hooks/summarize.d.ts +6 -1
  46. package/dist/hooks/summarize.d.ts.map +1 -1
  47. package/dist/hooks/summarize.js +12 -2
  48. package/dist/hooks/summarize.js.map +1 -1
  49. package/dist/index.d.ts +10 -17
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +172 -94
  52. package/dist/index.js.map +1 -1
  53. package/dist/mcp/server.js +17 -3
  54. package/dist/mcp/server.js.map +1 -1
  55. package/dist/migration.js +3 -3
  56. package/dist/migration.js.map +1 -1
  57. package/dist/search/hybrid-search.d.ts +262 -0
  58. package/dist/search/hybrid-search.d.ts.map +1 -0
  59. package/dist/search/hybrid-search.js +688 -0
  60. package/dist/search/hybrid-search.js.map +1 -0
  61. package/dist/search/index.d.ts +13 -0
  62. package/dist/search/index.d.ts.map +1 -0
  63. package/dist/search/index.js +13 -0
  64. package/dist/search/index.js.map +1 -0
  65. package/dist/search/token-economics.d.ts +161 -0
  66. package/dist/search/token-economics.d.ts.map +1 -0
  67. package/dist/search/token-economics.js +239 -0
  68. package/dist/search/token-economics.js.map +1 -0
  69. package/dist/types.d.ts +0 -68
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/package.json +5 -3
  73. package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
  74. package/src/__tests__/cache-manager.test.ts +499 -0
  75. package/src/__tests__/embedding-integration.test.ts +481 -0
  76. package/src/__tests__/hnsw-index.test.ts +727 -0
  77. package/src/__tests__/index.test.ts +432 -0
  78. package/src/better-sqlite3-backend.ts +1000 -0
  79. package/src/cli/setup.ts +358 -47
  80. package/src/cli/viewer.ts +28 -63
  81. package/src/cli/web-viewer.ts +936 -182
  82. package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
  83. package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
  84. package/src/embeddings/embedding-cache.ts +318 -0
  85. package/src/embeddings/index.ts +20 -0
  86. package/src/embeddings/local-embeddings.ts +419 -0
  87. package/src/hooks/__tests__/handlers.test.ts +58 -17
  88. package/src/hooks/__tests__/integration.test.ts +77 -26
  89. package/src/hooks/context.ts +13 -2
  90. package/src/hooks/observation.ts +13 -2
  91. package/src/hooks/service.ts +39 -100
  92. package/src/hooks/session-init.ts +13 -2
  93. package/src/hooks/summarize.ts +13 -2
  94. package/src/index.ts +210 -116
  95. package/src/mcp/server.ts +20 -3
  96. package/src/search/__tests__/hybrid-search.test.ts +669 -0
  97. package/src/search/__tests__/token-economics.test.ts +276 -0
  98. package/src/search/hybrid-search.ts +968 -0
  99. package/src/search/index.ts +29 -0
  100. package/src/search/token-economics.ts +367 -0
  101. package/src/types.ts +0 -96
  102. package/src/__tests__/sqljs-backend.test.ts +0 -410
  103. package/src/migration.ts +0 -574
  104. package/src/sql.js.d.ts +0 -70
  105. package/src/sqljs-backend.ts +0 -789
@@ -18,6 +18,15 @@ import { createSummarizeHook } from '../summarize.js';
18
18
 
19
19
  const TEST_DIR = path.join(process.cwd(), '.test-integration-hooks');
20
20
 
21
+ // Track hooks for cleanup (needed for Windows file locking)
22
+ let activeHooks: Array<{ shutdown: () => Promise<void> }> = [];
23
+
24
+ // Helper to track hooks for cleanup
25
+ function trackHook<T extends { shutdown: () => Promise<void> }>(hook: T): T {
26
+ activeHooks.push(hook);
27
+ return hook;
28
+ }
29
+
21
30
  function createTestInput(overrides: Partial<NormalizedHookInput> = {}): NormalizedHookInput {
22
31
  return {
23
32
  sessionId: 'integration-session',
@@ -30,17 +39,39 @@ function createTestInput(overrides: Partial<NormalizedHookInput> = {}): Normaliz
30
39
 
31
40
  describe('Hook System Integration', () => {
32
41
  beforeEach(() => {
42
+ activeHooks = [];
33
43
  // Clean up test directory
34
44
  if (existsSync(TEST_DIR)) {
35
- rmSync(TEST_DIR, { recursive: true });
45
+ try {
46
+ rmSync(TEST_DIR, { recursive: true });
47
+ } catch {
48
+ // Ignore errors on Windows
49
+ }
36
50
  }
37
51
  mkdirSync(TEST_DIR, { recursive: true });
38
52
  });
39
53
 
40
- afterEach(() => {
54
+ afterEach(async () => {
55
+ // Shutdown all hooks first (releases database locks)
56
+ for (const hook of activeHooks) {
57
+ try {
58
+ await hook.shutdown();
59
+ } catch {
60
+ // Ignore shutdown errors
61
+ }
62
+ }
63
+ activeHooks = [];
64
+
65
+ // Small delay for Windows file system
66
+ await new Promise((r) => setTimeout(r, 100));
67
+
41
68
  // Clean up test directory
42
69
  if (existsSync(TEST_DIR)) {
43
- rmSync(TEST_DIR, { recursive: true });
70
+ try {
71
+ rmSync(TEST_DIR, { recursive: true });
72
+ } catch {
73
+ // Ignore errors on Windows - files may still be locked
74
+ }
44
75
  }
45
76
  });
46
77
 
@@ -50,24 +81,26 @@ describe('Hook System Integration', () => {
50
81
  const project = 'test-project';
51
82
 
52
83
  // 1. Session Start - Context Hook (no previous context)
53
- const contextHook = createContextHook(TEST_DIR);
84
+ const contextHook = trackHook(createContextHook(TEST_DIR));
54
85
  const contextResult = await contextHook.execute(
55
86
  createTestInput({ sessionId, project })
56
87
  );
57
88
 
58
89
  expect(contextResult.continue).toBe(true);
59
90
  expect(contextResult.additionalContext).toBeUndefined(); // No previous sessions
91
+ await contextHook.shutdown();
60
92
 
61
93
  // 2. User Prompt Submit - Session Init Hook
62
- const sessionInitHook = createSessionInitHook(TEST_DIR);
94
+ const sessionInitHook = trackHook(createSessionInitHook(TEST_DIR));
63
95
  const sessionInitResult = await sessionInitHook.execute(
64
96
  createTestInput({ sessionId, project, prompt: 'Help me implement a feature' })
65
97
  );
66
98
 
67
99
  expect(sessionInitResult.continue).toBe(true);
100
+ await sessionInitHook.shutdown();
68
101
 
69
102
  // 3. Tool Uses - Observation Hooks
70
- const observationHook = createObservationHook(TEST_DIR);
103
+ const observationHook = trackHook(createObservationHook(TEST_DIR));
71
104
 
72
105
  // Simulate reading files
73
106
  await observationHook.execute(
@@ -112,9 +145,10 @@ describe('Hook System Integration', () => {
112
145
  toolResponse: { stdout: 'All tests passed' },
113
146
  })
114
147
  );
148
+ await observationHook.shutdown();
115
149
 
116
150
  // 4. Session End - Summarize Hook
117
- const summarizeHook = createSummarizeHook(TEST_DIR);
151
+ const summarizeHook = trackHook(createSummarizeHook(TEST_DIR));
118
152
  const summarizeResult = await summarizeHook.execute(
119
153
  createTestInput({ sessionId, project, stopReason: 'user_exit' })
120
154
  );
@@ -146,11 +180,12 @@ describe('Hook System Integration', () => {
146
180
  const project = 'test-project';
147
181
 
148
182
  // Init session 1
149
- const initHook1 = createSessionInitHook(TEST_DIR);
183
+ const initHook1 = trackHook(createSessionInitHook(TEST_DIR));
150
184
  await initHook1.execute(createTestInput({ sessionId: session1Id, project, prompt: 'First task' }));
185
+ await initHook1.shutdown();
151
186
 
152
187
  // Add observations to session 1
153
- const obsHook1 = createObservationHook(TEST_DIR);
188
+ const obsHook1 = trackHook(createObservationHook(TEST_DIR));
154
189
  await obsHook1.execute(createTestInput({
155
190
  sessionId: session1Id,
156
191
  project,
@@ -158,15 +193,16 @@ describe('Hook System Integration', () => {
158
193
  toolInput: { file_path: 'src/auth.ts' },
159
194
  toolResponse: {},
160
195
  }));
196
+ await obsHook1.shutdown();
161
197
 
162
198
  // Complete session 1
163
- const sumHook1 = createSummarizeHook(TEST_DIR);
199
+ const sumHook1 = trackHook(createSummarizeHook(TEST_DIR));
164
200
  await sumHook1.execute(createTestInput({ sessionId: session1Id, project }));
165
201
 
166
202
  // Session 2: Should see context from session 1
167
203
  const session2Id = 'current-session';
168
204
 
169
- const contextHook2 = createContextHook(TEST_DIR);
205
+ const contextHook2 = trackHook(createContextHook(TEST_DIR));
170
206
  const contextResult = await contextHook2.execute(
171
207
  createTestInput({ sessionId: session2Id, project })
172
208
  );
@@ -181,14 +217,15 @@ describe('Hook System Integration', () => {
181
217
 
182
218
  it('should handle multiple projects independently', async () => {
183
219
  // Session for project A
184
- const initHookA = createSessionInitHook(TEST_DIR);
220
+ const initHookA = trackHook(createSessionInitHook(TEST_DIR));
185
221
  await initHookA.execute(createTestInput({
186
222
  sessionId: 'session-a',
187
223
  project: 'project-a',
188
224
  prompt: 'Task for A',
189
225
  }));
226
+ await initHookA.shutdown();
190
227
 
191
- const obsHookA = createObservationHook(TEST_DIR);
228
+ const obsHookA = trackHook(createObservationHook(TEST_DIR));
192
229
  await obsHookA.execute(createTestInput({
193
230
  sessionId: 'session-a',
194
231
  project: 'project-a',
@@ -196,16 +233,18 @@ describe('Hook System Integration', () => {
196
233
  toolInput: { file_path: 'a.ts' },
197
234
  toolResponse: {},
198
235
  }));
236
+ await obsHookA.shutdown();
199
237
 
200
238
  // Session for project B
201
- const initHookB = createSessionInitHook(TEST_DIR);
239
+ const initHookB = trackHook(createSessionInitHook(TEST_DIR));
202
240
  await initHookB.execute(createTestInput({
203
241
  sessionId: 'session-b',
204
242
  project: 'project-b',
205
243
  prompt: 'Task for B',
206
244
  }));
245
+ await initHookB.shutdown();
207
246
 
208
- const obsHookB = createObservationHook(TEST_DIR);
247
+ const obsHookB = trackHook(createObservationHook(TEST_DIR));
209
248
  await obsHookB.execute(createTestInput({
210
249
  sessionId: 'session-b',
211
250
  project: 'project-b',
@@ -213,6 +252,7 @@ describe('Hook System Integration', () => {
213
252
  toolInput: { file_path: 'b.ts' },
214
253
  toolResponse: {},
215
254
  }));
255
+ await obsHookB.shutdown();
216
256
 
217
257
  // Verify isolation
218
258
  const service = new MemoryHookService(TEST_DIR);
@@ -260,10 +300,11 @@ describe('Hook System Integration', () => {
260
300
  expect(parsed.toolResponse).toEqual({ content: 'file contents here' });
261
301
 
262
302
  // Process through observation hook
263
- const observationHook = createObservationHook(TEST_DIR);
303
+ const observationHook = trackHook(createObservationHook(TEST_DIR));
264
304
  const result = await observationHook.execute(parsed);
265
305
 
266
306
  expect(result.continue).toBe(true);
307
+ await observationHook.shutdown();
267
308
 
268
309
  // Verify stored
269
310
  const service = new MemoryHookService(TEST_DIR);
@@ -282,11 +323,12 @@ describe('Hook System Integration', () => {
282
323
  const project = 'test-project';
283
324
 
284
325
  // Init session
285
- const initHook = createSessionInitHook(TEST_DIR);
326
+ const initHook = trackHook(createSessionInitHook(TEST_DIR));
286
327
  await initHook.execute(createTestInput({ sessionId, project }));
328
+ await initHook.shutdown();
287
329
 
288
330
  // Successful observation
289
- const obsHook = createObservationHook(TEST_DIR);
331
+ const obsHook = trackHook(createObservationHook(TEST_DIR));
290
332
  await obsHook.execute(createTestInput({
291
333
  sessionId,
292
334
  project,
@@ -303,6 +345,7 @@ describe('Hook System Integration', () => {
303
345
  toolInput: {},
304
346
  toolResponse: {},
305
347
  }));
348
+ await obsHook.shutdown();
306
349
 
307
350
  // Verify both observations stored
308
351
  const service = new MemoryHookService(TEST_DIR);
@@ -319,14 +362,16 @@ describe('Hook System Integration', () => {
319
362
  const project = 'test-project';
320
363
 
321
364
  // Start two sessions sequentially (SQLite doesn't handle concurrent writes well)
322
- const initHook1 = createSessionInitHook(TEST_DIR);
365
+ const initHook1 = trackHook(createSessionInitHook(TEST_DIR));
323
366
  await initHook1.execute(createTestInput({ sessionId: 'multi-1', project }));
367
+ await initHook1.shutdown();
324
368
 
325
- const initHook2 = createSessionInitHook(TEST_DIR);
369
+ const initHook2 = trackHook(createSessionInitHook(TEST_DIR));
326
370
  await initHook2.execute(createTestInput({ sessionId: 'multi-2', project }));
371
+ await initHook2.shutdown();
327
372
 
328
373
  // Add observations sequentially
329
- const obsHook1 = createObservationHook(TEST_DIR);
374
+ const obsHook1 = trackHook(createObservationHook(TEST_DIR));
330
375
  await obsHook1.execute(createTestInput({
331
376
  sessionId: 'multi-1',
332
377
  project,
@@ -334,8 +379,9 @@ describe('Hook System Integration', () => {
334
379
  toolInput: {},
335
380
  toolResponse: {},
336
381
  }));
382
+ await obsHook1.shutdown();
337
383
 
338
- const obsHook2 = createObservationHook(TEST_DIR);
384
+ const obsHook2 = trackHook(createObservationHook(TEST_DIR));
339
385
  await obsHook2.execute(createTestInput({
340
386
  sessionId: 'multi-2',
341
387
  project,
@@ -343,6 +389,7 @@ describe('Hook System Integration', () => {
343
389
  toolInput: {},
344
390
  toolResponse: {},
345
391
  }));
392
+ await obsHook2.shutdown();
346
393
 
347
394
  // Verify both sessions have their observations
348
395
  const service = new MemoryHookService(TEST_DIR);
@@ -366,11 +413,12 @@ describe('Hook System Integration', () => {
366
413
  const project = 'test-project';
367
414
 
368
415
  // Init session
369
- const initHook = createSessionInitHook(TEST_DIR);
416
+ const initHook = trackHook(createSessionInitHook(TEST_DIR));
370
417
  await initHook.execute(createTestInput({ sessionId, project }));
418
+ await initHook.shutdown();
371
419
 
372
420
  // Add many observations
373
- const obsHook = createObservationHook(TEST_DIR);
421
+ const obsHook = trackHook(createObservationHook(TEST_DIR));
374
422
  const observationCount = 50;
375
423
 
376
424
  for (let i = 0; i < observationCount; i++) {
@@ -382,6 +430,7 @@ describe('Hook System Integration', () => {
382
430
  toolResponse: { content: `content ${i}` },
383
431
  }));
384
432
  }
433
+ await obsHook.shutdown();
385
434
 
386
435
  // Verify all observations stored
387
436
  const service = new MemoryHookService(TEST_DIR);
@@ -401,11 +450,12 @@ describe('Hook System Integration', () => {
401
450
  const project = 'test-project';
402
451
 
403
452
  // Init session
404
- const initHook = createSessionInitHook(TEST_DIR);
453
+ const initHook = trackHook(createSessionInitHook(TEST_DIR));
405
454
  await initHook.execute(createTestInput({ sessionId, project }));
455
+ await initHook.shutdown();
406
456
 
407
457
  // Add observation with large response
408
- const obsHook = createObservationHook(TEST_DIR);
458
+ const obsHook = trackHook(createObservationHook(TEST_DIR));
409
459
  const largeContent = 'A'.repeat(100000); // 100KB
410
460
 
411
461
  await obsHook.execute(createTestInput({
@@ -415,6 +465,7 @@ describe('Hook System Integration', () => {
415
465
  toolInput: { file_path: 'large.ts' },
416
466
  toolResponse: { content: largeContent },
417
467
  }));
468
+ await obsHook.shutdown();
418
469
 
419
470
  // Verify response was truncated
420
471
  const service = new MemoryHookService(TEST_DIR);
@@ -23,9 +23,20 @@ import { MemoryHookService } from './service.js';
23
23
  */
24
24
  export class ContextHook implements EventHandler {
25
25
  private service: MemoryHookService;
26
+ private ownsService: boolean;
26
27
 
27
- constructor(service: MemoryHookService) {
28
+ constructor(service: MemoryHookService, ownsService = false) {
28
29
  this.service = service;
30
+ this.ownsService = ownsService;
31
+ }
32
+
33
+ /**
34
+ * Shutdown the hook (closes database if owned)
35
+ */
36
+ async shutdown(): Promise<void> {
37
+ if (this.ownsService) {
38
+ await this.service.shutdown();
39
+ }
29
40
  }
30
41
 
31
42
  /**
@@ -71,7 +82,7 @@ export class ContextHook implements EventHandler {
71
82
  */
72
83
  export function createContextHook(cwd: string): ContextHook {
73
84
  const service = new MemoryHookService(cwd);
74
- return new ContextHook(service);
85
+ return new ContextHook(service, true); // owns service
75
86
  }
76
87
 
77
88
  export default ContextHook;
@@ -32,9 +32,20 @@ const SKIP_TOOLS = new Set([
32
32
  */
33
33
  export class ObservationHook implements EventHandler {
34
34
  private service: MemoryHookService;
35
+ private ownsService: boolean;
35
36
 
36
- constructor(service: MemoryHookService) {
37
+ constructor(service: MemoryHookService, ownsService = false) {
37
38
  this.service = service;
39
+ this.ownsService = ownsService;
40
+ }
41
+
42
+ /**
43
+ * Shutdown the hook (closes database if owned)
44
+ */
45
+ async shutdown(): Promise<void> {
46
+ if (this.ownsService) {
47
+ await this.service.shutdown();
48
+ }
38
49
  }
39
50
 
40
51
  /**
@@ -96,7 +107,7 @@ export class ObservationHook implements EventHandler {
96
107
  */
97
108
  export function createObservationHook(cwd: string): ObservationHook {
98
109
  const service = new MemoryHookService(cwd);
99
- return new ObservationHook(service);
110
+ return new ObservationHook(service, true);
100
111
  }
101
112
 
102
113
  export default ObservationHook;
@@ -2,18 +2,15 @@
2
2
  * Memory Hook Service
3
3
  *
4
4
  * Lightweight service for hooks to store/retrieve memory.
5
- * Direct SQLite access without HTTP worker (simpler than claude-mem).
5
+ * Direct SQLite access without HTTP worker.
6
6
  *
7
7
  * @module @agentkits/memory/hooks/service
8
8
  */
9
9
 
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { existsSync, mkdirSync } from 'node:fs';
11
11
  import * as path from 'node:path';
12
- import { createRequire } from 'node:module';
13
- import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
14
-
15
- // ESM-compatible require for resolving sql.js WASM path
16
- const require = createRequire(import.meta.url);
12
+ import Database from 'better-sqlite3';
13
+ import type { Database as BetterDatabase } from 'better-sqlite3';
17
14
  import {
18
15
  Observation,
19
16
  SessionRecord,
@@ -60,8 +57,7 @@ const DEFAULT_CONFIG: MemoryHookServiceConfig = {
60
57
  */
61
58
  export class MemoryHookService {
62
59
  private config: MemoryHookServiceConfig;
63
- private db: SqlJsDatabase | null = null;
64
- private SQL: any = null;
60
+ private db: BetterDatabase | null = null;
65
61
  private initialized: boolean = false;
66
62
  private dbPath: string;
67
63
 
@@ -82,25 +78,11 @@ export class MemoryHookService {
82
78
  mkdirSync(dir, { recursive: true });
83
79
  }
84
80
 
85
- // Load sql.js - use local wasm file from node_modules
86
- this.SQL = await initSqlJs({
87
- locateFile: (file: string) => {
88
- // Try to find the wasm file in node_modules
89
- const localPath = path.join(
90
- path.dirname(require.resolve('sql.js')),
91
- file
92
- );
93
- return localPath;
94
- },
95
- });
96
-
97
- // Load or create database
98
- if (existsSync(this.dbPath)) {
99
- const buffer = readFileSync(this.dbPath);
100
- this.db = new this.SQL.Database(new Uint8Array(buffer));
101
- } else {
102
- this.db = new this.SQL.Database();
103
- }
81
+ // Open database with better-sqlite3
82
+ this.db = new Database(this.dbPath);
83
+
84
+ // Enable WAL mode for better performance
85
+ this.db.pragma('journal_mode = WAL');
104
86
 
105
87
  // Create schema
106
88
  this.createSchema();
@@ -108,24 +90,12 @@ export class MemoryHookService {
108
90
  this.initialized = true;
109
91
  }
110
92
 
111
- /**
112
- * Persist database to disk
113
- */
114
- async persist(): Promise<void> {
115
- if (!this.db) return;
116
-
117
- const data = this.db.export();
118
- const buffer = Buffer.from(data);
119
- writeFileSync(this.dbPath, buffer);
120
- }
121
-
122
93
  /**
123
94
  * Shutdown the service
124
95
  */
125
96
  async shutdown(): Promise<void> {
126
97
  if (!this.initialized || !this.db) return;
127
98
 
128
- await this.persist();
129
99
  this.db.close();
130
100
  this.db = null;
131
101
  this.initialized = false;
@@ -147,15 +117,13 @@ export class MemoryHookService {
147
117
 
148
118
  // Create new session
149
119
  const now = Date.now();
150
- this.db!.run(`
120
+ const result = this.db!.prepare(`
151
121
  INSERT INTO sessions (session_id, project, prompt, started_at, observation_count, status)
152
122
  VALUES (?, ?, ?, ?, 0, 'active')
153
- `, [sessionId, project, prompt || '', now]);
154
-
155
- await this.persist();
123
+ `).run(sessionId, project, prompt || '', now);
156
124
 
157
125
  return {
158
- id: this.db!.exec('SELECT last_insert_rowid()')[0]?.values[0]?.[0] as number || 0,
126
+ id: Number(result.lastInsertRowid),
159
127
  sessionId,
160
128
  project,
161
129
  prompt: prompt || '',
@@ -171,16 +139,12 @@ export class MemoryHookService {
171
139
  getSession(sessionId: string): SessionRecord | null {
172
140
  if (!this.db) return null;
173
141
 
174
- const stmt = this.db.prepare('SELECT * FROM sessions WHERE session_id = ?');
175
- stmt.bind([sessionId]);
142
+ const row = this.db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionId) as Record<string, unknown> | undefined;
176
143
 
177
- if (stmt.step()) {
178
- const row = stmt.getAsObject();
179
- stmt.free();
144
+ if (row) {
180
145
  return this.rowToSession(row);
181
146
  }
182
147
 
183
- stmt.free();
184
148
  return null;
185
149
  }
186
150
 
@@ -191,13 +155,11 @@ export class MemoryHookService {
191
155
  await this.ensureInitialized();
192
156
 
193
157
  const now = Date.now();
194
- this.db!.run(`
158
+ this.db!.prepare(`
195
159
  UPDATE sessions
196
160
  SET ended_at = ?, summary = ?, status = 'completed'
197
161
  WHERE session_id = ?
198
- `, [now, summary || '', sessionId]);
199
-
200
- await this.persist();
162
+ `).run(now, summary || '', sessionId);
201
163
  }
202
164
 
203
165
  /**
@@ -206,21 +168,14 @@ export class MemoryHookService {
206
168
  async getRecentSessions(project: string, limit: number = 5): Promise<SessionRecord[]> {
207
169
  await this.ensureInitialized();
208
170
 
209
- const stmt = this.db!.prepare(`
171
+ const rows = this.db!.prepare(`
210
172
  SELECT * FROM sessions
211
173
  WHERE project = ?
212
174
  ORDER BY started_at DESC
213
175
  LIMIT ?
214
- `);
215
- stmt.bind([project, limit]);
216
-
217
- const sessions: SessionRecord[] = [];
218
- while (stmt.step()) {
219
- sessions.push(this.rowToSession(stmt.getAsObject()));
220
- }
221
- stmt.free();
176
+ `).all(project, limit) as Record<string, unknown>[];
222
177
 
223
- return sessions;
178
+ return rows.map(row => this.rowToSession(row));
224
179
  }
225
180
 
226
181
  // ===== Observation Management =====
@@ -250,19 +205,17 @@ export class MemoryHookService {
250
205
  this.config.maxResponseSize
251
206
  );
252
207
 
253
- this.db!.run(`
208
+ this.db!.prepare(`
254
209
  INSERT INTO observations (id, session_id, project, tool_name, tool_input, tool_response, cwd, timestamp, type, title)
255
210
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
256
- `, [id, sessionId, project, toolName, inputStr, responseStr, cwd, now, type, title]);
211
+ `).run(id, sessionId, project, toolName, inputStr, responseStr, cwd, now, type, title);
257
212
 
258
213
  // Update session observation count
259
- this.db!.run(`
214
+ this.db!.prepare(`
260
215
  UPDATE sessions
261
216
  SET observation_count = observation_count + 1
262
217
  WHERE session_id = ?
263
- `, [sessionId]);
264
-
265
- await this.persist();
218
+ `).run(sessionId);
266
219
 
267
220
  return {
268
221
  id,
@@ -284,21 +237,14 @@ export class MemoryHookService {
284
237
  async getSessionObservations(sessionId: string, limit: number = 50): Promise<Observation[]> {
285
238
  await this.ensureInitialized();
286
239
 
287
- const stmt = this.db!.prepare(`
240
+ const rows = this.db!.prepare(`
288
241
  SELECT * FROM observations
289
242
  WHERE session_id = ?
290
243
  ORDER BY timestamp DESC
291
244
  LIMIT ?
292
- `);
293
- stmt.bind([sessionId, limit]);
245
+ `).all(sessionId, limit) as Record<string, unknown>[];
294
246
 
295
- const observations: Observation[] = [];
296
- while (stmt.step()) {
297
- observations.push(this.rowToObservation(stmt.getAsObject()));
298
- }
299
- stmt.free();
300
-
301
- return observations;
247
+ return rows.map(row => this.rowToObservation(row));
302
248
  }
303
249
 
304
250
  /**
@@ -307,21 +253,14 @@ export class MemoryHookService {
307
253
  async getRecentObservations(project: string, limit: number = 20): Promise<Observation[]> {
308
254
  await this.ensureInitialized();
309
255
 
310
- const stmt = this.db!.prepare(`
256
+ const rows = this.db!.prepare(`
311
257
  SELECT * FROM observations
312
258
  WHERE project = ?
313
259
  ORDER BY timestamp DESC
314
260
  LIMIT ?
315
- `);
316
- stmt.bind([project, limit]);
317
-
318
- const observations: Observation[] = [];
319
- while (stmt.step()) {
320
- observations.push(this.rowToObservation(stmt.getAsObject()));
321
- }
322
- stmt.free();
261
+ `).all(project, limit) as Record<string, unknown>[];
323
262
 
324
- return observations;
263
+ return rows.map(row => this.rowToObservation(row));
325
264
  }
326
265
 
327
266
  // ===== Context Generation =====
@@ -480,7 +419,7 @@ export class MemoryHookService {
480
419
  private createSchema(): void {
481
420
  if (!this.db) return;
482
421
 
483
- this.db.run(`
422
+ this.db.exec(`
484
423
  CREATE TABLE IF NOT EXISTS sessions (
485
424
  id INTEGER PRIMARY KEY AUTOINCREMENT,
486
425
  session_id TEXT UNIQUE NOT NULL,
@@ -494,7 +433,7 @@ export class MemoryHookService {
494
433
  )
495
434
  `);
496
435
 
497
- this.db.run(`
436
+ this.db.exec(`
498
437
  CREATE TABLE IF NOT EXISTS observations (
499
438
  id TEXT PRIMARY KEY,
500
439
  session_id TEXT NOT NULL,
@@ -510,13 +449,13 @@ export class MemoryHookService {
510
449
  )
511
450
  `);
512
451
 
513
- this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_session ON observations(session_id)');
514
- this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project)');
515
- this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_timestamp ON observations(timestamp)');
516
- this.db.run('CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project)');
452
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_obs_session ON observations(session_id)');
453
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project)');
454
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_obs_timestamp ON observations(timestamp)');
455
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project)');
517
456
  }
518
457
 
519
- private rowToSession(row: any): SessionRecord {
458
+ private rowToSession(row: Record<string, unknown>): SessionRecord {
520
459
  return {
521
460
  id: row.id as number,
522
461
  sessionId: row.session_id as string,
@@ -530,7 +469,7 @@ export class MemoryHookService {
530
469
  };
531
470
  }
532
471
 
533
- private rowToObservation(row: any): Observation {
472
+ private rowToObservation(row: Record<string, unknown>): Observation {
534
473
  return {
535
474
  id: row.id as string,
536
475
  sessionId: row.session_id as string,
@@ -540,7 +479,7 @@ export class MemoryHookService {
540
479
  toolResponse: row.tool_response as string,
541
480
  cwd: row.cwd as string,
542
481
  timestamp: row.timestamp as number,
543
- type: row.type as any,
482
+ type: row.type as Observation['type'],
544
483
  title: row.title as string | undefined,
545
484
  };
546
485
  }
@@ -22,9 +22,20 @@ import { MemoryHookService } from './service.js';
22
22
  */
23
23
  export class SessionInitHook implements EventHandler {
24
24
  private service: MemoryHookService;
25
+ private ownsService: boolean;
25
26
 
26
- constructor(service: MemoryHookService) {
27
+ constructor(service: MemoryHookService, ownsService = false) {
27
28
  this.service = service;
29
+ this.ownsService = ownsService;
30
+ }
31
+
32
+ /**
33
+ * Shutdown the hook (closes database if owned)
34
+ */
35
+ async shutdown(): Promise<void> {
36
+ if (this.ownsService) {
37
+ await this.service.shutdown();
38
+ }
28
39
  }
29
40
 
30
41
  /**
@@ -64,7 +75,7 @@ export class SessionInitHook implements EventHandler {
64
75
  */
65
76
  export function createSessionInitHook(cwd: string): SessionInitHook {
66
77
  const service = new MemoryHookService(cwd);
67
- return new SessionInitHook(service);
78
+ return new SessionInitHook(service, true);
68
79
  }
69
80
 
70
81
  export default SessionInitHook;