@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.
- package/dist/admin/export-tasks.d.ts.map +1 -1
- package/dist/admin/import-tasks.d.ts +10 -2
- package/dist/admin/import-tasks.d.ts.map +1 -1
- package/dist/agents/agent-schema.d.ts +358 -0
- package/dist/agents/agent-schema.d.ts.map +1 -0
- package/dist/agents/capacity.d.ts +57 -0
- package/dist/agents/capacity.d.ts.map +1 -0
- package/dist/agents/index.d.ts +17 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/registry.d.ts +115 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/retry.d.ts +83 -0
- package/dist/agents/retry.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +4 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +214 -0
- package/dist/hooks/payload-schemas.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16937 -2371
- package/dist/index.js.map +4 -4
- package/dist/inject/index.d.ts.map +1 -1
- package/dist/intelligence/impact.d.ts +51 -0
- package/dist/intelligence/impact.d.ts.map +1 -0
- package/dist/intelligence/index.d.ts +15 -0
- package/dist/intelligence/index.d.ts.map +1 -0
- package/dist/intelligence/patterns.d.ts +66 -0
- package/dist/intelligence/patterns.d.ts.map +1 -0
- package/dist/intelligence/prediction.d.ts +51 -0
- package/dist/intelligence/prediction.d.ts.map +1 -0
- package/dist/intelligence/types.d.ts +221 -0
- package/dist/intelligence/types.d.ts.map +1 -0
- package/dist/internal.d.ts +12 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/issue/template-parser.d.ts +8 -2
- package/dist/issue/template-parser.d.ts.map +1 -1
- package/dist/lifecycle/pipeline.d.ts +2 -2
- package/dist/lifecycle/pipeline.d.ts.map +1 -1
- package/dist/lifecycle/state-machine.d.ts +1 -1
- package/dist/lifecycle/state-machine.d.ts.map +1 -1
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-row-types.d.ts +40 -6
- package/dist/memory/brain-row-types.d.ts.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-similarity.d.ts.map +1 -1
- package/dist/memory/claude-mem-migration.d.ts.map +1 -1
- package/dist/nexus/discover.d.ts.map +1 -1
- package/dist/nexus/index.d.ts +2 -0
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/transfer-types.d.ts +123 -0
- package/dist/nexus/transfer-types.d.ts.map +1 -0
- package/dist/nexus/transfer.d.ts +31 -0
- package/dist/nexus/transfer.d.ts.map +1 -0
- package/dist/orchestration/bootstrap.d.ts.map +1 -1
- package/dist/orchestration/skill-ops.d.ts +4 -4
- package/dist/orchestration/skill-ops.d.ts.map +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.d.ts.map +1 -1
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/types.d.ts +8 -42
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/signaldock/signaldock-transport.d.ts +1 -1
- package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
- package/dist/skills/injection/subagent.d.ts +3 -3
- package/dist/skills/injection/subagent.d.ts.map +1 -1
- package/dist/skills/manifests/contribution.d.ts +2 -2
- package/dist/skills/manifests/contribution.d.ts.map +1 -1
- package/dist/skills/orchestrator/spawn.d.ts +6 -6
- package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
- package/dist/skills/orchestrator/startup.d.ts +1 -1
- package/dist/skills/orchestrator/startup.d.ts.map +1 -1
- package/dist/skills/orchestrator/validator.d.ts +2 -2
- package/dist/skills/orchestrator/validator.d.ts.map +1 -1
- package/dist/skills/precedence-types.d.ts +24 -1
- package/dist/skills/precedence-types.d.ts.map +1 -1
- package/dist/skills/types.d.ts +70 -4
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts +4 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/export.d.ts +5 -4
- package/dist/store/export.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.d.ts +4 -1
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts +4 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +14 -4
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/typed-query.d.ts +12 -0
- package/dist/store/typed-query.d.ts.map +1 -0
- package/dist/store/validation-schemas.d.ts +2423 -50
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/inject-generate.d.ts.map +1 -1
- package/dist/validation/doctor/checks.d.ts +5 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/engine.d.ts +10 -10
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/index.d.ts +6 -2
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/protocol-common.d.ts +10 -2
- package/dist/validation/protocol-common.d.ts.map +1 -1
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
- package/package.json +2 -2
- package/src/admin/export-tasks.ts +2 -5
- package/src/admin/import-tasks.ts +53 -29
- package/src/agents/__tests__/capacity.test.ts +219 -0
- package/src/agents/__tests__/registry.test.ts +457 -0
- package/src/agents/__tests__/retry.test.ts +289 -0
- package/src/agents/agent-schema.ts +107 -0
- package/src/agents/capacity.ts +151 -0
- package/src/agents/index.ts +68 -0
- package/src/agents/registry.ts +449 -0
- package/src/agents/retry.ts +255 -0
- package/src/hooks/index.ts +20 -1
- package/src/hooks/payload-schemas.ts +199 -0
- package/src/index.ts +69 -0
- package/src/inject/index.ts +14 -14
- package/src/intelligence/__tests__/impact.test.ts +453 -0
- package/src/intelligence/__tests__/patterns.test.ts +450 -0
- package/src/intelligence/__tests__/prediction.test.ts +418 -0
- package/src/intelligence/impact.ts +638 -0
- package/src/intelligence/index.ts +47 -0
- package/src/intelligence/patterns.ts +621 -0
- package/src/intelligence/prediction.ts +621 -0
- package/src/intelligence/types.ts +273 -0
- package/src/internal.ts +89 -2
- package/src/issue/template-parser.ts +65 -4
- package/src/lifecycle/pipeline.ts +14 -7
- package/src/lifecycle/state-machine.ts +6 -2
- package/src/memory/brain-lifecycle.ts +5 -11
- package/src/memory/brain-retrieval.ts +44 -38
- package/src/memory/brain-row-types.ts +43 -6
- package/src/memory/brain-search.ts +53 -32
- package/src/memory/brain-similarity.ts +9 -8
- package/src/memory/claude-mem-migration.ts +4 -3
- package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
- package/src/nexus/__tests__/transfer.test.ts +446 -0
- package/src/nexus/discover.ts +1 -0
- package/src/nexus/index.ts +14 -0
- package/src/nexus/transfer-types.ts +129 -0
- package/src/nexus/transfer.ts +314 -0
- package/src/orchestration/bootstrap.ts +11 -17
- package/src/orchestration/skill-ops.ts +52 -32
- package/src/otel/index.ts +48 -4
- package/src/sessions/__tests__/briefing.test.ts +31 -2
- package/src/sessions/briefing.ts +27 -42
- package/src/sessions/handoff.ts +52 -86
- package/src/sessions/index.ts +5 -1
- package/src/sessions/types.ts +9 -43
- package/src/signaldock/signaldock-transport.ts +5 -2
- package/src/skills/injection/subagent.ts +10 -16
- package/src/skills/manifests/contribution.ts +5 -13
- package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
- package/src/skills/orchestrator/spawn.ts +18 -31
- package/src/skills/orchestrator/startup.ts +78 -65
- package/src/skills/orchestrator/validator.ts +26 -31
- package/src/skills/precedence-types.ts +24 -1
- package/src/skills/types.ts +72 -5
- package/src/store/__tests__/test-db-helper.d.ts +4 -4
- package/src/store/__tests__/test-db-helper.js +5 -16
- package/src/store/__tests__/test-db-helper.ts +5 -18
- package/src/store/brain-sqlite.ts +7 -3
- package/src/store/chain-schema.ts +1 -1
- package/src/store/export.ts +22 -12
- package/src/store/nexus-sqlite.ts +7 -3
- package/src/store/sqlite.ts +9 -3
- package/src/store/tasks-schema.ts +65 -8
- package/src/store/typed-query.ts +17 -0
- package/src/store/validation-schemas.ts +347 -23
- package/src/system/inject-generate.ts +9 -23
- package/src/validation/doctor/checks.ts +24 -2
- package/src/validation/engine.ts +11 -11
- package/src/validation/index.ts +131 -3
- package/src/validation/protocol-common.ts +54 -3
- package/dist/tasks/reparent.d.ts +0 -38
- package/dist/tasks/reparent.d.ts.map +0 -1
- package/src/tasks/reparent.ts +0 -134
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/core",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.45",
|
|
4
4
|
"description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"write-file-atomic": "^6.0.0",
|
|
37
37
|
"yaml": "^2.8.2",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@cleocode/contracts": "2026.3.
|
|
39
|
+
"@cleocode/contracts": "2026.3.45"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=24.0.0"
|
|
@@ -175,15 +175,12 @@ export async function exportTasksPackage(params: ExportTasksParams): Promise<Exp
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const projectMeta = await accessor.getMetaValue<{ name?: string }>('project');
|
|
178
|
-
const
|
|
179
|
-
tasks: allTasks,
|
|
180
|
-
project: projectMeta,
|
|
181
|
-
} as import('@cleocode/contracts').TaskFile;
|
|
182
|
-
const pkg = buildExportPackage(selectedTasks, taskData, {
|
|
178
|
+
const pkg = buildExportPackage(selectedTasks, {
|
|
183
179
|
mode: exportMode,
|
|
184
180
|
rootTaskIds: parsedIds.length > 0 ? parsedIds : selectedTasks.map((t) => t.id),
|
|
185
181
|
includeChildren: subtreeMode,
|
|
186
182
|
filters: filters.length > 0 ? filters : undefined,
|
|
183
|
+
projectName: projectMeta?.name,
|
|
187
184
|
});
|
|
188
185
|
|
|
189
186
|
const content = JSON.stringify(pkg, null, 2);
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from CLI import-tasks command for dispatch layer access.
|
|
5
5
|
*
|
|
6
|
-
* @task T5323, T5328
|
|
6
|
+
* @task T5323, T5328, T046
|
|
7
7
|
* @epic T4545
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { constants as fsConstants } from 'node:fs';
|
|
11
11
|
import { access, readFile } from 'node:fs/promises';
|
|
12
12
|
import type { Task, TaskStatus } from '@cleocode/contracts';
|
|
13
|
+
import type { ImportFromPackageOptions, ImportFromPackageResult } from '../nexus/transfer-types.js';
|
|
13
14
|
import { getAccessor } from '../store/data-accessor.js';
|
|
14
15
|
import type { ExportPackage } from '../store/export.js';
|
|
15
16
|
import {
|
|
@@ -74,25 +75,13 @@ export interface ImportTasksResult {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* Import tasks from
|
|
78
|
+
* Import tasks from an in-memory ExportPackage with ID remapping.
|
|
79
|
+
* Core logic extracted from importTasksPackage for reuse by transfer engine.
|
|
78
80
|
*/
|
|
79
|
-
export async function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await access(file, fsConstants.R_OK);
|
|
84
|
-
} catch {
|
|
85
|
-
throw new Error(`Export file not found: ${file}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const content = await readFile(file, 'utf-8');
|
|
89
|
-
let exportPkg: ExportPackage;
|
|
90
|
-
try {
|
|
91
|
-
exportPkg = JSON.parse(content) as ExportPackage;
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error(`Invalid JSON in: ${file}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
81
|
+
export async function importFromPackage(
|
|
82
|
+
exportPkg: ExportPackage,
|
|
83
|
+
options: ImportFromPackageOptions = {},
|
|
84
|
+
): Promise<ImportFromPackageResult> {
|
|
96
85
|
if (exportPkg._meta?.format !== 'cleo-export') {
|
|
97
86
|
throw new Error(
|
|
98
87
|
`Invalid export format (expected 'cleo-export', got '${exportPkg._meta?.format}')`,
|
|
@@ -103,17 +92,17 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
|
|
|
103
92
|
throw new Error('Export package contains no tasks');
|
|
104
93
|
}
|
|
105
94
|
|
|
106
|
-
const accessor = await getAccessor(
|
|
95
|
+
const accessor = await getAccessor(options.cwd);
|
|
107
96
|
const { tasks: existingTasks } = await accessor.queryTasks({});
|
|
108
97
|
|
|
109
|
-
const onConflict: OnConflict =
|
|
110
|
-
const onMissingDep: OnMissingDep =
|
|
111
|
-
const force =
|
|
112
|
-
const parentId =
|
|
113
|
-
const phaseOverride =
|
|
114
|
-
const addLabel =
|
|
115
|
-
const resetStatus =
|
|
116
|
-
const addProvenance =
|
|
98
|
+
const onConflict: OnConflict = options.onConflict ?? 'fail';
|
|
99
|
+
const onMissingDep: OnMissingDep = options.onMissingDep === 'fail' ? 'fail' : 'strip';
|
|
100
|
+
const force = options.force ?? false;
|
|
101
|
+
const parentId = options.parent;
|
|
102
|
+
const phaseOverride = options.phase;
|
|
103
|
+
const addLabel = options.addLabel;
|
|
104
|
+
const resetStatus = options.resetStatus;
|
|
105
|
+
const addProvenance = options.provenance !== false;
|
|
117
106
|
|
|
118
107
|
if (parentId) {
|
|
119
108
|
const parentExists = existingTasks.some((t) => t.id === parentId);
|
|
@@ -195,7 +184,7 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
|
|
|
195
184
|
existingIds.add(remapped.id);
|
|
196
185
|
}
|
|
197
186
|
|
|
198
|
-
if (
|
|
187
|
+
if (options.dryRun) {
|
|
199
188
|
return {
|
|
200
189
|
imported: transformed.length,
|
|
201
190
|
skipped: skipped.length,
|
|
@@ -217,3 +206,38 @@ export async function importTasksPackage(params: ImportTasksParams): Promise<Imp
|
|
|
217
206
|
idRemap: idRemapJson,
|
|
218
207
|
};
|
|
219
208
|
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Import tasks from a cross-project export package file with ID remapping.
|
|
212
|
+
* Thin wrapper around importFromPackage that handles file I/O.
|
|
213
|
+
*/
|
|
214
|
+
export async function importTasksPackage(params: ImportTasksParams): Promise<ImportTasksResult> {
|
|
215
|
+
const { file } = params;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await access(file, fsConstants.R_OK);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error(`Export file not found: ${file}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const content = await readFile(file, 'utf-8');
|
|
224
|
+
let exportPkg: ExportPackage;
|
|
225
|
+
try {
|
|
226
|
+
exportPkg = JSON.parse(content) as ExportPackage;
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error(`Invalid JSON in: ${file}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return importFromPackage(exportPkg, {
|
|
232
|
+
cwd: params.cwd,
|
|
233
|
+
dryRun: params.dryRun,
|
|
234
|
+
parent: params.parent,
|
|
235
|
+
phase: params.phase,
|
|
236
|
+
addLabel: params.addLabel,
|
|
237
|
+
provenance: params.provenance,
|
|
238
|
+
resetStatus: params.resetStatus,
|
|
239
|
+
onConflict: params.onConflict,
|
|
240
|
+
onMissingDep: params.onMissingDep === 'placeholder' ? 'strip' : params.onMissingDep,
|
|
241
|
+
force: params.force,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for capacity tracking and load balancing.
|
|
3
|
+
*
|
|
4
|
+
* @module agents/__tests__/capacity.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
|
+
|
|
12
|
+
import {
|
|
13
|
+
findLeastLoadedAgent,
|
|
14
|
+
getAvailableCapacity,
|
|
15
|
+
getCapacitySummary,
|
|
16
|
+
isOverloaded,
|
|
17
|
+
updateCapacity,
|
|
18
|
+
} from '../capacity.js';
|
|
19
|
+
import { registerAgent, updateAgentStatus } from '../registry.js';
|
|
20
|
+
|
|
21
|
+
describe('Capacity Tracking', () => {
|
|
22
|
+
let tempDir: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-capacity-test-'));
|
|
26
|
+
await mkdir(join(tempDir, '.cleo'), { recursive: true });
|
|
27
|
+
await mkdir(join(tempDir, '.cleo', 'backups', 'operational'), { recursive: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const { closeAllDatabases } = await import('../../store/sqlite.js');
|
|
33
|
+
await closeAllDatabases();
|
|
34
|
+
} catch {
|
|
35
|
+
/* module may not be loaded */
|
|
36
|
+
}
|
|
37
|
+
await Promise.race([
|
|
38
|
+
rm(tempDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 300 }).catch(() => {}),
|
|
39
|
+
new Promise<void>((resolve) => setTimeout(resolve, 8_000)),
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ==========================================================================
|
|
44
|
+
// updateCapacity
|
|
45
|
+
// ==========================================================================
|
|
46
|
+
|
|
47
|
+
describe('updateCapacity', () => {
|
|
48
|
+
it('updates capacity value', async () => {
|
|
49
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
50
|
+
const updated = await updateCapacity(agent.id, 0.5, tempDir);
|
|
51
|
+
|
|
52
|
+
expect(updated).not.toBeNull();
|
|
53
|
+
expect(parseFloat(updated!.capacity)).toBeCloseTo(0.5, 4);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns null for non-existent agent', async () => {
|
|
57
|
+
const result = await updateCapacity('agt_nonexistent_abc123', 0.5, tempDir);
|
|
58
|
+
expect(result).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects capacity below 0', async () => {
|
|
62
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
63
|
+
await expect(updateCapacity(agent.id, -0.1, tempDir)).rejects.toThrow(
|
|
64
|
+
'Capacity must be between 0.0 and 1.0',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects capacity above 1', async () => {
|
|
69
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
70
|
+
await expect(updateCapacity(agent.id, 1.5, tempDir)).rejects.toThrow(
|
|
71
|
+
'Capacity must be between 0.0 and 1.0',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('accepts boundary values', async () => {
|
|
76
|
+
const agent = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
77
|
+
|
|
78
|
+
const zero = await updateCapacity(agent.id, 0, tempDir);
|
|
79
|
+
expect(parseFloat(zero!.capacity)).toBeCloseTo(0, 4);
|
|
80
|
+
|
|
81
|
+
const one = await updateCapacity(agent.id, 1, tempDir);
|
|
82
|
+
expect(parseFloat(one!.capacity)).toBeCloseTo(1, 4);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ==========================================================================
|
|
87
|
+
// getAvailableCapacity
|
|
88
|
+
// ==========================================================================
|
|
89
|
+
|
|
90
|
+
describe('getAvailableCapacity', () => {
|
|
91
|
+
it('sums capacity of active and idle agents', async () => {
|
|
92
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
93
|
+
const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
|
|
94
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
95
|
+
await updateAgentStatus(a2.id, { status: 'idle' }, tempDir);
|
|
96
|
+
await updateCapacity(a1.id, 0.7, tempDir);
|
|
97
|
+
await updateCapacity(a2.id, 0.3, tempDir);
|
|
98
|
+
|
|
99
|
+
const capacity = await getAvailableCapacity(tempDir);
|
|
100
|
+
expect(capacity).toBeCloseTo(1.0, 1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('excludes stopped and crashed agents', async () => {
|
|
104
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
105
|
+
const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
|
|
106
|
+
const a3 = await registerAgent({ agentType: 'validator' }, tempDir);
|
|
107
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
108
|
+
await updateAgentStatus(a2.id, { status: 'stopped' }, tempDir);
|
|
109
|
+
await updateAgentStatus(a3.id, { status: 'crashed' }, tempDir);
|
|
110
|
+
|
|
111
|
+
const capacity = await getAvailableCapacity(tempDir);
|
|
112
|
+
// Only a1 contributes (1.0 default)
|
|
113
|
+
expect(capacity).toBeCloseTo(1.0, 1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns 0 when no active agents', async () => {
|
|
117
|
+
const capacity = await getAvailableCapacity(tempDir);
|
|
118
|
+
expect(capacity).toBe(0);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ==========================================================================
|
|
123
|
+
// findLeastLoadedAgent
|
|
124
|
+
// ==========================================================================
|
|
125
|
+
|
|
126
|
+
describe('findLeastLoadedAgent', () => {
|
|
127
|
+
it('finds agent with highest capacity', async () => {
|
|
128
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
129
|
+
const a2 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
130
|
+
const a3 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
131
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
132
|
+
await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
|
|
133
|
+
await updateAgentStatus(a3.id, { status: 'active' }, tempDir);
|
|
134
|
+
await updateCapacity(a1.id, 0.2, tempDir);
|
|
135
|
+
await updateCapacity(a2.id, 0.8, tempDir);
|
|
136
|
+
await updateCapacity(a3.id, 0.5, tempDir);
|
|
137
|
+
|
|
138
|
+
const least = await findLeastLoadedAgent(undefined, tempDir);
|
|
139
|
+
expect(least).not.toBeNull();
|
|
140
|
+
expect(least!.id).toBe(a2.id);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('filters by agent type', async () => {
|
|
144
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
145
|
+
const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
|
|
146
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
147
|
+
await updateAgentStatus(a2.id, { status: 'active' }, tempDir);
|
|
148
|
+
await updateCapacity(a1.id, 0.3, tempDir);
|
|
149
|
+
await updateCapacity(a2.id, 0.9, tempDir);
|
|
150
|
+
|
|
151
|
+
const least = await findLeastLoadedAgent('executor', tempDir);
|
|
152
|
+
expect(least).not.toBeNull();
|
|
153
|
+
expect(least!.id).toBe(a1.id);
|
|
154
|
+
expect(least!.agentType).toBe('executor');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns null when no matching agents', async () => {
|
|
158
|
+
const result = await findLeastLoadedAgent('orchestrator', tempDir);
|
|
159
|
+
expect(result).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
// isOverloaded
|
|
165
|
+
// ==========================================================================
|
|
166
|
+
|
|
167
|
+
describe('isOverloaded', () => {
|
|
168
|
+
it('returns true when capacity below threshold', async () => {
|
|
169
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
170
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
171
|
+
await updateCapacity(a1.id, 0.05, tempDir);
|
|
172
|
+
|
|
173
|
+
expect(await isOverloaded(0.1, tempDir)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns false when capacity above threshold', async () => {
|
|
177
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
178
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
179
|
+
await updateCapacity(a1.id, 0.5, tempDir);
|
|
180
|
+
|
|
181
|
+
expect(await isOverloaded(0.1, tempDir)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('returns true when no active agents (0 capacity)', async () => {
|
|
185
|
+
expect(await isOverloaded(0.1, tempDir)).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
// getCapacitySummary
|
|
191
|
+
// ==========================================================================
|
|
192
|
+
|
|
193
|
+
describe('getCapacitySummary', () => {
|
|
194
|
+
it('produces correct summary', async () => {
|
|
195
|
+
const a1 = await registerAgent({ agentType: 'executor' }, tempDir);
|
|
196
|
+
const a2 = await registerAgent({ agentType: 'researcher' }, tempDir);
|
|
197
|
+
await updateAgentStatus(a1.id, { status: 'active' }, tempDir);
|
|
198
|
+
await updateAgentStatus(a2.id, { status: 'idle' }, tempDir);
|
|
199
|
+
await updateCapacity(a1.id, 0.6, tempDir);
|
|
200
|
+
await updateCapacity(a2.id, 0.4, tempDir);
|
|
201
|
+
|
|
202
|
+
const summary = await getCapacitySummary(0.1, tempDir);
|
|
203
|
+
|
|
204
|
+
expect(summary.totalCapacity).toBeCloseTo(1.0, 1);
|
|
205
|
+
expect(summary.activeAgentCount).toBe(2);
|
|
206
|
+
expect(summary.averageCapacity).toBeCloseTo(0.5, 1);
|
|
207
|
+
expect(summary.overloaded).toBe(false);
|
|
208
|
+
expect(summary.threshold).toBe(0.1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('reports overloaded when below threshold', async () => {
|
|
212
|
+
const summary = await getCapacitySummary(0.5, tempDir);
|
|
213
|
+
|
|
214
|
+
expect(summary.totalCapacity).toBe(0);
|
|
215
|
+
expect(summary.activeAgentCount).toBe(0);
|
|
216
|
+
expect(summary.overloaded).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|