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.
- package/README.md +15 -9
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +218 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +4 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +226 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +145 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/cursor.d.ts +4 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +138 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/http.d.ts +4 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +88 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +65 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +4 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +104 -230
- package/dist/cli.js.map +1 -1
- package/dist/coordination/events.d.ts +59 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +28 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/index.d.ts +10 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +87 -3
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/peer-decisions.d.ts +40 -0
- package/dist/coordination/peer-decisions.d.ts.map +1 -0
- package/dist/coordination/peer-decisions.js +82 -0
- package/dist/coordination/peer-decisions.js.map +1 -0
- package/dist/coordination/plugin-loader.d.ts +18 -0
- package/dist/coordination/plugin-loader.d.ts.map +1 -0
- package/dist/coordination/plugin-loader.js +55 -0
- package/dist/coordination/plugin-loader.js.map +1 -0
- package/dist/coordination/plugin.d.ts +40 -0
- package/dist/coordination/plugin.d.ts.map +1 -0
- package/dist/coordination/plugin.js +22 -0
- package/dist/coordination/plugin.js.map +1 -0
- package/dist/coordination/routes.d.ts +2 -1
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +899 -76
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +72 -14
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +84 -3
- package/dist/coordination/schemas.d.ts.map +1 -1
- package/dist/coordination/schemas.js +71 -1
- package/dist/coordination/schemas.js.map +1 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +2 -1
- package/dist/coordination/stale.js.map +1 -1
- package/dist/coordination/types.d.ts +252 -0
- package/dist/coordination/types.d.ts.map +1 -0
- package/dist/coordination/types.js +8 -0
- package/dist/coordination/types.js.map +1 -0
- package/dist/coordination/write-mutex.d.ts +26 -0
- package/dist/coordination/write-mutex.d.ts.map +1 -0
- package/dist/coordination/write-mutex.js +63 -0
- package/dist/coordination/write-mutex.js.map +1 -0
- package/dist/core/embeddings.d.ts +2 -0
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +4 -0
- package/dist/core/embeddings.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +16 -3
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +15 -6
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/retraction.d.ts +3 -1
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +19 -6
- package/dist/engine/retraction.js.map +1 -1
- package/dist/index.js +6 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +52 -3
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +6 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +39 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +234 -0
- package/src/adapters/codex.ts +262 -0
- package/src/adapters/common.ts +172 -0
- package/src/adapters/cursor.ts +150 -0
- package/src/adapters/http.ts +100 -0
- package/src/adapters/index.ts +31 -0
- package/src/adapters/types.ts +75 -0
- package/src/cli.ts +107 -238
- package/src/coordination/events.ts +90 -0
- package/src/coordination/index.ts +102 -3
- package/src/coordination/peer-decisions.ts +105 -0
- package/src/coordination/plugin-loader.ts +60 -0
- package/src/coordination/plugin.ts +44 -0
- package/src/coordination/routes.ts +1176 -105
- package/src/coordination/schema.ts +67 -14
- package/src/coordination/schemas.ts +85 -1
- package/src/coordination/stale.ts +3 -2
- package/src/coordination/types.ts +311 -0
- package/src/coordination/write-mutex.ts +69 -0
- package/src/core/embeddings.ts +5 -0
- package/src/engine/activation.ts +13 -3
- package/src/engine/consolidation.ts +15 -6
- package/src/engine/retraction.ts +22 -6
- package/src/index.ts +6 -15
- package/src/mcp.ts +73 -9
- 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,
|
|
16
|
+
import { resolve, join, dirname } from 'node:path';
|
|
17
17
|
import { execSync } from 'node:child_process';
|
|
18
|
-
import {
|
|
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 [
|
|
49
|
-
|
|
50
|
-
awm mcp Start MCP server (
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
--
|
|
69
|
-
--
|
|
70
|
-
--
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
88
|
+
let target = 'claude-code';
|
|
89
|
+
let agentId: string | undefined;
|
|
84
90
|
let dbPath: string | null = null;
|
|
85
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
} else if (!args[i].startsWith('--')) {
|
|
110
|
+
// Positional arg = target
|
|
111
|
+
target = args[i];
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
//
|
|
108
|
-
const
|
|
109
|
-
const
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
119
|
-
if (!
|
|
120
|
-
|
|
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
|
-
//
|
|
125
|
-
|
|
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
|
-
//
|
|
137
|
-
const
|
|
138
|
-
const
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
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
|
-
|
|
247
|
-
let
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
316
|
-
|
|
172
|
+
if (checkAll) {
|
|
173
|
+
targets = listAdapters();
|
|
174
|
+
} else if (targets.length === 0) {
|
|
175
|
+
targets = listAdapters();
|
|
317
176
|
}
|
|
318
177
|
|
|
319
|
-
const
|
|
320
|
-
console.log(`
|
|
321
|
-
AWM configured ${isGlobal ? 'globally' : 'for: ' + cwd}
|
|
178
|
+
const ctx = buildSetupContext({ isGlobal: true, hookPort: '8401' });
|
|
322
179
|
|
|
323
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
}
|