@cleocode/core 2026.3.43 → 2026.3.45

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 (183) hide show
  1. package/dist/admin/export-tasks.d.ts.map +1 -1
  2. package/dist/admin/import-tasks.d.ts +10 -2
  3. package/dist/admin/import-tasks.d.ts.map +1 -1
  4. package/dist/agents/agent-schema.d.ts +358 -0
  5. package/dist/agents/agent-schema.d.ts.map +1 -0
  6. package/dist/agents/capacity.d.ts +57 -0
  7. package/dist/agents/capacity.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +17 -0
  9. package/dist/agents/index.d.ts.map +1 -0
  10. package/dist/agents/registry.d.ts +115 -0
  11. package/dist/agents/registry.d.ts.map +1 -0
  12. package/dist/agents/retry.d.ts +83 -0
  13. package/dist/agents/retry.d.ts.map +1 -0
  14. package/dist/hooks/index.d.ts +4 -1
  15. package/dist/hooks/index.d.ts.map +1 -1
  16. package/dist/hooks/payload-schemas.d.ts +214 -0
  17. package/dist/hooks/payload-schemas.d.ts.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +16937 -2371
  21. package/dist/index.js.map +4 -4
  22. package/dist/inject/index.d.ts.map +1 -1
  23. package/dist/intelligence/impact.d.ts +51 -0
  24. package/dist/intelligence/impact.d.ts.map +1 -0
  25. package/dist/intelligence/index.d.ts +15 -0
  26. package/dist/intelligence/index.d.ts.map +1 -0
  27. package/dist/intelligence/patterns.d.ts +66 -0
  28. package/dist/intelligence/patterns.d.ts.map +1 -0
  29. package/dist/intelligence/prediction.d.ts +51 -0
  30. package/dist/intelligence/prediction.d.ts.map +1 -0
  31. package/dist/intelligence/types.d.ts +221 -0
  32. package/dist/intelligence/types.d.ts.map +1 -0
  33. package/dist/internal.d.ts +12 -1
  34. package/dist/internal.d.ts.map +1 -1
  35. package/dist/issue/template-parser.d.ts +8 -2
  36. package/dist/issue/template-parser.d.ts.map +1 -1
  37. package/dist/lifecycle/pipeline.d.ts +2 -2
  38. package/dist/lifecycle/pipeline.d.ts.map +1 -1
  39. package/dist/lifecycle/state-machine.d.ts +1 -1
  40. package/dist/lifecycle/state-machine.d.ts.map +1 -1
  41. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  42. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  43. package/dist/memory/brain-row-types.d.ts +40 -6
  44. package/dist/memory/brain-row-types.d.ts.map +1 -1
  45. package/dist/memory/brain-search.d.ts.map +1 -1
  46. package/dist/memory/brain-similarity.d.ts.map +1 -1
  47. package/dist/memory/claude-mem-migration.d.ts.map +1 -1
  48. package/dist/nexus/discover.d.ts.map +1 -1
  49. package/dist/nexus/index.d.ts +2 -0
  50. package/dist/nexus/index.d.ts.map +1 -1
  51. package/dist/nexus/transfer-types.d.ts +123 -0
  52. package/dist/nexus/transfer-types.d.ts.map +1 -0
  53. package/dist/nexus/transfer.d.ts +31 -0
  54. package/dist/nexus/transfer.d.ts.map +1 -0
  55. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  56. package/dist/orchestration/skill-ops.d.ts +4 -4
  57. package/dist/orchestration/skill-ops.d.ts.map +1 -1
  58. package/dist/otel/index.d.ts +1 -1
  59. package/dist/otel/index.d.ts.map +1 -1
  60. package/dist/sessions/briefing.d.ts.map +1 -1
  61. package/dist/sessions/handoff.d.ts.map +1 -1
  62. package/dist/sessions/index.d.ts +1 -1
  63. package/dist/sessions/index.d.ts.map +1 -1
  64. package/dist/sessions/types.d.ts +8 -42
  65. package/dist/sessions/types.d.ts.map +1 -1
  66. package/dist/signaldock/signaldock-transport.d.ts +1 -1
  67. package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
  68. package/dist/skills/injection/subagent.d.ts +3 -3
  69. package/dist/skills/injection/subagent.d.ts.map +1 -1
  70. package/dist/skills/manifests/contribution.d.ts +2 -2
  71. package/dist/skills/manifests/contribution.d.ts.map +1 -1
  72. package/dist/skills/orchestrator/spawn.d.ts +6 -6
  73. package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
  74. package/dist/skills/orchestrator/startup.d.ts +1 -1
  75. package/dist/skills/orchestrator/startup.d.ts.map +1 -1
  76. package/dist/skills/orchestrator/validator.d.ts +2 -2
  77. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  78. package/dist/skills/precedence-types.d.ts +24 -1
  79. package/dist/skills/precedence-types.d.ts.map +1 -1
  80. package/dist/skills/types.d.ts +70 -4
  81. package/dist/skills/types.d.ts.map +1 -1
  82. package/dist/store/brain-sqlite.d.ts +4 -1
  83. package/dist/store/brain-sqlite.d.ts.map +1 -1
  84. package/dist/store/export.d.ts +5 -4
  85. package/dist/store/export.d.ts.map +1 -1
  86. package/dist/store/nexus-sqlite.d.ts +4 -1
  87. package/dist/store/nexus-sqlite.d.ts.map +1 -1
  88. package/dist/store/sqlite.d.ts +4 -1
  89. package/dist/store/sqlite.d.ts.map +1 -1
  90. package/dist/store/tasks-schema.d.ts +14 -4
  91. package/dist/store/tasks-schema.d.ts.map +1 -1
  92. package/dist/store/typed-query.d.ts +12 -0
  93. package/dist/store/typed-query.d.ts.map +1 -0
  94. package/dist/store/validation-schemas.d.ts +2423 -50
  95. package/dist/store/validation-schemas.d.ts.map +1 -1
  96. package/dist/system/inject-generate.d.ts.map +1 -1
  97. package/dist/validation/doctor/checks.d.ts +5 -0
  98. package/dist/validation/doctor/checks.d.ts.map +1 -1
  99. package/dist/validation/engine.d.ts +10 -10
  100. package/dist/validation/engine.d.ts.map +1 -1
  101. package/dist/validation/index.d.ts +6 -2
  102. package/dist/validation/index.d.ts.map +1 -1
  103. package/dist/validation/protocol-common.d.ts +10 -2
  104. package/dist/validation/protocol-common.d.ts.map +1 -1
  105. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
  106. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
  107. package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
  108. package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
  109. package/package.json +2 -2
  110. package/src/admin/export-tasks.ts +2 -5
  111. package/src/admin/import-tasks.ts +53 -29
  112. package/src/agents/__tests__/capacity.test.ts +219 -0
  113. package/src/agents/__tests__/registry.test.ts +457 -0
  114. package/src/agents/__tests__/retry.test.ts +289 -0
  115. package/src/agents/agent-schema.ts +107 -0
  116. package/src/agents/capacity.ts +151 -0
  117. package/src/agents/index.ts +68 -0
  118. package/src/agents/registry.ts +449 -0
  119. package/src/agents/retry.ts +255 -0
  120. package/src/hooks/index.ts +20 -1
  121. package/src/hooks/payload-schemas.ts +199 -0
  122. package/src/index.ts +69 -0
  123. package/src/inject/index.ts +14 -14
  124. package/src/intelligence/__tests__/impact.test.ts +453 -0
  125. package/src/intelligence/__tests__/patterns.test.ts +450 -0
  126. package/src/intelligence/__tests__/prediction.test.ts +418 -0
  127. package/src/intelligence/impact.ts +638 -0
  128. package/src/intelligence/index.ts +47 -0
  129. package/src/intelligence/patterns.ts +621 -0
  130. package/src/intelligence/prediction.ts +621 -0
  131. package/src/intelligence/types.ts +273 -0
  132. package/src/internal.ts +89 -2
  133. package/src/issue/template-parser.ts +65 -4
  134. package/src/lifecycle/pipeline.ts +14 -7
  135. package/src/lifecycle/state-machine.ts +6 -2
  136. package/src/memory/brain-lifecycle.ts +5 -11
  137. package/src/memory/brain-retrieval.ts +44 -38
  138. package/src/memory/brain-row-types.ts +43 -6
  139. package/src/memory/brain-search.ts +53 -32
  140. package/src/memory/brain-similarity.ts +9 -8
  141. package/src/memory/claude-mem-migration.ts +4 -3
  142. package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
  143. package/src/nexus/__tests__/transfer.test.ts +446 -0
  144. package/src/nexus/discover.ts +1 -0
  145. package/src/nexus/index.ts +14 -0
  146. package/src/nexus/transfer-types.ts +129 -0
  147. package/src/nexus/transfer.ts +314 -0
  148. package/src/orchestration/bootstrap.ts +11 -17
  149. package/src/orchestration/skill-ops.ts +52 -32
  150. package/src/otel/index.ts +48 -4
  151. package/src/sessions/__tests__/briefing.test.ts +31 -2
  152. package/src/sessions/briefing.ts +27 -42
  153. package/src/sessions/handoff.ts +52 -86
  154. package/src/sessions/index.ts +5 -1
  155. package/src/sessions/types.ts +9 -43
  156. package/src/signaldock/signaldock-transport.ts +5 -2
  157. package/src/skills/injection/subagent.ts +10 -16
  158. package/src/skills/manifests/contribution.ts +5 -13
  159. package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
  160. package/src/skills/orchestrator/spawn.ts +18 -31
  161. package/src/skills/orchestrator/startup.ts +78 -65
  162. package/src/skills/orchestrator/validator.ts +26 -31
  163. package/src/skills/precedence-types.ts +24 -1
  164. package/src/skills/types.ts +72 -5
  165. package/src/store/__tests__/test-db-helper.d.ts +4 -4
  166. package/src/store/__tests__/test-db-helper.js +5 -16
  167. package/src/store/__tests__/test-db-helper.ts +5 -18
  168. package/src/store/brain-sqlite.ts +7 -3
  169. package/src/store/chain-schema.ts +1 -1
  170. package/src/store/export.ts +22 -12
  171. package/src/store/nexus-sqlite.ts +7 -3
  172. package/src/store/sqlite.ts +9 -3
  173. package/src/store/tasks-schema.ts +65 -8
  174. package/src/store/typed-query.ts +17 -0
  175. package/src/store/validation-schemas.ts +347 -23
  176. package/src/system/inject-generate.ts +9 -23
  177. package/src/validation/doctor/checks.ts +24 -2
  178. package/src/validation/engine.ts +11 -11
  179. package/src/validation/index.ts +131 -3
  180. package/src/validation/protocol-common.ts +54 -3
  181. package/dist/tasks/reparent.d.ts +0 -38
  182. package/dist/tasks/reparent.d.ts.map +0 -1
  183. package/src/tasks/reparent.ts +0 -134
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Tests for retry logic, exponential backoff, and self-healing recovery.
3
+ *
4
+ * @module agents/__tests__/retry.test
5
+ */
6
+
7
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11
+ import { registerAgent, updateAgentStatus } from '../registry.js';
12
+ import {
13
+ calculateDelay,
14
+ createRetryPolicy,
15
+ DEFAULT_RETRY_POLICY,
16
+ recoverCrashedAgents,
17
+ shouldRetry,
18
+ withRetry,
19
+ } from '../retry.js';
20
+
21
+ // ==========================================================================
22
+ // Retry Policy
23
+ // ==========================================================================
24
+
25
+ describe('Retry Policy', () => {
26
+ describe('createRetryPolicy', () => {
27
+ it('returns default policy when no overrides', () => {
28
+ const policy = createRetryPolicy();
29
+ expect(policy.maxRetries).toBe(3);
30
+ expect(policy.baseDelayMs).toBe(1_000);
31
+ expect(policy.maxDelayMs).toBe(30_000);
32
+ expect(policy.backoffMultiplier).toBe(2);
33
+ expect(policy.jitter).toBe(true);
34
+ expect(policy.retryOnUnknown).toBe(true);
35
+ });
36
+
37
+ it('merges partial overrides with defaults', () => {
38
+ const policy = createRetryPolicy({ maxRetries: 5, baseDelayMs: 500 });
39
+ expect(policy.maxRetries).toBe(5);
40
+ expect(policy.baseDelayMs).toBe(500);
41
+ expect(policy.maxDelayMs).toBe(30_000); // unchanged default
42
+ });
43
+
44
+ it('DEFAULT_RETRY_POLICY is frozen', () => {
45
+ expect(Object.isFrozen(DEFAULT_RETRY_POLICY)).toBe(true);
46
+ });
47
+ });
48
+
49
+ describe('calculateDelay', () => {
50
+ it('applies exponential backoff', () => {
51
+ const policy = createRetryPolicy({ jitter: false, baseDelayMs: 100, backoffMultiplier: 2 });
52
+
53
+ expect(calculateDelay(0, policy)).toBe(100); // 100 * 2^0
54
+ expect(calculateDelay(1, policy)).toBe(200); // 100 * 2^1
55
+ expect(calculateDelay(2, policy)).toBe(400); // 100 * 2^2
56
+ expect(calculateDelay(3, policy)).toBe(800); // 100 * 2^3
57
+ });
58
+
59
+ it('caps at maxDelay', () => {
60
+ const policy = createRetryPolicy({
61
+ jitter: false,
62
+ baseDelayMs: 100,
63
+ maxDelayMs: 500,
64
+ backoffMultiplier: 10,
65
+ });
66
+
67
+ expect(calculateDelay(0, policy)).toBe(100);
68
+ expect(calculateDelay(1, policy)).toBe(500); // capped
69
+ expect(calculateDelay(2, policy)).toBe(500); // capped
70
+ });
71
+
72
+ it('adds jitter when enabled', () => {
73
+ const policy = createRetryPolicy({
74
+ jitter: true,
75
+ baseDelayMs: 1000,
76
+ backoffMultiplier: 1,
77
+ });
78
+
79
+ // With jitter, delay should be >= base but <= base * 1.25
80
+ const delays = Array.from({ length: 20 }, () => calculateDelay(0, policy));
81
+ const allInRange = delays.every((d) => d >= 1000 && d <= 1250);
82
+ expect(allInRange).toBe(true);
83
+
84
+ // With 20 samples, at least some should differ (jitter is random)
85
+ const uniqueDelays = new Set(delays);
86
+ expect(uniqueDelays.size).toBeGreaterThan(1);
87
+ });
88
+ });
89
+
90
+ describe('shouldRetry', () => {
91
+ it('allows retry for retriable errors within limit', () => {
92
+ const policy = createRetryPolicy({ maxRetries: 3 });
93
+ expect(shouldRetry(new Error('ECONNREFUSED'), 0, policy)).toBe(true);
94
+ expect(shouldRetry(new Error('timeout'), 1, policy)).toBe(true);
95
+ expect(shouldRetry(new Error('503 Service Unavailable'), 2, policy)).toBe(true);
96
+ });
97
+
98
+ it('denies retry when attempt exceeds maxRetries', () => {
99
+ const policy = createRetryPolicy({ maxRetries: 3 });
100
+ expect(shouldRetry(new Error('ECONNREFUSED'), 3, policy)).toBe(false);
101
+ expect(shouldRetry(new Error('ECONNREFUSED'), 4, policy)).toBe(false);
102
+ });
103
+
104
+ it('denies retry for permanent errors', () => {
105
+ const policy = createRetryPolicy({ maxRetries: 10 });
106
+ expect(shouldRetry(new Error('Permission denied'), 0, policy)).toBe(false);
107
+ expect(shouldRetry(new Error('401 Unauthorized'), 0, policy)).toBe(false);
108
+ });
109
+
110
+ it('respects retryOnUnknown policy', () => {
111
+ const retryUnknown = createRetryPolicy({ retryOnUnknown: true });
112
+ const noRetryUnknown = createRetryPolicy({ retryOnUnknown: false });
113
+
114
+ const unknownError = new Error('Something weird');
115
+ expect(shouldRetry(unknownError, 0, retryUnknown)).toBe(true);
116
+ expect(shouldRetry(unknownError, 0, noRetryUnknown)).toBe(false);
117
+ });
118
+ });
119
+ });
120
+
121
+ // ==========================================================================
122
+ // withRetry wrapper
123
+ // ==========================================================================
124
+
125
+ describe('withRetry', () => {
126
+ it('succeeds on first attempt', async () => {
127
+ let callCount = 0;
128
+ const result = await withRetry(async () => {
129
+ callCount++;
130
+ return 'success';
131
+ });
132
+
133
+ expect(result.success).toBe(true);
134
+ expect(result.value).toBe('success');
135
+ expect(result.attempts).toBe(1);
136
+ expect(callCount).toBe(1);
137
+ });
138
+
139
+ it('retries on retriable error and eventually succeeds', async () => {
140
+ let callCount = 0;
141
+ const result = await withRetry(
142
+ async () => {
143
+ callCount++;
144
+ if (callCount < 3) throw new Error('ECONNREFUSED');
145
+ return 'recovered';
146
+ },
147
+ { baseDelayMs: 1, maxDelayMs: 5, jitter: false },
148
+ );
149
+
150
+ expect(result.success).toBe(true);
151
+ expect(result.value).toBe('recovered');
152
+ expect(result.attempts).toBe(3);
153
+ expect(callCount).toBe(3);
154
+ });
155
+
156
+ it('fails immediately on permanent error', async () => {
157
+ let callCount = 0;
158
+ const result = await withRetry(
159
+ async () => {
160
+ callCount++;
161
+ throw new Error('Permission denied');
162
+ },
163
+ { baseDelayMs: 1, jitter: false },
164
+ );
165
+
166
+ expect(result.success).toBe(false);
167
+ expect(result.error?.message).toBe('Permission denied');
168
+ expect(result.attempts).toBe(1);
169
+ expect(callCount).toBe(1);
170
+ });
171
+
172
+ it('exhausts retries and fails', async () => {
173
+ let callCount = 0;
174
+ const result = await withRetry(
175
+ async () => {
176
+ callCount++;
177
+ throw new Error('ECONNREFUSED');
178
+ },
179
+ { maxRetries: 2, baseDelayMs: 1, maxDelayMs: 5, jitter: false },
180
+ );
181
+
182
+ expect(result.success).toBe(false);
183
+ expect(result.attempts).toBe(3); // 1 initial + 2 retries
184
+ expect(callCount).toBe(3);
185
+ });
186
+
187
+ it('tracks total delay time', async () => {
188
+ let callCount = 0;
189
+ const result = await withRetry(
190
+ async () => {
191
+ callCount++;
192
+ if (callCount <= 2) throw new Error('timeout');
193
+ return 'ok';
194
+ },
195
+ { baseDelayMs: 10, backoffMultiplier: 1, jitter: false },
196
+ );
197
+
198
+ expect(result.success).toBe(true);
199
+ expect(result.totalDelayMs).toBeGreaterThanOrEqual(20); // 10 + 10
200
+ });
201
+
202
+ it('uses custom retry policy', async () => {
203
+ let callCount = 0;
204
+ const result = await withRetry(
205
+ async () => {
206
+ callCount++;
207
+ throw new Error('rate limit');
208
+ },
209
+ { maxRetries: 1, baseDelayMs: 1, jitter: false },
210
+ );
211
+
212
+ expect(result.success).toBe(false);
213
+ expect(result.attempts).toBe(2); // 1 initial + 1 retry
214
+ expect(callCount).toBe(2);
215
+ });
216
+ });
217
+
218
+ // ==========================================================================
219
+ // Self-Healing Recovery
220
+ // ==========================================================================
221
+
222
+ describe('recoverCrashedAgents', () => {
223
+ let tempDir: string;
224
+
225
+ beforeEach(async () => {
226
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-recover-test-'));
227
+ await mkdir(join(tempDir, '.cleo'), { recursive: true });
228
+ await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
229
+ });
230
+
231
+ afterEach(async () => {
232
+ try {
233
+ const { closeAllDatabases } = await import('../../store/sqlite.js');
234
+ await closeAllDatabases();
235
+ } catch {
236
+ /* module may not be loaded */
237
+ }
238
+ await Promise.race([
239
+ rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
240
+ new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
241
+ ]);
242
+ });
243
+
244
+ it('recovers crashed agents with retriable errors', async () => {
245
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
246
+ await updateAgentStatus(agent.id, { status: 'crashed', error: 'ECONNREFUSED' }, tempDir);
247
+
248
+ const results = await recoverCrashedAgents(30_000, tempDir);
249
+
250
+ expect(results.length).toBe(1);
251
+ expect(results[0]!.recovered).toBe(true);
252
+ expect(results[0]!.action).toBe('restarted');
253
+ });
254
+
255
+ it('abandons agents with permanent errors', async () => {
256
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
257
+ await updateAgentStatus(agent.id, { status: 'crashed', error: 'Permission denied' }, tempDir);
258
+
259
+ const results = await recoverCrashedAgents(30_000, tempDir);
260
+
261
+ expect(results.length).toBe(1);
262
+ expect(results[0]!.recovered).toBe(false);
263
+ expect(results[0]!.action).toBe('abandoned');
264
+ });
265
+
266
+ it('abandons agents exceeding error threshold', async () => {
267
+ const agent = await registerAgent({ agentType: 'executor' }, tempDir);
268
+
269
+ // Simulate 5+ errors
270
+ for (let i = 0; i < 5; i++) {
271
+ await updateAgentStatus(agent.id, { status: 'error', error: `Error ${i}` }, tempDir);
272
+ }
273
+ await updateAgentStatus(agent.id, { status: 'crashed' }, tempDir);
274
+
275
+ const results = await recoverCrashedAgents(30_000, tempDir);
276
+
277
+ expect(results.length).toBe(1);
278
+ expect(results[0]!.recovered).toBe(false);
279
+ expect(results[0]!.action).toBe('abandoned');
280
+ expect(results[0]!.reason).toContain('exceeds threshold');
281
+ });
282
+
283
+ it('returns empty results when no crashed agents', async () => {
284
+ await registerAgent({ agentType: 'executor' }, tempDir);
285
+
286
+ const results = await recoverCrashedAgents(60_000, tempDir);
287
+ expect(results.length).toBe(0);
288
+ });
289
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Drizzle ORM schema for the CLEO Agent dimension.
3
+ *
4
+ * Defines the `agent_instances` table that tracks live agent processes,
5
+ * their health (heartbeat protocol), capacity, and error history.
6
+ *
7
+ * This is the DB-backed runtime registry -- distinct from the file-based
8
+ * skill agent registry in `skills/agents/registry.ts` which tracks
9
+ * installed agent *definitions*. This table tracks running agent *instances*.
10
+ *
11
+ * @module agents/agent-schema
12
+ */
13
+
14
+ import { sql } from 'drizzle-orm';
15
+ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
16
+
17
+ // ============================================================================
18
+ // Canonical enum constants
19
+ // ============================================================================
20
+
21
+ /** Agent instance status values matching DB CHECK constraint. */
22
+ export const AGENT_INSTANCE_STATUSES = [
23
+ 'starting',
24
+ 'active',
25
+ 'idle',
26
+ 'error',
27
+ 'crashed',
28
+ 'stopped',
29
+ ] as const;
30
+
31
+ /** Agent type values for classification. */
32
+ export const AGENT_TYPES = [
33
+ 'orchestrator',
34
+ 'executor',
35
+ 'researcher',
36
+ 'architect',
37
+ 'validator',
38
+ 'documentor',
39
+ 'custom',
40
+ ] as const;
41
+
42
+ // ============================================================================
43
+ // Agent Instances Table
44
+ // ============================================================================
45
+
46
+ export const agentInstances = sqliteTable(
47
+ 'agent_instances',
48
+ {
49
+ id: text('id').primaryKey(),
50
+ agentType: text('agent_type', { enum: AGENT_TYPES }).notNull(),
51
+ status: text('status', { enum: AGENT_INSTANCE_STATUSES }).notNull().default('starting'),
52
+ sessionId: text('session_id'),
53
+ taskId: text('task_id'),
54
+ startedAt: text('started_at').notNull().default(sql`(datetime('now'))`),
55
+ lastHeartbeat: text('last_heartbeat').notNull().default(sql`(datetime('now'))`),
56
+ stoppedAt: text('stopped_at'),
57
+ errorCount: integer('error_count').notNull().default(0),
58
+ totalTasksCompleted: integer('total_tasks_completed').notNull().default(0),
59
+ capacity: text('capacity').notNull().default('1.0'),
60
+ metadataJson: text('metadata_json').default('{}'),
61
+ parentAgentId: text('parent_agent_id'),
62
+ },
63
+ (table) => [
64
+ index('idx_agent_instances_status').on(table.status),
65
+ index('idx_agent_instances_agent_type').on(table.agentType),
66
+ index('idx_agent_instances_session_id').on(table.sessionId),
67
+ index('idx_agent_instances_task_id').on(table.taskId),
68
+ index('idx_agent_instances_parent_agent_id').on(table.parentAgentId),
69
+ index('idx_agent_instances_last_heartbeat').on(table.lastHeartbeat),
70
+ ],
71
+ );
72
+
73
+ // ============================================================================
74
+ // Agent Error Log Table
75
+ // ============================================================================
76
+
77
+ export const agentErrorLog = sqliteTable(
78
+ 'agent_error_log',
79
+ {
80
+ id: integer('id').primaryKey({ autoIncrement: true }),
81
+ agentId: text('agent_id').notNull(),
82
+ errorType: text('error_type', {
83
+ enum: ['retriable', 'permanent', 'unknown'],
84
+ }).notNull(),
85
+ message: text('message').notNull(),
86
+ stack: text('stack'),
87
+ occurredAt: text('occurred_at').notNull().default(sql`(datetime('now'))`),
88
+ resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
89
+ },
90
+ (table) => [
91
+ index('idx_agent_error_log_agent_id').on(table.agentId),
92
+ index('idx_agent_error_log_error_type').on(table.errorType),
93
+ index('idx_agent_error_log_occurred_at').on(table.occurredAt),
94
+ ],
95
+ );
96
+
97
+ // ============================================================================
98
+ // Type exports
99
+ // ============================================================================
100
+
101
+ export type AgentInstanceRow = typeof agentInstances.$inferSelect;
102
+ export type NewAgentInstanceRow = typeof agentInstances.$inferInsert;
103
+ export type AgentErrorLogRow = typeof agentErrorLog.$inferSelect;
104
+ export type NewAgentErrorLogRow = typeof agentErrorLog.$inferInsert;
105
+ export type AgentInstanceStatus = (typeof AGENT_INSTANCE_STATUSES)[number];
106
+ export type AgentType = (typeof AGENT_TYPES)[number];
107
+ export type AgentErrorType = 'retriable' | 'permanent' | 'unknown';
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Capacity tracking and load balancing for the Agent dimension.
3
+ *
4
+ * Tracks per-agent capacity (0.0-1.0) and provides queries for
5
+ * capacity-aware work distribution.
6
+ *
7
+ * @module agents/capacity
8
+ */
9
+
10
+ import { eq } from 'drizzle-orm';
11
+ import { getDb } from '../store/sqlite.js';
12
+ import { type AgentInstanceRow, type AgentType, agentInstances } from './agent-schema.js';
13
+ import { listAgentInstances } from './registry.js';
14
+
15
+ // ============================================================================
16
+ // Capacity Updates
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Update the capacity value for an agent instance.
21
+ *
22
+ * @param id - Agent instance ID
23
+ * @param capacity - New capacity value (0.0 to 1.0)
24
+ * @param cwd - Working directory
25
+ * @returns Updated agent row, or null if not found
26
+ */
27
+ export async function updateCapacity(
28
+ id: string,
29
+ capacity: number,
30
+ cwd?: string,
31
+ ): Promise<AgentInstanceRow | null> {
32
+ if (capacity < 0 || capacity > 1) {
33
+ throw new Error(`Capacity must be between 0.0 and 1.0, got ${capacity}`);
34
+ }
35
+
36
+ const db = await getDb(cwd);
37
+ const existing = await db.select().from(agentInstances).where(eq(agentInstances.id, id)).get();
38
+
39
+ if (!existing) return null;
40
+
41
+ const capacityStr = capacity.toFixed(4);
42
+ await db.update(agentInstances).set({ capacity: capacityStr }).where(eq(agentInstances.id, id));
43
+
44
+ return { ...existing, capacity: capacityStr };
45
+ }
46
+
47
+ // ============================================================================
48
+ // Capacity Queries
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Get the total available capacity across all active agents.
53
+ *
54
+ * Only considers agents in 'active' or 'idle' status.
55
+ * Returns the sum of all capacity values.
56
+ */
57
+ export async function getAvailableCapacity(cwd?: string): Promise<number> {
58
+ const agents = await listAgentInstances({ status: ['active', 'idle'] }, cwd);
59
+ return agents.reduce((sum, agent) => sum + parseCapacity(agent.capacity), 0);
60
+ }
61
+
62
+ /**
63
+ * Find the agent with the most available capacity.
64
+ *
65
+ * @param agentType - Optional type filter
66
+ * @param cwd - Working directory
67
+ * @returns Agent with highest capacity, or null if no active agents
68
+ */
69
+ export async function findLeastLoadedAgent(
70
+ agentType?: AgentType,
71
+ cwd?: string,
72
+ ): Promise<AgentInstanceRow | null> {
73
+ const filters: import('./registry.js').ListAgentFilters = agentType
74
+ ? { status: ['active', 'idle'] as ('active' | 'idle')[], agentType }
75
+ : { status: ['active', 'idle'] as ('active' | 'idle')[] };
76
+
77
+ const agents = await listAgentInstances(filters, cwd);
78
+
79
+ if (agents.length === 0) return null;
80
+
81
+ let best = agents[0]!;
82
+ let bestCapacity = parseCapacity(best.capacity);
83
+
84
+ for (let i = 1; i < agents.length; i++) {
85
+ const cap = parseCapacity(agents[i]!.capacity);
86
+ if (cap > bestCapacity) {
87
+ best = agents[i]!;
88
+ bestCapacity = cap;
89
+ }
90
+ }
91
+
92
+ return best;
93
+ }
94
+
95
+ /**
96
+ * Check if the system is overloaded (total capacity below threshold).
97
+ *
98
+ * @param threshold - Minimum acceptable capacity (default: 0.1)
99
+ * @param cwd - Working directory
100
+ * @returns true if total available capacity is below the threshold
101
+ */
102
+ export async function isOverloaded(threshold: number = 0.1, cwd?: string): Promise<boolean> {
103
+ const capacity = await getAvailableCapacity(cwd);
104
+ return capacity < threshold;
105
+ }
106
+
107
+ /** Capacity summary for reporting. */
108
+ export interface CapacitySummary {
109
+ totalCapacity: number;
110
+ activeAgentCount: number;
111
+ averageCapacity: number;
112
+ overloaded: boolean;
113
+ threshold: number;
114
+ }
115
+
116
+ /**
117
+ * Get a capacity summary across the entire agent pool.
118
+ *
119
+ * @param threshold - Overload threshold (default: 0.1)
120
+ * @param cwd - Working directory
121
+ */
122
+ export async function getCapacitySummary(
123
+ threshold: number = 0.1,
124
+ cwd?: string,
125
+ ): Promise<CapacitySummary> {
126
+ const agents = await listAgentInstances({ status: ['active', 'idle'] }, cwd);
127
+ const totalCapacity = agents.reduce((sum, a) => sum + parseCapacity(a.capacity), 0);
128
+ const activeAgentCount = agents.length;
129
+
130
+ return {
131
+ totalCapacity,
132
+ activeAgentCount,
133
+ averageCapacity: activeAgentCount > 0 ? totalCapacity / activeAgentCount : 0,
134
+ overloaded: totalCapacity < threshold,
135
+ threshold,
136
+ };
137
+ }
138
+
139
+ // ============================================================================
140
+ // Utilities
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Parse a capacity string to a number.
145
+ * The DB stores capacity as TEXT to avoid floating-point representation issues.
146
+ */
147
+ function parseCapacity(value: string | null | undefined): number {
148
+ if (!value) return 0;
149
+ const parsed = parseFloat(value);
150
+ return Number.isNaN(parsed) ? 0 : Math.max(0, Math.min(1, parsed));
151
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Agent dimension -- runtime tracking, health monitoring, self-healing, and capacity.
3
+ *
4
+ * This module provides the complete Agent dimension for the BRAIN specification:
5
+ *
6
+ * - **Registry**: CRUD for agent instances (register, deregister, heartbeat, queries)
7
+ * - **Health**: Crash detection via heartbeat protocol, health reports
8
+ * - **Self-Healing**: Retry with exponential backoff, crashed agent recovery
9
+ * - **Capacity**: Load awareness, least-loaded agent selection, overload detection
10
+ *
11
+ * @module agents
12
+ */
13
+
14
+ // Schema & types
15
+ export {
16
+ AGENT_INSTANCE_STATUSES,
17
+ AGENT_TYPES,
18
+ type AgentErrorLogRow,
19
+ type AgentErrorType,
20
+ type AgentInstanceRow,
21
+ type AgentInstanceStatus,
22
+ type AgentType,
23
+ agentErrorLog,
24
+ agentInstances,
25
+ type NewAgentErrorLogRow,
26
+ type NewAgentInstanceRow,
27
+ } from './agent-schema.js';
28
+ // Capacity tracking
29
+ export {
30
+ type CapacitySummary,
31
+ findLeastLoadedAgent,
32
+ getAvailableCapacity,
33
+ getCapacitySummary,
34
+ isOverloaded,
35
+ updateCapacity,
36
+ } from './capacity.js';
37
+ // Registry (CRUD, heartbeat, health, errors)
38
+ export {
39
+ type AgentHealthReport,
40
+ checkAgentHealth,
41
+ classifyError,
42
+ deregisterAgent,
43
+ generateAgentId,
44
+ getAgentErrorHistory,
45
+ getAgentInstance,
46
+ getHealthReport,
47
+ heartbeat,
48
+ incrementTasksCompleted,
49
+ type ListAgentFilters,
50
+ listAgentInstances,
51
+ markCrashed,
52
+ type RegisterAgentOptions,
53
+ registerAgent,
54
+ type UpdateStatusOptions,
55
+ updateAgentStatus,
56
+ } from './registry.js';
57
+ // Retry & self-healing
58
+ export {
59
+ type AgentRecoveryResult,
60
+ calculateDelay,
61
+ createRetryPolicy,
62
+ DEFAULT_RETRY_POLICY,
63
+ type RetryPolicy,
64
+ type RetryResult,
65
+ recoverCrashedAgents,
66
+ shouldRetry,
67
+ withRetry,
68
+ } from './retry.js';