agent-working-memory 0.10.0 → 0.12.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 (136) hide show
  1. package/README.md +118 -19
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +63 -3
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/common.d.ts.map +1 -1
  6. package/dist/adapters/common.js +329 -302
  7. package/dist/adapters/common.js.map +1 -1
  8. package/dist/api/routes.d.ts.map +1 -1
  9. package/dist/api/routes.js +31 -8
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/cli/migrate.js +29 -29
  12. package/dist/cli.js +82 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coordination/circuit-breaker.js +23 -23
  15. package/dist/coordination/routes.d.ts.map +1 -1
  16. package/dist/coordination/routes.js +174 -170
  17. package/dist/coordination/routes.js.map +1 -1
  18. package/dist/core/embeddings.d.ts.map +1 -1
  19. package/dist/core/embeddings.js +3 -0
  20. package/dist/core/embeddings.js.map +1 -1
  21. package/dist/core/entity-extract.d.ts +3 -0
  22. package/dist/core/entity-extract.d.ts.map +1 -0
  23. package/dist/core/entity-extract.js +47 -0
  24. package/dist/core/entity-extract.js.map +1 -0
  25. package/dist/core/salience.d.ts.map +1 -1
  26. package/dist/core/salience.js +14 -2
  27. package/dist/core/salience.js.map +1 -1
  28. package/dist/core/whoami.d.ts +24 -0
  29. package/dist/core/whoami.d.ts.map +1 -0
  30. package/dist/core/whoami.js +66 -0
  31. package/dist/core/whoami.js.map +1 -0
  32. package/dist/core/write-pipeline.d.ts +9 -0
  33. package/dist/core/write-pipeline.d.ts.map +1 -1
  34. package/dist/core/write-pipeline.js +109 -68
  35. package/dist/core/write-pipeline.js.map +1 -1
  36. package/dist/core/write-telemetry.d.ts +33 -0
  37. package/dist/core/write-telemetry.d.ts.map +1 -0
  38. package/dist/core/write-telemetry.js +110 -0
  39. package/dist/core/write-telemetry.js.map +1 -0
  40. package/dist/engine/activation.d.ts +22 -12
  41. package/dist/engine/activation.d.ts.map +1 -1
  42. package/dist/engine/activation.js +133 -17
  43. package/dist/engine/activation.js.map +1 -1
  44. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  45. package/dist/engine/consolidation-scheduler.js +1 -1
  46. package/dist/engine/consolidation.d.ts +1 -0
  47. package/dist/engine/consolidation.d.ts.map +1 -1
  48. package/dist/engine/consolidation.js +18 -0
  49. package/dist/engine/consolidation.js.map +1 -1
  50. package/dist/engine/eval.d.ts.map +1 -1
  51. package/dist/engine/eval.js +5 -1
  52. package/dist/engine/eval.js.map +1 -1
  53. package/dist/index.js +21 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.d.ts.map +1 -1
  57. package/dist/mcp.js +212 -97
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/onboard/index.d.ts +68 -0
  60. package/dist/onboard/index.d.ts.map +1 -0
  61. package/dist/onboard/index.js +265 -0
  62. package/dist/onboard/index.js.map +1 -0
  63. package/dist/recipes/index.d.ts +57 -0
  64. package/dist/recipes/index.d.ts.map +1 -0
  65. package/dist/recipes/index.js +81 -0
  66. package/dist/recipes/index.js.map +1 -0
  67. package/dist/storage/pglite-schema.d.ts.map +1 -1
  68. package/dist/storage/pglite-schema.js +170 -143
  69. package/dist/storage/pglite-schema.js.map +1 -1
  70. package/dist/storage/pglite.d.ts +5 -0
  71. package/dist/storage/pglite.d.ts.map +1 -1
  72. package/dist/storage/pglite.js +180 -138
  73. package/dist/storage/pglite.js.map +1 -1
  74. package/dist/storage/postgres.d.ts +5 -0
  75. package/dist/storage/postgres.d.ts.map +1 -1
  76. package/dist/storage/postgres.js +44 -2
  77. package/dist/storage/postgres.js.map +1 -1
  78. package/dist/storage/sqlite.d.ts +9 -0
  79. package/dist/storage/sqlite.d.ts.map +1 -1
  80. package/dist/storage/sqlite.js +394 -326
  81. package/dist/storage/sqlite.js.map +1 -1
  82. package/dist/types/engram.d.ts +14 -0
  83. package/dist/types/engram.d.ts.map +1 -1
  84. package/dist/types/engram.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +27 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +9 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +538 -511
  92. package/src/api/index.ts +3 -3
  93. package/src/api/routes.ts +999 -970
  94. package/src/cli/migrate.ts +307 -307
  95. package/src/cli.ts +77 -2
  96. package/src/coordination/circuit-breaker.ts +83 -83
  97. package/src/coordination/failure-modes.ts +50 -50
  98. package/src/coordination/routes.ts +2155 -2150
  99. package/src/core/decay.ts +63 -63
  100. package/src/core/embeddings.ts +113 -110
  101. package/src/core/entity-extract.ts +47 -0
  102. package/src/core/index.ts +5 -5
  103. package/src/core/logger.ts +36 -36
  104. package/src/core/ml-worker-entry.ts +194 -194
  105. package/src/core/ml-worker.ts +281 -281
  106. package/src/core/query-expander.ts +122 -122
  107. package/src/core/reranker.ts +119 -119
  108. package/src/core/salience.ts +529 -514
  109. package/src/core/whoami.ts +92 -0
  110. package/src/core/write-pipeline.ts +60 -8
  111. package/src/core/write-telemetry.ts +131 -0
  112. package/src/engine/activation.ts +1468 -1369
  113. package/src/engine/confidence.ts +120 -120
  114. package/src/engine/consolidation-scheduler.ts +242 -242
  115. package/src/engine/consolidation.ts +887 -869
  116. package/src/engine/eval.ts +107 -102
  117. package/src/engine/eviction.ts +101 -101
  118. package/src/engine/index.ts +8 -8
  119. package/src/engine/retraction.ts +366 -366
  120. package/src/engine/staging.ts +74 -74
  121. package/src/index.ts +248 -226
  122. package/src/mcp.ts +1341 -1211
  123. package/src/onboard/index.ts +298 -0
  124. package/src/recipes/index.ts +125 -0
  125. package/src/storage/index.ts +3 -3
  126. package/src/storage/pglite-schema.ts +193 -166
  127. package/src/storage/pglite.ts +1420 -1372
  128. package/src/storage/postgres.ts +50 -2
  129. package/src/storage/sqlite.ts +1936 -1861
  130. package/src/storage/store.ts +80 -80
  131. package/src/types/agent.ts +67 -67
  132. package/src/types/checkpoint.ts +46 -46
  133. package/src/types/engram.ts +22 -0
  134. package/src/types/eval.ts +100 -100
  135. package/src/types/index.ts +6 -6
  136. package/src/version.ts +26 -0
