agent-working-memory 0.5.5 → 0.6.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 (82) hide show
  1. package/README.md +428 -399
  2. package/dist/api/routes.d.ts.map +1 -1
  3. package/dist/api/routes.js +60 -5
  4. package/dist/api/routes.js.map +1 -1
  5. package/dist/cli.js +468 -68
  6. package/dist/cli.js.map +1 -1
  7. package/dist/coordination/index.d.ts +11 -0
  8. package/dist/coordination/index.d.ts.map +1 -0
  9. package/dist/coordination/index.js +39 -0
  10. package/dist/coordination/index.js.map +1 -0
  11. package/dist/coordination/mcp-tools.d.ts +8 -0
  12. package/dist/coordination/mcp-tools.d.ts.map +1 -0
  13. package/dist/coordination/mcp-tools.js +221 -0
  14. package/dist/coordination/mcp-tools.js.map +1 -0
  15. package/dist/coordination/routes.d.ts +9 -0
  16. package/dist/coordination/routes.d.ts.map +1 -0
  17. package/dist/coordination/routes.js +573 -0
  18. package/dist/coordination/routes.js.map +1 -0
  19. package/dist/coordination/schema.d.ts +12 -0
  20. package/dist/coordination/schema.d.ts.map +1 -0
  21. package/dist/coordination/schema.js +125 -0
  22. package/dist/coordination/schema.js.map +1 -0
  23. package/dist/coordination/schemas.d.ts +227 -0
  24. package/dist/coordination/schemas.d.ts.map +1 -0
  25. package/dist/coordination/schemas.js +125 -0
  26. package/dist/coordination/schemas.js.map +1 -0
  27. package/dist/coordination/stale.d.ts +27 -0
  28. package/dist/coordination/stale.d.ts.map +1 -0
  29. package/dist/coordination/stale.js +58 -0
  30. package/dist/coordination/stale.js.map +1 -0
  31. package/dist/engine/activation.d.ts.map +1 -1
  32. package/dist/engine/activation.js +119 -23
  33. package/dist/engine/activation.js.map +1 -1
  34. package/dist/engine/consolidation.d.ts.map +1 -1
  35. package/dist/engine/consolidation.js +27 -6
  36. package/dist/engine/consolidation.js.map +1 -1
  37. package/dist/index.js +100 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp.js +149 -80
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/storage/sqlite.d.ts +21 -0
  42. package/dist/storage/sqlite.d.ts.map +1 -1
  43. package/dist/storage/sqlite.js +331 -282
  44. package/dist/storage/sqlite.js.map +1 -1
  45. package/dist/types/engram.d.ts +24 -0
  46. package/dist/types/engram.d.ts.map +1 -1
  47. package/dist/types/engram.js.map +1 -1
  48. package/package.json +57 -55
  49. package/src/api/index.ts +3 -3
  50. package/src/api/routes.ts +600 -536
  51. package/src/cli.ts +850 -397
  52. package/src/coordination/index.ts +47 -0
  53. package/src/coordination/mcp-tools.ts +318 -0
  54. package/src/coordination/routes.ts +846 -0
  55. package/src/coordination/schema.ts +120 -0
  56. package/src/coordination/schemas.ts +155 -0
  57. package/src/coordination/stale.ts +97 -0
  58. package/src/core/decay.ts +63 -63
  59. package/src/core/embeddings.ts +88 -88
  60. package/src/core/hebbian.ts +93 -93
  61. package/src/core/index.ts +5 -5
  62. package/src/core/logger.ts +36 -36
  63. package/src/core/query-expander.ts +66 -66
  64. package/src/core/reranker.ts +101 -101
  65. package/src/engine/activation.ts +758 -656
  66. package/src/engine/connections.ts +103 -103
  67. package/src/engine/consolidation-scheduler.ts +125 -125
  68. package/src/engine/consolidation.ts +29 -6
  69. package/src/engine/eval.ts +102 -102
  70. package/src/engine/eviction.ts +101 -101
  71. package/src/engine/index.ts +8 -8
  72. package/src/engine/retraction.ts +100 -100
  73. package/src/engine/staging.ts +74 -74
  74. package/src/index.ts +208 -121
  75. package/src/mcp.ts +1093 -1013
  76. package/src/storage/index.ts +3 -3
  77. package/src/storage/sqlite.ts +1017 -963
  78. package/src/types/agent.ts +67 -67
  79. package/src/types/checkpoint.ts +46 -46
  80. package/src/types/engram.ts +245 -217
  81. package/src/types/eval.ts +100 -100
  82. package/src/types/index.ts +6 -6
