agent-working-memory 0.7.17 → 0.8.6

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 (188) hide show
  1. package/README.md +156 -15
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +289 -202
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/adapters/http.js +22 -22
  6. package/dist/api/routes.d.ts +1 -1
  7. package/dist/api/routes.d.ts.map +1 -1
  8. package/dist/api/routes.js +258 -79
  9. package/dist/api/routes.js.map +1 -1
  10. package/dist/cli/migrate.d.ts +42 -0
  11. package/dist/cli/migrate.d.ts.map +1 -0
  12. package/dist/cli/migrate.js +268 -0
  13. package/dist/cli/migrate.js.map +1 -0
  14. package/dist/cli.js +147 -103
  15. package/dist/cli.js.map +1 -1
  16. package/dist/coordination/circuit-breaker.d.ts +24 -0
  17. package/dist/coordination/circuit-breaker.d.ts.map +1 -0
  18. package/dist/coordination/circuit-breaker.js +70 -0
  19. package/dist/coordination/circuit-breaker.js.map +1 -0
  20. package/dist/coordination/failure-modes.d.ts +21 -0
  21. package/dist/coordination/failure-modes.d.ts.map +1 -0
  22. package/dist/coordination/failure-modes.js +49 -0
  23. package/dist/coordination/failure-modes.js.map +1 -0
  24. package/dist/coordination/peer-decisions.js +7 -7
  25. package/dist/coordination/routes.d.ts.map +1 -1
  26. package/dist/coordination/routes.js +44 -1
  27. package/dist/coordination/routes.js.map +1 -1
  28. package/dist/coordination/schema.d.ts.map +1 -1
  29. package/dist/coordination/schema.js +18 -0
  30. package/dist/coordination/schema.js.map +1 -1
  31. package/dist/coordination/schemas.d.ts +9 -9
  32. package/dist/coordination/stale.d.ts +9 -1
  33. package/dist/coordination/stale.d.ts.map +1 -1
  34. package/dist/coordination/stale.js +53 -9
  35. package/dist/coordination/stale.js.map +1 -1
  36. package/dist/core/embeddings.d.ts +16 -10
  37. package/dist/core/embeddings.d.ts.map +1 -1
  38. package/dist/core/embeddings.js +54 -38
  39. package/dist/core/embeddings.js.map +1 -1
  40. package/dist/core/ml-worker-entry.d.ts +17 -0
  41. package/dist/core/ml-worker-entry.d.ts.map +1 -0
  42. package/dist/core/ml-worker-entry.js +193 -0
  43. package/dist/core/ml-worker-entry.js.map +1 -0
  44. package/dist/core/ml-worker.d.ts +59 -0
  45. package/dist/core/ml-worker.d.ts.map +1 -0
  46. package/dist/core/ml-worker.js +253 -0
  47. package/dist/core/ml-worker.js.map +1 -0
  48. package/dist/core/query-expander.d.ts +13 -15
  49. package/dist/core/query-expander.d.ts.map +1 -1
  50. package/dist/core/query-expander.js +48 -54
  51. package/dist/core/query-expander.js.map +1 -1
  52. package/dist/core/reranker.d.ts +9 -19
  53. package/dist/core/reranker.d.ts.map +1 -1
  54. package/dist/core/reranker.js +33 -35
  55. package/dist/core/reranker.js.map +1 -1
  56. package/dist/core/salience.d.ts +32 -5
  57. package/dist/core/salience.d.ts.map +1 -1
  58. package/dist/core/salience.js +201 -28
  59. package/dist/core/salience.js.map +1 -1
  60. package/dist/core/write-pipeline.d.ts +10 -2
  61. package/dist/core/write-pipeline.d.ts.map +1 -1
  62. package/dist/core/write-pipeline.js +218 -49
  63. package/dist/core/write-pipeline.js.map +1 -1
  64. package/dist/engine/activation.d.ts +2 -2
  65. package/dist/engine/activation.d.ts.map +1 -1
  66. package/dist/engine/activation.js +238 -115
  67. package/dist/engine/activation.js.map +1 -1
  68. package/dist/engine/confidence.d.ts +62 -0
  69. package/dist/engine/confidence.d.ts.map +1 -0
  70. package/dist/engine/confidence.js +100 -0
  71. package/dist/engine/confidence.js.map +1 -0
  72. package/dist/engine/connections.d.ts +47 -8
  73. package/dist/engine/connections.d.ts.map +1 -1
  74. package/dist/engine/connections.js +83 -22
  75. package/dist/engine/connections.js.map +1 -1
  76. package/dist/engine/consolidation-scheduler.d.ts +46 -10
  77. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  78. package/dist/engine/consolidation-scheduler.js +170 -50
  79. package/dist/engine/consolidation-scheduler.js.map +1 -1
  80. package/dist/engine/consolidation.d.ts +16 -2
  81. package/dist/engine/consolidation.d.ts.map +1 -1
  82. package/dist/engine/consolidation.js +143 -44
  83. package/dist/engine/consolidation.js.map +1 -1
  84. package/dist/engine/eval.d.ts +2 -2
  85. package/dist/engine/eval.d.ts.map +1 -1
  86. package/dist/engine/eval.js +9 -9
  87. package/dist/engine/eval.js.map +1 -1
  88. package/dist/engine/eviction.d.ts +4 -4
  89. package/dist/engine/eviction.d.ts.map +1 -1
  90. package/dist/engine/eviction.js +15 -15
  91. package/dist/engine/eviction.js.map +1 -1
  92. package/dist/engine/retraction.d.ts +93 -5
  93. package/dist/engine/retraction.d.ts.map +1 -1
  94. package/dist/engine/retraction.js +238 -19
  95. package/dist/engine/retraction.js.map +1 -1
  96. package/dist/engine/staging.d.ts +1 -1
  97. package/dist/engine/staging.d.ts.map +1 -1
  98. package/dist/engine/staging.js +3 -3
  99. package/dist/engine/staging.js.map +1 -1
  100. package/dist/hooks/sidecar.d.ts +1 -1
  101. package/dist/hooks/sidecar.d.ts.map +1 -1
  102. package/dist/hooks/sidecar.js +4 -4
  103. package/dist/hooks/sidecar.js.map +1 -1
  104. package/dist/index.js +48 -31
  105. package/dist/index.js.map +1 -1
  106. package/dist/mcp.js +167 -141
  107. package/dist/mcp.js.map +1 -1
  108. package/dist/storage/factory.d.ts +55 -0
  109. package/dist/storage/factory.d.ts.map +1 -0
  110. package/dist/storage/factory.js +135 -0
  111. package/dist/storage/factory.js.map +1 -0
  112. package/dist/storage/pglite-schema.d.ts +20 -0
  113. package/dist/storage/pglite-schema.d.ts.map +1 -0
  114. package/dist/storage/pglite-schema.js +165 -0
  115. package/dist/storage/pglite-schema.js.map +1 -0
  116. package/dist/storage/pglite.d.ts +187 -0
  117. package/dist/storage/pglite.d.ts.map +1 -0
  118. package/dist/storage/pglite.js +1114 -0
  119. package/dist/storage/pglite.js.map +1 -0
  120. package/dist/storage/sqlite.d.ts +117 -0
  121. package/dist/storage/sqlite.d.ts.map +1 -1
  122. package/dist/storage/sqlite.js +357 -8
  123. package/dist/storage/sqlite.js.map +1 -1
  124. package/dist/storage/store.d.ts +63 -0
  125. package/dist/storage/store.d.ts.map +1 -0
  126. package/dist/storage/store.js +27 -0
  127. package/dist/storage/store.js.map +1 -0
  128. package/dist/types/engram.d.ts +92 -2
  129. package/dist/types/engram.d.ts.map +1 -1
  130. package/dist/types/engram.js.map +1 -1
  131. package/package.json +3 -1
  132. package/src/adapters/claude-code.ts +218 -218
  133. package/src/adapters/codex.ts +252 -252
  134. package/src/adapters/common.ts +498 -411
  135. package/src/adapters/cursor.ts +135 -135
  136. package/src/adapters/http.ts +93 -93
  137. package/src/adapters/index.ts +31 -31
  138. package/src/adapters/types.ts +75 -75
  139. package/src/api/index.ts +3 -3
  140. package/src/api/routes.ts +970 -714
  141. package/src/cli/migrate.ts +307 -0
  142. package/src/cli.ts +755 -719
  143. package/src/coordination/circuit-breaker.ts +83 -0
  144. package/src/coordination/events.ts +90 -90
  145. package/src/coordination/failure-modes.ts +50 -0
  146. package/src/coordination/peer-decisions.ts +105 -105
  147. package/src/coordination/plugin-loader.ts +60 -60
  148. package/src/coordination/plugin.ts +44 -44
  149. package/src/coordination/routes.ts +56 -1
  150. package/src/coordination/schema.ts +13 -0
  151. package/src/coordination/stale.ts +80 -11
  152. package/src/coordination/types.ts +311 -311
  153. package/src/coordination/write-mutex.ts +69 -69
  154. package/src/core/auto-tagger.ts +168 -168
  155. package/src/core/decay.ts +63 -63
  156. package/src/core/embeddings.ts +110 -93
  157. package/src/core/index.ts +5 -5
  158. package/src/core/logger.ts +36 -36
  159. package/src/core/ml-worker-entry.ts +194 -0
  160. package/src/core/ml-worker.ts +281 -0
  161. package/src/core/query-expander.ts +122 -128
  162. package/src/core/reranker.ts +119 -125
  163. package/src/core/salience.ts +200 -33
  164. package/src/core/write-pipeline.ts +521 -343
  165. package/src/engine/activation.ts +235 -109
  166. package/src/engine/confidence.ts +120 -0
  167. package/src/engine/connections.ts +162 -103
  168. package/src/engine/consolidation-scheduler.ts +242 -125
  169. package/src/engine/consolidation.ts +138 -45
  170. package/src/engine/eval.ts +102 -102
  171. package/src/engine/eviction.ts +101 -101
  172. package/src/engine/index.ts +8 -8
  173. package/src/engine/retraction.ts +366 -116
  174. package/src/engine/staging.ts +74 -74
  175. package/src/hooks/sidecar.ts +5 -5
  176. package/src/index.ts +226 -212
  177. package/src/mcp.ts +1145 -1121
  178. package/src/storage/factory.ts +147 -0
  179. package/src/storage/index.ts +3 -3
  180. package/src/storage/pglite-schema.ts +166 -0
  181. package/src/storage/pglite.ts +1363 -0
  182. package/src/storage/sqlite.ts +402 -7
  183. package/src/storage/store.ts +80 -0
  184. package/src/types/agent.ts +67 -67
  185. package/src/types/checkpoint.ts +46 -46
  186. package/src/types/engram.ts +101 -3
  187. package/src/types/eval.ts +100 -100
  188. package/src/types/index.ts +6 -6
