agent-working-memory 0.11.0 → 0.12.3

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 (115) hide show
  1. package/README.md +71 -297
  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 +358 -306
  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 +29 -7
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/coordination/routes.d.ts.map +1 -1
  12. package/dist/coordination/routes.js +174 -170
  13. package/dist/coordination/routes.js.map +1 -1
  14. package/dist/core/embeddings.d.ts.map +1 -1
  15. package/dist/core/embeddings.js +4 -1
  16. package/dist/core/embeddings.js.map +1 -1
  17. package/dist/core/entity-extract.d.ts +3 -0
  18. package/dist/core/entity-extract.d.ts.map +1 -0
  19. package/dist/core/entity-extract.js +47 -0
  20. package/dist/core/entity-extract.js.map +1 -0
  21. package/dist/core/format-recall.d.ts +16 -0
  22. package/dist/core/format-recall.d.ts.map +1 -0
  23. package/dist/core/format-recall.js +24 -0
  24. package/dist/core/format-recall.js.map +1 -0
  25. package/dist/core/query-expander.js +1 -1
  26. package/dist/core/query-expander.js.map +1 -1
  27. package/dist/core/reranker.js +1 -1
  28. package/dist/core/reranker.js.map +1 -1
  29. package/dist/core/salience.d.ts.map +1 -1
  30. package/dist/core/salience.js +14 -2
  31. package/dist/core/salience.js.map +1 -1
  32. package/dist/core/whoami.d.ts +24 -0
  33. package/dist/core/whoami.d.ts.map +1 -0
  34. package/dist/core/whoami.js +66 -0
  35. package/dist/core/whoami.js.map +1 -0
  36. package/dist/core/write-pipeline.d.ts +9 -0
  37. package/dist/core/write-pipeline.d.ts.map +1 -1
  38. package/dist/core/write-pipeline.js +109 -68
  39. package/dist/core/write-pipeline.js.map +1 -1
  40. package/dist/core/write-telemetry.d.ts +33 -0
  41. package/dist/core/write-telemetry.d.ts.map +1 -0
  42. package/dist/core/write-telemetry.js +110 -0
  43. package/dist/core/write-telemetry.js.map +1 -0
  44. package/dist/engine/activation.d.ts +22 -12
  45. package/dist/engine/activation.d.ts.map +1 -1
  46. package/dist/engine/activation.js +133 -17
  47. package/dist/engine/activation.js.map +1 -1
  48. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  49. package/dist/engine/consolidation-scheduler.js +1 -1
  50. package/dist/engine/consolidation.d.ts +1 -0
  51. package/dist/engine/consolidation.d.ts.map +1 -1
  52. package/dist/engine/consolidation.js +18 -0
  53. package/dist/engine/consolidation.js.map +1 -1
  54. package/dist/engine/eval.d.ts.map +1 -1
  55. package/dist/engine/eval.js +5 -1
  56. package/dist/engine/eval.js.map +1 -1
  57. package/dist/hooks/sidecar.d.ts +26 -0
  58. package/dist/hooks/sidecar.d.ts.map +1 -1
  59. package/dist/hooks/sidecar.js +30 -0
  60. package/dist/hooks/sidecar.js.map +1 -1
  61. package/dist/index.js +20 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/mcp.d.ts +2 -1
  64. package/dist/mcp.d.ts.map +1 -1
  65. package/dist/mcp.js +222 -108
  66. package/dist/mcp.js.map +1 -1
  67. package/dist/recipes/index.d.ts +57 -0
  68. package/dist/recipes/index.d.ts.map +1 -0
  69. package/dist/recipes/index.js +81 -0
  70. package/dist/recipes/index.js.map +1 -0
  71. package/dist/storage/pglite-schema.d.ts.map +1 -1
  72. package/dist/storage/pglite-schema.js +27 -0
  73. package/dist/storage/pglite-schema.js.map +1 -1
  74. package/dist/storage/pglite.d.ts +5 -0
  75. package/dist/storage/pglite.d.ts.map +1 -1
  76. package/dist/storage/pglite.js +180 -138
  77. package/dist/storage/pglite.js.map +1 -1
  78. package/dist/storage/postgres.d.ts +5 -0
  79. package/dist/storage/postgres.d.ts.map +1 -1
  80. package/dist/storage/postgres.js +180 -138
  81. package/dist/storage/postgres.js.map +1 -1
  82. package/dist/storage/sqlite.d.ts +9 -0
  83. package/dist/storage/sqlite.d.ts.map +1 -1
  84. package/dist/storage/sqlite.js +394 -326
  85. package/dist/storage/sqlite.js.map +1 -1
  86. package/dist/types/engram.d.ts +14 -0
  87. package/dist/types/engram.d.ts.map +1 -1
  88. package/dist/types/engram.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +567 -515
  92. package/src/api/routes.ts +999 -971
  93. package/src/coordination/routes.ts +2155 -2150
  94. package/src/core/embeddings.ts +4 -1
  95. package/src/core/entity-extract.ts +47 -0
  96. package/src/core/format-recall.ts +25 -0
  97. package/src/core/query-expander.ts +1 -1
  98. package/src/core/reranker.ts +1 -1
  99. package/src/core/salience.ts +529 -514
  100. package/src/core/whoami.ts +92 -0
  101. package/src/core/write-pipeline.ts +60 -8
  102. package/src/core/write-telemetry.ts +131 -0
  103. package/src/engine/activation.ts +1468 -1369
  104. package/src/engine/consolidation-scheduler.ts +1 -1
  105. package/src/engine/consolidation.ts +887 -869
  106. package/src/engine/eval.ts +6 -1
  107. package/src/hooks/sidecar.ts +55 -0
  108. package/src/index.ts +248 -227
  109. package/src/mcp.ts +1387 -1270
  110. package/src/recipes/index.ts +125 -0
  111. package/src/storage/pglite-schema.ts +27 -0
  112. package/src/storage/pglite.ts +1420 -1372
  113. package/src/storage/postgres.ts +1523 -1475
  114. package/src/storage/sqlite.ts +1936 -1861
  115. package/src/types/engram.ts +22 -0