@@ -1,74 +1,74 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Staging Buffer — weak signal handler.
5
- *
6
- * Observations that don't meet the salience threshold for active memory
7
- * go to staging. The staging buffer periodically:
8
- * 1. Checks staged engrams against active memory for resonance
9
- * 2. Promotes resonant engrams to active
10
- * 3. Discards expired engrams that never resonated
11
- *
12
- * Modeled on hippocampal consolidation — provisional encoding
13
- * that only persists if reactivated.
14
- */
15
-
16
- import type { EngramStore } from '../storage/sqlite.js';
17
- import type { ActivationEngine } from './activation.js';
18
-
19
- export class StagingBuffer {
20
- private store: EngramStore;
21
- private engine: ActivationEngine;
22
- private checkInterval: ReturnType<typeof setInterval> | null = null;
23
-
24
- constructor(store: EngramStore, engine: ActivationEngine) {
25
- this.store = store;
26
- this.engine = engine;
27
- }
28
-
29
- /**
30
- * Start the periodic staging check.
31
- */
32
- start(intervalMs: number = 60_000): void {
33
- this.checkInterval = setInterval(() => this.sweep(), intervalMs);
34
- }
35
-
36
- stop(): void {
37
- if (this.checkInterval) {
38
- clearInterval(this.checkInterval);
39
- this.checkInterval = null;
40
- }
41
- }
42
-
43
- /**
44
- * Sweep staged engrams: promote or discard.
45
- */
46
- async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
47
- const promoted: string[] = [];
48
- const discarded: string[] = [];
49
-
50
- const expired = this.store.getExpiredStaging();
51
- for (const engram of expired) {
52
- // Check if this engram resonates with active memory
53
- const results = await this.engine.activate({
54
- agentId: engram.agentId,
55
- context: `${engram.concept} ${engram.content}`,
56
- limit: 3,
57
- minScore: 0.3,
58
- internal: true,
59
- });
60
-
61
- if (results.length > 0) {
62
- // Resonance found — promote to active
63
- this.store.updateStage(engram.id, 'active');
64
- promoted.push(engram.id);
65
- } else {
66
- // No resonance — discard
67
- this.store.deleteEngram(engram.id);
68
- discarded.push(engram.id);
69
- }
70
- }
71
-
72
- return { promoted, discarded };
73
- }
74
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Staging Buffer — weak signal handler.
5
+ *
6
+ * Observations that don't meet the salience threshold for active memory
7
+ * go to staging. The staging buffer periodically:
8
+ * 1. Checks staged engrams against active memory for resonance
9
+ * 2. Promotes resonant engrams to active
10
+ * 3. Discards expired engrams that never resonated
11
+ *
12
+ * Modeled on hippocampal consolidation — provisional encoding
13
+ * that only persists if reactivated.
14
+ */
15
+
16
+ import type { EngramStore } from '../storage/sqlite.js';
17
+ import type { ActivationEngine } from './activation.js';
18
+
19
+ export class StagingBuffer {
20
+ private store: EngramStore;
21
+ private engine: ActivationEngine;
22
+ private checkInterval: ReturnType<typeof setInterval> | null = null;
23
+
24
+ constructor(store: EngramStore, engine: ActivationEngine) {
25
+ this.store = store;
26
+ this.engine = engine;
27
+ }
28
+
29
+ /**
30
+ * Start the periodic staging check.
31
+ */
32
+ start(intervalMs: number = 60_000): void {
33
+ this.checkInterval = setInterval(() => this.sweep(), intervalMs);
34
+ }
35
+
36
+ stop(): void {
37
+ if (this.checkInterval) {
38
+ clearInterval(this.checkInterval);
39
+ this.checkInterval = null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Sweep staged engrams: promote or discard.
45
+ */
46
+ async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
47
+ const promoted: string[] = [];
48
+ const discarded: string[] = [];
49
+
50
+ const expired = this.store.getExpiredStaging();
51
+ for (const engram of expired) {
52
+ // Check if this engram resonates with active memory
53
+ const results = await this.engine.activate({
54
+ agentId: engram.agentId,
55
+ context: `${engram.concept} ${engram.content}`,
56
+ limit: 3,
57
+ minScore: 0.3,
58
+ internal: true,
59
+ });
60
+
61
+ if (results.length > 0) {
62
+ // Resonance found — promote to active
63
+ this.store.updateStage(engram.id, 'active');
64
+ promoted.push(engram.id);
65
+ } else {
66
+ // No resonance — discard
67
+ this.store.deleteEngram(engram.id);
68
+ discarded.push(engram.id);
69
+ }
70
+ }
71
+
72
+ return { promoted, discarded };
73
+ }
74
+ }
package/src/index.ts CHANGED
@@ -1,121 +1,208 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- import { readFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
4
- import { resolve, dirname, basename } from 'node:path';
5
- import Fastify from 'fastify';
6
-
7
- // Load .env file if present (no external dependency)
8
- try {
9
- const envPath = resolve(process.cwd(), '.env');
10
- const envContent = readFileSync(envPath, 'utf-8');
11
- for (const line of envContent.split('\n')) {
12
- const trimmed = line.trim();
13
- if (!trimmed || trimmed.startsWith('#')) continue;
14
- const eqIdx = trimmed.indexOf('=');
15
- if (eqIdx === -1) continue;
16
- const key = trimmed.slice(0, eqIdx).trim();
17
- const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
18
- if (!process.env[key]) process.env[key] = val; // Don't override existing env
19
- }
20
- } catch { /* No .env file — that's fine */ }
21
- import { EngramStore } from './storage/sqlite.js';
22
- import { ActivationEngine } from './engine/activation.js';
23
- import { ConnectionEngine } from './engine/connections.js';
24
- import { StagingBuffer } from './engine/staging.js';
25
- import { EvictionEngine } from './engine/eviction.js';
26
- import { RetractionEngine } from './engine/retraction.js';
27
- import { EvalEngine } from './engine/eval.js';
28
- import { ConsolidationEngine } from './engine/consolidation.js';
29
- import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
30
- import { registerRoutes } from './api/routes.js';
31
- import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
32
- import { getEmbedder } from './core/embeddings.js';
33
- import { getReranker } from './core/reranker.js';
34
- import { getExpander } from './core/query-expander.js';
35
- import { initLogger } from './core/logger.js';
36
-
37
- const PORT = parseInt(process.env.AWM_PORT ?? '8400', 10);
38
- const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
39
- const API_KEY = process.env.AWM_API_KEY ?? null;
40
-
41
- async function main() {
42
- // Auto-backup: copy DB to backups/ on startup (cheap insurance)
43
- if (existsSync(DB_PATH)) {
44
- const dbDir = dirname(resolve(DB_PATH));
45
- const backupDir = resolve(dbDir, 'backups');
46
- mkdirSync(backupDir, { recursive: true });
47
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
48
- const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
49
- try {
50
- copyFileSync(resolve(DB_PATH), backupPath);
51
- console.log(`Backup: ${backupPath}`);
52
- } catch (err) {
53
- console.log(`Backup skipped: ${(err as Error).message}`);
54
- }
55
- }
56
-
57
- // Logger — write activity to awm.log alongside the DB
58
- initLogger(DB_PATH);
59
-
60
- // Storage
61
- const store = new EngramStore(DB_PATH);
62
-
63
- // Engines
64
- const activationEngine = new ActivationEngine(store);
65
- const connectionEngine = new ConnectionEngine(store, activationEngine);
66
- const stagingBuffer = new StagingBuffer(store, activationEngine);
67
- const evictionEngine = new EvictionEngine(store);
68
- const retractionEngine = new RetractionEngine(store);
69
- const evalEngine = new EvalEngine(store);
70
- const consolidationEngine = new ConsolidationEngine(store);
71
- const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
72
-
73
- // API
74
- const app = Fastify({ logger: true });
75
-
76
- // Bearer token auth — only enforced when AWM_API_KEY is explicitly set and non-empty
77
- if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
78
- app.addHook('onRequest', async (req, reply) => {
79
- if (req.url === '/health') return; // Health check is always public
80
- const bearer = req.headers.authorization;
81
- const xApiKey = req.headers['x-api-key'] as string | undefined;
82
- if (bearer === `Bearer ${API_KEY}` || xApiKey === API_KEY) return;
83
- reply.code(401).send({ error: 'Unauthorized' });
84
- });
85
- console.log('API key auth enabled (AWM_API_KEY set)');
86
- }
87
-
88
- registerRoutes(app, {
89
- store, activationEngine, connectionEngine,
90
- evictionEngine, retractionEngine, evalEngine,
91
- consolidationEngine, consolidationScheduler,
92
- });
93
-
94
- // Background tasks
95
- stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
96
- consolidationScheduler.start();
97
-
98
- // Pre-load ML models (downloads on first run: embeddings ~22MB, reranker ~22MB, expander ~80MB)
99
- getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
100
- getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
101
- getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
102
-
103
- // Start server
104
- await app.listen({ port: PORT, host: '0.0.0.0' });
105
- console.log(`AgentWorkingMemory v0.5.4 listening on port ${PORT}`);
106
-
107
- // Graceful shutdown
108
- const shutdown = () => {
109
- consolidationScheduler.stop();
110
- stagingBuffer.stop();
111
- store.close();
112
- process.exit(0);
113
- };
114
- process.on('SIGINT', shutdown);
115
- process.on('SIGTERM', shutdown);
116
- }
117
-
118
- main().catch(err => {
119
- console.error('Failed to start:', err);
120
- process.exit(1);
121
- });
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { readFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
4
+ import { resolve, dirname, basename } from 'node:path';
5
+ import Fastify from 'fastify';
6
+
7
+ // Load .env file if present (no external dependency)
8
+ try {
9
+ const envPath = resolve(process.cwd(), '.env');
10
+ const envContent = readFileSync(envPath, 'utf-8');
11
+ for (const line of envContent.split('\n')) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed || trimmed.startsWith('#')) continue;
14
+ const eqIdx = trimmed.indexOf('=');
15
+ if (eqIdx === -1) continue;
16
+ const key = trimmed.slice(0, eqIdx).trim();
17
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
18
+ if (!process.env[key]) process.env[key] = val; // Don't override existing env
19
+ }
20
+ } catch { /* No .env file — that's fine */ }
21
+ import { EngramStore } from './storage/sqlite.js';
22
+ import { ActivationEngine } from './engine/activation.js';
23
+ import { ConnectionEngine } from './engine/connections.js';
24
+ import { StagingBuffer } from './engine/staging.js';
25
+ import { EvictionEngine } from './engine/eviction.js';
26
+ import { RetractionEngine } from './engine/retraction.js';
27
+ import { EvalEngine } from './engine/eval.js';
28
+ import { ConsolidationEngine } from './engine/consolidation.js';
29
+ import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
30
+ import { registerRoutes } from './api/routes.js';
31
+ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
32
+ import { getEmbedder } from './core/embeddings.js';
33
+ import { getReranker } from './core/reranker.js';
34
+ import { getExpander } from './core/query-expander.js';
35
+ import { initLogger } from './core/logger.js';
36
+
37
+ const PORT = parseInt(process.env.AWM_PORT ?? '8400', 10);
38
+ const DB_PATH = process.env.AWM_DB_PATH ?? 'memory.db';
39
+ const API_KEY = process.env.AWM_API_KEY ?? null;
40
+
41
+ async function main() {
42
+ // Auto-backup: copy DB to backups/ on startup (cheap insurance)
43
+ if (existsSync(DB_PATH)) {
44
+ const dbDir = dirname(resolve(DB_PATH));
45
+ const backupDir = resolve(dbDir, 'backups');
46
+ mkdirSync(backupDir, { recursive: true });
47
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
48
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
49
+ try {
50
+ copyFileSync(resolve(DB_PATH), backupPath);
51
+ console.log(`Backup: ${backupPath}`);
52
+ } catch (err) {
53
+ console.log(`Backup skipped: ${(err as Error).message}`);
54
+ }
55
+ }
56
+
57
+ // Logger — write activity to awm.log alongside the DB
58
+ initLogger(DB_PATH);
59
+
60
+ // Storage
61
+ const store = new EngramStore(DB_PATH);
62
+
63
+ // Integrity check
64
+ const integrity = store.integrityCheck();
65
+ if (!integrity.ok) {
66
+ console.error(`DB integrity check FAILED: ${integrity.result}`);
67
+ // Close corrupt DB, restore from backup, and exit for process manager to restart
68
+ store.close();
69
+ const dbDir = dirname(resolve(DB_PATH));
70
+ const backupDir = resolve(dbDir, 'backups');
71
+ if (existsSync(backupDir)) {
72
+ const backups = readdirSync(backupDir)
73
+ .filter(f => f.endsWith('.db'))
74
+ .sort()
75
+ .reverse();
76
+ if (backups.length > 0) {
77
+ const restorePath = resolve(backupDir, backups[0]);
78
+ console.error(`Attempting restore from: ${restorePath}`);
79
+ try {
80
+ copyFileSync(restorePath, resolve(DB_PATH));
81
+ console.error('Restore complete exiting for restart with restored DB');
82
+ process.exit(1);
83
+ } catch (restoreErr) {
84
+ console.error(`Restore failed: ${(restoreErr as Error).message}`);
85
+ }
86
+ }
87
+ }
88
+ console.error('No backup available — continuing with potentially corrupt DB');
89
+ } else {
90
+ console.log(' DB integrity check: ok');
91
+ }
92
+
93
+ // Engines
94
+ const activationEngine = new ActivationEngine(store);
95
+ const connectionEngine = new ConnectionEngine(store, activationEngine);
96
+ const stagingBuffer = new StagingBuffer(store, activationEngine);
97
+ const evictionEngine = new EvictionEngine(store);
98
+ const retractionEngine = new RetractionEngine(store);
99
+ const evalEngine = new EvalEngine(store);
100
+ const consolidationEngine = new ConsolidationEngine(store);
101
+ const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
102
+
103
+ // API — disable Fastify's default request logging (too noisy for hive polling)
104
+ const app = Fastify({ logger: false });
105
+
106
+ // Bearer token auth — only enforced when AWM_API_KEY is explicitly set and non-empty
107
+ if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
108
+ app.addHook('onRequest', async (req, reply) => {
109
+ if (req.url === '/health') return; // Health check is always public
110
+ const bearer = req.headers.authorization;
111
+ const xApiKey = req.headers['x-api-key'] as string | undefined;
112
+ if (bearer === `Bearer ${API_KEY}` || xApiKey === API_KEY) return;
113
+ reply.code(401).send({ error: 'Unauthorized' });
114
+ });
115
+ console.log('API key auth enabled (AWM_API_KEY set)');
116
+ }
117
+
118
+ registerRoutes(app, {
119
+ store, activationEngine, connectionEngine,
120
+ evictionEngine, retractionEngine, evalEngine,
121
+ consolidationEngine, consolidationScheduler,
122
+ });
123
+
124
+ // Coordination module (opt-in via AWM_COORDINATION=true)
125
+ let heartbeatPruneTimer: ReturnType<typeof setInterval> | null = null;
126
+ const { isCoordinationEnabled, initCoordination } = await import('./coordination/index.js');
127
+ if (isCoordinationEnabled()) {
128
+ initCoordination(app, store.getDb());
129
+ // Prune stale heartbeat events every 30s (keeps assignment/command events permanently)
130
+ // Purge dead agents older than 24h every 30s to prevent table bloat
131
+ const { pruneOldHeartbeats, purgeDeadAgents } = await import('./coordination/stale.js');
132
+ heartbeatPruneTimer = setInterval(() => {
133
+ const pruned = pruneOldHeartbeats(store.getDb());
134
+ if (pruned > 0) console.log(`[coordination] pruned ${pruned} old heartbeat event(s)`);
135
+ const purged = purgeDeadAgents(store.getDb());
136
+ if (purged > 0) console.log(`[coordination] purged ${purged} dead agent(s) older than 24h`);
137
+ }, 30_000);
138
+ } else {
139
+ console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
140
+ }
141
+
142
+ // Background tasks
143
+ stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
144
+ consolidationScheduler.start();
145
+
146
+ // Periodic hot backup every 10 minutes (keep last 6 = 1hr coverage)
147
+ const dbDir = dirname(resolve(DB_PATH));
148
+ const backupDir = resolve(dbDir, 'backups');
149
+ mkdirSync(backupDir, { recursive: true });
150
+
151
+ // Cleanup old backups on startup (older than 2 hours)
152
+ try {
153
+ const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
154
+ const now = Date.now();
155
+ for (const f of readdirSync(backupDir).filter(f => f.endsWith('.db'))) {
156
+ const match = f.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
157
+ if (match) {
158
+ const fileDate = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
159
+ if (now - fileDate.getTime() > TWO_HOURS_MS) {
160
+ unlinkSync(resolve(backupDir, f));
161
+ }
162
+ }
163
+ }
164
+ } catch { /* cleanup is non-fatal */ }
165
+
166
+ const backupTimer = setInterval(() => {
167
+ try {
168
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
169
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
170
+ store.backup(backupPath);
171
+ // Prune: keep only last 6 backups
172
+ const backups = readdirSync(backupDir).filter(f => f.endsWith('.db')).sort();
173
+ while (backups.length > 6) {
174
+ const old = backups.shift()!;
175
+ try { unlinkSync(resolve(backupDir, old)); } catch { /* non-fatal */ }
176
+ }
177
+ } catch (err) {
178
+ console.warn(`[backup] failed: ${(err as Error).message}`);
179
+ }
180
+ }, 10 * 60_000); // 10 minutes
181
+
182
+ // Pre-load ML models (downloads on first run: embeddings ~22MB, reranker ~22MB, expander ~80MB)
183
+ getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
184
+ getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
185
+ getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
186
+
187
+ // Start server
188
+ await app.listen({ port: PORT, host: '0.0.0.0' });
189
+ console.log(`AgentWorkingMemory v0.6.0 listening on port ${PORT}`);
190
+
191
+ // Graceful shutdown
192
+ const shutdown = () => {
193
+ clearInterval(backupTimer);
194
+ if (heartbeatPruneTimer) clearInterval(heartbeatPruneTimer);
195
+ consolidationScheduler.stop();
196
+ stagingBuffer.stop();
197
+ try { store.walCheckpoint(); } catch { /* non-fatal */ }
198
+ store.close();
199
+ process.exit(0);
200
+ };
201
+ process.on('SIGINT', shutdown);
202
+ process.on('SIGTERM', shutdown);
203
+ }
204
+
205
+ main().catch(err => {
206
+ console.error('Failed to start:', err);
207
+ process.exit(1);
208
+ });