agent-working-memory 0.6.0 → 0.6.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.
Files changed (122) hide show
  1. package/README.md +15 -9
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.d.ts.map +1 -0
  4. package/dist/adapters/claude-code.js +218 -0
  5. package/dist/adapters/claude-code.js.map +1 -0
  6. package/dist/adapters/codex.d.ts +4 -0
  7. package/dist/adapters/codex.d.ts.map +1 -0
  8. package/dist/adapters/codex.js +226 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/common.d.ts +34 -0
  11. package/dist/adapters/common.d.ts.map +1 -0
  12. package/dist/adapters/common.js +145 -0
  13. package/dist/adapters/common.js.map +1 -0
  14. package/dist/adapters/cursor.d.ts +4 -0
  15. package/dist/adapters/cursor.d.ts.map +1 -0
  16. package/dist/adapters/cursor.js +138 -0
  17. package/dist/adapters/cursor.js.map +1 -0
  18. package/dist/adapters/http.d.ts +4 -0
  19. package/dist/adapters/http.d.ts.map +1 -0
  20. package/dist/adapters/http.js +88 -0
  21. package/dist/adapters/http.js.map +1 -0
  22. package/dist/adapters/index.d.ts +7 -0
  23. package/dist/adapters/index.d.ts.map +1 -0
  24. package/dist/adapters/index.js +21 -0
  25. package/dist/adapters/index.js.map +1 -0
  26. package/dist/adapters/types.d.ts +65 -0
  27. package/dist/adapters/types.d.ts.map +1 -0
  28. package/dist/adapters/types.js +4 -0
  29. package/dist/adapters/types.js.map +1 -0
  30. package/dist/cli.js +104 -230
  31. package/dist/cli.js.map +1 -1
  32. package/dist/coordination/events.d.ts +59 -0
  33. package/dist/coordination/events.d.ts.map +1 -0
  34. package/dist/coordination/events.js +28 -0
  35. package/dist/coordination/events.js.map +1 -0
  36. package/dist/coordination/index.d.ts +10 -1
  37. package/dist/coordination/index.d.ts.map +1 -1
  38. package/dist/coordination/index.js +87 -3
  39. package/dist/coordination/index.js.map +1 -1
  40. package/dist/coordination/peer-decisions.d.ts +40 -0
  41. package/dist/coordination/peer-decisions.d.ts.map +1 -0
  42. package/dist/coordination/peer-decisions.js +82 -0
  43. package/dist/coordination/peer-decisions.js.map +1 -0
  44. package/dist/coordination/plugin-loader.d.ts +18 -0
  45. package/dist/coordination/plugin-loader.d.ts.map +1 -0
  46. package/dist/coordination/plugin-loader.js +55 -0
  47. package/dist/coordination/plugin-loader.js.map +1 -0
  48. package/dist/coordination/plugin.d.ts +40 -0
  49. package/dist/coordination/plugin.d.ts.map +1 -0
  50. package/dist/coordination/plugin.js +22 -0
  51. package/dist/coordination/plugin.js.map +1 -0
  52. package/dist/coordination/routes.d.ts +2 -1
  53. package/dist/coordination/routes.d.ts.map +1 -1
  54. package/dist/coordination/routes.js +899 -76
  55. package/dist/coordination/routes.js.map +1 -1
  56. package/dist/coordination/schema.d.ts.map +1 -1
  57. package/dist/coordination/schema.js +72 -14
  58. package/dist/coordination/schema.js.map +1 -1
  59. package/dist/coordination/schemas.d.ts +84 -3
  60. package/dist/coordination/schemas.d.ts.map +1 -1
  61. package/dist/coordination/schemas.js +71 -1
  62. package/dist/coordination/schemas.js.map +1 -1
  63. package/dist/coordination/stale.d.ts.map +1 -1
  64. package/dist/coordination/stale.js +2 -1
  65. package/dist/coordination/stale.js.map +1 -1
  66. package/dist/coordination/types.d.ts +252 -0
  67. package/dist/coordination/types.d.ts.map +1 -0
  68. package/dist/coordination/types.js +8 -0
  69. package/dist/coordination/types.js.map +1 -0
  70. package/dist/coordination/write-mutex.d.ts +26 -0
  71. package/dist/coordination/write-mutex.d.ts.map +1 -0
  72. package/dist/coordination/write-mutex.js +63 -0
  73. package/dist/coordination/write-mutex.js.map +1 -0
  74. package/dist/core/embeddings.d.ts +2 -0
  75. package/dist/core/embeddings.d.ts.map +1 -1
  76. package/dist/core/embeddings.js +4 -0
  77. package/dist/core/embeddings.js.map +1 -1
  78. package/dist/engine/activation.d.ts.map +1 -1
  79. package/dist/engine/activation.js +16 -3
  80. package/dist/engine/activation.js.map +1 -1
  81. package/dist/engine/consolidation.d.ts.map +1 -1
  82. package/dist/engine/consolidation.js +15 -6
  83. package/dist/engine/consolidation.js.map +1 -1
  84. package/dist/engine/retraction.d.ts +3 -1
  85. package/dist/engine/retraction.d.ts.map +1 -1
  86. package/dist/engine/retraction.js +19 -6
  87. package/dist/engine/retraction.js.map +1 -1
  88. package/dist/index.js +6 -18
  89. package/dist/index.js.map +1 -1
  90. package/dist/mcp.js +52 -3
  91. package/dist/mcp.js.map +1 -1
  92. package/dist/storage/sqlite.d.ts +6 -1
  93. package/dist/storage/sqlite.d.ts.map +1 -1
  94. package/dist/storage/sqlite.js +39 -3
  95. package/dist/storage/sqlite.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/adapters/claude-code.ts +234 -0
  98. package/src/adapters/codex.ts +262 -0
  99. package/src/adapters/common.ts +172 -0
  100. package/src/adapters/cursor.ts +150 -0
  101. package/src/adapters/http.ts +100 -0
  102. package/src/adapters/index.ts +31 -0
  103. package/src/adapters/types.ts +75 -0
  104. package/src/cli.ts +107 -238
  105. package/src/coordination/events.ts +90 -0
  106. package/src/coordination/index.ts +102 -3
  107. package/src/coordination/peer-decisions.ts +105 -0
  108. package/src/coordination/plugin-loader.ts +60 -0
  109. package/src/coordination/plugin.ts +44 -0
  110. package/src/coordination/routes.ts +1176 -105
  111. package/src/coordination/schema.ts +67 -14
  112. package/src/coordination/schemas.ts +85 -1
  113. package/src/coordination/stale.ts +3 -2
  114. package/src/coordination/types.ts +311 -0
  115. package/src/coordination/write-mutex.ts +69 -0
  116. package/src/core/embeddings.ts +5 -0
  117. package/src/engine/activation.ts +13 -3
  118. package/src/engine/consolidation.ts +15 -6
  119. package/src/engine/retraction.ts +22 -6
  120. package/src/index.ts +6 -15
  121. package/src/mcp.ts +73 -9
  122. package/src/storage/sqlite.ts +39 -3