@@ -36,8 +36,12 @@ function createChannelMetrics() {
36
36
  */
37
37
  function sessionTokenOk(db, agentId, req) {
38
38
  const provided = req.headers['x-session-token'];
39
- if (!provided)
40
- return true;
39
+ if (!provided) {
40
+ // D2 (2026-07-30): AWM_COORD_REQUIRE_TOKENS=1 closes the omit-the-header
41
+ // bypass. Default stays backward-compatible (registered workers that never
42
+ // send tokens keep working); the network perimeter is the bind+API-key gate.
43
+ return process.env.AWM_COORD_REQUIRE_TOKENS !== '1';
44
+ }
41
45
  const row = db.prepare(`SELECT session_token FROM coord_agents WHERE id = ?`).get(agentId);
42
46
  if (!row || !row.session_token)
43
47
  return true; // not found or no token stored — backward compat
@@ -128,15 +132,15 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
128
132
  coordLog(`${name} reconnected (reusing UUID ${existing.id.slice(0, 8)})`);
129
133
  // Auto-register channel session if channelUrl provided
130
134
  if (channelUrl) {
131
- db.prepare(`
132
- INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
133
- VALUES (?, ?, datetime('now'), 'connected')
134
- ON CONFLICT(agent_id) DO UPDATE SET
135
- channel_id = excluded.channel_id,
136
- connected_at = datetime('now'),
137
- status = 'connected',
138
- push_count = 0,
139
- last_push_at = NULL
135
+ db.prepare(`
136
+ INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
137
+ VALUES (?, ?, datetime('now'), 'connected')
138
+ ON CONFLICT(agent_id) DO UPDATE SET
139
+ channel_id = excluded.channel_id,
140
+ connected_at = datetime('now'),
141
+ status = 'connected',
142
+ push_count = 0,
143
+ last_push_at = NULL
140
144
  `).run(existing.id, channelUrl);
141
145
  coordLog(`channel auto-registered: ${name} (${existing.id.slice(0, 8)}) → ${channelUrl}`);
142
146
  }
@@ -150,15 +154,15 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
150
154
  db.prepare(`INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'registered', ?)`).run(id, `${name} joined as ${role ?? 'worker'}${workspace ? ' [' + workspace + ']' : ''}${capabilities ? ' [' + capabilities.join(', ') + ']' : ''}`);
151
155
  // Auto-register channel session if channelUrl provided
152
156
  if (channelUrl) {
153
- db.prepare(`
154
- INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
155
- VALUES (?, ?, datetime('now'), 'connected')
156
- ON CONFLICT(agent_id) DO UPDATE SET
157
- channel_id = excluded.channel_id,
158
- connected_at = datetime('now'),
159
- status = 'connected',
160
- push_count = 0,
161
- last_push_at = NULL
157
+ db.prepare(`
158
+ INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
159
+ VALUES (?, ?, datetime('now'), 'connected')
160
+ ON CONFLICT(agent_id) DO UPDATE SET
161
+ channel_id = excluded.channel_id,
162
+ connected_at = datetime('now'),
163
+ status = 'connected',
164
+ push_count = 0,
165
+ last_push_at = NULL
162
166
  `).run(id, channelUrl);
163
167
  coordLog(`channel auto-registered: ${name} (${id.slice(0, 8)}) → ${channelUrl}`);
164
168
  }
@@ -266,25 +270,25 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
266
270
  }
267
271
  // Auto-register channel session if channelUrl provided
268
272
  if (channelUrl) {
269
- db.prepare(`
270
- INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
271
- VALUES (?, ?, datetime('now'), 'connected')
272
- ON CONFLICT(agent_id) DO UPDATE SET
273
- channel_id = excluded.channel_id,
274
- connected_at = datetime('now'),
275
- status = 'connected',
276
- push_count = 0,
277
- last_push_at = NULL
273
+ db.prepare(`
274
+ INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
275
+ VALUES (?, ?, datetime('now'), 'connected')
276
+ ON CONFLICT(agent_id) DO UPDATE SET
277
+ channel_id = excluded.channel_id,
278
+ connected_at = datetime('now'),
279
+ status = 'connected',
280
+ push_count = 0,
281
+ last_push_at = NULL
278
282
  `).run(agentId, channelUrl);
279
283
  coordLog(`channel auto-registered via /next: ${name} (${agentId.slice(0, 8)}) → ${channelUrl}`);
280
284
  }
281
285
  // Step 2: Get active commands
282
286
  const activeCommands = workspace
283
- ? db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
284
- FROM coord_commands WHERE cleared_at IS NULL AND (workspace = ? OR workspace IS NULL)
287
+ ? db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
288
+ FROM coord_commands WHERE cleared_at IS NULL AND (workspace = ? OR workspace IS NULL)
285
289
  ORDER BY issued_at DESC`).all(workspace)
286
- : db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
287
- FROM coord_commands WHERE cleared_at IS NULL
290
+ : db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
291
+ FROM coord_commands WHERE cleared_at IS NULL
288
292
  ORDER BY issued_at DESC`).all();
289
293
  // Step 3: Get or auto-claim assignment
290
294
  let assignment = db.prepare(`SELECT * FROM coord_assignments WHERE agent_id = ? AND status IN ('assigned', 'in_progress') ORDER BY created_at DESC LIMIT 1`).get(agentId);
@@ -336,9 +340,9 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
336
340
  // Read current agent status after all mutations
337
341
  const agentRow = db.prepare(`SELECT status FROM coord_agents WHERE id = ?`).get(agentId);
338
342
  // Deliver queued mailbox messages (persistent messages that survived disconnects/restarts)
339
- const mailbox = db.prepare(`SELECT id, message, source, created_at FROM coord_mailbox
340
- WHERE worker_name = ? AND delivered_at IS NULL
341
- AND (workspace = ? OR workspace IS NULL)
343
+ const mailbox = db.prepare(`SELECT id, message, source, created_at FROM coord_mailbox
344
+ WHERE worker_name = ? AND delivered_at IS NULL
345
+ AND (workspace = ? OR workspace IS NULL)
342
346
  ORDER BY created_at ASC LIMIT 10`).all(name, workspace ?? null);
343
347
  if (mailbox.length > 0) {
344
348
  const ids = mailbox.map(m => m.id);
@@ -664,13 +668,13 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
664
668
  }
665
669
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
666
670
  const total = db.prepare(`SELECT COUNT(*) AS count FROM coord_assignments a ${where}`).get(...params).count;
667
- const assignments = db.prepare(`SELECT a.*, g.name AS agent_name,
668
- CASE WHEN a.blocked_by IS NOT NULL AND a.blocked_by NOT IN (SELECT id FROM coord_assignments WHERE status = 'completed')
669
- THEN 1 ELSE 0 END AS is_blocked
670
- FROM coord_assignments a
671
- LEFT JOIN coord_agents g ON a.agent_id = g.id
672
- ${where}
673
- ORDER BY a.priority DESC, a.created_at DESC
671
+ const assignments = db.prepare(`SELECT a.*, g.name AS agent_name,
672
+ CASE WHEN a.blocked_by IS NOT NULL AND a.blocked_by NOT IN (SELECT id FROM coord_assignments WHERE status = 'completed')
673
+ THEN 1 ELSE 0 END AS is_blocked
674
+ FROM coord_assignments a
675
+ LEFT JOIN coord_agents g ON a.agent_id = g.id
676
+ ${where}
677
+ ORDER BY a.priority DESC, a.created_at DESC
674
678
  LIMIT ? OFFSET ?`).all(...params, q.limit, q.offset);
675
679
  return reply.send({ assignments, total });
676
680
  });
@@ -809,8 +813,8 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
809
813
  return reply.send({ ok: true });
810
814
  });
811
815
  app.get('/locks', async (_req, reply) => {
812
- const locks = db.prepare(`SELECT l.file_path, l.agent_id, a.name AS agent_name, l.locked_at, l.reason
813
- FROM coord_locks l JOIN coord_agents a ON l.agent_id = a.id
816
+ const locks = db.prepare(`SELECT l.file_path, l.agent_id, a.name AS agent_name, l.locked_at, l.reason
817
+ FROM coord_locks l JOIN coord_agents a ON l.agent_id = a.id
814
818
  ORDER BY l.locked_at DESC LIMIT 200`).all();
815
819
  return reply.send({ locks });
816
820
  });
@@ -841,11 +845,11 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
841
845
  app.get('/command', async (req, reply) => {
842
846
  const workspace = req.query.workspace;
843
847
  const active = workspace
844
- ? db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
845
- FROM coord_commands WHERE cleared_at IS NULL AND (workspace = ? OR workspace IS NULL)
848
+ ? db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
849
+ FROM coord_commands WHERE cleared_at IS NULL AND (workspace = ? OR workspace IS NULL)
846
850
  ORDER BY issued_at DESC`).all(workspace)
847
- : db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
848
- FROM coord_commands WHERE cleared_at IS NULL
851
+ : db.prepare(`SELECT id, command, reason, issued_by, issued_at, workspace
852
+ FROM coord_commands WHERE cleared_at IS NULL
849
853
  ORDER BY issued_at DESC`).all();
850
854
  if (active.length === 0) {
851
855
  return reply.send({ active: false, commands: [] });
@@ -875,11 +879,11 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
875
879
  const q = commandWaitQuerySchema.safeParse(req.query);
876
880
  const { status: targetStatus, workspace } = q.success ? q.data : { status: 'idle', workspace: undefined };
877
881
  const agents = workspace
878
- ? db.prepare(`SELECT id, name, role, status, current_task, last_seen
879
- FROM coord_agents WHERE status NOT IN ('dead') AND workspace = ?
882
+ ? db.prepare(`SELECT id, name, role, status, current_task, last_seen
883
+ FROM coord_agents WHERE status NOT IN ('dead') AND workspace = ?
880
884
  ORDER BY name`).all(workspace)
881
- : db.prepare(`SELECT id, name, role, status, current_task, last_seen
882
- FROM coord_agents WHERE status NOT IN ('dead')
885
+ : db.prepare(`SELECT id, name, role, status, current_task, last_seen
886
+ FROM coord_agents WHERE status NOT IN ('dead')
883
887
  ORDER BY name`).all();
884
888
  const ready = agents.filter(a => a.status === targetStatus || a.role === 'orchestrator' || a.role === 'coordinator');
885
889
  const notReady = agents.filter(a => a.status !== targetStatus && a.role !== 'orchestrator' && a.role !== 'coordinator');
@@ -898,7 +902,7 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
898
902
  const { agentId, category, severity, filePath, lineNumber, description, suggestion } = parsed.data;
899
903
  if (!sessionTokenOk(db, agentId, req))
900
904
  return reply.code(403).send({ error: 'invalid session token' });
901
- db.prepare(`INSERT INTO coord_findings (agent_id, category, severity, file_path, line_number, description, suggestion)
905
+ db.prepare(`INSERT INTO coord_findings (agent_id, category, severity, file_path, line_number, description, suggestion)
902
906
  VALUES (?, ?, ?, ?, ?, ?, ?)`).run(agentId, category, severity ?? 'info', filePath ?? null, lineNumber ?? null, description, suggestion ?? null);
903
907
  db.prepare(`INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'finding', ?)`).run(agentId, `[${severity ?? 'info'}] ${category}: ${description.slice(0, 100)}`);
904
908
  return reply.code(201).send({ ok: true });
@@ -906,12 +910,12 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
906
910
  app.get('/findings', async (req, reply) => {
907
911
  const q = findingsQuerySchema.safeParse(req.query);
908
912
  const { category, severity, status, limit } = q.success ? q.data : { category: undefined, severity: undefined, status: undefined, limit: 50 };
909
- let sql = `
910
- SELECT f.id, f.category, f.severity, f.file_path, f.line_number,
911
- f.description, f.suggestion, f.status, f.created_at,
912
- a.name AS agent_name
913
- FROM coord_findings f JOIN coord_agents a ON f.agent_id = a.id
914
- WHERE 1=1
913
+ let sql = `
914
+ SELECT f.id, f.category, f.severity, f.file_path, f.line_number,
915
+ f.description, f.suggestion, f.status, f.created_at,
916
+ a.name AS agent_name
917
+ FROM coord_findings f JOIN coord_agents a ON f.agent_id = a.id
918
+ WHERE 1=1
915
919
  `;
916
920
  const params = [];
917
921
  if (category) {
@@ -926,9 +930,9 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
926
930
  sql += ` AND f.status = ?`;
927
931
  params.push(status);
928
932
  }
929
- sql += ` ORDER BY
930
- CASE f.severity WHEN 'critical' THEN 0 WHEN 'error' THEN 1 WHEN 'warn' THEN 2 ELSE 3 END,
931
- f.created_at DESC
933
+ sql += ` ORDER BY
934
+ CASE f.severity WHEN 'critical' THEN 0 WHEN 'error' THEN 1 WHEN 'warn' THEN 2 ELSE 3 END,
935
+ f.created_at DESC
932
936
  LIMIT ?`;
933
937
  params.push(limit);
934
938
  const findings = db.prepare(sql).all(...params);
@@ -978,10 +982,10 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
978
982
  app.get('/decisions', async (req, reply) => {
979
983
  const q = decisionsQuerySchema.safeParse(req.query);
980
984
  const { since_id, assignment_id, workspace, limit } = q.success ? q.data : { since_id: 0, assignment_id: undefined, workspace: undefined, limit: 20 };
981
- let sql = `
982
- SELECT d.id, d.author_id, a.name AS author_name, d.assignment_id, d.tags, d.summary, d.created_at
983
- FROM coord_decisions d JOIN coord_agents a ON d.author_id = a.id
984
- WHERE d.id > ?
985
+ let sql = `
986
+ SELECT d.id, d.author_id, a.name AS author_name, d.assignment_id, d.tags, d.summary, d.created_at
987
+ FROM coord_decisions d JOIN coord_agents a ON d.author_id = a.id
988
+ WHERE d.id > ?
985
989
  `;
986
990
  const params = [since_id];
987
991
  if (assignment_id) {
@@ -1011,30 +1015,30 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1011
1015
  });
1012
1016
  // ─── Status ─────────────────────────────────────────────────────
1013
1017
  app.get('/status', async (_req, reply) => {
1014
- const agents = db.prepare(`SELECT id, name, role, status, current_task, last_seen,
1015
- ROUND((julianday('now') - julianday(last_seen)) * 86400) AS seconds_since_seen
1016
- FROM coord_agents WHERE status != 'dead'
1018
+ const agents = db.prepare(`SELECT id, name, role, status, current_task, last_seen,
1019
+ ROUND((julianday('now') - julianday(last_seen)) * 86400) AS seconds_since_seen
1020
+ FROM coord_agents WHERE status != 'dead'
1017
1021
  ORDER BY role, name LIMIT 200`).all();
1018
- const assignments = db.prepare(`SELECT a.id, a.task, a.description, a.status, a.agent_id, ag.name AS agent_name,
1019
- a.created_at, a.started_at, a.completed_at
1020
- FROM coord_assignments a LEFT JOIN coord_agents ag ON a.agent_id = ag.id
1021
- WHERE a.status NOT IN ('completed', 'failed')
1022
+ const assignments = db.prepare(`SELECT a.id, a.task, a.description, a.status, a.agent_id, ag.name AS agent_name,
1023
+ a.created_at, a.started_at, a.completed_at
1024
+ FROM coord_assignments a LEFT JOIN coord_agents ag ON a.agent_id = ag.id
1025
+ WHERE a.status NOT IN ('completed', 'failed')
1022
1026
  ORDER BY a.created_at LIMIT 200`).all();
1023
- const locks = db.prepare(`SELECT l.file_path, l.agent_id, a.name AS agent_name, l.locked_at, l.reason
1027
+ const locks = db.prepare(`SELECT l.file_path, l.agent_id, a.name AS agent_name, l.locked_at, l.reason
1024
1028
  FROM coord_locks l JOIN coord_agents a ON l.agent_id = a.id LIMIT 200`).all();
1025
- const stats = db.prepare(`SELECT
1026
- (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS alive_agents,
1027
- (SELECT COUNT(*) FROM coord_agents WHERE status = 'working') AS busy_agents,
1028
- (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
1029
- (SELECT COUNT(*) FROM coord_assignments WHERE status IN ('assigned', 'in_progress')) AS active_tasks,
1030
- (SELECT COUNT(*) FROM coord_locks) AS active_locks,
1031
- (SELECT COUNT(*) FROM coord_findings WHERE status = 'open') AS open_findings,
1029
+ const stats = db.prepare(`SELECT
1030
+ (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS alive_agents,
1031
+ (SELECT COUNT(*) FROM coord_agents WHERE status = 'working') AS busy_agents,
1032
+ (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
1033
+ (SELECT COUNT(*) FROM coord_assignments WHERE status IN ('assigned', 'in_progress')) AS active_tasks,
1034
+ (SELECT COUNT(*) FROM coord_locks) AS active_locks,
1035
+ (SELECT COUNT(*) FROM coord_findings WHERE status = 'open') AS open_findings,
1032
1036
  (SELECT COUNT(*) FROM coord_findings WHERE status = 'open' AND severity IN ('critical', 'error')) AS urgent_findings`).get();
1033
- const recentFindings = db.prepare(`SELECT f.id, f.category, f.severity, f.file_path, f.description, a.name AS agent_name, f.created_at
1034
- FROM coord_findings f JOIN coord_agents a ON f.agent_id = a.id
1035
- WHERE f.status = 'open'
1036
- ORDER BY CASE f.severity WHEN 'critical' THEN 0 WHEN 'error' THEN 1 WHEN 'warn' THEN 2 ELSE 3 END,
1037
- f.created_at DESC
1037
+ const recentFindings = db.prepare(`SELECT f.id, f.category, f.severity, f.file_path, f.description, a.name AS agent_name, f.created_at
1038
+ FROM coord_findings f JOIN coord_agents a ON f.agent_id = a.id
1039
+ WHERE f.status = 'open'
1040
+ ORDER BY CASE f.severity WHEN 'critical' THEN 0 WHEN 'error' THEN 1 WHEN 'warn' THEN 2 ELSE 3 END,
1041
+ f.created_at DESC
1038
1042
  LIMIT 10`).all();
1039
1043
  return reply.send({ agents, assignments, locks, stats, recentFindings });
1040
1044
  });
@@ -1049,19 +1053,19 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1049
1053
  // spawns. Channel sessions get probed every 60s (coordination/index.ts:111),
1050
1054
  // so a stale channel-server.js gets status='disconnected' within 60-120s.
1051
1055
  let workers = workspace
1052
- ? db.prepare(`SELECT a.id, a.name, a.role, a.status, a.current_task, a.capabilities, a.workspace, a.last_seen,
1053
- ROUND((julianday('now') - julianday(a.last_seen)) * 86400) AS seconds_since_seen,
1054
- cs.status AS channel_status, cs.last_push_at AS channel_last_push
1055
- FROM coord_agents a
1056
- LEFT JOIN coord_channel_sessions cs ON cs.agent_id = a.id
1057
- WHERE a.status != 'dead' AND a.role NOT IN ('orchestrator', 'coordinator') AND a.workspace = ?
1056
+ ? db.prepare(`SELECT a.id, a.name, a.role, a.status, a.current_task, a.capabilities, a.workspace, a.last_seen,
1057
+ ROUND((julianday('now') - julianday(a.last_seen)) * 86400) AS seconds_since_seen,
1058
+ cs.status AS channel_status, cs.last_push_at AS channel_last_push
1059
+ FROM coord_agents a
1060
+ LEFT JOIN coord_channel_sessions cs ON cs.agent_id = a.id
1061
+ WHERE a.status != 'dead' AND a.role NOT IN ('orchestrator', 'coordinator') AND a.workspace = ?
1058
1062
  ORDER BY a.name LIMIT 200`).all(workspace)
1059
- : db.prepare(`SELECT a.id, a.name, a.role, a.status, a.current_task, a.capabilities, a.workspace, a.last_seen,
1060
- ROUND((julianday('now') - julianday(a.last_seen)) * 86400) AS seconds_since_seen,
1061
- cs.status AS channel_status, cs.last_push_at AS channel_last_push
1062
- FROM coord_agents a
1063
- LEFT JOIN coord_channel_sessions cs ON cs.agent_id = a.id
1064
- WHERE a.status != 'dead' AND a.role NOT IN ('orchestrator', 'coordinator')
1063
+ : db.prepare(`SELECT a.id, a.name, a.role, a.status, a.current_task, a.capabilities, a.workspace, a.last_seen,
1064
+ ROUND((julianday('now') - julianday(a.last_seen)) * 86400) AS seconds_since_seen,
1065
+ cs.status AS channel_status, cs.last_push_at AS channel_last_push
1066
+ FROM coord_agents a
1067
+ LEFT JOIN coord_channel_sessions cs ON cs.agent_id = a.id
1068
+ WHERE a.status != 'dead' AND a.role NOT IN ('orchestrator', 'coordinator')
1065
1069
  ORDER BY a.name LIMIT 200`).all();
1066
1070
  if (capability) {
1067
1071
  workers = workers.filter(w => {
@@ -1126,9 +1130,9 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1126
1130
  }
1127
1131
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1128
1132
  params.push(limit);
1129
- const events = db.prepare(`SELECT e.id, e.agent_id, a.name AS agent_name, e.event_type, e.detail, e.created_at
1130
- FROM coord_events e LEFT JOIN coord_agents a ON e.agent_id = a.id
1131
- ${where}
1133
+ const events = db.prepare(`SELECT e.id, e.agent_id, a.name AS agent_name, e.event_type, e.detail, e.created_at
1134
+ FROM coord_events e LEFT JOIN coord_agents a ON e.agent_id = a.id
1135
+ ${where}
1132
1136
  ORDER BY e.id ASC LIMIT ?`).all(...params);
1133
1137
  const last_id = events.length > 0 ? events[events.length - 1].id : since_id;
1134
1138
  return reply.send({ events, last_id });
@@ -1156,8 +1160,8 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1156
1160
  if (!params.success)
1157
1161
  return reply.code(400).send({ error: params.error.issues[0].message });
1158
1162
  const { id } = params.data;
1159
- const agent = db.prepare(`SELECT id, name, role, status, current_task, pid, capabilities, workspace, metadata, last_seen, started_at,
1160
- ROUND((julianday('now') - julianday(last_seen)) * 86400) AS seconds_since_seen
1163
+ const agent = db.prepare(`SELECT id, name, role, status, current_task, pid, capabilities, workspace, metadata, last_seen, started_at,
1164
+ ROUND((julianday('now') - julianday(last_seen)) * 86400) AS seconds_since_seen
1161
1165
  FROM coord_agents WHERE id = ?`).get(id);
1162
1166
  if (!agent)
1163
1167
  return reply.code(404).send({ error: 'agent not found' });
@@ -1177,7 +1181,7 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1177
1181
  if (agent.status === 'dead')
1178
1182
  return reply.send({ ok: true, action: 'already_dead', agent_name: agent.name });
1179
1183
  // Fail active assignments
1180
- const failedAssignments = db.prepare(`UPDATE coord_assignments SET status = 'failed', result = 'agent killed by coordinator', completed_at = datetime('now')
1184
+ const failedAssignments = db.prepare(`UPDATE coord_assignments SET status = 'failed', result = 'agent killed by coordinator', completed_at = datetime('now')
1181
1185
  WHERE agent_id = ? AND status IN ('assigned', 'in_progress')`).run(id);
1182
1186
  if (failedAssignments.changes > 0) {
1183
1187
  db.prepare(`INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'assignment_failed', ?)`).run(id, `killed: failed ${failedAssignments.changes} active assignment(s)`);
@@ -1210,49 +1214,49 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1210
1214
  }
1211
1215
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
1212
1216
  params.push(limit);
1213
- const timeline = db.prepare(`SELECT e.created_at AS timestamp, a.name AS agent_name, e.event_type, e.detail,
1214
- t.task AS assignment_task
1215
- FROM coord_events e
1216
- LEFT JOIN coord_agents a ON e.agent_id = a.id
1217
- LEFT JOIN coord_assignments t ON a.current_task = t.id
1218
- ${where}
1219
- ORDER BY e.created_at DESC, e.id DESC
1217
+ const timeline = db.prepare(`SELECT e.created_at AS timestamp, a.name AS agent_name, e.event_type, e.detail,
1218
+ t.task AS assignment_task
1219
+ FROM coord_events e
1220
+ LEFT JOIN coord_agents a ON e.agent_id = a.id
1221
+ LEFT JOIN coord_assignments t ON a.current_task = t.id
1222
+ ${where}
1223
+ ORDER BY e.created_at DESC, e.id DESC
1220
1224
  LIMIT ?`).all(...params);
1221
1225
  return reply.send({ timeline });
1222
1226
  });
1223
1227
  // ─── Stats ──────────────────────────────────────────────────────
1224
1228
  app.get('/stats', async (_req, reply) => {
1225
- const workers = db.prepare(`
1226
- SELECT
1227
- COUNT(*) AS total,
1228
- SUM(CASE WHEN status != 'dead' THEN 1 ELSE 0 END) AS alive,
1229
- SUM(CASE WHEN status = 'idle' THEN 1 ELSE 0 END) AS idle,
1230
- SUM(CASE WHEN status = 'working' THEN 1 ELSE 0 END) AS working
1231
- FROM coord_agents
1229
+ const workers = db.prepare(`
1230
+ SELECT
1231
+ COUNT(*) AS total,
1232
+ SUM(CASE WHEN status != 'dead' THEN 1 ELSE 0 END) AS alive,
1233
+ SUM(CASE WHEN status = 'idle' THEN 1 ELSE 0 END) AS idle,
1234
+ SUM(CASE WHEN status = 'working' THEN 1 ELSE 0 END) AS working
1235
+ FROM coord_agents
1232
1236
  `).get();
1233
- const tasks = db.prepare(`
1234
- SELECT
1235
- COUNT(*) AS total_assigned,
1236
- SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1237
- SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
1238
- SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
1239
- AVG(CASE
1240
- WHEN status = 'completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL
1241
- THEN ROUND((julianday(completed_at) - julianday(started_at)) * 86400)
1242
- ELSE NULL
1243
- END) AS avg_completion_seconds
1244
- FROM coord_assignments
1237
+ const tasks = db.prepare(`
1238
+ SELECT
1239
+ COUNT(*) AS total_assigned,
1240
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
1241
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
1242
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
1243
+ AVG(CASE
1244
+ WHEN status = 'completed' AND started_at IS NOT NULL AND completed_at IS NOT NULL
1245
+ THEN ROUND((julianday(completed_at) - julianday(started_at)) * 86400)
1246
+ ELSE NULL
1247
+ END) AS avg_completion_seconds
1248
+ FROM coord_assignments
1245
1249
  `).get();
1246
- const decisions = db.prepare(`
1247
- SELECT
1248
- COALESCE(COUNT(*), 0) AS total,
1249
- COALESCE(SUM(CASE WHEN created_at >= datetime('now', '-1 hour') THEN 1 ELSE 0 END), 0) AS last_hour
1250
- FROM coord_decisions
1250
+ const decisions = db.prepare(`
1251
+ SELECT
1252
+ COALESCE(COUNT(*), 0) AS total,
1253
+ COALESCE(SUM(CASE WHEN created_at >= datetime('now', '-1 hour') THEN 1 ELSE 0 END), 0) AS last_hour
1254
+ FROM coord_decisions
1251
1255
  `).get();
1252
1256
  // Uptime = seconds since the earliest non-dead agent started
1253
- const uptime = db.prepare(`
1254
- SELECT ROUND((julianday('now') - julianday(MIN(started_at))) * 86400) AS uptime_seconds
1255
- FROM coord_agents WHERE status != 'dead'
1257
+ const uptime = db.prepare(`
1258
+ SELECT ROUND((julianday('now') - julianday(MIN(started_at))) * 86400) AS uptime_seconds
1259
+ FROM coord_agents WHERE status != 'dead'
1256
1260
  `).get();
1257
1261
  return reply.send({
1258
1262
  workers,
@@ -1327,11 +1331,11 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1327
1331
  const dbHealthy = store ? store.integrityCheck().ok : true;
1328
1332
  const agents = db.prepare(`SELECT COUNT(*) AS alive FROM coord_agents WHERE status != 'dead'`).get();
1329
1333
  const staleThreshold = 300;
1330
- const staleCount = db.prepare(`SELECT COUNT(*) AS c FROM coord_agents
1331
- WHERE status != 'dead'
1334
+ const staleCount = db.prepare(`SELECT COUNT(*) AS c FROM coord_agents
1335
+ WHERE status != 'dead'
1332
1336
  AND (julianday('now') - julianday(last_seen)) * 86400 > ?`).get(staleThreshold).c;
1333
1337
  const pending = db.prepare(`SELECT COUNT(*) AS c FROM coord_assignments WHERE status IN ('pending', 'assigned', 'in_progress')`).get().c;
1334
- const uptimeRow = db.prepare(`SELECT ROUND((julianday('now') - julianday(MIN(started_at))) * 86400) AS s
1338
+ const uptimeRow = db.prepare(`SELECT ROUND((julianday('now') - julianday(MIN(started_at))) * 86400) AS s
1335
1339
  FROM coord_agents WHERE status != 'dead'`).get();
1336
1340
  // WAL file size and autocheckpoint setting
1337
1341
  let walSizeBytes = null;
@@ -1370,15 +1374,15 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1370
1374
  const agent = db.prepare('SELECT id FROM coord_agents WHERE id = ?').get(agentId);
1371
1375
  if (!agent)
1372
1376
  return reply.status(404).send({ error: 'Agent not found' });
1373
- db.prepare(`
1374
- INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
1375
- VALUES (?, ?, datetime('now'), 'connected')
1376
- ON CONFLICT(agent_id) DO UPDATE SET
1377
- channel_id = excluded.channel_id,
1378
- connected_at = datetime('now'),
1379
- status = 'connected',
1380
- push_count = 0,
1381
- last_push_at = NULL
1377
+ db.prepare(`
1378
+ INSERT INTO coord_channel_sessions (agent_id, channel_id, connected_at, status)
1379
+ VALUES (?, ?, datetime('now'), 'connected')
1380
+ ON CONFLICT(agent_id) DO UPDATE SET
1381
+ channel_id = excluded.channel_id,
1382
+ connected_at = datetime('now'),
1383
+ status = 'connected',
1384
+ push_count = 0,
1385
+ last_push_at = NULL
1382
1386
  `).run(agentId, channelId);
1383
1387
  db.prepare(`INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'channel_register', ?)`).run(agentId, JSON.stringify({ channelId }));
1384
1388
  coordLog(`channel/register: ${agentId} → ${channelId}`);
@@ -1443,9 +1447,9 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1443
1447
  let { agentId } = parsed.data;
1444
1448
  // Role-based addressing — resolve to a concrete agentId
1445
1449
  if (!agentId && parsed.data.role && parsed.data.workspace) {
1446
- const resolved = db.prepare(`SELECT id FROM coord_agents
1447
- WHERE role = ? AND workspace = ? AND status != 'dead'
1448
- ORDER BY last_seen DESC
1450
+ const resolved = db.prepare(`SELECT id FROM coord_agents
1451
+ WHERE role = ? AND workspace = ? AND status != 'dead'
1452
+ ORDER BY last_seen DESC
1449
1453
  LIMIT 1`).get(parsed.data.role, parsed.data.workspace);
1450
1454
  if (!resolved) {
1451
1455
  return reply.status(404).send({
@@ -1491,21 +1495,21 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1491
1495
  });
1492
1496
  /** GET /channel/sessions — List all active channel sessions with agent names. */
1493
1497
  app.get('/channel/sessions', async (_request, reply) => {
1494
- const sessions = db.prepare(`
1495
- SELECT cs.agent_id, a.name AS agent_name, cs.channel_id,
1496
- cs.connected_at, cs.last_push_at, cs.push_count, cs.status
1497
- FROM coord_channel_sessions cs
1498
- JOIN coord_agents a ON a.id = cs.agent_id
1499
- WHERE cs.status = 'connected'
1500
- ORDER BY cs.connected_at DESC
1498
+ const sessions = db.prepare(`
1499
+ SELECT cs.agent_id, a.name AS agent_name, cs.channel_id,
1500
+ cs.connected_at, cs.last_push_at, cs.push_count, cs.status
1501
+ FROM coord_channel_sessions cs
1502
+ JOIN coord_agents a ON a.id = cs.agent_id
1503
+ WHERE cs.status = 'connected'
1504
+ ORDER BY cs.connected_at DESC
1501
1505
  `).all();
1502
1506
  return reply.send({ sessions });
1503
1507
  });
1504
1508
  /** POST /channel/probe — Probe all connected channel sessions, mark dead ones as disconnected. */
1505
1509
  app.post('/channel/probe', async (_request, reply) => {
1506
- const sessions = db.prepare(`SELECT cs.agent_id, a.name AS agent_name, cs.channel_id
1507
- FROM coord_channel_sessions cs
1508
- JOIN coord_agents a ON a.id = cs.agent_id
1510
+ const sessions = db.prepare(`SELECT cs.agent_id, a.name AS agent_name, cs.channel_id
1511
+ FROM coord_channel_sessions cs
1512
+ JOIN coord_agents a ON a.id = cs.agent_id
1509
1513
  WHERE cs.status = 'connected'`).all();
1510
1514
  const results = [];
1511
1515
  for (const session of sessions) {
@@ -1549,12 +1553,12 @@ export function registerCoordinationRoutes(app, db, store, eventBus) {
1549
1553
  * }
1550
1554
  */
1551
1555
  app.get('/telemetry/channels', async (_request, reply) => {
1552
- const perAgent = db.prepare(`
1553
- SELECT a.name AS agent_name, cs.push_count, cs.last_push_at, cs.status,
1554
- cs.connected_at
1555
- FROM coord_channel_sessions cs
1556
- JOIN coord_agents a ON a.id = cs.agent_id
1557
- ORDER BY cs.push_count DESC, cs.connected_at DESC
1556
+ const perAgent = db.prepare(`
1557
+ SELECT a.name AS agent_name, cs.push_count, cs.last_push_at, cs.status,
1558
+ cs.connected_at
1559
+ FROM coord_channel_sessions cs
1560
+ JOIN coord_agents a ON a.id = cs.agent_id
1561
+ ORDER BY cs.push_count DESC, cs.connected_at DESC
1558
1562
  `).all();
1559
1563
  const deliveryRate = channelMetrics.attempts > 0
1560
1564
  ? channelMetrics.delivered / channelMetrics.attempts