@jungjaehoon/mama-core 1.0.4 → 1.1.1

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/src/db-manager.js DELETED
@@ -1,679 +0,0 @@
1
- /**
2
- * MAMA Database Manager (SQLite-only)
3
- *
4
- * SQLite-exclusive database interface for MAMA Plugin.
5
- * Uses better-sqlite3 + sqlite-vec for local storage.
6
- *
7
- * PostgreSQL support is only available in the legacy mcp-server repository.
8
- *
9
- * Features:
10
- * - WAL mode for better concurrency
11
- * - synchronous=NORMAL for performance
12
- * - Automatic migration management
13
- * - Vector similarity search (when sqlite-vec available)
14
- *
15
- * @module db-manager
16
- * @version 2.1 (Plugin - SQLite-only)
17
- * @date 2026-02-01
18
- * @source-of-truth packages/mama-core/src/db-manager.js (mama-core)
19
- */
20
-
21
- const { info, warn, error: logError } = require('./debug-logger');
22
- const { logProgress: _logProgress, logComplete, logSearching } = require('./progress-indicator');
23
- const { createAdapter } = require('./db-adapter');
24
- const path = require('path');
25
-
26
- // Database adapter instance (singleton)
27
- let dbAdapter = null;
28
- let dbConnection = null;
29
- let isInitialized = false;
30
- let initializingPromise = null; // Single-flight guard for concurrent callers
31
-
32
- // Migration directory (moved to src/db/migrations for M1.2)
33
- const MIGRATIONS_DIR = path.join(__dirname, '..', 'db', 'migrations');
34
-
35
- /**
36
- * Initialize SQLite database adapter and connect
37
- *
38
- * Lazy initialization: Only connects when first accessed
39
- * Creates database file at ~/.claude/mama-memory.db by default
40
- *
41
- * Single-flight guard: Concurrent callers await the same promise
42
- * to prevent multiple adapters/migrations running simultaneously.
43
- *
44
- * @returns {Promise<Object>} SQLite database connection
45
- */
46
- async function initDB() {
47
- // Already initialized - return immediately
48
- if (isInitialized) {
49
- return dbConnection;
50
- }
51
-
52
- // Single-flight guard: If initialization is in progress, wait for it
53
- if (initializingPromise) {
54
- return initializingPromise;
55
- }
56
-
57
- // Start initialization and store promise for concurrent callers
58
- initializingPromise = (async () => {
59
- try {
60
- logSearching('Initializing database...');
61
-
62
- // Create SQLite adapter
63
- dbAdapter = createAdapter();
64
-
65
- // Connect to database
66
- dbConnection = await dbAdapter.connect();
67
-
68
- // Run migrations (includes 012-create-checkpoints-table.sql)
69
- await dbAdapter.runMigrations(MIGRATIONS_DIR);
70
-
71
- isInitialized = true;
72
-
73
- info(`[db-manager] Database initialized (${dbAdapter.constructor.name})`);
74
- logComplete('Database ready');
75
-
76
- return dbConnection;
77
- } catch (error) {
78
- // Clear state on failure so retry is possible
79
- initializingPromise = null;
80
- dbAdapter = null;
81
- dbConnection = null;
82
- isInitialized = false;
83
- throw new Error(`Failed to initialize database: ${error.message}`);
84
- }
85
- })();
86
-
87
- return initializingPromise;
88
- }
89
-
90
- /**
91
- * Get database connection (singleton pattern)
92
- *
93
- * Returns better-sqlite3 Database instance
94
- *
95
- * Note: Synchronous for backward compatibility with memory-store.js
96
- * Will throw if database not initialized
97
- *
98
- * @returns {Object} SQLite database connection
99
- */
100
- function getDB() {
101
- if (!dbConnection) {
102
- throw new Error('Database not initialized. Call await initDB() first.');
103
- }
104
- return dbConnection;
105
- }
106
-
107
- /**
108
- * Get database adapter instance
109
- *
110
- * Used for advanced operations (vectorSearch, insertEmbedding, etc.)
111
- *
112
- * @returns {DatabaseAdapter} Adapter instance
113
- */
114
- function getAdapter() {
115
- if (!dbAdapter) {
116
- throw new Error('Database adapter not initialized. Call await initDB() first.');
117
- }
118
- return dbAdapter;
119
- }
120
-
121
- /**
122
- * Close database connection
123
- *
124
- * Call this on process exit
125
- */
126
- async function closeDB() {
127
- if (dbAdapter) {
128
- await dbAdapter.disconnect();
129
- dbAdapter = null;
130
- dbConnection = null;
131
- isInitialized = false;
132
- initializingPromise = null; // Clear to allow re-initialization
133
- info('[db-manager] Database connection closed');
134
- }
135
- }
136
-
137
- /**
138
- * Insert embedding into vector search table
139
- *
140
- * Uses sqlite-vec for vector similarity search
141
- * Gracefully degrades if sqlite-vec is not available
142
- *
143
- * @param {number} decisionRowid - SQLite rowid
144
- * @param {Float32Array|Array<number>} embedding - 384-dim embedding vector
145
- * @returns {Promise<void>}
146
- */
147
- async function insertEmbedding(decisionRowid, embedding) {
148
- const adapter = getAdapter();
149
-
150
- try {
151
- await adapter.insertEmbedding(decisionRowid, embedding);
152
- } catch (error) {
153
- // Graceful degradation: Log warning but don't fail
154
- logError(
155
- `[db-manager] Failed to insert embedding (vector search unavailable): ${error.message}`
156
- );
157
- }
158
- }
159
-
160
- /**
161
- * Perform vector similarity search
162
- *
163
- * Returns empty array if vector search not available (no keyword fallback)
164
- *
165
- * @param {Float32Array|Array<number>} queryEmbedding - Query embedding (384-dim)
166
- * @param {number} limit - Max results to return (default: 5)
167
- * @param {number} threshold - Minimum similarity threshold (default: 0.7)
168
- * @returns {Promise<Array<Object>>} Array of decisions with similarity scores, or empty array
169
- */
170
- async function vectorSearch(queryEmbedding, limit = 5, threshold = 0.7) {
171
- const adapter = getAdapter();
172
-
173
- try {
174
- // SQLite adapter returns null if sqlite-vec not available
175
- const results = await adapter.vectorSearch(queryEmbedding, limit * 3);
176
-
177
- if (!results || results.length === 0) {
178
- return []; // No keyword fallback - fast fail
179
- }
180
-
181
- const stmt = adapter.prepare(`SELECT * FROM decisions WHERE rowid = ?`);
182
- const decisions = [];
183
-
184
- for (const row of results) {
185
- const decision = stmt.get(row.rowid);
186
-
187
- if (!decision) {
188
- continue;
189
- }
190
-
191
- const similarity = row.similarity ?? Math.max(0, 1.0 - (row.distance ?? 1));
192
- const distance = row.distance ?? Math.max(0, 1.0 - similarity);
193
-
194
- if (similarity >= threshold) {
195
- decisions.push({
196
- ...decision,
197
- distance,
198
- similarity,
199
- });
200
- }
201
-
202
- if (decisions.length >= limit) {
203
- break;
204
- }
205
- }
206
-
207
- return decisions;
208
- } catch (error) {
209
- logError(`[db-manager] Vector search failed: ${error.message}`);
210
- return []; // No keyword fallback - fast fail
211
- }
212
- }
213
-
214
- /**
215
- * Insert decision with embedding
216
- *
217
- * Combined operation: Insert decision + Generate embedding + Insert embedding
218
- * SQLite-only implementation
219
- *
220
- * @param {Object} decision - Decision object
221
- * @returns {Promise<string>} Decision ID
222
- */
223
- async function insertDecisionWithEmbedding(decision) {
224
- const adapter = getAdapter();
225
- const { generateEnhancedEmbedding } = require('./embeddings');
226
-
227
- try {
228
- // Generate embedding BEFORE transaction (required for SQLite's sync transaction)
229
- // Note: Redact topic for privacy - only log length
230
- info(
231
- `[db-manager] Generating embedding for decision (topic length: ${decision.topic?.length || 0})`
232
- );
233
- let embedding = null;
234
- try {
235
- embedding = await generateEnhancedEmbedding(decision);
236
- info(`[db-manager] Embedding generated: ${embedding ? embedding.length : 'null'} dimensions`);
237
- } catch (embGenErr) {
238
- // Non-fatal: save decision without embedding (e.g. ONNX model unavailable on CI)
239
- logError(
240
- `[db-manager] ⚠️ Embedding generation failed, saving without vector: ${embGenErr.message}`
241
- );
242
- }
243
-
244
- // SQLite: Synchronous transaction including embedding
245
- // eslint-disable-next-line no-unused-vars
246
- const decisionRowid = adapter.transaction(() => {
247
- // Prepare INSERT statement
248
- const stmt = adapter.prepare(`
249
- INSERT INTO decisions (
250
- id, topic, decision, reasoning,
251
- outcome, failure_reason, limitation,
252
- user_involvement, session_id,
253
- supersedes, superseded_by, refined_from,
254
- confidence, created_at, updated_at,
255
- needs_validation, validation_attempts, last_validated_at, usage_count,
256
- trust_context, usage_success, usage_failure, time_saved,
257
- evidence, alternatives, risks
258
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
259
- `);
260
-
261
- const insertResult = stmt.run(
262
- decision.id,
263
- decision.topic,
264
- decision.decision,
265
- decision.reasoning || null,
266
- decision.outcome || null,
267
- decision.failure_reason || null,
268
- decision.limitation || null,
269
- decision.user_involvement || null,
270
- decision.session_id || null,
271
- decision.supersedes || null,
272
- decision.superseded_by || null,
273
- decision.refined_from ? JSON.stringify(decision.refined_from) : null,
274
- decision.confidence !== undefined ? decision.confidence : 0.5,
275
- // IMPORTANT: All timestamps are stored in milliseconds (Date.now()).
276
- // The schema DEFAULT uses unixepoch() (seconds) but is never used
277
- // since all inserts go through this function which always provides ms.
278
- decision.created_at || Date.now(),
279
- decision.updated_at || Date.now(),
280
- decision.needs_validation !== undefined ? decision.needs_validation : 0,
281
- decision.validation_attempts || 0,
282
- decision.last_validated_at || null,
283
- decision.usage_count || 0,
284
- decision.trust_context || null,
285
- decision.usage_success || 0,
286
- decision.usage_failure || 0,
287
- decision.time_saved || 0,
288
- decision.evidence || null,
289
- decision.alternatives || null,
290
- decision.risks || null
291
- );
292
-
293
- const rowid = Number(insertResult.lastInsertRowid);
294
-
295
- // Insert embedding in same transaction to ensure rowid matching
296
- info(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
297
- if (adapter.vectorSearchEnabled && embedding) {
298
- try {
299
- info(`[db-manager] Inserting embedding for rowid: ${rowid}`);
300
- adapter.insertEmbedding(rowid, embedding);
301
- info(`[db-manager] ✅ Embedding inserted successfully`);
302
- } catch (embErr) {
303
- // Log but don't fail transaction if embedding fails
304
- logError(`[db-manager] ❌ Embedding insert failed: ${embErr.message}`);
305
- }
306
- } else {
307
- info(`[db-manager] ⚠️ Vector search disabled, skipping embedding`);
308
- }
309
-
310
- return rowid;
311
- });
312
-
313
- if (process.env.MAMA_DEBUG) {
314
- info(`[db-manager] Decision stored: ${decision.id}`);
315
- }
316
-
317
- return decision.id;
318
- } catch (error) {
319
- throw new Error(`Failed to insert decision with embedding: ${error.message}`);
320
- }
321
- }
322
-
323
- /**
324
- * Query decision graph for topic
325
- *
326
- * Recursive CTE to traverse supersedes chain
327
- * SQLite implementation using WITH RECURSIVE
328
- *
329
- * @param {string} topic - Decision topic to query
330
- * @returns {Promise<Array<Object>>} Array of decisions (ordered by recency)
331
- */
332
- async function queryDecisionGraph(topic) {
333
- const adapter = getAdapter();
334
-
335
- try {
336
- // Story 014.14 Fix: Prioritize exact topic match over fuzzy matching
337
- // First try exact match, then fallback to fuzzy if no results
338
-
339
- // Try exact match first
340
- let stmt = adapter.prepare(`
341
- WITH RECURSIVE decision_chain AS (
342
- -- Base case: Get current decision (not superseded)
343
- SELECT * FROM decisions
344
- WHERE topic = ? AND superseded_by IS NULL
345
-
346
- UNION ALL
347
-
348
- -- Recursive case: Get previous decisions
349
- SELECT d.* FROM decisions d
350
- JOIN decision_chain dc ON d.id = dc.supersedes
351
- )
352
- SELECT * FROM decision_chain
353
- ORDER BY created_at DESC
354
- `);
355
-
356
- let decisions = await stmt.all(topic);
357
-
358
- // If no exact match, try fuzzy matching as fallback
359
- if (decisions.length === 0) {
360
- const topicKeyword = topic.split('_')[0];
361
-
362
- stmt = adapter.prepare(`
363
- WITH RECURSIVE decision_chain AS (
364
- -- Base case: Get current decision (not superseded)
365
- SELECT * FROM decisions
366
- WHERE topic LIKE ? || '%' AND superseded_by IS NULL
367
-
368
- UNION ALL
369
-
370
- -- Recursive case: Get previous decisions
371
- SELECT d.* FROM decisions d
372
- JOIN decision_chain dc ON d.id = dc.supersedes
373
- )
374
- SELECT * FROM decision_chain
375
- ORDER BY created_at DESC
376
- `);
377
-
378
- decisions = await stmt.all(topicKeyword);
379
- }
380
-
381
- // Join with decision_edges to include relationships
382
- // Prepare statement once outside loop for performance
383
- const edgesStmt = adapter.prepare(`
384
- SELECT * FROM decision_edges
385
- WHERE from_id = ?
386
- AND (approved_by_user = 1 OR approved_by_user IS NULL)
387
- `);
388
- for (const decision of decisions) {
389
- decision.edges = await edgesStmt.all(decision.id);
390
-
391
- // Parse refined_from JSON if exists
392
- if (decision.refined_from) {
393
- try {
394
- decision.refined_from = JSON.parse(decision.refined_from);
395
- } catch (e) {
396
- decision.refined_from = [];
397
- }
398
- }
399
- }
400
-
401
- return decisions;
402
- } catch (error) {
403
- throw new Error(`Decision graph query failed: ${error.message}`);
404
- }
405
- }
406
-
407
- /**
408
- * Query semantic edges for a list of decisions
409
- *
410
- * Returns both outgoing (from_id) and incoming (to_id) edges
411
- * for refines and contradicts relationships
412
- *
413
- * @param {Array<string>} decisionIds - Decision IDs to query edges for
414
- * @returns {Promise<Object>} Categorized edges { refines, refined_by, contradicts, contradicted_by, builds_on, built_on_by, debates, debated_by, synthesizes, synthesized_by }
415
- */
416
- async function querySemanticEdges(decisionIds) {
417
- const adapter = getAdapter();
418
-
419
- if (!decisionIds || decisionIds.length === 0) {
420
- return {
421
- refines: [],
422
- refined_by: [],
423
- contradicts: [],
424
- contradicted_by: [],
425
- // Story 2.1: Extended edge types
426
- builds_on: [],
427
- built_on_by: [],
428
- debates: [],
429
- debated_by: [],
430
- synthesizes: [],
431
- synthesized_by: [],
432
- };
433
- }
434
-
435
- try {
436
- // Build placeholders for IN clause
437
- const placeholders = decisionIds.map(() => '?').join(',');
438
-
439
- // Story 2.1: Include new edge types in query
440
- const edgeTypes = ['refines', 'contradicts', 'builds_on', 'debates', 'synthesizes'];
441
- const edgeTypePlaceholders = edgeTypes.map(() => '?').join(',');
442
-
443
- // Query outgoing edges (from_id = decision)
444
- const outgoingStmt = adapter.prepare(`
445
- SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
446
- FROM decision_edges e
447
- JOIN decisions d ON e.to_id = d.id
448
- WHERE e.from_id IN (${placeholders})
449
- AND e.relationship IN (${edgeTypePlaceholders})
450
- AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
451
- ORDER BY e.created_at DESC
452
- `);
453
- const outgoingEdges = await outgoingStmt.all(...decisionIds, ...edgeTypes);
454
-
455
- // Query incoming edges (to_id = decision)
456
- const incomingStmt = adapter.prepare(`
457
- SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
458
- FROM decision_edges e
459
- JOIN decisions d ON e.from_id = d.id
460
- WHERE e.to_id IN (${placeholders})
461
- AND e.relationship IN (${edgeTypePlaceholders})
462
- AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
463
- ORDER BY e.created_at DESC
464
- `);
465
- const incomingEdges = await incomingStmt.all(...decisionIds, ...edgeTypes);
466
-
467
- // Categorize edges (original + v1.3 extended)
468
- const refines = outgoingEdges.filter((e) => e.relationship === 'refines');
469
- const refined_by = incomingEdges.filter((e) => e.relationship === 'refines');
470
- const contradicts = outgoingEdges.filter((e) => e.relationship === 'contradicts');
471
- const contradicted_by = incomingEdges.filter((e) => e.relationship === 'contradicts');
472
- // Story 2.1: New edge type categories
473
- const builds_on = outgoingEdges.filter((e) => e.relationship === 'builds_on');
474
- const built_on_by = incomingEdges.filter((e) => e.relationship === 'builds_on');
475
- const debates = outgoingEdges.filter((e) => e.relationship === 'debates');
476
- const debated_by = incomingEdges.filter((e) => e.relationship === 'debates');
477
- const synthesizes = outgoingEdges.filter((e) => e.relationship === 'synthesizes');
478
- const synthesized_by = incomingEdges.filter((e) => e.relationship === 'synthesizes');
479
-
480
- return {
481
- refines,
482
- refined_by,
483
- contradicts,
484
- contradicted_by,
485
- builds_on,
486
- built_on_by,
487
- debates,
488
- debated_by,
489
- synthesizes,
490
- synthesized_by,
491
- };
492
- } catch (error) {
493
- throw new Error(`Semantic edges query failed: ${error.message}`);
494
- }
495
- }
496
-
497
- /**
498
- * Query vector search with time window and threshold
499
- *
500
- * Story 014.14: AC #1 - Vector Search for Related Decisions
501
- *
502
- * @param {Object} params - Search parameters
503
- * @param {string} params.query - Search query text
504
- * @param {number} params.limit - Max results (default: 10)
505
- * @param {number} params.threshold - Minimum cosine similarity (0.0-1.0, default: 0.75)
506
- * @param {number} params.timeWindow - Time window in ms (optional, default: 90 days)
507
- * @returns {Promise<Array>} Results with similarity scores and decision data
508
- */
509
- async function queryVectorSearch({
510
- query,
511
- limit = 10,
512
- threshold = 0.75,
513
- timeWindow = 90 * 24 * 60 * 60 * 1000,
514
- }) {
515
- const adapter = getAdapter();
516
- const { generateEmbedding } = require('./embeddings');
517
-
518
- try {
519
- // Generate embedding for query
520
- const embedding = await generateEmbedding(query);
521
-
522
- const cutoffTime = Date.now() - timeWindow;
523
- const candidates = await adapter.vectorSearch(embedding, limit * 5);
524
-
525
- if (!candidates || candidates.length === 0) {
526
- return [];
527
- }
528
-
529
- const stmt = adapter.prepare(`SELECT * FROM decisions WHERE rowid = ?`);
530
- const results = [];
531
-
532
- for (const candidate of candidates) {
533
- const decision = stmt.get(candidate.rowid);
534
- if (!decision) {
535
- continue;
536
- }
537
-
538
- if (decision.created_at < cutoffTime) {
539
- continue;
540
- }
541
-
542
- const similarity = candidate.similarity ?? Math.max(0, 1 - (candidate.distance ?? 1));
543
- const distance = candidate.distance ?? Math.max(0, 1 - similarity);
544
-
545
- if (similarity < threshold) {
546
- continue;
547
- }
548
-
549
- results.push({
550
- ...decision,
551
- similarity,
552
- distance,
553
- });
554
-
555
- if (results.length >= limit) {
556
- break;
557
- }
558
- }
559
-
560
- return results;
561
- } catch (error) {
562
- logError(`[db-manager] queryVectorSearch failed: ${error.message}`);
563
- return []; // Return empty array on error (graceful degradation)
564
- }
565
- }
566
-
567
- /**
568
- * Update decision outcome
569
- *
570
- * @param {string} decisionId - Decision ID
571
- * @param {Object} outcomeData - Outcome data
572
- * @returns {Promise<void>}
573
- */
574
- async function updateDecisionOutcome(decisionId, outcomeData) {
575
- const adapter = getAdapter();
576
-
577
- try {
578
- const stmt = adapter.prepare(`
579
- UPDATE decisions
580
- SET
581
- outcome = ?,
582
- failure_reason = ?,
583
- limitation = ?,
584
- duration_days = ?,
585
- confidence = COALESCE(?, confidence),
586
- updated_at = ?
587
- WHERE id = ?
588
- `);
589
-
590
- await stmt.run(
591
- outcomeData.outcome || null,
592
- outcomeData.failure_reason || null,
593
- outcomeData.limitation || null,
594
- outcomeData.duration_days || null,
595
- outcomeData.confidence !== undefined ? outcomeData.confidence : null,
596
- Date.now(),
597
- decisionId
598
- );
599
-
600
- info(`[db-manager] Decision outcome updated: ${decisionId} → ${outcomeData.outcome}`);
601
- } catch (error) {
602
- throw new Error(`Failed to update decision outcome: ${error.message}`);
603
- }
604
- }
605
-
606
- /**
607
- * Get prepared statement
608
- *
609
- * For backward compatibility with memory-store.js
610
- * Returns a compatibility shim that proxies to adapter.prepare()
611
- *
612
- * @param {string} sql - SQL statement
613
- * @returns {Object} Statement-like object with run/get/all methods
614
- */
615
- function getPreparedStmt(sql) {
616
- if (!dbAdapter) {
617
- warn('[db-manager] getPreparedStmt() called before initialization');
618
- // Return no-op object for feature detection (won't throw)
619
- return {
620
- run: () => ({ changes: 0, lastInsertRowid: 0 }),
621
- get: () => null,
622
- all: () => [],
623
- };
624
- }
625
-
626
- // Proxy to adapter.prepare() for actual usage
627
- try {
628
- return dbAdapter.prepare(sql);
629
- } catch (error) {
630
- warn(`[db-manager] getPreparedStmt() failed: ${error.message}`);
631
- // Return no-op object on error (graceful degradation)
632
- return {
633
- run: () => ({ changes: 0, lastInsertRowid: 0 }),
634
- get: () => null,
635
- all: () => [],
636
- };
637
- }
638
- }
639
-
640
- /**
641
- * Get database file path
642
- *
643
- * @returns {string} Actual database path or 'Not initialized'
644
- */
645
- function getDbPath() {
646
- if (!dbAdapter) {
647
- return 'Not initialized';
648
- }
649
- // Use adapter's getDbPath method if available, fallback to description
650
- if (typeof dbAdapter.getDbPath === 'function') {
651
- return dbAdapter.getDbPath();
652
- }
653
- // Fallback: try to get path from adapter properties
654
- if (dbAdapter.dbPath) {
655
- return dbAdapter.dbPath;
656
- }
657
- return `${dbAdapter.constructor.name} (path unavailable)`;
658
- }
659
-
660
- // Export API (same interface as memory-store.js, but async where needed)
661
- module.exports = {
662
- initDB, // Async
663
- getDB, // Sync (throws if not initialized)
664
- getAdapter, // Sync (throws if not initialized)
665
- closeDB, // Async
666
- insertEmbedding, // Async
667
- vectorSearch, // Async
668
- queryVectorSearch, // Async - Story 014.14
669
- querySemanticEdges, // Async - Graph traversal enhancement
670
- insertDecisionWithEmbedding, // Async
671
- queryDecisionGraph, // Async
672
- updateDecisionOutcome, // Async
673
- getPreparedStmt, // Compatibility shim
674
- getDbPath, // Returns actual path
675
- };
676
-
677
- // Note: Removed auto-registered SIGINT/SIGTERM handlers that called process.exit(0)
678
- // This was causing issues with host cleanup in parent processes.
679
- // If graceful shutdown is needed, the host application should handle closeDB().