@@ -1,44 +1,44 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * AWM Plugin contract.
5
- *
6
- * Plugins extend coordination with custom behavior by subscribing to
7
- * events, adding routes, or querying the DB. They are loaded at startup
8
- * via AWM_PLUGINS env var (comma-separated module paths).
9
- *
10
- * Example plugin:
11
- * import type { AWMPlugin } from 'agent-working-memory';
12
- * export default {
13
- * name: 'my-plugin',
14
- * register(ctx) {
15
- * ctx.events.on('assignment.completed', (evt) => {
16
- * console.log(`Task done: ${evt.assignmentId}`);
17
- * });
18
- * },
19
- * } satisfies AWMPlugin;
20
- */
21
-
22
- import type { FastifyInstance } from 'fastify';
23
- import type Database from 'better-sqlite3';
24
- import type { CoordinationEventBus } from './events.js';
25
-
26
- /** Context passed to plugin register(). */
27
- export interface AWMPluginContext {
28
- /** Typed event bus — subscribe to coordination events. */
29
- events: CoordinationEventBus;
30
- /** Raw better-sqlite3 database handle (coordination tables). */
31
- db: Database.Database;
32
- /** Fastify instance — add custom routes if needed. */
33
- fastify: FastifyInstance;
34
- }
35
-
36
- /** Plugin contract. Every AWM plugin must export this shape. */
37
- export interface AWMPlugin {
38
- /** Unique plugin name (for logging and dedup). */
39
- name: string;
40
- /** Called once at startup with the plugin context. */
41
- register(ctx: AWMPluginContext): void | Promise<void>;
42
- /** Optional cleanup on shutdown. */
43
- teardown?(): void | Promise<void>;
44
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * AWM Plugin contract.
5
+ *
6
+ * Plugins extend coordination with custom behavior by subscribing to
7
+ * events, adding routes, or querying the DB. They are loaded at startup
8
+ * via AWM_PLUGINS env var (comma-separated module paths).
9
+ *
10
+ * Example plugin:
11
+ * import type { AWMPlugin } from 'agent-working-memory';
12
+ * export default {
13
+ * name: 'my-plugin',
14
+ * register(ctx) {
15
+ * ctx.events.on('assignment.completed', (evt) => {
16
+ * console.log(`Task done: ${evt.assignmentId}`);
17
+ * });
18
+ * },
19
+ * } satisfies AWMPlugin;
20
+ */
21
+
22
+ import type { FastifyInstance } from 'fastify';
23
+ import type Database from 'better-sqlite3';
24
+ import type { CoordinationEventBus } from './events.js';
25
+
26
+ /** Context passed to plugin register(). */
27
+ export interface AWMPluginContext {
28
+ /** Typed event bus — subscribe to coordination events. */
29
+ events: CoordinationEventBus;
30
+ /** Raw better-sqlite3 database handle (coordination tables). */
31
+ db: Database.Database;
32
+ /** Fastify instance — add custom routes if needed. */
33
+ fastify: FastifyInstance;
34
+ }
35
+
36
+ /** Plugin contract. Every AWM plugin must export this shape. */
37
+ export interface AWMPlugin {
38
+ /** Unique plugin name (for logging and dedup). */
39
+ name: string;
40
+ /** Called once at startup with the plugin context. */
41
+ register(ctx: AWMPluginContext): void | Promise<void>;
42
+ /** Optional cleanup on shutdown. */
43
+ teardown?(): void | Promise<void>;
44
+ }
@@ -21,7 +21,9 @@ import {
21
21
  agentIdParamSchema, timelineQuerySchema,
22
22
  channelRegisterSchema, channelDeregisterSchema, channelPushSchema,
23
23
  } from './schemas.js';
24
- import { detectStale, cleanupStale } from './stale.js';
24
+ import { detectStale, cleanupStale, retryOrFailAssignment } from './stale.js';
25
+ import { classifyFailure, FailureMode } from './failure-modes.js';
26
+ import { recordSuccess, recordFailure as circuitRecordFailure, isAvailable } from './circuit-breaker.js';
25
27
 
26
28
  /** Pretty timestamp for coordination logs. */
27
29
  function ts(): string {
@@ -444,6 +446,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
444
446
  `SELECT * FROM coord_assignments WHERE status = 'pending' AND agent_id IS NULL ${blockedFilter} ORDER BY priority DESC, created_at ASC LIMIT 1`
445
447
  ).get() as { id: string } | undefined);