package/src/cli.ts CHANGED
@@ -13,11 +13,10 @@
13
13
  */
14
14
 
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
- import { resolve, basename, join, dirname } from 'node:path';
16
+ import { resolve, join, dirname } from 'node:path';
17
17
  import { execSync } from 'node:child_process';
18
- import { randomBytes, randomUUID } from 'node:crypto';
18
+ import { randomUUID } from 'node:crypto';
19
19
  import { fileURLToPath } from 'node:url';
20
- import { homedir as osHomedir } from 'node:os';
21
20
 
22
21
  const __filename = fileURLToPath(import.meta.url);
23
22
  const __dirname = dirname(__filename);
@@ -45,9 +44,9 @@ function printUsage() {
45
44
  AgentWorkingMemory — Cognitive memory for AI agents
46
45
 
47
46
  Usage:
48
- awm setup [--global] [--agent-id <id>] [--db-path <path>] [--no-claude-md]
49
- [--no-hooks] [--hook-port <port>] Configure MCP for Claude Code
50
- awm mcp Start MCP server (used by Claude Code)
47
+ awm setup [target] [options] Configure AWM for an AI CLI
48
+ awm doctor [target|--all] Validate AWM integrations
49
+ awm mcp Start MCP server (stdio)
51
50
  awm serve [--port <port>] Start HTTP API server
52
51
  awm health [--port <port>] Check server health
53
52
  awm export --db <path> [--agent <id>] [--output <file>] [--active-only]
@@ -58,31 +57,38 @@ Usage:
58
57
  [--remap uuid=name] [--remap-all-uuids <name>]
59
58
  [--dedupe] [--dry-run] Merge multiple memory DBs
60
59
 
61
- Setup:
62
- awm setup --global Recommended. Writes ~/.mcp.json so AWM is available
63
- in every project — one brain across all your work.
64
-
65
- awm setup Project-level. Writes .mcp.json in the current directory
66
- and appends workflow instructions to CLAUDE.md.
67
-
68
- --no-claude-md Skip CLAUDE.md modification
69
- --no-hooks Skip hook installation (no auto-checkpoint)
70
- --hook-port PORT Sidecar port for hooks (default: 8401)
71
-
72
- Restart Claude Code after setup to pick up the new MCP server.
60
+ Setup targets:
61
+ claude-code (default) .mcp.json + CLAUDE.md + hooks
62
+ codex ~/.codex/config.toml + AGENTS.md
63
+ cursor .cursor/mcp.json + .cursorrules
64
+ http Connection info for HTTP API
65
+
66
+ Setup options:
67
+ --global Use global scope (recommended for claude-code)
68
+ --agent-id <id> Agent identifier (default: project name)
69
+ --db-path <path> Database path (default: <awm>/data/memory.db)
70
+ --no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
71
+ --no-claude-md Alias for --no-instructions
72
+ --no-hooks Skip hook installation
73
+ --hook-port PORT Sidecar port for hooks (default: 8401)
74
+
75
+ Examples:
76
+ awm setup --global Claude Code, global (recommended)
77
+ awm setup codex Codex CLI
78
+ awm setup cursor Cursor IDE
79
+ awm setup http Generic HTTP integration
80
+ awm doctor --all Check all configured targets
73
81
  `.trim());
74
82
  }
75
83
 
76
84
  // ─── SETUP ──────────────────────────────────────
77
85
 
78
- function setup() {
79
- const cwd = process.cwd();
80
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, '-');
81
-
86
+ async function setup() {
82
87
  // Parse flags
83
- let agentId = projectName;
88
+ let target = 'claude-code';
89
+ let agentId: string | undefined;
84
90
  let dbPath: string | null = null;
85
- let skipClaudeMd = false;
91
+ let skipInstructions = false;
86
92
  let isGlobal = false;
87
93
  let skipHooks = false;
88
94
  let hookPort = '8401';
@@ -92,247 +98,107 @@ function setup() {
92
98
  agentId = args[++i];
93
99
  } else if (args[i] === '--db-path' && args[i + 1]) {
94
100
  dbPath = args[++i];
95
- } else if (args[i] === '--no-claude-md') {
96
- skipClaudeMd = true;
101
+ } else if (args[i] === '--no-claude-md' || args[i] === '--no-instructions') {
102
+ skipInstructions = true;
97
103
  } else if (args[i] === '--no-hooks') {
98
104
  skipHooks = true;
99
105
  } else if (args[i] === '--hook-port' && args[i + 1]) {
100
106
  hookPort = args[++i];
101
107
  } else if (args[i] === '--global') {
102
108
  isGlobal = true;
103
- agentId = 'claude'; // unified agent ID for global setup
109
+ } else if (!args[i].startsWith('--')) {
110
+ // Positional arg = target
111
+ target = args[i];
104
112
  }
105
113
  }
106
114
 
107
- // Find the package root (where src/mcp.ts lives)
108
- const packageRoot = resolve(__dirname, '..');
109
- const mcpScript = join(packageRoot, 'src', 'mcp.ts');
110
- const mcpDist = join(packageRoot, 'dist', 'mcp.js');
115
+ // Load adapter
116
+ const { getAdapter } = await import('./adapters/index.js');
117
+ const { buildSetupContext } = await import('./adapters/common.js');
111
118
 
112
- // Determine DB path — default to <awm-root>/data/memory.db (shared across projects)
113
- if (!dbPath) {
114
- dbPath = join(packageRoot, 'data', 'memory.db');
119
+ let adapter;
120
+ try {
121
+ adapter = await getAdapter(target);
122
+ } catch (e: any) {
123
+ console.error(e.message);
124
+ process.exit(1);
115
125
  }
116
- const dbDir = dirname(dbPath);
117
126
 
118
- // Ensure data directory exists
119
- if (!existsSync(dbDir)) {
120
- mkdirSync(dbDir, { recursive: true });
121
- console.log(`Created data directory: ${dbDir}`);
127
+ // Force global for adapters that don't support project scope
128
+ if (!adapter.supportsProjectScope && !isGlobal) {
129
+ isGlobal = true;
122
130
  }
123
131
 
124
- // Generate hook secret (or reuse existing one)
125
- let hookSecret = '';
126
- const secretPath = join(dirname(dbPath!), '.awm-hook-secret');
127
- if (existsSync(secretPath)) {
128
- hookSecret = readFileSync(secretPath, 'utf-8').trim();
129
- }
130
- if (!hookSecret) {
131
- hookSecret = randomBytes(32).toString('hex');
132
- mkdirSync(dirname(secretPath), { recursive: true });
133
- writeFileSync(secretPath, hookSecret + '\n');
134
- }
132
+ // Build context
133
+ const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort });
135
134
 
136
- // Determine command based on platform and whether dist exists
137
- const isWindows = process.platform === 'win32';
138
- const hasDist = existsSync(mcpDist);
135
+ // Run adapter
136
+ const configAction = adapter.writeMcpConfig(ctx);
137
+ const instructionsAction = adapter.writeInstructions(ctx, skipInstructions);
138
+ const hooksAction = adapter.writeHooks(ctx, skipHooks);
139
139
 
140
- const envVars: Record<string, string> = {
141
- AWM_DB_PATH: (isWindows ? dbPath!.replace(/\\/g, '/') : dbPath!),
142
- AWM_AGENT_ID: agentId,
143
- AWM_HOOK_PORT: hookPort,
144
- AWM_HOOK_SECRET: hookSecret,
145
- };
140
+ console.log(`
141
+ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
146
142
 
147
- let mcpConfig: { command: string; args: string[]; env: Record<string, string> };
148
-
149
- if (hasDist) {
150
- mcpConfig = {
151
- command: 'node',
152
- args: [mcpDist.replace(/\\/g, '/')],
153
- env: envVars,
154
- };
155
- } else if (isWindows) {
156
- mcpConfig = {
157
- command: 'cmd',
158
- args: ['/c', 'npx', 'tsx', mcpScript.replace(/\\/g, '/')],
159
- env: envVars,
160
- };
161
- } else {
162
- mcpConfig = {
163
- command: 'npx',
164
- args: ['tsx', mcpScript],
165
- env: envVars,
166
- };
167
- }
143
+ Agent ID: ${ctx.agentId}
144
+ DB path: ${ctx.dbPath}
145
+ ${configAction}
146
+ ${instructionsAction}
147
+ ${hooksAction}
168
148
 
169
- // Read or create .mcp.json
170
- const mcpJsonPath = isGlobal ? join(osHomedir(), '.mcp.json') : join(cwd, '.mcp.json');
171
- let existing: any = { mcpServers: {} };
172
- if (existsSync(mcpJsonPath)) {
173
- try {
174
- existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
175
- if (!existing.mcpServers) existing.mcpServers = {};
176
- } catch {
177
- existing = { mcpServers: {} };
178
- }
179
- }
149
+ Next steps:
150
+ 1. Restart ${adapter.name} to pick up the MCP server
151
+ 2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
152
+ `.trim());
153
+ }
180
154
 
181
- existing.mcpServers['agent-working-memory'] = mcpConfig;
182
- writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
183
-
184
- // Auto-append CLAUDE.md snippet unless --no-claude-md
185
- let claudeMdAction = '';
186
- const claudeMdSnippet = `
187
-
188
- ## Memory (AWM)
189
- You have persistent memory via the agent-working-memory MCP server.
190
-
191
- ### Lifecycle (always do these)
192
- - Session start: call memory_restore to recover previous context
193
- - Starting a task: call memory_task_begin (checkpoints + recalls relevant memories)
194
- - Finishing a task: call memory_task_end with a summary
195
- - Auto-checkpoint: hooks handle compaction, session end, and 15-min timer (no action needed)
196
-
197
- ### Write memory when:
198
- - A project decision is made or changed
199
- - A root cause is discovered after debugging
200
- - A reusable implementation pattern is established
201
- - A user preference, constraint, or requirement is clarified
202
- - A prior assumption is found to be wrong
203
- - A significant piece of work is completed
204
-
205
- ### Recall memory when:
206
- - Starting work on a new task or subsystem
207
- - Re-entering code you haven't touched recently
208
- - After a failed attempt — check if there's prior knowledge
209
- - Before refactoring or making architectural changes
210
- - When a topic comes up that you might have prior context on
211
-
212
- ### Also:
213
- - After using a recalled memory: call memory_feedback (useful/not-useful)
214
- - To correct wrong info: call memory_retract
215
- - To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
216
- `;
217
-
218
- // For global: write to ~/.claude/CLAUDE.md (loaded by Claude Code in every session)
219
- // For project: write to ./CLAUDE.md in the current directory
220
- const claudeMdPath = isGlobal
221
- ? join(osHomedir(), '.claude', 'CLAUDE.md')
222
- : join(cwd, 'CLAUDE.md');
223
-
224
- // Ensure parent directory exists (for ~/.claude/CLAUDE.md)
225
- const claudeMdDir = dirname(claudeMdPath);
226
- if (!existsSync(claudeMdDir)) {
227
- mkdirSync(claudeMdDir, { recursive: true });
228
- }
155
+ // ─── DOCTOR ──────────────────────────────────────
229
156
 
230
- if (skipClaudeMd) {
231
- claudeMdAction = ' CLAUDE.md: skipped (--no-claude-md)';
232
- } else if (existsSync(claudeMdPath)) {
233
- const content = readFileSync(claudeMdPath, 'utf-8');
234
- if (content.includes('## Memory (AWM)')) {
235
- claudeMdAction = ' CLAUDE.md: already has AWM section (skipped)';
236
- } else {
237
- writeFileSync(claudeMdPath, content.trimEnd() + '\n' + claudeMdSnippet);
238
- claudeMdAction = ' CLAUDE.md: appended AWM workflow section';
239
- }
240
- } else {
241
- const title = isGlobal ? '# Global Instructions' : `# ${basename(cwd)}`;
242
- writeFileSync(claudeMdPath, `${title}\n${claudeMdSnippet}`);
243
- claudeMdAction = ' CLAUDE.md: created with AWM workflow section';
244
- }
157
+ async function doctor() {
158
+ const { getAdapter, listAdapters } = await import('./adapters/index.js');
159
+ const { buildSetupContext } = await import('./adapters/common.js');
245
160
 
246
- // --- Hook configuration ---
247
- let hookAction = '';
248
- if (skipHooks) {
249
- hookAction = ' Hooks: skipped (--no-hooks)';
250
- } else {
251
- // Write hooks to Claude Code settings (~/.claude/settings.json)
252
- const settingsPath = join(osHomedir(), '.claude', 'settings.json');
253
- let settings: any = {};
254
- if (existsSync(settingsPath)) {
255
- try {
256
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
257
- } catch {
258
- settings = {};
259
- }
260
- }
161
+ let targets: string[] = [];
162
+ let checkAll = false;
261
163
 
262
- if (!settings.hooks) settings.hooks = {};
263
-
264
- const hookUrl = `http://127.0.0.1:${hookPort}/hooks/checkpoint`;
265
-
266
- // Stop — remind Claude to write/recall/switch tasks after each response
267
- settings.hooks.Stop = [{
268
- matcher: '',
269
- hooks: [{
270
- type: 'command',
271
- command: 'echo "MEMORY: (1) Did you learn anything new? Call memory_write. (2) Are you about to work on a topic you might have prior knowledge about? Call memory_recall. (3) Switching tasks? Call memory_task_begin."',
272
- timeout: 5,
273
- async: true,
274
- }],
275
- }];
276
-
277
- // Build hook command with multi-port fallback for separate memory pools.
278
- // When users have work (port 8401) and personal (port 8402) pools via
279
- // per-folder .mcp.json, the hook needs to try both ports since the global
280
- // settings.json can't know which pool is active in the current session.
281
- const altPort = hookPort === '8401' ? '8402' : '8401';
282
- const hookUrlAlt = `http://127.0.0.1:${altPort}/hooks/checkpoint`;
283
- const buildHookCmd = (event: string, maxTime: number) => {
284
- const primary = `curl -sf -X POST ${hookUrl} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
285
- const fallback = `curl -sf -X POST ${hookUrlAlt} -H "Content-Type: application/json" -H "Authorization: Bearer ${hookSecret}" -d "{\\"hook_event_name\\":\\"${event}\\"}" --max-time ${maxTime}`;
286
- return `${primary} || ${fallback}`;
287
- };
288
-
289
- // PreCompact — auto-checkpoint before context compaction
290
- settings.hooks.PreCompact = [{
291
- matcher: '',
292
- hooks: [{
293
- type: 'command',
294
- command: buildHookCmd('PreCompact', 5),
295
- timeout: 10,
296
- }],
297
- }];
298
-
299
- // SessionEnd — auto-checkpoint on session close (fast timeout to avoid cancellation)
300
- settings.hooks.SessionEnd = [{
301
- matcher: '',
302
- hooks: [{
303
- type: 'command',
304
- command: buildHookCmd('SessionEnd', 2),
305
- timeout: 5,
306
- }],
307
- }];
308
-
309
- // Ensure settings directory exists
310
- const settingsDir = dirname(settingsPath);
311
- if (!existsSync(settingsDir)) {
312
- mkdirSync(settingsDir, { recursive: true });
164
+ for (let i = 1; i < args.length; i++) {
165
+ if (args[i] === '--all') {
166
+ checkAll = true;
167
+ } else if (!args[i].startsWith('--')) {
168
+ targets.push(args[i]);
313
169
  }
170
+ }
314
171
 
315
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
316
- hookAction = ` Hooks: Stop (memory reminder) + PreCompact + SessionEnd → auto-checkpoint (port ${hookPort})`;
172
+ if (checkAll) {
173
+ targets = listAdapters();
174
+ } else if (targets.length === 0) {
175
+ targets = listAdapters();
317
176
  }
318
177
 
319
- const scope = isGlobal ? 'globally (all projects)' : cwd;
320
- console.log(`
321
- AWM configured ${isGlobal ? 'globally' : 'for: ' + cwd}
178
+ const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
322
179
 
323
- Agent ID: ${agentId}
324
- DB path: ${dbPath}
325
- MCP config: ${mcpJsonPath}
326
- Hook port: ${hookPort}
327
- Hook secret: ${hookSecret.slice(0, 8)}...
328
- ${claudeMdAction}
329
- ${hookAction}
180
+ console.log('AWM Doctor\n');
330
181
 
331
- Next steps:
332
- 1. Restart Claude Code to pick up the MCP server
333
- 2. The memory tools will appear automatically
334
- 3. Hooks auto-checkpoint on context compaction and session end${isGlobal ? '\n 4. One brain across all your projects — no per-project setup needed' : ''}
335
- `.trim());
182
+ for (const targetId of targets) {
183
+ let adapter;
184
+ try {
185
+ adapter = await getAdapter(targetId);
186
+ } catch {
187
+ console.log(` ? ${targetId}: unknown target (skipped)`);
188
+ continue;
189
+ }
190
+
191
+ console.log(` ${adapter.name}:`);
192
+ const results = adapter.diagnose(ctx);
193
+ for (const r of results) {
194
+ const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
195
+ console.log(` [${icon}] ${r.check}: ${r.message}`);
196
+ if (r.fix) {
197
+ console.log(` Fix: ${r.fix}`);
198
+ }
199
+ }
200
+ console.log();
201
+ }
336
202
  }
337
203
 
338
204
  // ─── MCP ──────────────────────────────────────
@@ -818,7 +684,10 @@ async function mergeMemories() {
818
684
 
819
685
  switch (command) {
820
686
  case 'setup':
821
- setup();
687
+ await setup();
688
+ break;
689
+ case 'doctor':
690
+ await doctor();
822
691
  break;
823
692
  case 'mcp':
824
693
  mcp();
@@ -0,0 +1,90 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Typed internal event emitter for coordination.
5
+ * Lightweight Node EventEmitter wrapper with typed events.
6
+ * Used to decouple route handlers from side-effects (channel push, logging, etc).
7
+ */
8
+
9
+ import { EventEmitter } from 'events';
10
+
11
+ // ─── Event Payloads ──────────────────────────────────────────────
12
+
13
+ export interface AssignmentCreatedEvent {
14
+ assignmentId: string;
15
+ agentId: string;
16
+ task: string;
17
+ workspace?: string;
18
+ }
19
+
20
+ export interface AssignmentUpdatedEvent {
21
+ assignmentId: string;
22
+ agentId: string | null;
23
+ status: string;
24
+ result?: string;
25
+ }
26
+
27
+ export interface AssignmentCompletedEvent {
28
+ assignmentId: string;
29
+ agentId: string | null;
30
+ result: string | null;
31
+ }
32
+
33
+ export interface AgentCheckinEvent {
34
+ agentId: string;
35
+ name: string;
36
+ role: string;
37
+ workspace?: string;
38
+ }
39
+
40
+ export interface AgentCheckoutEvent {
41
+ agentId: string;
42
+ name: string;
43
+ }
44
+
45
+ export interface SessionStartedEvent {
46
+ agentId: string;
47
+ channelId: string;
48
+ }
49
+
50
+ export interface SessionClosedEvent {
51
+ agentId: string;
52
+ channelId: string;
53
+ }
54
+
55
+ // ─── Event Map ───────────────────────────────────────────────────
56
+
57
+ export interface CoordinationEvents {
58
+ 'assignment.created': [AssignmentCreatedEvent];
59
+ 'assignment.updated': [AssignmentUpdatedEvent];
60
+ 'assignment.completed': [AssignmentCompletedEvent];
61
+ 'agent.checkin': [AgentCheckinEvent];
62
+ 'agent.checkout': [AgentCheckoutEvent];
63
+ 'session.started': [SessionStartedEvent];
64
+ 'session.closed': [SessionClosedEvent];
65
+ }
66
+
67
+ // ─── Typed Event Bus ─────────────────────────────────────────────
68
+
69
+ export class CoordinationEventBus extends EventEmitter {
70
+ emit<K extends keyof CoordinationEvents>(event: K, ...args: CoordinationEvents[K]): boolean {
71
+ return super.emit(event, ...args);
72
+ }
73
+
74
+ on<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
75
+ return super.on(event, listener as (...args: unknown[]) => void);
76
+ }
77
+
78
+ once<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
79
+ return super.once(event, listener as (...args: unknown[]) => void);
80
+ }
81
+
82
+ off<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
83
+ return super.off(event, listener as (...args: unknown[]) => void);
84
+ }
85
+ }
86
+
87
+ /** Create a new coordination event bus. */
88
+ export function createEventBus(): CoordinationEventBus {
89
+ return new CoordinationEventBus();
90
+ }
@@ -7,10 +7,29 @@
7
7
 
8
8
  import type { FastifyInstance } from 'fastify';
9
9
  import type Database from 'better-sqlite3';
10
+ import type { EngramStore } from '../storage/sqlite.js';
10
11
  import { ZodError } from 'zod';
11
12
  import { initCoordinationTables } from './schema.js';
12
13
  import { registerCoordinationRoutes } from './routes.js';
13
- import { cleanSlate } from './stale.js';
14
+ import { cleanSlate, pruneOldHeartbeats, purgeDeadAgents } from './stale.js';
15
+ import { createWriteMutex, needsWriteLock } from './write-mutex.js';
16
+ import { createEventBus, type CoordinationEventBus } from './events.js';
17
+ import { loadPlugins, teardownPlugins } from './plugin-loader.js';
18
+
19
+ export type * from './types.js';
20
+ export { type CoordinationEventBus, type CoordinationEvents } from './events.js';
21
+ export type { AWMPlugin, AWMPluginContext } from './plugin.js';
22
+
23
+ /** Active cleanup intervals — cleared on shutdown. */
24
+ const cleanupIntervals: NodeJS.Timeout[] = [];
25
+
26
+ /** Singleton event bus for this coordination module instance. */
27
+ let coordinationEventBus: CoordinationEventBus | null = null;
28
+
29
+ /** Get the coordination event bus (available after initCoordination). */
30
+ export function getEventBus(): CoordinationEventBus | null {
31
+ return coordinationEventBus;
32
+ }
14
33
 
15
34
  /** Check if coordination is enabled via environment variable. */
16
35
  export function isCoordinationEnabled(): boolean {
@@ -19,15 +38,49 @@ export function isCoordinationEnabled(): boolean {
19
38
  }
20
39
 
21
40
  /** Initialize the coordination module: create tables, clean slate, mount routes, error handler. */
22
- export function initCoordination(app: FastifyInstance, db: Database.Database): void {
41
+ export function initCoordination(app: FastifyInstance, db: Database.Database, store?: EngramStore): void {
23
42
  // Create coordination tables (idempotent)
24
43
  initCoordinationTables(db);
25
44
 
26
45
  // Clean slate: mark stale agents as dead from previous sessions
27
46
  cleanSlate(db);
28
47
 
48
+ // CORS — allow localhost origins only (coordination is local-only)
49
+ app.addHook('onRequest', async (request, reply) => {
50
+ const origin = request.headers.origin ?? '';
51
+ if (/^https?:\/\/localhost(:\d+)?$/.test(origin) || /^https?:\/\/127\.0\.0\.1(:\d+)?$/.test(origin)) {
52
+ reply.header('Access-Control-Allow-Origin', origin);
53
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
54
+ reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
55
+ }
56
+ if (request.method === 'OPTIONS') {
57
+ return reply.code(204).send();
58
+ }
59
+ });
60
+
61
+ // Body size limit — 256KB max for coordination requests (tasks with context can be large)
62
+ app.addHook('onRoute', (routeOptions) => {
63
+ if (!routeOptions.bodyLimit) {
64
+ routeOptions.bodyLimit = 256_000;
65
+ }
66
+ });
67
+
68
+ // Write serialization: serialize POST/PATCH/PUT/DELETE through a mutex
69
+ // to prevent SQLITE_BUSY under 5+ concurrent worker burst
70
+ const writeMutex = createWriteMutex();
71
+ app.addHook('preHandler', async (request, reply) => {
72
+ if (needsWriteLock(request.method, request.url)) {
73
+ const release = await writeMutex.acquire();
74
+ // Release after response is sent (onResponse fires after reply)
75
+ reply.raw.on('finish', release);
76
+ }
77
+ });
78
+
79
+ // Create event bus for decoupled side-effects
80
+ coordinationEventBus = createEventBus();
81
+
29
82
  // Mount all coordination HTTP routes
30
- registerCoordinationRoutes(app, db);
83
+ registerCoordinationRoutes(app, db, store, coordinationEventBus);
31
84
 
32
85
  // ZodError handler — coordination routes use .parse() which throws on invalid params
33
86
  app.setErrorHandler((error: Error & { statusCode?: number }, _request, reply) => {
@@ -43,5 +96,51 @@ export function initCoordination(app: FastifyInstance, db: Database.Database): v
43
96
  });
44
97
  });
45
98
 
99
+ // Periodic cleanup: prune heartbeat events every 30 min, purge dead agents every hour
100
+ cleanupIntervals.push(
101
+ setInterval(() => {
102
+ try { pruneOldHeartbeats(db); } catch { /* db may be closed */ }
103
+ }, 30 * 60 * 1000),
104
+ setInterval(() => {
105
+ try { purgeDeadAgents(db, 24); } catch { /* db may be closed */ }
106
+ }, 60 * 60 * 1000),
107
+ );
108
+
109
+ // Periodic channel liveness probe every 60s — mark unreachable sessions as disconnected
110
+ cleanupIntervals.push(
111
+ setInterval(async () => {
112
+ try {
113
+ const sessions = db.prepare(
114
+ `SELECT agent_id, channel_id FROM coord_channel_sessions WHERE status = 'connected'`
115
+ ).all() as Array<{ agent_id: string; channel_id: string }>;
116
+
117
+ for (const session of sessions) {
118
+ try {
119
+ const res = await fetch(`${session.channel_id}/health`, {
120
+ signal: AbortSignal.timeout(3000),
121
+ });
122
+ if (!res.ok) {
123
+ db.prepare(`UPDATE coord_channel_sessions SET status = 'disconnected' WHERE agent_id = ?`).run(session.agent_id);
124
+ }
125
+ } catch {
126
+ db.prepare(`UPDATE coord_channel_sessions SET status = 'disconnected' WHERE agent_id = ?`).run(session.agent_id);
127
+ }
128
+ }
129
+ } catch { /* db may be closed */ }
130
+ }, 60_000),
131
+ );
132
+
133
+ // Load plugins (async — fire and forget, errors logged per-plugin)
134
+ loadPlugins({ events: coordinationEventBus, db, fastify: app }).catch((err) => {
135
+ console.error(' [plugin] Plugin loader error:', (err as Error).message);
136
+ });
137
+
46
138
  console.log(' Coordination module enabled');
47
139
  }
140
+
141
+ /** Stop periodic cleanup intervals and teardown plugins. Call on server shutdown. */
142
+ export async function stopCoordinationCleanup(): Promise<void> {
143
+ for (const id of cleanupIntervals) clearInterval(id);
144
+ cleanupIntervals.length = 0;
145
+ await teardownPlugins();
146
+ }