@@ -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 { IEngramStore as EngramStore } from '../storage/store.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 = await 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
- await this.store.updateStage(engram.id, 'active');
64
- promoted.push(engram.id);
65
- } else {
66
- // No resonance — discard
67
- await 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 { IEngramStore as EngramStore } from '../storage/store.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 = await 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
+ await this.store.updateStage(engram.id, 'active');
64
+ promoted.push(engram.id);
65
+ } else {
66
+ // No resonance — discard
67
+ await 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,226 +1,248 @@
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
- });
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
+ import { VERSION } from './version.js';
7
+
8
+ // Load .env file if present (no external dependency)
9
+ try {
10
+ const envPath = resolve(process.cwd(), '.env');
11
+ const envContent = readFileSync(envPath, 'utf-8');
12
+ for (const line of envContent.split('\n')) {
13
+ const trimmed = line.trim();
14
+ if (!trimmed || trimmed.startsWith('#')) continue;
15
+ const eqIdx = trimmed.indexOf('=');
16
+ if (eqIdx === -1) continue;
17
+ const key = trimmed.slice(0, eqIdx).trim();
18
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
19
+ if (!process.env[key]) process.env[key] = val; // Don't override existing env
20
+ }
21
+ } catch { /* No .env file — that's fine */ }
22
+ import { EngramStore } from './storage/sqlite.js';
23
+ import { ActivationEngine } from './engine/activation.js';
24
+ import { ConnectionEngine } from './engine/connections.js';
25
+ import { StagingBuffer } from './engine/staging.js';
26
+ import { startLoopLagMonitor } from './core/write-telemetry.js';
27
+ import { EvictionEngine } from './engine/eviction.js';
28
+ import { RetractionEngine } from './engine/retraction.js';
29
+ import { EvalEngine } from './engine/eval.js';
30
+ import { ConsolidationEngine } from './engine/consolidation.js';
31
+ import { ConsolidationScheduler } from './engine/consolidation-scheduler.js';
32
+ import { registerRoutes } from './api/routes.js';
33
+ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
34
+ import { getEmbedder } from './core/embeddings.js';
35
+ import { getReranker } from './core/reranker.js';
36
+ import { getExpander } from './core/query-expander.js';
37
+ import { initLogger } from './core/logger.js';
38
+ import { openStore, getConfiguredBackend, type StoreBackend } from './storage/factory.js';
39
+ import type { IEngramStore } from './storage/store.js';
40
+
41
+ const PORT = parseInt(process.env.AWM_PORT ?? '8400', 10);
42
+ const BACKEND: StoreBackend = getConfiguredBackend();
43
+ const DB_PATH = process.env.AWM_DB_PATH ?? (BACKEND === 'pglite' ? 'memory-pglite' : 'memory.db');
44
+ const API_KEY = process.env.AWM_API_KEY ?? null;
45
+
46
+ // D2 (2026-07-30) security defaults: bind loopback unless AWM_BIND widens it.
47
+ // Widening without an API key is refused (fail-closed) — a local-first store
48
+ // on hotel Wi-Fi must not be one env var away from an open network listener.
49
+ // Escape hatch for trusted networks: AWM_ALLOW_INSECURE=1.
50
+ const HOST = process.env.AWM_BIND ?? '127.0.0.1';
51
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
52
+ if (!LOOPBACK_HOSTS.has(HOST)
53
+ && !(API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1)
54
+ && process.env.AWM_ALLOW_INSECURE !== '1') {
55
+ console.error(
56
+ `AWM refusing to start: AWM_BIND=${HOST} exposes the API beyond loopback with no AWM_API_KEY set. ` +
57
+ 'Set AWM_API_KEY, or AWM_ALLOW_INSECURE=1 to override on a trusted network.',
58
+ );
59
+ process.exit(1);
60
+ }
61
+
62
+ async function main() {
63
+ // Auto-backup: copy DB to backups/ on startup (SQLite-only; PGlite is a
64
+ // directory and is backed up by snapshot/replication at the OS level).
65
+ if (BACKEND === 'sqlite' && existsSync(DB_PATH)) {
66
+ const dbDir = dirname(resolve(DB_PATH));
67
+ const backupDir = resolve(dbDir, 'backups');
68
+ mkdirSync(backupDir, { recursive: true });
69
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
70
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
71
+ try {
72
+ copyFileSync(resolve(DB_PATH), backupPath);
73
+ console.log(`Backup: ${backupPath}`);
74
+ } catch (err) {
75
+ console.log(`Backup skipped: ${(err as Error).message}`);
76
+ }
77
+ }
78
+
79
+ // Logger write activity to awm.log alongside the DB
80
+ initLogger(DB_PATH);
81
+
82
+ // Storage — backend-agnostic open via the factory.
83
+ // Some routes (export, coordination health) are still SQLite-only and
84
+ // gracefully return 501 / skip when running on PGlite.
85
+ const { store: storeAny } = await openStore();
86
+ const store = storeAny as unknown as EngramStore;
87
+
88
+ // Integrity check — SQLite-only. PGlite has its own consistency model.
89
+ const integrity = BACKEND === 'sqlite'
90
+ ? store.integrityCheck()
91
+ : { ok: true, result: 'pglite: skipped' };
92
+ if (!integrity.ok) {
93
+ console.error(`DB integrity check FAILED: ${integrity.result}`);
94
+ // Close corrupt DB, restore from backup, and exit for process manager to restart
95
+ store.close();
96
+ const dbDir = dirname(resolve(DB_PATH));
97
+ const backupDir = resolve(dbDir, 'backups');
98
+ if (existsSync(backupDir)) {
99
+ const backups = readdirSync(backupDir)
100
+ .filter(f => f.endsWith('.db'))
101
+ .sort()
102
+ .reverse();
103
+ if (backups.length > 0) {
104
+ const restorePath = resolve(backupDir, backups[0]);
105
+ console.error(`Attempting restore from: ${restorePath}`);
106
+ try {
107
+ copyFileSync(restorePath, resolve(DB_PATH));
108
+ console.error('Restore complete exiting for restart with restored DB');
109
+ process.exit(1);
110
+ } catch (restoreErr) {
111
+ console.error(`Restore failed: ${(restoreErr as Error).message}`);
112
+ }
113
+ }
114
+ }
115
+ console.error('No backup available — continuing with potentially corrupt DB');
116
+ } else {
117
+ console.log(' DB integrity check: ok');
118
+ }
119
+
120
+ // Engines
121
+ const activationEngine = new ActivationEngine(store);
122
+ const connectionEngine = new ConnectionEngine(store, activationEngine);
123
+ const stagingBuffer = new StagingBuffer(store, activationEngine);
124
+ const evictionEngine = new EvictionEngine(store);
125
+ const retractionEngine = new RetractionEngine(store);
126
+ const evalEngine = new EvalEngine(store);
127
+ const consolidationEngine = new ConsolidationEngine(store, connectionEngine);
128
+ const consolidationScheduler = new ConsolidationScheduler(store, consolidationEngine);
129
+
130
+ // API — disable Fastify's default request logging (too noisy for hive polling)
131
+ // bodyLimit: 512KB to prevent Content-Length mismatch errors with large task payloads
132
+ const app = Fastify({ logger: false, bodyLimit: 512_000 });
133
+
134
+ // Bearer token auth only enforced when AWM_API_KEY is explicitly set and non-empty
135
+ if (API_KEY && API_KEY !== 'NONE' && API_KEY.length > 1) {
136
+ app.addHook('onRequest', async (req, reply) => {
137
+ if (req.url === '/health') return; // Health check is always public
138
+ const bearer = req.headers.authorization;
139
+ const xApiKey = req.headers['x-api-key'] as string | undefined;
140
+ if (bearer === `Bearer ${API_KEY}` || xApiKey === API_KEY) return;
141
+ reply.code(401).send({ error: 'Unauthorized' });
142
+ });
143
+ console.log('API key auth enabled (AWM_API_KEY set)');
144
+ }
145
+
146
+ registerRoutes(app, {
147
+ store, activationEngine, connectionEngine,
148
+ evictionEngine, retractionEngine, evalEngine,
149
+ consolidationEngine, consolidationScheduler,
150
+ });
151
+
152
+ // Coordination module (opt-in via AWM_COORDINATION=true) — SQLite-only.
153
+ const { isCoordinationEnabled, initCoordination, stopCoordinationCleanup } = await import('./coordination/index.js');
154
+ if (isCoordinationEnabled() && BACKEND === 'sqlite') {
155
+ initCoordination(app, store.getDb(), store);
156
+ if (process.env.AWM_COORD_REQUIRE_TOKENS !== '1') {
157
+ console.log(' Coordination: session tokens optional (set AWM_COORD_REQUIRE_TOKENS=1 to require them)');
158
+ }
159
+ } else if (isCoordinationEnabled()) {
160
+ console.log(` Coordination requested but disabled — requires SQLite backend (current: ${BACKEND})`);
161
+ } else {
162
+ console.log(' Coordination module disabled (set AWM_COORDINATION=true to enable)');
163
+ }
164
+
165
+ // Background tasks
166
+ stagingBuffer.start(DEFAULT_AGENT_CONFIG.stagingTtlMs);
167
+ consolidationScheduler.start();
168
+ startLoopLagMonitor();
169
+
170
+ // Periodic hot backup every 10 minutes (keep last 6 = 1hr coverage)
171
+ const dbDir = dirname(resolve(DB_PATH));
172
+ const backupDir = resolve(dbDir, 'backups');
173
+ mkdirSync(backupDir, { recursive: true });
174
+
175
+ // Cleanup old backups on startup (older than 2 hours)
176
+ try {
177
+ const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
178
+ const now = Date.now();
179
+ for (const f of readdirSync(backupDir).filter(f => f.endsWith('.db'))) {
180
+ const match = f.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
181
+ if (match) {
182
+ const fileDate = new Date(`${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}:${match[6]}Z`);
183
+ if (now - fileDate.getTime() > TWO_HOURS_MS) {
184
+ unlinkSync(resolve(backupDir, f));
185
+ }
186
+ }
187
+ }
188
+ } catch { /* cleanup is non-fatal */ }
189
+
190
+ // Periodic hot backup — SQLite-only. PGlite directory backups are an OS-level concern.
191
+ const backupTimer: ReturnType<typeof setInterval> | null = BACKEND === 'sqlite' ? setInterval(() => {
192
+ try {
193
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
194
+ const backupPath = resolve(backupDir, `${basename(DB_PATH, '.db')}-${ts}.db`);
195
+ store.backup(backupPath);
196
+ // Prune: keep only last 6 backups
197
+ const backups = readdirSync(backupDir).filter(f => f.endsWith('.db')).sort();
198
+ while (backups.length > 6) {
199
+ const old = backups.shift()!;
200
+ try { unlinkSync(resolve(backupDir, old)); } catch { /* non-fatal */ }
201
+ }
202
+ } catch (err) {
203
+ console.warn(`[backup] failed: ${(err as Error).message}`);
204
+ }
205
+ }, 10 * 60_000) : null;
206
+
207
+ // Pre-load ML models (downloads on first run: embeddings ~22MB, reranker ~22MB, expander ~80MB)
208
+ getEmbedder().catch(err => console.warn('Embedding model unavailable:', err.message));
209
+ getReranker().catch(err => console.warn('Reranker model unavailable:', err.message));
210
+ getExpander().catch(err => console.warn('Query expander model unavailable:', err.message));
211
+
212
+ if (BACKEND === 'sqlite') {
213
+ setImmediate(() => {
214
+ try {
215
+ const t0 = Date.now();
216
+ store.warmSlimCache();
217
+ const stats = store.getSlimCacheStats();
218
+ console.log(` Slim cache warmed: ${stats.size} entries in ${Date.now() - t0}ms`);
219
+ } catch (err) {
220
+ console.warn(` Slim cache warm failed: ${(err as Error).message}`);
221
+ }
222
+ });
223
+ }
224
+
225
+ // Start server
226
+ await app.listen({ port: PORT, host: HOST });
227
+ console.log(`AgentWorkingMemory v${VERSION} listening on ${HOST}:${PORT}`);
228
+
229
+ // Graceful shutdown
230
+ const shutdown = async () => {
231
+ if (backupTimer) clearInterval(backupTimer);
232
+ await stopCoordinationCleanup();
233
+ consolidationScheduler.stop();
234
+ stagingBuffer.stop();
235
+ if (BACKEND === 'sqlite') {
236
+ try { store.walCheckpoint(); } catch { /* non-fatal */ }
237
+ }
238
+ try { await (store as any).close?.(); } catch { /* best-effort */ }
239
+ process.exit(0);
240
+ };
241
+ process.on('SIGINT', shutdown);
242
+ process.on('SIGTERM', shutdown);
243
+ }
244
+
245
+ main().catch(err => {
246
+ console.error('Failed to start:', err);
247
+ process.exit(1);
248
+ });