446
448
 
449
+ // Circuit breaker: refuse assignment if worker is in open state
450
+ if (pending && !isAvailable(db, agentId)) {
451
+ return reply.code(423).send({ status: 'idle', assignment: null, circuit_open: true, reason: 'circuit_open' });
452
+ }
453
+
447
454
  if (pending) {
448
455
  const claimed = db.prepare(
449
456
  `UPDATE coord_assignments SET agent_id = ?, status = 'assigned', started_at = datetime('now') WHERE id = ? AND status = 'pending'`
@@ -724,6 +731,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
724
731
  `SELECT * FROM coord_assignments WHERE status = 'pending' AND agent_id IS NULL ${blockedFilter} ORDER BY priority DESC, created_at ASC LIMIT 1`
725
732
  ).get() as { id: string } | undefined);
726
733
 
734
+ // Circuit breaker: refuse assignment if worker is in open state
735
+ if (pending && !isAvailable(db, agentId)) {
736
+ return reply.code(423).send({ assignment: null, circuit_open: true, reason: 'circuit_open' });
737
+ }
738
+
727
739
  if (pending) {
728
740
  const claimed = db.prepare(
729
741
  `UPDATE coord_assignments SET agent_id = ?, status = 'assigned', started_at = datetime('now') WHERE id = ? AND status = 'pending'`
@@ -847,6 +859,12 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
847
859
  coordLog(`${assignInfo?.agent_name ?? 'unknown'} ${status}: ${assignInfo?.task?.slice(0, 80) ?? id}`);
848
860
  }
849
861
 
862
+ // Circuit breaker: track success/failure per worker
863
+ if (assignInfo?.agent_id) {
864
+ if (status === 'completed') recordSuccess(db, assignInfo.agent_id);
865
+ else if (status === 'failed') circuitRecordFailure(db, assignInfo.agent_id);
866
+ }
867
+
850
868
  // Emit events
851
869
  eventBus?.emit('assignment.updated', { assignmentId: id, agentId: assignInfo?.agent_id ?? null, status, result });
852
870
  if (status === 'completed') {
@@ -1011,6 +1029,38 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
1011
1029
  return reply.send({ ok: true });
1012
1030
  });
