agent-working-memory 0.7.17 → 0.8.5

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 (186) hide show
  1. package/README.md +156 -15
  2. package/dist/adapters/common.js +202 -202
  3. package/dist/adapters/http.js +22 -22
  4. package/dist/api/routes.d.ts +1 -1
  5. package/dist/api/routes.d.ts.map +1 -1
  6. package/dist/api/routes.js +258 -79
  7. package/dist/api/routes.js.map +1 -1
  8. package/dist/cli/migrate.d.ts +42 -0
  9. package/dist/cli/migrate.d.ts.map +1 -0
  10. package/dist/cli/migrate.js +268 -0
  11. package/dist/cli/migrate.js.map +1 -0
  12. package/dist/cli.js +147 -103
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coordination/circuit-breaker.d.ts +24 -0
  15. package/dist/coordination/circuit-breaker.d.ts.map +1 -0
  16. package/dist/coordination/circuit-breaker.js +70 -0
  17. package/dist/coordination/circuit-breaker.js.map +1 -0
  18. package/dist/coordination/failure-modes.d.ts +21 -0
  19. package/dist/coordination/failure-modes.d.ts.map +1 -0
  20. package/dist/coordination/failure-modes.js +49 -0
  21. package/dist/coordination/failure-modes.js.map +1 -0
  22. package/dist/coordination/peer-decisions.js +7 -7
  23. package/dist/coordination/routes.d.ts.map +1 -1
  24. package/dist/coordination/routes.js +44 -1
  25. package/dist/coordination/routes.js.map +1 -1
  26. package/dist/coordination/schema.d.ts.map +1 -1
  27. package/dist/coordination/schema.js +18 -0
  28. package/dist/coordination/schema.js.map +1 -1
  29. package/dist/coordination/schemas.d.ts +9 -9
  30. package/dist/coordination/stale.d.ts +9 -1
  31. package/dist/coordination/stale.d.ts.map +1 -1
  32. package/dist/coordination/stale.js +53 -9
  33. package/dist/coordination/stale.js.map +1 -1
  34. package/dist/core/embeddings.d.ts +16 -10
  35. package/dist/core/embeddings.d.ts.map +1 -1
  36. package/dist/core/embeddings.js +54 -38
  37. package/dist/core/embeddings.js.map +1 -1
  38. package/dist/core/ml-worker-entry.d.ts +17 -0
  39. package/dist/core/ml-worker-entry.d.ts.map +1 -0
  40. package/dist/core/ml-worker-entry.js +193 -0
  41. package/dist/core/ml-worker-entry.js.map +1 -0
  42. package/dist/core/ml-worker.d.ts +59 -0
  43. package/dist/core/ml-worker.d.ts.map +1 -0
  44. package/dist/core/ml-worker.js +253 -0
  45. package/dist/core/ml-worker.js.map +1 -0
  46. package/dist/core/query-expander.d.ts +13 -15
  47. package/dist/core/query-expander.d.ts.map +1 -1
  48. package/dist/core/query-expander.js +48 -54
  49. package/dist/core/query-expander.js.map +1 -1
  50. package/dist/core/reranker.d.ts +9 -19
  51. package/dist/core/reranker.d.ts.map +1 -1
  52. package/dist/core/reranker.js +33 -35
  53. package/dist/core/reranker.js.map +1 -1
  54. package/dist/core/salience.d.ts +32 -5
  55. package/dist/core/salience.d.ts.map +1 -1
  56. package/dist/core/salience.js +201 -28
  57. package/dist/core/salience.js.map +1 -1
  58. package/dist/core/write-pipeline.d.ts +10 -2
  59. package/dist/core/write-pipeline.d.ts.map +1 -1
  60. package/dist/core/write-pipeline.js +218 -49
  61. package/dist/core/write-pipeline.js.map +1 -1
  62. package/dist/engine/activation.d.ts +2 -2
  63. package/dist/engine/activation.d.ts.map +1 -1
  64. package/dist/engine/activation.js +238 -115
  65. package/dist/engine/activation.js.map +1 -1
  66. package/dist/engine/confidence.d.ts +62 -0
  67. package/dist/engine/confidence.d.ts.map +1 -0
  68. package/dist/engine/confidence.js +100 -0
  69. package/dist/engine/confidence.js.map +1 -0
  70. package/dist/engine/connections.d.ts +47 -8
  71. package/dist/engine/connections.d.ts.map +1 -1
  72. package/dist/engine/connections.js +83 -22
  73. package/dist/engine/connections.js.map +1 -1
  74. package/dist/engine/consolidation-scheduler.d.ts +46 -10
  75. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  76. package/dist/engine/consolidation-scheduler.js +170 -50
  77. package/dist/engine/consolidation-scheduler.js.map +1 -1
  78. package/dist/engine/consolidation.d.ts +16 -2
  79. package/dist/engine/consolidation.d.ts.map +1 -1
  80. package/dist/engine/consolidation.js +143 -44
  81. package/dist/engine/consolidation.js.map +1 -1
  82. package/dist/engine/eval.d.ts +2 -2
  83. package/dist/engine/eval.d.ts.map +1 -1
  84. package/dist/engine/eval.js +9 -9
  85. package/dist/engine/eval.js.map +1 -1
  86. package/dist/engine/eviction.d.ts +4 -4
  87. package/dist/engine/eviction.d.ts.map +1 -1
  88. package/dist/engine/eviction.js +15 -15
  89. package/dist/engine/eviction.js.map +1 -1
  90. package/dist/engine/retraction.d.ts +93 -5
  91. package/dist/engine/retraction.d.ts.map +1 -1
  92. package/dist/engine/retraction.js +238 -19
  93. package/dist/engine/retraction.js.map +1 -1
  94. package/dist/engine/staging.d.ts +1 -1
  95. package/dist/engine/staging.d.ts.map +1 -1
  96. package/dist/engine/staging.js +3 -3
  97. package/dist/engine/staging.js.map +1 -1
  98. package/dist/hooks/sidecar.d.ts +1 -1
  99. package/dist/hooks/sidecar.d.ts.map +1 -1
  100. package/dist/hooks/sidecar.js +4 -4
  101. package/dist/hooks/sidecar.js.map +1 -1
  102. package/dist/index.js +48 -31
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp.js +167 -141
  105. package/dist/mcp.js.map +1 -1
  106. package/dist/storage/factory.d.ts +55 -0
  107. package/dist/storage/factory.d.ts.map +1 -0
  108. package/dist/storage/factory.js +135 -0
  109. package/dist/storage/factory.js.map +1 -0
  110. package/dist/storage/pglite-schema.d.ts +20 -0
  111. package/dist/storage/pglite-schema.d.ts.map +1 -0
  112. package/dist/storage/pglite-schema.js +165 -0
  113. package/dist/storage/pglite-schema.js.map +1 -0
  114. package/dist/storage/pglite.d.ts +187 -0
  115. package/dist/storage/pglite.d.ts.map +1 -0
  116. package/dist/storage/pglite.js +1114 -0
  117. package/dist/storage/pglite.js.map +1 -0
  118. package/dist/storage/sqlite.d.ts +117 -0
  119. package/dist/storage/sqlite.d.ts.map +1 -1
  120. package/dist/storage/sqlite.js +357 -8
  121. package/dist/storage/sqlite.js.map +1 -1
  122. package/dist/storage/store.d.ts +63 -0
  123. package/dist/storage/store.d.ts.map +1 -0
  124. package/dist/storage/store.js +27 -0
  125. package/dist/storage/store.js.map +1 -0
  126. package/dist/types/engram.d.ts +92 -2
  127. package/dist/types/engram.d.ts.map +1 -1
  128. package/dist/types/engram.js.map +1 -1
  129. package/package.json +3 -1
  130. package/src/adapters/claude-code.ts +218 -218
  131. package/src/adapters/codex.ts +252 -252
  132. package/src/adapters/common.ts +411 -411
  133. package/src/adapters/cursor.ts +135 -135
  134. package/src/adapters/http.ts +93 -93
  135. package/src/adapters/index.ts +31 -31
  136. package/src/adapters/types.ts +75 -75
  137. package/src/api/index.ts +3 -3
  138. package/src/api/routes.ts +970 -714
  139. package/src/cli/migrate.ts +307 -0
  140. package/src/cli.ts +755 -719
  141. package/src/coordination/circuit-breaker.ts +83 -0
  142. package/src/coordination/events.ts +90 -90
  143. package/src/coordination/failure-modes.ts +50 -0
  144. package/src/coordination/peer-decisions.ts +105 -105
  145. package/src/coordination/plugin-loader.ts +60 -60
  146. package/src/coordination/plugin.ts +44 -44
  147. package/src/coordination/routes.ts +56 -1
  148. package/src/coordination/schema.ts +13 -0
  149. package/src/coordination/stale.ts +80 -11
  150. package/src/coordination/types.ts +311 -311
  151. package/src/coordination/write-mutex.ts +69 -69
  152. package/src/core/auto-tagger.ts +168 -168
  153. package/src/core/decay.ts +63 -63
  154. package/src/core/embeddings.ts +110 -93
  155. package/src/core/index.ts +5 -5
  156. package/src/core/logger.ts +36 -36
  157. package/src/core/ml-worker-entry.ts +194 -0
  158. package/src/core/ml-worker.ts +281 -0
  159. package/src/core/query-expander.ts +122 -128
  160. package/src/core/reranker.ts +119 -125
  161. package/src/core/salience.ts +200 -33
  162. package/src/core/write-pipeline.ts +521 -343
  163. package/src/engine/activation.ts +235 -109
  164. package/src/engine/confidence.ts +120 -0
  165. package/src/engine/connections.ts +162 -103
  166. package/src/engine/consolidation-scheduler.ts +242 -125
  167. package/src/engine/consolidation.ts +138 -45
  168. package/src/engine/eval.ts +102 -102
  169. package/src/engine/eviction.ts +101 -101
  170. package/src/engine/index.ts +8 -8
  171. package/src/engine/retraction.ts +366 -116
  172. package/src/engine/staging.ts +74 -74
  173. package/src/hooks/sidecar.ts +5 -5
  174. package/src/index.ts +226 -212
  175. package/src/mcp.ts +1145 -1121
  176. package/src/storage/factory.ts +147 -0
  177. package/src/storage/index.ts +3 -3
  178. package/src/storage/pglite-schema.ts +166 -0
  179. package/src/storage/pglite.ts +1363 -0
  180. package/src/storage/sqlite.ts +402 -7
  181. package/src/storage/store.ts +80 -0
  182. package/src/types/agent.ts +67 -67
  183. package/src/types/checkpoint.ts +46 -46
  184. package/src/types/engram.ts +101 -3
  185. package/src/types/eval.ts +100 -100
  186. package/src/types/index.ts +6 -6
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
20
20
  import { readFileSync, existsSync } from 'node:fs';
21
- import type { EngramStore } from '../storage/sqlite.js';
21
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
22
22
  import type { ConsciousState } from '../types/checkpoint.js';
23
23
  import { log, getLogPath } from '../core/logger.js';
24
24
 
@@ -211,7 +211,7 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
211
211
  episodeId: null,
212
212
  };
213
213
 
214
- store.saveCheckpoint(agentId, state);
214
+ await store.saveCheckpoint(agentId, state);
215
215
  log(agentId, `hook:${event}`, `auto-checkpoint files=${state.activeFiles.length} task="${state.currentTask.slice(0, 80)}"`);
216
216
 
217
217
  // On SessionEnd: run full consolidation (sleep cycle) before process dies
@@ -255,9 +255,9 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
255
255
  });
256
256
 
257
257
  // --- Silent auto-checkpoint every 15 minutes ---
258
- const autoCheckpointTimer = setInterval(() => {
258
+ const autoCheckpointTimer = setInterval(async () => {
259
259
  try {
260
- const checkpoint = store.getCheckpoint(agentId);
260
+ const checkpoint = await store.getCheckpoint(agentId);
261
261
  if (!checkpoint) return; // No state to save yet
262
262
 
263
263
  // Only checkpoint if there's been activity since last auto-checkpoint
@@ -275,7 +275,7 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
275
275
  episodeId: checkpoint.executionState?.episodeId ?? null,
276
276
  };
277
277
 
278
- store.saveCheckpoint(agentId, state);
278
+ await store.saveCheckpoint(agentId, state);
279
279
  log(agentId, 'hook:timer', `auto-checkpoint (${Math.round(sinceActivity / 60_000)}min since activity)`);
280
280
  } catch { /* timer failure is non-fatal */ }
281
281
  }, AUTO_CHECKPOINT_INTERVAL_MS);
package/src/index.ts CHANGED
@@ -1,212 +1,226 @@
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
- // bodyLimit: 512KB to prevent Content-Length mismatch errors with large task payloads
105
- const app = Fastify({ logger: false, bodyLimit: 512_000 });
106
-
107
- // Bearer token auth — only enforced when AWM_API_KEY is explicitly set and non-empty
108
- if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
109
- app.addHook('onRequest', async (req, reply) => {
110
- if (req.url === '/health') return; // Health check is always public
111
- const bearer = req.headers.authorization;
112
- const xApiKey = req.headers['x-api-key'] as string | undefined;
113
- if (bearer === `Bearer ${API_KEY}` || xApiKey === API_KEY) return;
114
- reply.code(401).send({ error: 'Unauthorized' });
115
- });
116
- console.log('API key auth enabled (AWM_API_KEY set)');
117
- }
118
-
119
- registerRoutes(app, {
120
- store, activationEngine, connectionEngine,
121
- evictionEngine, retractionEngine, evalEngine,
122
- consolidationEngine, consolidationScheduler,
123
- });
124
-
125
- // Coordination module (opt-in via AWM_COORDINATION=true)
126
- const { isCoordinationEnabled, initCoordination, stopCoordinationCleanup } = await import('./coordination/index.js');
127
- if (isCoordinationEnabled()) {
128
- initCoordination(app, store.getDb(), store);
129
- } else {
130
- console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
131
- }
132
-
133
- // Background tasks
134
- stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
135
- consolidationScheduler.start();
136
-
137
- // Periodic hot backup every 10 minutes (keep last 6 = 1hr coverage)
138
- const dbDir = dirname(resolve(DB_PATH));
139
- const backupDir = resolve(dbDir, 'backups');
140
- mkdirSync(backupDir, { recursive: true });
141
-
142
- // Cleanup old backups on startup (older than 2 hours)
143
- try {
144
- const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
145
- const now = Date.now();
146
- for (const f of readdirSync(backupDir).filter(f => f.endsWith('.db'))) {
147
- const match = f.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
148
- if (match) {
149
- const fileDate = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
150
- if (now - fileDate.getTime() > TWO_HOURS_MS) {
151
- unlinkSync(resolve(backupDir, f));
152
- }
153
- }
154
- }
155
- } catch { /* cleanup is non-fatal */ }
156
-
157
- const backupTimer = setInterval(() => {
158
- try {
159
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
160
- const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
161
- store.backup(backupPath);
162
- // Prune: keep only last 6 backups
163
- const backups = readdirSync(backupDir).filter(f => f.endsWith('.db')).sort();
164
- while (backups.length > 6) {
165
- const old = backups.shift()!;
166
- try { unlinkSync(resolve(backupDir, old)); } catch { /* non-fatal */ }
167
- }
168
- } catch (err) {
169
- console.warn(`[backup] failed: ${(err as Error).message}`);
170
- }
171
- }, 10 * 60_000); // 10 minutes
172
-
173
- // Pre-load ML models (downloads on first run: embeddings ~22MB, reranker ~22MB, expander ~80MB)
174
- getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
175
- getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
176
- getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
177
-
178
- // Eager slim-cache populate pays the ~600ms first-fetch cost at startup
179
- // instead of charging it to the first user recall (0.7.14+).
180
- setImmediate(() => {
181
- try {
182
- const t0 = Date.now();
183
- store.warmSlimCache();
184
- const stats = store.getSlimCacheStats();
185
- console.log(` Slim cache warmed: ${stats.size} entries in ${Date.now() - t0}ms`);
186
- } catch (err) {
187
- console.warn(` Slim cache warm failed: ${(err as Error).message}`);
188
- }
189
- });
190
-
191
- // Start server
192
- await app.listen({ port: PORT, host: '0.0.0.0' });
193
- console.log(`AgentWorkingMemory v0.7.17 listening on port ${PORT}`);
194
-
195
- // Graceful shutdown
196
- const shutdown = async () => {
197
- clearInterval(backupTimer);
198
- await stopCoordinationCleanup();
199
- consolidationScheduler.stop();
200
- stagingBuffer.stop();
201
- try { store.walCheckpoint(); } catch { /* non-fatal */ }
202
- store.close();
203
- process.exit(0);
204
- };
205
- process.on('SIGINT', shutdown);
206
- process.on('SIGTERM', shutdown);
207
- }
208
-
209
- main().catch(err => {
210
- console.error('Failed to start:', err);
211
- process.exit(1);
212
- });
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
+ import { openStore, getConfiguredBackend, type StoreBackend } from './storage/factory.js';
37
+ import type { IEngramStore } from './storage/store.js';
38
+
39
+ const PORT = parseInt(process.env.AWM_PORT ?? '8400', 10);
40
+ const BACKEND: StoreBackend = getConfiguredBackend();
41
+ const DB_PATH = process.env.AWM_DB_PATH ?? (BACKEND === 'pglite' ? 'memory-pglite' : 'memory.db');
42
+ const API_KEY = process.env.AWM_API_KEY ?? null;
43
+
44
+ async function main() {
45
+ // Auto-backup: copy DB to backups/ on startup (SQLite-only; PGlite is a
46
+ // directory and is backed up by snapshot/replication at the OS level).
47
+ if (BACKEND === 'sqlite' && existsSync(DB_PATH)) {
48
+ const dbDir = dirname(resolve(DB_PATH));
49
+ const backupDir = resolve(dbDir, 'backups');
50
+ mkdirSync(backupDir, { recursive: true });
51
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
52
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
53
+ try {
54
+ copyFileSync(resolve(DB_PATH), backupPath);
55
+ console.log(`Backup: ${backupPath}`);
56
+ } catch (err) {
57
+ console.log(`Backup skipped: ${(err as Error).message}`);
58
+ }
59
+ }
60
+
61
+ // Logger write activity to awm.log alongside the DB
62
+ initLogger(DB_PATH);
63
+
64
+ // Storage backend-agnostic open via the factory.
65
+ // Some routes (export, coordination health) are still SQLite-only and
66
+ // gracefully return 501 / skip when running on PGlite.
67
+ const { store: storeAny } = await openStore();
68
+ const store = storeAny as unknown as EngramStore;
69
+
70
+ // Integrity check SQLite-only. PGlite has its own consistency model.
71
+ const integrity = BACKEND === 'sqlite'
72
+ ? store.integrityCheck()
73
+ : { ok: true, result: 'pglite: skipped' };
74
+ if (!integrity.ok) {
75
+ console.error(`DB integrity check FAILED: ${integrity.result}`);
76
+ // Close corrupt DB, restore from backup, and exit for process manager to restart
77
+ store.close();
78
+ const dbDir = dirname(resolve(DB_PATH));
79
+ const backupDir = resolve(dbDir, 'backups');
80
+ if (existsSync(backupDir)) {
81
+ const backups = readdirSync(backupDir)
82
+ .filter(f => f.endsWith('.db'))
83
+ .sort()
84
+ .reverse();
85
+ if (backups.length > 0) {
86
+ const restorePath = resolve(backupDir, backups[0]);
87
+ console.error(`Attempting restore from: ${restorePath}`);
88
+ try {
89
+ copyFileSync(restorePath, resolve(DB_PATH));
90
+ console.error('Restore complete exiting for restart with restored DB');
91
+ process.exit(1);
92
+ } catch (restoreErr) {
93
+ console.error(`Restore failed: ${(restoreErr as Error).message}`);
94
+ }
95
+ }
96
+ }
97
+ console.error('No backup available continuing with potentially corrupt DB');
98
+ } else {
99
+ console.log(' DB integrity check: ok');
100
+ }
101
+
102
+ // Engines
103
+ const activationEngine = new ActivationEngine(store);
104
+ const connectionEngine = new ConnectionEngine(store, activationEngine);
105
+ const stagingBuffer = new StagingBuffer(store, activationEngine);
106
+ const evictionEngine = new EvictionEngine(store);
107
+ const retractionEngine = new RetractionEngine(store);
108
+ const evalEngine = new EvalEngine(store);
109
+ const consolidationEngine = new ConsolidationEngine(store, connectionEngine);
110
+ const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
111
+
112
+ // API disable Fastify's default request logging (too noisy for hive polling)
113
+ // bodyLimit: 512KB to prevent Content-Length mismatch errors with large task payloads
114
+ const app = Fastify({ logger: false, bodyLimit: 512_000 });
115
+
116
+ // Bearer token auth only enforced when AWM_API_KEY is explicitly set and non-empty
117
+ if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
118
+ app.addHook('onRequest', async (req, reply) => {
119
+ if (req.url === '/health') return; // Health check is always public
120
+ const bearer = req.headers.authorization;
121
+ const xApiKey = req.headers['x-api-key'] as string | undefined;
122
+ if (bearer === `Bearer ${API_KEY}` || xApiKey === API_KEY) return;
123
+ reply.code(401).send({ error: 'Unauthorized' });
124
+ });
125
+ console.log('API key auth enabled (AWM_API_KEY set)');
126
+ }
127
+
128
+ registerRoutes(app, {
129
+ store, activationEngine, connectionEngine,
130
+ evictionEngine, retractionEngine, evalEngine,
131
+ consolidationEngine, consolidationScheduler,
132
+ });
133
+
134
+ // Coordination module (opt-in via AWM_COORDINATION=true) — SQLite-only.
135
+ const { isCoordinationEnabled, initCoordination, stopCoordinationCleanup } = await import('./coordination/index.js');
136
+ if (isCoordinationEnabled() && BACKEND === 'sqlite') {
137
+ initCoordination(app, store.getDb(), store);
138
+ } else if (isCoordinationEnabled()) {
139
+ console.log(` Coordination requested but disabled — requires SQLite backend (current: ${BACKEND})`);
140
+ } else {
141
+ console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
142
+ }
143
+
144
+ // Background tasks
145
+ stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
146
+ consolidationScheduler.start();
147
+
148
+ // Periodic hot backup every 10 minutes (keep last 6 = 1hr coverage)
149
+ const dbDir = dirname(resolve(DB_PATH));
150
+ const backupDir = resolve(dbDir, 'backups');
151
+ mkdirSync(backupDir, { recursive: true });
152
+
153
+ // Cleanup old backups on startup (older than 2 hours)
154
+ try {
155
+ const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
156
+ const now = Date.now();
157
+ for (const f of readdirSync(backupDir).filter(f => f.endsWith('.db'))) {
158
+ const match = f.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
159
+ if (match) {
160
+ const fileDate = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
161
+ if (now - fileDate.getTime() > TWO_HOURS_MS) {
162
+ unlinkSync(resolve(backupDir, f));
163
+ }
164
+ }
165
+ }
166
+ } catch { /* cleanup is non-fatal */ }
167
+
168
+ // Periodic hot backup — SQLite-only. PGlite directory backups are an OS-level concern.
169
+ const backupTimer: ReturnType<typeof setInterval> | null = BACKEND === 'sqlite' ? setInterval(() => {
170
+ try {
171
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
172
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
173
+ store.backup(backupPath);
174
+ // Prune: keep only last 6 backups
175
+ const backups = readdirSync(backupDir).filter(f => f.endsWith('.db')).sort();
176
+ while (backups.length > 6) {
177
+ const old = backups.shift()!;
178
+ try { unlinkSync(resolve(backupDir, old)); } catch { /* non-fatal */ }
179
+ }
180
+ } catch (err) {
181
+ console.warn(`[backup] failed: ${(err as Error).message}`);
182
+ }
183
+ }, 10 * 60_000) : null;
184
+
185
+ // Pre-load ML models (downloads on first run: embeddings ~22MB, reranker ~22MB, expander ~80MB)
186
+ getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
187
+ getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
188
+ getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
189
+
190
+ if (BACKEND === 'sqlite') {
191
+ setImmediate(() => {
192
+ try {
193
+ const t0 = Date.now();
194
+ store.warmSlimCache();
195
+ const stats = store.getSlimCacheStats();
196
+ console.log(` Slim cache warmed: ${stats.size} entries in ${Date.now() - t0}ms`);
197
+ } catch (err) {
198
+ console.warn(` Slim cache warm failed: ${(err as Error).message}`);
199
+ }
200
+ });
201
+ }
202
+
203
+ // Start server
204
+ await app.listen({ port: PORT, host: '0.0.0.0' });
205
+ console.log(`AgentWorkingMemory v0.8.5 listening on port ${PORT}`);
206
+
207
+ // Graceful shutdown
208
+ const shutdown = async () => {
209
+ if (backupTimer) clearInterval(backupTimer);
210
+ await stopCoordinationCleanup();
211
+ consolidationScheduler.stop();
212
+ stagingBuffer.stop();
213
+ if (BACKEND === 'sqlite') {
214
+ try { store.walCheckpoint(); } catch { /* non-fatal */ }
215
+ }
216
+ try { await (store as any).close?.(); } catch { /* best-effort */ }
217
+ process.exit(0);
218
+ };
219
+ process.on('SIGINT', shutdown);
220
+ process.on('SIGTERM', shutdown);
221
+ }
222
+
223
+ main().catch(err => {
224
+ console.error('Failed to start:', err);
225
+ process.exit(1);
226
+ });