1013
1031
 
1032
+ /** POST /assignment/:id/fail — Worker voluntarily fails an assignment with retry logic.
1033
+ * Body: { result: string, mode?: FailureMode }
1034
+ * Returns: { outcome: 'retried' | 'failed', attempt_count, last_failure_mode }
1035
+ */
1036
+ app.post('/assignment/:id/fail', async (req, reply) => {
1037
+ const { id } = assignmentIdParamSchema.parse(req.params);
1038
+ const body = req.body as { result?: string; mode?: string } | undefined;
1039
+ const result = body?.result ?? 'worker-initiated failure';
1040
+ const mode = body?.mode as FailureMode | undefined;
1041
+
1042
+ const row = db.prepare(
1043
+ `SELECT id, agent_id, status FROM coord_assignments WHERE id = ?`
1044
+ ).get(id) as { id: string; agent_id: string | null; status: string } | undefined;
1045
+
1046
+ if (!row) return reply.code(404).send({ error: 'assignment not found' });
1047
+ if (['completed', 'failed'].includes(row.status)) {
1048
+ return reply.code(400).send({ error: `cannot fail a ${row.status} assignment` });
1049
+ }
1050
+
1051
+ const agentId = row.agent_id ?? 'unknown';
1052
+ const outcome = retryOrFailAssignment(db, id, agentId, result, mode);
1053
+
1054
+ // Circuit breaker: record failure on voluntary fail
1055
+ if (agentId !== 'unknown') circuitRecordFailure(db, agentId);
1056
+
1057
+ const updated = db.prepare(
1058
+ `SELECT attempt_count, last_failure_mode FROM coord_assignments WHERE id = ?`
1059
+ ).get(id) as { attempt_count: number; last_failure_mode: string | null } | undefined;
1060
+
1061
+ return reply.send({ ok: true, outcome, attempt_count: updated?.attempt_count ?? 0, last_failure_mode: updated?.last_failure_mode ?? null });
1062
+ });
1063
+
1014
1064
  app.put('/assignment/:id', async (req, reply) => {
1015
1065
  const { id } = assignmentIdParamSchema.parse(req.params);
1016
1066
  const parsed = assignmentUpdateSchema.safeParse(req.body);
@@ -1961,6 +2011,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
1961
2011
  const agent = db.prepare(`SELECT name, workspace FROM coord_agents WHERE id = ?`).get(agentId) as { name: string; workspace: string | null } | undefined;
1962
2012
  if (!agent) return reply.status(404).send({ error: 'Agent not found' });
1963
2013
 
2014
+ // Circuit breaker: refuse push to open-circuit workers
2015
+ if (!isAvailable(db, agentId)) {
2016
+ return reply.status(423).send({ error: 'circuit_open', reason: 'Worker circuit is open — too many consecutive failures. Try again after 30s or after a successful assignment.' });
2017
+ }
2018
+
1964
2019
  // Try live channel delivery first
1965
2020
  const session = db.prepare(
1966
2021
  `SELECT agent_id, channel_id FROM coord_channel_sessions WHERE agent_id = ? AND status = 'connected'`
@@ -130,6 +130,15 @@ CREATE TABLE IF NOT EXISTS coord_events (
130
130
  detail TEXT,
131
131
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
132
132
  );
133
+
134
+ -- Coordination: per-worker circuit breaker state (AWM 0.8.1)
135
+ CREATE TABLE IF NOT EXISTS coord_circuit_state (
136
+ agent_id TEXT PRIMARY KEY REFERENCES coord_agents(id),
137
+ state TEXT NOT NULL DEFAULT 'closed',
138
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
139
+ opened_at TEXT NULL,
140
+ last_transition_at TEXT NOT NULL DEFAULT (datetime('now'))
141
+ );
133
142
  `;
134
143
 
135
144
  /**
@@ -170,4 +179,8 @@ export function initCoordinationTables(db: Database.Database): void {
170
179
 
171
180
  // Migration: session token for hijack prevention (added 2026-03-26)
172
181
  try { db.exec(`ALTER TABLE coord_agents ADD COLUMN session_token TEXT`); } catch { /* exists */ }
182
+
183
+ // Migrations: control-layer columns for retry tracking (AWM 0.8.1)
184
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0`); } catch { /* exists */ }
185
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN last_failure_mode TEXT`); } catch { /* exists */ }
173
186
  }
@@ -5,6 +5,8 @@
5
5
  */
6
6
 
7
7
  import type Database from 'better-sqlite3';
8
+ import { classifyFailure, FailureMode, MUTATION_HINTS } from './failure-modes.js';
9
+ import { recordFailure as circuitRecordFailure } from './circuit-breaker.js';
8
10
 
9
11
  interface StaleAgent {
10
12
  id: string;
@@ -26,22 +28,89 @@ export function detectStale(db: Database.Database, thresholdSeconds: number): St
26
28
  ).all(thresholdSeconds) as StaleAgent[];
27
29
  }
28
30
 
29
- /** Clean up stale agents: fail assignments, release locks, mark dead. */
31
+ /** Maximum retry attempts before an assignment is permanently failed. */
32
+ const MAX_RETRY_ATTEMPTS = 3;
33
+
34
+ /**
35
+ * Retry or permanently fail a single assignment.
36
+ * Called from cleanupStale (stale path) and POST /assignment/:id/fail (voluntary path).
37
+ *
38
+ * @returns 'retried' | 'failed'
39
+ */
40
+ export function retryOrFailAssignment(
41
+ db: Database.Database,
42
+ assignmentId: string,
43
+ agentId: string,
44
+ failureResult: string,
45
+ mode?: FailureMode,
46
+ ): 'retried' | 'failed' {
47
+ const row = db.prepare(
48
+ `SELECT attempt_count, description FROM coord_assignments WHERE id = ?`
49
+ ).get(assignmentId) as { attempt_count: number; description: string | null } | undefined;
50
+
51
+ const attemptCount = row?.attempt_count ?? 0;
52
+ const failureMode = mode ?? classifyFailure(failureResult);
53
+
54
+ if (attemptCount < MAX_RETRY_ATTEMPTS) {
55
+ const hint = MUTATION_HINTS[failureMode];
56
+ const nextAttempt = attemptCount + 1;
57
+ const hintBlock = `\n\n--- RETRY HINT (attempt ${nextAttempt}) ---\n${hint}`;
58
+ const newDescription = (row?.description ?? '') + hintBlock;
59
+
60
+ db.prepare(
61
+ `UPDATE coord_assignments
62
+ SET status = 'pending', agent_id = NULL, started_at = NULL, result = NULL,
63
+ attempt_count = ?, last_failure_mode = ?, description = ?
64
+ WHERE id = ?`
65
+ ).run(nextAttempt, failureMode, newDescription, assignmentId);
66
+
67
+ db.prepare(
68
+ `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'assignment_retry', ?)`
69
+ ).run(agentId, `attempt ${nextAttempt}: ${failureMode} — ${failureResult.slice(0, 200)}`);
70
+
71
+ return 'retried';
72
+ } else {
73
+ db.prepare(
74
+ `UPDATE coord_assignments
75
+ SET status = 'failed', result = ?, last_failure_mode = ?, completed_at = datetime('now')
76
+ WHERE id = ?`
77
+ ).run(failureResult, failureMode, assignmentId);
78
+
79
+ db.prepare(
80
+ `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'assignment_failed', ?)`
81
+ ).run(agentId, `permanently failed after ${attemptCount} attempt(s): ${failureResult.slice(0, 200)}`);
82
+
83
+ return 'failed';
84
+ }
85
+ }
86
+
87
+ /** Clean up stale agents: retry or fail assignments, release locks, mark dead. */
30
88
  export function cleanupStale(db: Database.Database, thresholdSeconds: number): { stale: StaleAgent[]; cleaned: number } {
31
89
  const stale = detectStale(db, thresholdSeconds);
32
90
  let cleaned = 0;
33
91
 
34
92
  for (const agent of stale) {
35
- // Fail active assignments
36
- const orphaned = db.prepare(
37
- `UPDATE coord_assignments SET status = 'failed', result = 'agent disconnected (stale)', completed_at = datetime('now')
38
- WHERE agent_id = ? AND status IN ('assigned', 'in_progress')`
39
- ).run(agent.id);
93
+ // Handle each orphaned assignment with retry logic
94
+ const orphanedRows = db.prepare(
95
+ `SELECT id FROM coord_assignments WHERE agent_id = ? AND status IN ('assigned', 'in_progress')`
96
+ ).all(agent.id) as Array<{ id: string }>;
97
+
98
+ let retried = 0;
99
+ let failed = 0;
100
+
101
+ for (const row of orphanedRows) {
102
+ const outcome = retryOrFailAssignment(db, row.id, agent.id, 'agent disconnected (stale)');
103
+ if (outcome === 'retried') retried++;
104
+ else failed++;
105
+ }
40
106
 
41
- if (orphaned.changes > 0) {
107
+ if (orphanedRows.length > 0) {
42
108
  db.prepare(
43
109
  `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'assignment_failed', ?)`
44
- ).run(agent.id, `auto-failed ${orphaned.changes} orphaned assignment(s) — agent stale`);
110
+ ).run(agent.id, `stale cleanup: retried ${retried}, permanently failed ${failed}`);
111
+
112
+ // Record failure in circuit breaker for each orphaned assignment
113
+ circuitRecordFailure(db, agent.id);
45
114
  }
46
115
 
47
116
  // Release locks
@@ -50,12 +119,12 @@ export function cleanupStale(db: Database.Database, thresholdSeconds: number): {
50
119
  // Mark dead
51
120
  db.prepare(`UPDATE coord_agents SET status = 'dead', current_task = NULL WHERE id = ?`).run(agent.id);
52
121
 
53
- cleaned += orphaned.changes + locks.changes;
122
+ cleaned += orphanedRows.length + locks.changes;
54
123
 
55
- if (orphaned.changes > 0 || locks.changes > 0) {
124
+ if (orphanedRows.length > 0 || locks.changes > 0) {
56
125
  db.prepare(
57
126
  `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'stale_cleanup', ?)`
58
- ).run(agent.id, `failed ${orphaned.changes} assignment(s), released ${locks.changes} lock(s)`);
127
+ ).run(agent.id, `failed ${orphanedRows.length} assignment(s), released ${locks.changes} lock(s)`);
59
128
  }
60
129
  }
61
130