@herdctl/core 0.0.1 → 0.0.2
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/config/__tests__/agent.test.js +31 -13
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +9 -2
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/__tests__/schema.test.js +350 -1
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +828 -24
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +118 -6
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/errors.test.js +1 -49
- package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +109 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js +1 -1
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.d.ts +164 -0
- package/dist/fleet-manager/config-reload.d.ts.map +1 -0
- package/dist/fleet-manager/config-reload.js +445 -0
- package/dist/fleet-manager/config-reload.js.map +1 -0
- package/dist/fleet-manager/context.d.ts +76 -0
- package/dist/fleet-manager/context.d.ts.map +1 -0
- package/dist/fleet-manager/context.js +11 -0
- package/dist/fleet-manager/context.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +0 -25
- package/dist/fleet-manager/errors.d.ts.map +1 -1
- package/dist/fleet-manager/errors.js +0 -38
- package/dist/fleet-manager/errors.js.map +1 -1
- package/dist/fleet-manager/event-emitters.d.ts +123 -0
- package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
- package/dist/fleet-manager/event-emitters.js +136 -0
- package/dist/fleet-manager/event-emitters.js.map +1 -0
- package/dist/fleet-manager/event-types.d.ts +0 -15
- package/dist/fleet-manager/event-types.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +40 -653
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +95 -1720
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/index.d.ts +13 -2
- package/dist/fleet-manager/index.d.ts.map +1 -1
- package/dist/fleet-manager/index.js +19 -6
- package/dist/fleet-manager/index.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +64 -0
- package/dist/fleet-manager/job-control.d.ts.map +1 -0
- package/dist/fleet-manager/job-control.js +296 -0
- package/dist/fleet-manager/job-control.js.map +1 -0
- package/dist/fleet-manager/log-streaming.d.ts +171 -0
- package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
- package/dist/fleet-manager/log-streaming.js +503 -0
- package/dist/fleet-manager/log-streaming.js.map +1 -0
- package/dist/fleet-manager/schedule-executor.d.ts +63 -0
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-executor.js +209 -0
- package/dist/fleet-manager/schedule-executor.js.map +1 -0
- package/dist/fleet-manager/schedule-management.d.ts +71 -0
- package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
- package/dist/fleet-manager/schedule-management.js +171 -0
- package/dist/fleet-manager/schedule-management.js.map +1 -0
- package/dist/fleet-manager/status-queries.d.ts +105 -0
- package/dist/fleet-manager/status-queries.d.ts.map +1 -0
- package/dist/fleet-manager/status-queries.js +247 -0
- package/dist/fleet-manager/status-queries.js.map +1 -0
- package/dist/fleet-manager/types.d.ts +0 -39
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +206 -1
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/job-executor.d.ts +9 -0
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +78 -4
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/types.d.ts +2 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
- package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/cron.test.js +867 -0
- package/dist/scheduler/__tests__/cron.test.js.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.js +164 -5
- package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
- package/dist/scheduler/cron.d.ts +126 -0
- package/dist/scheduler/cron.d.ts.map +1 -0
- package/dist/scheduler/cron.js +390 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/errors.d.ts +81 -1
- package/dist/scheduler/errors.d.ts.map +1 -1
- package/dist/scheduler/errors.js +81 -6
- package/dist/scheduler/errors.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts +2 -2
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +20 -8
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +4 -4
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +86 -20
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/state/schemas/job-metadata.d.ts +2 -2
- package/package.json +33 -8
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -219
- package/.turbo/turbo-typecheck.log +0 -4
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -51
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -251
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/config/index.html +0 -191
- package/coverage/src/config/index.ts.html +0 -442
- package/coverage/src/config/interpolate.ts.html +0 -652
- package/coverage/src/config/loader.ts.html +0 -1501
- package/coverage/src/config/merge.ts.html +0 -823
- package/coverage/src/config/parser.ts.html +0 -1213
- package/coverage/src/config/schema.ts.html +0 -1123
- package/coverage/src/fleet-manager/errors.ts.html +0 -2326
- package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
- package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
- package/coverage/src/fleet-manager/index.html +0 -206
- package/coverage/src/fleet-manager/index.ts.html +0 -469
- package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
- package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
- package/coverage/src/fleet-manager/types.ts.html +0 -2602
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -181
- package/coverage/src/runner/errors.ts.html +0 -1006
- package/coverage/src/runner/index.html +0 -191
- package/coverage/src/runner/index.ts.html +0 -256
- package/coverage/src/runner/job-executor.ts.html +0 -1429
- package/coverage/src/runner/message-processor.ts.html +0 -1150
- package/coverage/src/runner/sdk-adapter.ts.html +0 -658
- package/coverage/src/runner/types.ts.html +0 -559
- package/coverage/src/scheduler/errors.ts.html +0 -388
- package/coverage/src/scheduler/index.html +0 -206
- package/coverage/src/scheduler/index.ts.html +0 -244
- package/coverage/src/scheduler/interval.ts.html +0 -652
- package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
- package/coverage/src/scheduler/schedule-state.ts.html +0 -718
- package/coverage/src/scheduler/scheduler.ts.html +0 -1795
- package/coverage/src/scheduler/types.ts.html +0 -733
- package/coverage/src/state/directory.ts.html +0 -736
- package/coverage/src/state/errors.ts.html +0 -376
- package/coverage/src/state/fleet-state.ts.html +0 -937
- package/coverage/src/state/index.html +0 -221
- package/coverage/src/state/index.ts.html +0 -322
- package/coverage/src/state/job-metadata.ts.html +0 -1420
- package/coverage/src/state/job-output.ts.html +0 -1033
- package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
- package/coverage/src/state/schemas/index.html +0 -176
- package/coverage/src/state/schemas/index.ts.html +0 -286
- package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
- package/coverage/src/state/schemas/job-output.ts.html +0 -616
- package/coverage/src/state/schemas/session-info.ts.html +0 -361
- package/coverage/src/state/session.ts.html +0 -844
- package/coverage/src/state/types.ts.html +0 -262
- package/coverage/src/state/utils/atomic.ts.html +0 -748
- package/coverage/src/state/utils/index.html +0 -146
- package/coverage/src/state/utils/index.ts.html +0 -103
- package/coverage/src/state/utils/reads.ts.html +0 -1621
- package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
- package/coverage/src/work-sources/adapters/index.html +0 -131
- package/coverage/src/work-sources/adapters/index.ts.html +0 -277
- package/coverage/src/work-sources/errors.ts.html +0 -298
- package/coverage/src/work-sources/index.html +0 -176
- package/coverage/src/work-sources/index.ts.html +0 -529
- package/coverage/src/work-sources/manager.ts.html +0 -1324
- package/coverage/src/work-sources/registry.ts.html +0 -619
- package/coverage/src/work-sources/types.ts.html +0 -568
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
- package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
- package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
- package/src/config/__tests__/agent.test.ts +0 -864
- package/src/config/__tests__/interpolate.test.ts +0 -644
- package/src/config/__tests__/loader.test.ts +0 -784
- package/src/config/__tests__/merge.test.ts +0 -751
- package/src/config/__tests__/parser.test.ts +0 -533
- package/src/config/__tests__/schema.test.ts +0 -873
- package/src/config/index.ts +0 -119
- package/src/config/interpolate.ts +0 -189
- package/src/config/loader.ts +0 -472
- package/src/config/merge.ts +0 -246
- package/src/config/parser.ts +0 -376
- package/src/config/schema.ts +0 -346
- package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
- package/src/fleet-manager/__tests__/errors.test.ts +0 -660
- package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
- package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
- package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
- package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
- package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
- package/src/fleet-manager/__tests__/reload.test.ts +0 -751
- package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
- package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
- package/src/fleet-manager/errors.ts +0 -747
- package/src/fleet-manager/event-types.ts +0 -378
- package/src/fleet-manager/fleet-manager.ts +0 -2315
- package/src/fleet-manager/index.ts +0 -128
- package/src/fleet-manager/job-manager.ts +0 -663
- package/src/fleet-manager/job-queue.ts +0 -798
- package/src/fleet-manager/types.ts +0 -839
- package/src/index.ts +0 -32
- package/src/runner/__tests__/errors.test.ts +0 -382
- package/src/runner/__tests__/job-executor.test.ts +0 -1708
- package/src/runner/__tests__/message-processor.test.ts +0 -960
- package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
- package/src/runner/errors.ts +0 -307
- package/src/runner/index.ts +0 -57
- package/src/runner/job-executor.ts +0 -448
- package/src/runner/message-processor.ts +0 -355
- package/src/runner/sdk-adapter.ts +0 -191
- package/src/runner/types.ts +0 -158
- package/src/scheduler/__tests__/errors.test.ts +0 -159
- package/src/scheduler/__tests__/interval.test.ts +0 -515
- package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
- package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
- package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
- package/src/scheduler/errors.ts +0 -101
- package/src/scheduler/index.ts +0 -53
- package/src/scheduler/interval.ts +0 -189
- package/src/scheduler/schedule-runner.ts +0 -442
- package/src/scheduler/schedule-state.ts +0 -211
- package/src/scheduler/scheduler.ts +0 -570
- package/src/scheduler/types.ts +0 -216
- package/src/state/__tests__/directory.test.ts +0 -595
- package/src/state/__tests__/fleet-state.test.ts +0 -868
- package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
- package/src/state/__tests__/job-metadata.test.ts +0 -831
- package/src/state/__tests__/job-output.test.ts +0 -856
- package/src/state/__tests__/session-schema.test.ts +0 -378
- package/src/state/__tests__/session.test.ts +0 -604
- package/src/state/directory.ts +0 -217
- package/src/state/errors.ts +0 -97
- package/src/state/fleet-state.ts +0 -284
- package/src/state/index.ts +0 -79
- package/src/state/job-metadata.ts +0 -445
- package/src/state/job-output.ts +0 -316
- package/src/state/schemas/__tests__/job-output.test.ts +0 -338
- package/src/state/schemas/fleet-state.ts +0 -120
- package/src/state/schemas/index.ts +0 -67
- package/src/state/schemas/job-metadata.ts +0 -181
- package/src/state/schemas/job-output.ts +0 -177
- package/src/state/schemas/session-info.ts +0 -92
- package/src/state/session.ts +0 -253
- package/src/state/types.ts +0 -59
- package/src/state/utils/__tests__/atomic.test.ts +0 -723
- package/src/state/utils/__tests__/reads.test.ts +0 -1071
- package/src/state/utils/atomic.ts +0 -221
- package/src/state/utils/index.ts +0 -6
- package/src/state/utils/reads.ts +0 -512
- package/src/work-sources/__tests__/github.test.ts +0 -1800
- package/src/work-sources/__tests__/manager.test.ts +0 -529
- package/src/work-sources/__tests__/registry.test.ts +0 -477
- package/src/work-sources/__tests__/types.test.ts +0 -479
- package/src/work-sources/adapters/github.ts +0 -1166
- package/src/work-sources/adapters/index.ts +0 -64
- package/src/work-sources/errors.ts +0 -71
- package/src/work-sources/index.ts +0 -148
- package/src/work-sources/manager.ts +0 -413
- package/src/work-sources/registry.ts +0 -178
- package/src/work-sources/types.ts +0 -161
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -19
|
@@ -1,49 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FleetManager
|
|
2
|
+
* FleetManager - High-level orchestration layer for autonomous agents
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* import { FleetManager } from '@herdctl/core';
|
|
11
|
-
*
|
|
12
|
-
* const manager = new FleetManager({
|
|
13
|
-
* configPath: './herdctl.yaml',
|
|
14
|
-
* stateDir: './.herdctl',
|
|
15
|
-
* });
|
|
16
|
-
*
|
|
17
|
-
* await manager.initialize();
|
|
18
|
-
* await manager.start();
|
|
4
|
+
* The FleetManager class provides a simple interface for library consumers
|
|
5
|
+
* to initialize and run agent fleets. It coordinates between:
|
|
6
|
+
* - Configuration loading and validation
|
|
7
|
+
* - State directory management
|
|
8
|
+
* - Scheduler setup and lifecycle
|
|
9
|
+
* - Event emission for monitoring
|
|
19
10
|
*
|
|
20
|
-
*
|
|
21
|
-
* await manager.stop();
|
|
22
|
-
* ```
|
|
11
|
+
* @module fleet-manager
|
|
23
12
|
*/
|
|
24
13
|
import { EventEmitter } from "node:events";
|
|
25
14
|
import { resolve } from "node:path";
|
|
26
15
|
import { loadConfig, ConfigNotFoundError, ConfigError, } from "../config/index.js";
|
|
27
|
-
import { initStateDirectory
|
|
16
|
+
import { initStateDirectory } from "../state/index.js";
|
|
28
17
|
import { Scheduler } from "../scheduler/index.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Default check interval in milliseconds (1 second)
|
|
39
|
-
*/
|
|
18
|
+
import { InvalidStateError, ConfigurationError, FleetManagerStateDirError, FleetManagerShutdownError, } from "./errors.js";
|
|
19
|
+
// Module classes
|
|
20
|
+
import { StatusQueries } from "./status-queries.js";
|
|
21
|
+
import { ScheduleManagement } from "./schedule-management.js";
|
|
22
|
+
import { ConfigReload, computeConfigChanges } from "./config-reload.js";
|
|
23
|
+
import { JobControl } from "./job-control.js";
|
|
24
|
+
import { LogStreaming } from "./log-streaming.js";
|
|
25
|
+
import { ScheduleExecutor } from "./schedule-executor.js";
|
|
40
26
|
const DEFAULT_CHECK_INTERVAL = 1000;
|
|
41
|
-
// =============================================================================
|
|
42
|
-
// Default Logger
|
|
43
|
-
// =============================================================================
|
|
44
|
-
/**
|
|
45
|
-
* Create a default console-based logger
|
|
46
|
-
*/
|
|
47
27
|
function createDefaultLogger() {
|
|
48
28
|
return {
|
|
49
29
|
debug: (message) => console.debug(`[fleet-manager] ${message}`),
|
|
@@ -52,66 +32,11 @@ function createDefaultLogger() {
|
|
|
52
32
|
error: (message) => console.error(`[fleet-manager] ${message}`),
|
|
53
33
|
};
|
|
54
34
|
}
|
|
55
|
-
// =============================================================================
|
|
56
|
-
// FleetManager Class
|
|
57
|
-
// =============================================================================
|
|
58
35
|
/**
|
|
59
|
-
* FleetManager provides
|
|
60
|
-
*
|
|
61
|
-
* This class is the primary entry point for library consumers who want to
|
|
62
|
-
* run herdctl programmatically. It handles:
|
|
63
|
-
*
|
|
64
|
-
* - Configuration loading and validation
|
|
65
|
-
* - State directory initialization
|
|
66
|
-
* - Scheduler lifecycle management
|
|
67
|
-
* - Event emission for monitoring
|
|
68
|
-
*
|
|
69
|
-
* ## Lifecycle
|
|
70
|
-
*
|
|
71
|
-
* 1. **Construction**: Create with options (configPath, stateDir)
|
|
72
|
-
* 2. **Initialize**: Call `initialize()` to load config and prepare state
|
|
73
|
-
* 3. **Start**: Call `start()` to begin scheduler and process schedules
|
|
74
|
-
* 4. **Stop**: Call `stop()` to gracefully shut down
|
|
75
|
-
*
|
|
76
|
-
* ## Events
|
|
77
|
-
*
|
|
78
|
-
* The FleetManager emits events for monitoring:
|
|
79
|
-
* - `initialized` - After successful initialization
|
|
80
|
-
* - `started` - When the scheduler starts running
|
|
81
|
-
* - `stopped` - When the scheduler stops
|
|
82
|
-
* - `error` - When an error occurs
|
|
83
|
-
* - `schedule:trigger` - When a schedule triggers an agent
|
|
84
|
-
* - `schedule:complete` - When an agent run completes
|
|
85
|
-
* - `schedule:error` - When an agent run fails
|
|
36
|
+
* FleetManager provides high-level orchestration for autonomous agents
|
|
86
37
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* The FleetManager also supports strongly-typed events via TypeScript:
|
|
90
|
-
* - `config:reloaded` - When configuration is hot-reloaded
|
|
91
|
-
* - `agent:started` - When an agent is started
|
|
92
|
-
* - `agent:stopped` - When an agent is stopped
|
|
93
|
-
* - `schedule:triggered` - When a schedule triggers (with payload)
|
|
94
|
-
* - `schedule:skipped` - When a schedule is skipped
|
|
95
|
-
* - `job:created` - When a job is created
|
|
96
|
-
* - `job:output` - When a job produces output
|
|
97
|
-
* - `job:completed` - When a job completes successfully
|
|
98
|
-
* - `job:failed` - When a job fails
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```typescript
|
|
102
|
-
* // Subscribe to typed events
|
|
103
|
-
* manager.on('job:created', (payload) => {
|
|
104
|
-
* console.log(`Job ${payload.job.id} created for ${payload.agentName}`);
|
|
105
|
-
* });
|
|
106
|
-
*
|
|
107
|
-
* manager.on('job:output', (payload) => {
|
|
108
|
-
* process.stdout.write(payload.output);
|
|
109
|
-
* });
|
|
110
|
-
*
|
|
111
|
-
* manager.on('job:completed', (payload) => {
|
|
112
|
-
* console.log(`Job completed in ${payload.durationSeconds}s`);
|
|
113
|
-
* });
|
|
114
|
-
* ```
|
|
38
|
+
* Implements FleetManagerContext to provide clean access to internal state
|
|
39
|
+
* for composed module classes.
|
|
115
40
|
*/
|
|
116
41
|
export class FleetManager extends EventEmitter {
|
|
117
42
|
// Configuration
|
|
@@ -129,44 +54,40 @@ export class FleetManager extends EventEmitter {
|
|
|
129
54
|
startedAt = null;
|
|
130
55
|
stoppedAt = null;
|
|
131
56
|
lastError = null;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* // Minimal configuration
|
|
140
|
-
* const manager = new FleetManager({
|
|
141
|
-
* configPath: './herdctl.yaml',
|
|
142
|
-
* stateDir: './.herdctl',
|
|
143
|
-
* });
|
|
144
|
-
*
|
|
145
|
-
* // With custom logger
|
|
146
|
-
* const manager = new FleetManager({
|
|
147
|
-
* configPath: './herdctl.yaml',
|
|
148
|
-
* stateDir: './.herdctl',
|
|
149
|
-
* logger: myLogger,
|
|
150
|
-
* checkInterval: 5000, // 5 seconds
|
|
151
|
-
* });
|
|
152
|
-
* ```
|
|
153
|
-
*/
|
|
57
|
+
// Module class instances
|
|
58
|
+
statusQueries;
|
|
59
|
+
scheduleManagement;
|
|
60
|
+
configReloadModule;
|
|
61
|
+
jobControl;
|
|
62
|
+
logStreaming;
|
|
63
|
+
scheduleExecutor;
|
|
154
64
|
constructor(options) {
|
|
155
65
|
super();
|
|
156
66
|
this.configPath = options.configPath;
|
|
157
67
|
this.stateDir = resolve(options.stateDir);
|
|
158
68
|
this.logger = options.logger ?? createDefaultLogger();
|
|
159
69
|
this.checkInterval = options.checkInterval ?? DEFAULT_CHECK_INTERVAL;
|
|
70
|
+
// Initialize modules in constructor so they work before initialize() is called
|
|
71
|
+
this.initializeModules();
|
|
160
72
|
}
|
|
161
73
|
// ===========================================================================
|
|
74
|
+
// FleetManagerContext Implementation
|
|
75
|
+
// ===========================================================================
|
|
76
|
+
getConfig() { return this.config; }
|
|
77
|
+
getStateDir() { return this.stateDir; }
|
|
78
|
+
getStateDirInfo() { return this.stateDirInfo; }
|
|
79
|
+
getLogger() { return this.logger; }
|
|
80
|
+
getScheduler() { return this.scheduler; }
|
|
81
|
+
getStatus() { return this.status; }
|
|
82
|
+
getInitializedAt() { return this.initializedAt; }
|
|
83
|
+
getStartedAt() { return this.startedAt; }
|
|
84
|
+
getStoppedAt() { return this.stoppedAt; }
|
|
85
|
+
getLastError() { return this.lastError; }
|
|
86
|
+
getCheckInterval() { return this.checkInterval; }
|
|
87
|
+
getEmitter() { return this; }
|
|
88
|
+
// ===========================================================================
|
|
162
89
|
// Public State Accessors
|
|
163
90
|
// ===========================================================================
|
|
164
|
-
/**
|
|
165
|
-
* Get the current fleet manager state
|
|
166
|
-
*
|
|
167
|
-
* This provides a snapshot of the fleet manager's current status and
|
|
168
|
-
* configuration for monitoring purposes.
|
|
169
|
-
*/
|
|
170
91
|
get state() {
|
|
171
92
|
return {
|
|
172
93
|
status: this.status,
|
|
@@ -177,473 +98,26 @@ export class FleetManager extends EventEmitter {
|
|
|
177
98
|
lastError: this.lastError,
|
|
178
99
|
};
|
|
179
100
|
}
|
|
180
|
-
|
|
181
|
-
* Get the loaded configuration
|
|
182
|
-
*
|
|
183
|
-
* @returns The resolved configuration, or null if not initialized
|
|
184
|
-
*/
|
|
185
|
-
getConfig() {
|
|
186
|
-
return this.config;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Get the loaded agents
|
|
190
|
-
*
|
|
191
|
-
* @returns Array of resolved agents, or empty array if not initialized
|
|
192
|
-
*/
|
|
193
|
-
getAgents() {
|
|
194
|
-
return this.config?.agents ?? [];
|
|
195
|
-
}
|
|
196
|
-
// ===========================================================================
|
|
197
|
-
// Fleet Status Query Methods (US-3)
|
|
198
|
-
// ===========================================================================
|
|
199
|
-
/**
|
|
200
|
-
* Get overall fleet status
|
|
201
|
-
*
|
|
202
|
-
* Returns a comprehensive snapshot of the fleet state including:
|
|
203
|
-
* - Current state and uptime
|
|
204
|
-
* - Agent counts (total, idle, running, error)
|
|
205
|
-
* - Job counts
|
|
206
|
-
* - Scheduler information
|
|
207
|
-
*
|
|
208
|
-
* This method works whether the fleet is running or stopped.
|
|
209
|
-
*
|
|
210
|
-
* @returns A consistent FleetStatus snapshot
|
|
211
|
-
*
|
|
212
|
-
* @example
|
|
213
|
-
* ```typescript
|
|
214
|
-
* const status = await manager.getFleetStatus();
|
|
215
|
-
* console.log(`Fleet: ${status.state}`);
|
|
216
|
-
* console.log(`Uptime: ${status.uptimeSeconds}s`);
|
|
217
|
-
* console.log(`Running jobs: ${status.counts.runningJobs}`);
|
|
218
|
-
* ```
|
|
219
|
-
*/
|
|
220
|
-
async getFleetStatus() {
|
|
221
|
-
// Get agent info to compute counts
|
|
222
|
-
const agentInfoList = await this.getAgentInfo();
|
|
223
|
-
// Compute counts from agent info
|
|
224
|
-
const counts = this.computeFleetCounts(agentInfoList);
|
|
225
|
-
// Compute uptime
|
|
226
|
-
let uptimeSeconds = null;
|
|
227
|
-
if (this.startedAt) {
|
|
228
|
-
const startTime = new Date(this.startedAt).getTime();
|
|
229
|
-
const endTime = this.stoppedAt
|
|
230
|
-
? new Date(this.stoppedAt).getTime()
|
|
231
|
-
: Date.now();
|
|
232
|
-
uptimeSeconds = Math.floor((endTime - startTime) / 1000);
|
|
233
|
-
}
|
|
234
|
-
// Get scheduler state
|
|
235
|
-
const schedulerState = this.scheduler?.getState();
|
|
236
|
-
return {
|
|
237
|
-
state: this.status,
|
|
238
|
-
uptimeSeconds,
|
|
239
|
-
initializedAt: this.initializedAt,
|
|
240
|
-
startedAt: this.startedAt,
|
|
241
|
-
stoppedAt: this.stoppedAt,
|
|
242
|
-
counts,
|
|
243
|
-
scheduler: {
|
|
244
|
-
status: schedulerState?.status ?? "stopped",
|
|
245
|
-
checkCount: schedulerState?.checkCount ?? 0,
|
|
246
|
-
triggerCount: schedulerState?.triggerCount ?? 0,
|
|
247
|
-
lastCheckAt: schedulerState?.lastCheckAt ?? null,
|
|
248
|
-
checkIntervalMs: this.checkInterval,
|
|
249
|
-
},
|
|
250
|
-
lastError: this.lastError,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Get information about all configured agents
|
|
255
|
-
*
|
|
256
|
-
* Returns detailed information for each agent including:
|
|
257
|
-
* - Current status and job information
|
|
258
|
-
* - Schedule details with runtime state
|
|
259
|
-
* - Configuration details
|
|
260
|
-
*
|
|
261
|
-
* This method works whether the fleet is running or stopped.
|
|
262
|
-
*
|
|
263
|
-
* @returns Array of AgentInfo objects with current state
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* ```typescript
|
|
267
|
-
* const agents = await manager.getAgentInfo();
|
|
268
|
-
* for (const agent of agents) {
|
|
269
|
-
* console.log(`${agent.name}: ${agent.status}`);
|
|
270
|
-
* console.log(` Schedules: ${agent.scheduleCount}`);
|
|
271
|
-
* }
|
|
272
|
-
* ```
|
|
273
|
-
*/
|
|
274
|
-
async getAgentInfo() {
|
|
275
|
-
const agents = this.config?.agents ?? [];
|
|
276
|
-
// Read fleet state for runtime information
|
|
277
|
-
const fleetState = await this.readFleetStateSnapshot();
|
|
278
|
-
return agents.map((agent) => {
|
|
279
|
-
const agentState = fleetState.agents[agent.name];
|
|
280
|
-
return this.buildAgentInfo(agent, agentState);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Get information about a specific agent by name
|
|
285
|
-
*
|
|
286
|
-
* Returns detailed information for the specified agent including:
|
|
287
|
-
* - Current status and job information
|
|
288
|
-
* - Schedule details with runtime state
|
|
289
|
-
* - Configuration details
|
|
290
|
-
*
|
|
291
|
-
* This method works whether the fleet is running or stopped.
|
|
292
|
-
*
|
|
293
|
-
* @param name - The agent name to look up
|
|
294
|
-
* @returns AgentInfo for the specified agent
|
|
295
|
-
* @throws {AgentNotFoundError} If no agent with that name exists
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* ```typescript
|
|
299
|
-
* const agent = await manager.getAgentInfoByName('my-agent');
|
|
300
|
-
* console.log(`Agent: ${agent.name}`);
|
|
301
|
-
* console.log(`Status: ${agent.status}`);
|
|
302
|
-
* console.log(`Running: ${agent.runningCount}/${agent.maxConcurrent}`);
|
|
303
|
-
* ```
|
|
304
|
-
*/
|
|
305
|
-
async getAgentInfoByName(name) {
|
|
306
|
-
const agents = this.config?.agents ?? [];
|
|
307
|
-
const agent = agents.find((a) => a.name === name);
|
|
308
|
-
if (!agent) {
|
|
309
|
-
throw new AgentNotFoundError(name);
|
|
310
|
-
}
|
|
311
|
-
// Read fleet state for runtime information
|
|
312
|
-
const fleetState = await this.readFleetStateSnapshot();
|
|
313
|
-
const agentState = fleetState.agents[name];
|
|
314
|
-
return this.buildAgentInfo(agent, agentState);
|
|
315
|
-
}
|
|
316
|
-
// ===========================================================================
|
|
317
|
-
// Private Status Query Helpers
|
|
318
|
-
// ===========================================================================
|
|
319
|
-
/**
|
|
320
|
-
* Read fleet state from disk for status queries
|
|
321
|
-
*
|
|
322
|
-
* This provides a consistent snapshot of the fleet state.
|
|
323
|
-
*/
|
|
324
|
-
async readFleetStateSnapshot() {
|
|
325
|
-
if (!this.stateDirInfo) {
|
|
326
|
-
// Not initialized yet, return empty state
|
|
327
|
-
return { fleet: {}, agents: {} };
|
|
328
|
-
}
|
|
329
|
-
return await readFleetState(this.stateDirInfo.stateFile, {
|
|
330
|
-
logger: { warn: this.logger.warn },
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Build AgentInfo from configuration and state
|
|
335
|
-
*/
|
|
336
|
-
buildAgentInfo(agent, agentState) {
|
|
337
|
-
// Build schedule info
|
|
338
|
-
const schedules = this.buildScheduleInfoList(agent, agentState);
|
|
339
|
-
// Get running count from scheduler or state
|
|
340
|
-
const runningCount = this.scheduler?.getRunningJobCount(agent.name) ?? 0;
|
|
341
|
-
// Determine workspace path
|
|
342
|
-
let workspace;
|
|
343
|
-
if (typeof agent.workspace === "string") {
|
|
344
|
-
workspace = agent.workspace;
|
|
345
|
-
}
|
|
346
|
-
else if (agent.workspace?.root) {
|
|
347
|
-
workspace = agent.workspace.root;
|
|
348
|
-
}
|
|
349
|
-
return {
|
|
350
|
-
name: agent.name,
|
|
351
|
-
description: agent.description,
|
|
352
|
-
status: agentState?.status ?? "idle",
|
|
353
|
-
currentJobId: agentState?.current_job ?? null,
|
|
354
|
-
lastJobId: agentState?.last_job ?? null,
|
|
355
|
-
maxConcurrent: agent.instances?.max_concurrent ?? 1,
|
|
356
|
-
runningCount,
|
|
357
|
-
errorMessage: agentState?.error_message ?? null,
|
|
358
|
-
scheduleCount: schedules.length,
|
|
359
|
-
schedules,
|
|
360
|
-
model: agent.model,
|
|
361
|
-
workspace,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Build schedule info list from agent configuration and state
|
|
366
|
-
*/
|
|
367
|
-
buildScheduleInfoList(agent, agentState) {
|
|
368
|
-
if (!agent.schedules) {
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
return Object.entries(agent.schedules).map(([name, schedule]) => {
|
|
372
|
-
const scheduleState = agentState?.schedules?.[name];
|
|
373
|
-
return {
|
|
374
|
-
name,
|
|
375
|
-
agentName: agent.name,
|
|
376
|
-
type: schedule.type,
|
|
377
|
-
interval: schedule.interval,
|
|
378
|
-
expression: schedule.expression,
|
|
379
|
-
status: scheduleState?.status ?? "idle",
|
|
380
|
-
lastRunAt: scheduleState?.last_run_at ?? null,
|
|
381
|
-
nextRunAt: scheduleState?.next_run_at ?? null,
|
|
382
|
-
lastError: scheduleState?.last_error ?? null,
|
|
383
|
-
};
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Compute fleet counts from agent info list
|
|
388
|
-
*/
|
|
389
|
-
computeFleetCounts(agentInfoList) {
|
|
390
|
-
let idleAgents = 0;
|
|
391
|
-
let runningAgents = 0;
|
|
392
|
-
let errorAgents = 0;
|
|
393
|
-
let totalSchedules = 0;
|
|
394
|
-
let runningSchedules = 0;
|
|
395
|
-
let runningJobs = 0;
|
|
396
|
-
for (const agent of agentInfoList) {
|
|
397
|
-
switch (agent.status) {
|
|
398
|
-
case "idle":
|
|
399
|
-
idleAgents++;
|
|
400
|
-
break;
|
|
401
|
-
case "running":
|
|
402
|
-
runningAgents++;
|
|
403
|
-
break;
|
|
404
|
-
case "error":
|
|
405
|
-
errorAgents++;
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
totalSchedules += agent.scheduleCount;
|
|
409
|
-
runningJobs += agent.runningCount;
|
|
410
|
-
for (const schedule of agent.schedules) {
|
|
411
|
-
if (schedule.status === "running") {
|
|
412
|
-
runningSchedules++;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
totalAgents: agentInfoList.length,
|
|
418
|
-
idleAgents,
|
|
419
|
-
runningAgents,
|
|
420
|
-
errorAgents,
|
|
421
|
-
totalSchedules,
|
|
422
|
-
runningSchedules,
|
|
423
|
-
runningJobs,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
// ===========================================================================
|
|
427
|
-
// Schedule Management Methods (US-7)
|
|
428
|
-
// ===========================================================================
|
|
429
|
-
/**
|
|
430
|
-
* Get all schedules across all agents
|
|
431
|
-
*
|
|
432
|
-
* Returns a list of all configured schedules with their current state,
|
|
433
|
-
* including next trigger times.
|
|
434
|
-
*
|
|
435
|
-
* @returns Array of ScheduleInfo objects with current state
|
|
436
|
-
*
|
|
437
|
-
* @example
|
|
438
|
-
* ```typescript
|
|
439
|
-
* const schedules = await manager.getSchedules();
|
|
440
|
-
* for (const schedule of schedules) {
|
|
441
|
-
* console.log(`${schedule.agentName}/${schedule.name}: ${schedule.status}`);
|
|
442
|
-
* console.log(` Next run: ${schedule.nextRunAt}`);
|
|
443
|
-
* }
|
|
444
|
-
* ```
|
|
445
|
-
*/
|
|
446
|
-
async getSchedules() {
|
|
447
|
-
const agents = this.config?.agents ?? [];
|
|
448
|
-
const fleetState = await this.readFleetStateSnapshot();
|
|
449
|
-
const allSchedules = [];
|
|
450
|
-
for (const agent of agents) {
|
|
451
|
-
const agentState = fleetState.agents[agent.name];
|
|
452
|
-
const schedules = this.buildScheduleInfoList(agent, agentState);
|
|
453
|
-
allSchedules.push(...schedules);
|
|
454
|
-
}
|
|
455
|
-
return allSchedules;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Get a specific schedule by agent name and schedule name
|
|
459
|
-
*
|
|
460
|
-
* @param agentName - The name of the agent
|
|
461
|
-
* @param scheduleName - The name of the schedule
|
|
462
|
-
* @returns The schedule info with current state
|
|
463
|
-
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
464
|
-
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
465
|
-
*
|
|
466
|
-
* @example
|
|
467
|
-
* ```typescript
|
|
468
|
-
* const schedule = await manager.getSchedule('my-agent', 'hourly');
|
|
469
|
-
* console.log(`Status: ${schedule.status}`);
|
|
470
|
-
* console.log(`Last run: ${schedule.lastRunAt}`);
|
|
471
|
-
* console.log(`Next run: ${schedule.nextRunAt}`);
|
|
472
|
-
* ```
|
|
473
|
-
*/
|
|
474
|
-
async getSchedule(agentName, scheduleName) {
|
|
475
|
-
const agents = this.config?.agents ?? [];
|
|
476
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
477
|
-
if (!agent) {
|
|
478
|
-
throw new AgentNotFoundError(agentName, {
|
|
479
|
-
availableAgents: agents.map((a) => a.name),
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
483
|
-
const availableSchedules = agent.schedules
|
|
484
|
-
? Object.keys(agent.schedules)
|
|
485
|
-
: [];
|
|
486
|
-
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
487
|
-
availableSchedules,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
const fleetState = await this.readFleetStateSnapshot();
|
|
491
|
-
const agentState = fleetState.agents[agentName];
|
|
492
|
-
const schedule = agent.schedules[scheduleName];
|
|
493
|
-
const scheduleState = agentState?.schedules?.[scheduleName];
|
|
494
|
-
return {
|
|
495
|
-
name: scheduleName,
|
|
496
|
-
agentName,
|
|
497
|
-
type: schedule.type,
|
|
498
|
-
interval: schedule.interval,
|
|
499
|
-
expression: schedule.expression,
|
|
500
|
-
status: scheduleState?.status ?? "idle",
|
|
501
|
-
lastRunAt: scheduleState?.last_run_at ?? null,
|
|
502
|
-
nextRunAt: scheduleState?.next_run_at ?? null,
|
|
503
|
-
lastError: scheduleState?.last_error ?? null,
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Enable a disabled schedule
|
|
508
|
-
*
|
|
509
|
-
* Enables a schedule that was previously disabled, allowing it to trigger
|
|
510
|
-
* again on its configured interval. The enabled state is persisted to the
|
|
511
|
-
* state directory and survives restarts.
|
|
512
|
-
*
|
|
513
|
-
* @param agentName - The name of the agent
|
|
514
|
-
* @param scheduleName - The name of the schedule
|
|
515
|
-
* @returns The updated schedule info
|
|
516
|
-
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
517
|
-
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
518
|
-
*
|
|
519
|
-
* @example
|
|
520
|
-
* ```typescript
|
|
521
|
-
* // Enable a previously disabled schedule
|
|
522
|
-
* const schedule = await manager.enableSchedule('my-agent', 'hourly');
|
|
523
|
-
* console.log(`Schedule status: ${schedule.status}`); // 'idle'
|
|
524
|
-
* ```
|
|
525
|
-
*/
|
|
526
|
-
async enableSchedule(agentName, scheduleName) {
|
|
527
|
-
// Validate the agent and schedule exist
|
|
528
|
-
const agents = this.config?.agents ?? [];
|
|
529
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
530
|
-
if (!agent) {
|
|
531
|
-
throw new AgentNotFoundError(agentName, {
|
|
532
|
-
availableAgents: agents.map((a) => a.name),
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
536
|
-
const availableSchedules = agent.schedules
|
|
537
|
-
? Object.keys(agent.schedules)
|
|
538
|
-
: [];
|
|
539
|
-
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
540
|
-
availableSchedules,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
// Update schedule state to enabled (idle)
|
|
544
|
-
const { updateScheduleState } = await import("../scheduler/schedule-state.js");
|
|
545
|
-
await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "idle" }, { logger: { warn: this.logger.warn } });
|
|
546
|
-
this.logger.info(`Enabled schedule ${agentName}/${scheduleName}`);
|
|
547
|
-
// Return the updated schedule info
|
|
548
|
-
return this.getSchedule(agentName, scheduleName);
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Disable a schedule
|
|
552
|
-
*
|
|
553
|
-
* Disables a schedule, preventing it from triggering on its configured
|
|
554
|
-
* interval. The schedule remains in the configuration but won't run until
|
|
555
|
-
* re-enabled. The disabled state is persisted to the state directory and
|
|
556
|
-
* survives restarts.
|
|
557
|
-
*
|
|
558
|
-
* @param agentName - The name of the agent
|
|
559
|
-
* @param scheduleName - The name of the schedule
|
|
560
|
-
* @returns The updated schedule info
|
|
561
|
-
* @throws {AgentNotFoundError} If the agent doesn't exist
|
|
562
|
-
* @throws {ScheduleNotFoundError} If the schedule doesn't exist
|
|
563
|
-
*
|
|
564
|
-
* @example
|
|
565
|
-
* ```typescript
|
|
566
|
-
* // Disable a schedule temporarily
|
|
567
|
-
* const schedule = await manager.disableSchedule('my-agent', 'hourly');
|
|
568
|
-
* console.log(`Schedule status: ${schedule.status}`); // 'disabled'
|
|
569
|
-
*
|
|
570
|
-
* // Later, re-enable it
|
|
571
|
-
* await manager.enableSchedule('my-agent', 'hourly');
|
|
572
|
-
* ```
|
|
573
|
-
*/
|
|
574
|
-
async disableSchedule(agentName, scheduleName) {
|
|
575
|
-
// Validate the agent and schedule exist
|
|
576
|
-
const agents = this.config?.agents ?? [];
|
|
577
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
578
|
-
if (!agent) {
|
|
579
|
-
throw new AgentNotFoundError(agentName, {
|
|
580
|
-
availableAgents: agents.map((a) => a.name),
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
584
|
-
const availableSchedules = agent.schedules
|
|
585
|
-
? Object.keys(agent.schedules)
|
|
586
|
-
: [];
|
|
587
|
-
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
588
|
-
availableSchedules,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
// Update schedule state to disabled
|
|
592
|
-
const { updateScheduleState } = await import("../scheduler/schedule-state.js");
|
|
593
|
-
await updateScheduleState(this.stateDir, agentName, scheduleName, { status: "disabled" }, { logger: { warn: this.logger.warn } });
|
|
594
|
-
this.logger.info(`Disabled schedule ${agentName}/${scheduleName}`);
|
|
595
|
-
// Return the updated schedule info
|
|
596
|
-
return this.getSchedule(agentName, scheduleName);
|
|
597
|
-
}
|
|
101
|
+
getAgents() { return this.config?.agents ?? []; }
|
|
598
102
|
// ===========================================================================
|
|
599
103
|
// Lifecycle Methods
|
|
600
104
|
// ===========================================================================
|
|
601
|
-
/**
|
|
602
|
-
* Initialize the fleet manager
|
|
603
|
-
*
|
|
604
|
-
* This method:
|
|
605
|
-
* 1. Loads and validates the configuration file
|
|
606
|
-
* 2. Initializes the state directory structure
|
|
607
|
-
* 3. Prepares the scheduler (but does not start it)
|
|
608
|
-
*
|
|
609
|
-
* After initialization, the fleet manager is ready to start.
|
|
610
|
-
*
|
|
611
|
-
* @throws {FleetManagerStateError} If already initialized or running
|
|
612
|
-
* @throws {FleetManagerConfigError} If configuration is invalid or not found
|
|
613
|
-
* @throws {FleetManagerStateDirError} If state directory cannot be created
|
|
614
|
-
*
|
|
615
|
-
* @example
|
|
616
|
-
* ```typescript
|
|
617
|
-
* const manager = new FleetManager({ ... });
|
|
618
|
-
* await manager.initialize();
|
|
619
|
-
* console.log(`Loaded ${manager.state.agentCount} agents`);
|
|
620
|
-
* ```
|
|
621
|
-
*/
|
|
622
105
|
async initialize() {
|
|
623
|
-
// Validate current state
|
|
624
106
|
if (this.status !== "uninitialized" && this.status !== "stopped") {
|
|
625
|
-
throw new
|
|
107
|
+
throw new InvalidStateError("initialize", this.status, ["uninitialized", "stopped"]);
|
|
626
108
|
}
|
|
627
109
|
this.logger.info("Initializing fleet manager...");
|
|
628
110
|
try {
|
|
629
|
-
// Load configuration
|
|
630
|
-
this.logger.debug(this.configPath
|
|
631
|
-
? `Loading config from: ${this.configPath}`
|
|
632
|
-
: "Auto-discovering config...");
|
|
633
111
|
this.config = await this.loadConfiguration();
|
|
634
112
|
this.logger.info(`Loaded ${this.config.agents.length} agent(s) from config`);
|
|
635
|
-
// Initialize state directory
|
|
636
|
-
this.logger.debug(`Initializing state directory: ${this.stateDir}`);
|
|
637
113
|
this.stateDirInfo = await this.initializeStateDir();
|
|
638
114
|
this.logger.debug("State directory initialized");
|
|
639
|
-
// Create scheduler (but don't start it)
|
|
640
115
|
this.scheduler = new Scheduler({
|
|
641
116
|
stateDir: this.stateDir,
|
|
642
117
|
checkInterval: this.checkInterval,
|
|
643
118
|
logger: this.logger,
|
|
644
119
|
onTrigger: (info) => this.handleScheduleTrigger(info),
|
|
645
120
|
});
|
|
646
|
-
// Update state
|
|
647
121
|
this.status = "initialized";
|
|
648
122
|
this.initializedAt = new Date().toISOString();
|
|
649
123
|
this.lastError = null;
|
|
@@ -657,40 +131,14 @@ export class FleetManager extends EventEmitter {
|
|
|
657
131
|
throw error;
|
|
658
132
|
}
|
|
659
133
|
}
|
|
660
|
-
/**
|
|
661
|
-
* Start the fleet manager
|
|
662
|
-
*
|
|
663
|
-
* This begins the scheduler, which will:
|
|
664
|
-
* 1. Check agent schedules at the configured interval
|
|
665
|
-
* 2. Trigger agents when their schedules are due
|
|
666
|
-
* 3. Track schedule state in the state directory
|
|
667
|
-
*
|
|
668
|
-
* @throws {FleetManagerStateError} If not initialized
|
|
669
|
-
*
|
|
670
|
-
* @example
|
|
671
|
-
* ```typescript
|
|
672
|
-
* await manager.initialize();
|
|
673
|
-
* await manager.start();
|
|
674
|
-
*
|
|
675
|
-
* // The manager is now running and processing schedules
|
|
676
|
-
* manager.on('schedule:trigger', (agent, schedule) => {
|
|
677
|
-
* console.log(`Triggered ${agent}/${schedule}`);
|
|
678
|
-
* });
|
|
679
|
-
* ```
|
|
680
|
-
*/
|
|
681
134
|
async start() {
|
|
682
|
-
// Validate current state
|
|
683
135
|
if (this.status !== "initialized") {
|
|
684
|
-
throw new
|
|
136
|
+
throw new InvalidStateError("start", this.status, "initialized");
|
|
685
137
|
}
|
|
686
138
|
this.logger.info("Starting fleet manager...");
|
|
687
139
|
this.status = "starting";
|
|
688
140
|
try {
|
|
689
|
-
|
|
690
|
-
const agents = this.config.agents;
|
|
691
|
-
// Start scheduler in background (don't await the loop)
|
|
692
|
-
this.startSchedulerAsync(agents);
|
|
693
|
-
// Update state
|
|
141
|
+
this.startSchedulerAsync(this.config.agents);
|
|
694
142
|
this.status = "running";
|
|
695
143
|
this.startedAt = new Date().toISOString();
|
|
696
144
|
this.stoppedAt = null;
|
|
@@ -704,75 +152,29 @@ export class FleetManager extends EventEmitter {
|
|
|
704
152
|
throw error;
|
|
705
153
|
}
|
|
706
154
|
}
|
|
707
|
-
/**
|
|
708
|
-
* Stop the fleet manager gracefully
|
|
709
|
-
*
|
|
710
|
-
* This will:
|
|
711
|
-
* 1. Signal the scheduler to stop accepting new triggers
|
|
712
|
-
* 2. Wait for running jobs to complete (with timeout)
|
|
713
|
-
* 3. If timeout is reached and cancelOnTimeout is true, cancel remaining jobs
|
|
714
|
-
* 4. Persist all state before shutdown completes
|
|
715
|
-
* 5. Emit 'stopped' event when complete
|
|
716
|
-
*
|
|
717
|
-
* @param options - Stop options for controlling shutdown behavior
|
|
718
|
-
* @throws {FleetManagerShutdownError} If shutdown times out and cancelOnTimeout is false
|
|
719
|
-
*
|
|
720
|
-
* @example
|
|
721
|
-
* ```typescript
|
|
722
|
-
* // Normal shutdown - wait for jobs with default 30s timeout
|
|
723
|
-
* await manager.stop();
|
|
724
|
-
*
|
|
725
|
-
* // Shutdown with custom timeout
|
|
726
|
-
* await manager.stop({ timeout: 60000 });
|
|
727
|
-
*
|
|
728
|
-
* // Shutdown without waiting for jobs (not recommended)
|
|
729
|
-
* await manager.stop({ waitForJobs: false });
|
|
730
|
-
*
|
|
731
|
-
* // Cancel jobs if they don't complete in time
|
|
732
|
-
* await manager.stop({
|
|
733
|
-
* timeout: 30000,
|
|
734
|
-
* cancelOnTimeout: true,
|
|
735
|
-
* cancelTimeout: 10000,
|
|
736
|
-
* });
|
|
737
|
-
* ```
|
|
738
|
-
*/
|
|
739
155
|
async stop(options) {
|
|
740
156
|
if (this.status !== "running" && this.status !== "starting") {
|
|
741
157
|
this.logger.debug(`Stop called but status is '${this.status}', ignoring`);
|
|
742
158
|
return;
|
|
743
159
|
}
|
|
744
|
-
const waitForJobs = options
|
|
745
|
-
const timeout = options?.timeout ?? 30000;
|
|
746
|
-
const cancelOnTimeout = options?.cancelOnTimeout ?? false;
|
|
747
|
-
const cancelTimeout = options?.cancelTimeout ?? 10000;
|
|
160
|
+
const { waitForJobs = true, timeout = 30000, cancelOnTimeout = false, cancelTimeout = 10000 } = options ?? {};
|
|
748
161
|
this.logger.info("Stopping fleet manager...");
|
|
749
162
|
this.status = "stopping";
|
|
750
163
|
try {
|
|
751
|
-
// Stop the scheduler - don't wait for jobs here, we'll handle it ourselves
|
|
752
|
-
// This stops new triggers from being accepted
|
|
753
164
|
if (this.scheduler) {
|
|
754
165
|
try {
|
|
755
|
-
await this.scheduler.stop({
|
|
756
|
-
waitForJobs,
|
|
757
|
-
timeout,
|
|
758
|
-
});
|
|
166
|
+
await this.scheduler.stop({ waitForJobs, timeout });
|
|
759
167
|
}
|
|
760
168
|
catch (error) {
|
|
761
|
-
// Check if it's a scheduler shutdown timeout
|
|
762
169
|
if (error instanceof Error && error.name === "SchedulerShutdownError") {
|
|
763
170
|
if (cancelOnTimeout) {
|
|
764
|
-
// Cancel all running jobs
|
|
765
171
|
this.logger.info("Timeout reached, cancelling running jobs...");
|
|
766
|
-
await this.cancelRunningJobs(cancelTimeout);
|
|
172
|
+
await this.jobControl.cancelRunningJobs(cancelTimeout);
|
|
767
173
|
}
|
|
768
174
|
else {
|
|
769
|
-
// Re-throw the error
|
|
770
175
|
this.status = "error";
|
|
771
176
|
this.lastError = error.message;
|
|
772
|
-
throw new FleetManagerShutdownError(error.message, {
|
|
773
|
-
timedOut: true,
|
|
774
|
-
cause: error,
|
|
775
|
-
});
|
|
177
|
+
throw new FleetManagerShutdownError(error.message, { timedOut: true, cause: error });
|
|
776
178
|
}
|
|
777
179
|
}
|
|
778
180
|
else {
|
|
@@ -780,9 +182,7 @@ export class FleetManager extends EventEmitter {
|
|
|
780
182
|
}
|
|
781
183
|
}
|
|
782
184
|
}
|
|
783
|
-
// Persist fleet state before completing shutdown
|
|
784
185
|
await this.persistShutdownState();
|
|
785
|
-
// Update state
|
|
786
186
|
this.status = "stopped";
|
|
787
187
|
this.stoppedAt = new Date().toISOString();
|
|
788
188
|
this.logger.info("Fleet manager stopped");
|
|
@@ -794,974 +194,54 @@ export class FleetManager extends EventEmitter {
|
|
|
794
194
|
throw error;
|
|
795
195
|
}
|
|
796
196
|
}
|
|
797
|
-
/**
|
|
798
|
-
* Reload configuration without restarting the fleet
|
|
799
|
-
*
|
|
800
|
-
* This method provides hot configuration reload capability:
|
|
801
|
-
* 1. Loads and validates the new configuration
|
|
802
|
-
* 2. If validation fails, keeps the old configuration (fails gracefully)
|
|
803
|
-
* 3. Running jobs continue with their original configuration
|
|
804
|
-
* 4. New jobs will use the new configuration
|
|
805
|
-
* 5. Updates the scheduler with new agent definitions and schedules
|
|
806
|
-
* 6. Emits a 'config:reloaded' event with a list of changes
|
|
807
|
-
*
|
|
808
|
-
* @returns The reload result with change details
|
|
809
|
-
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
810
|
-
* @throws {FleetManagerConfigError} If the new configuration is invalid (re-thrown after logging)
|
|
811
|
-
*
|
|
812
|
-
* @example
|
|
813
|
-
* ```typescript
|
|
814
|
-
* // Reload configuration
|
|
815
|
-
* const result = await manager.reload();
|
|
816
|
-
* console.log(`Reloaded with ${result.changes.length} changes`);
|
|
817
|
-
*
|
|
818
|
-
* // Subscribe to reload events
|
|
819
|
-
* manager.on('config:reloaded', (payload) => {
|
|
820
|
-
* console.log(`Config reloaded: ${payload.changes.length} changes`);
|
|
821
|
-
* for (const change of payload.changes) {
|
|
822
|
-
* console.log(` ${change.type} ${change.category}: ${change.name}`);
|
|
823
|
-
* }
|
|
824
|
-
* });
|
|
825
|
-
* ```
|
|
826
|
-
*/
|
|
827
|
-
async reload() {
|
|
828
|
-
// Validate state - must be at least initialized
|
|
829
|
-
if (this.status === "uninitialized") {
|
|
830
|
-
throw new InvalidStateError("reload", this.status, ["initialized", "starting", "running", "stopping", "stopped"]);
|
|
831
|
-
}
|
|
832
|
-
this.logger.info("Reloading configuration...");
|
|
833
|
-
// Store old config for comparison
|
|
834
|
-
const oldConfig = this.config;
|
|
835
|
-
// Try to load new configuration
|
|
836
|
-
let newConfig;
|
|
837
|
-
try {
|
|
838
|
-
newConfig = await this.loadConfiguration();
|
|
839
|
-
}
|
|
840
|
-
catch (error) {
|
|
841
|
-
// Log the error but don't update config - fail gracefully
|
|
842
|
-
this.logger.error(`Failed to reload configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
843
|
-
this.logger.info("Keeping existing configuration");
|
|
844
|
-
// Re-throw so caller knows reload failed
|
|
845
|
-
throw error;
|
|
846
|
-
}
|
|
847
|
-
// Compute changes between old and new config
|
|
848
|
-
const changes = this.computeConfigChanges(oldConfig, newConfig);
|
|
849
|
-
// Update the stored configuration
|
|
850
|
-
this.config = newConfig;
|
|
851
|
-
// Update the scheduler with new agents (if scheduler exists and is running)
|
|
852
|
-
if (this.scheduler) {
|
|
853
|
-
this.scheduler.setAgents(newConfig.agents);
|
|
854
|
-
this.logger.debug(`Updated scheduler with ${newConfig.agents.length} agents`);
|
|
855
|
-
}
|
|
856
|
-
const timestamp = new Date().toISOString();
|
|
857
|
-
// Build the reload payload
|
|
858
|
-
const payload = {
|
|
859
|
-
agentCount: newConfig.agents.length,
|
|
860
|
-
agentNames: newConfig.agents.map((a) => a.name),
|
|
861
|
-
configPath: newConfig.configPath,
|
|
862
|
-
changes,
|
|
863
|
-
timestamp,
|
|
864
|
-
};
|
|
865
|
-
// Emit the config:reloaded event
|
|
866
|
-
this.emit("config:reloaded", payload);
|
|
867
|
-
this.logger.info(`Configuration reloaded: ${newConfig.agents.length} agents, ${changes.length} changes`);
|
|
868
|
-
return payload;
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Compute the list of changes between old and new configuration
|
|
872
|
-
*/
|
|
873
|
-
computeConfigChanges(oldConfig, newConfig) {
|
|
874
|
-
const changes = [];
|
|
875
|
-
const oldAgents = oldConfig?.agents ?? [];
|
|
876
|
-
const newAgents = newConfig.agents;
|
|
877
|
-
const oldAgentNames = new Set(oldAgents.map((a) => a.name));
|
|
878
|
-
const newAgentNames = new Set(newAgents.map((a) => a.name));
|
|
879
|
-
// Find added agents
|
|
880
|
-
for (const agent of newAgents) {
|
|
881
|
-
if (!oldAgentNames.has(agent.name)) {
|
|
882
|
-
changes.push({
|
|
883
|
-
type: "added",
|
|
884
|
-
category: "agent",
|
|
885
|
-
name: agent.name,
|
|
886
|
-
details: agent.description,
|
|
887
|
-
});
|
|
888
|
-
// Also add all schedules for new agents
|
|
889
|
-
if (agent.schedules) {
|
|
890
|
-
for (const scheduleName of Object.keys(agent.schedules)) {
|
|
891
|
-
changes.push({
|
|
892
|
-
type: "added",
|
|
893
|
-
category: "schedule",
|
|
894
|
-
name: `${agent.name}/${scheduleName}`,
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
// Find removed agents
|
|
901
|
-
for (const agent of oldAgents) {
|
|
902
|
-
if (!newAgentNames.has(agent.name)) {
|
|
903
|
-
changes.push({
|
|
904
|
-
type: "removed",
|
|
905
|
-
category: "agent",
|
|
906
|
-
name: agent.name,
|
|
907
|
-
});
|
|
908
|
-
// Also mark all schedules as removed
|
|
909
|
-
if (agent.schedules) {
|
|
910
|
-
for (const scheduleName of Object.keys(agent.schedules)) {
|
|
911
|
-
changes.push({
|
|
912
|
-
type: "removed",
|
|
913
|
-
category: "schedule",
|
|
914
|
-
name: `${agent.name}/${scheduleName}`,
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
// Find modified agents and schedules
|
|
921
|
-
for (const newAgent of newAgents) {
|
|
922
|
-
const oldAgent = oldAgents.find((a) => a.name === newAgent.name);
|
|
923
|
-
if (!oldAgent) {
|
|
924
|
-
continue; // Already handled as "added"
|
|
925
|
-
}
|
|
926
|
-
// Check for agent-level modifications
|
|
927
|
-
const agentModified = this.isAgentModified(oldAgent, newAgent);
|
|
928
|
-
if (agentModified) {
|
|
929
|
-
changes.push({
|
|
930
|
-
type: "modified",
|
|
931
|
-
category: "agent",
|
|
932
|
-
name: newAgent.name,
|
|
933
|
-
details: agentModified,
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
// Check for schedule changes
|
|
937
|
-
const oldScheduleNames = new Set(oldAgent.schedules ? Object.keys(oldAgent.schedules) : []);
|
|
938
|
-
const newScheduleNames = new Set(newAgent.schedules ? Object.keys(newAgent.schedules) : []);
|
|
939
|
-
// Added schedules
|
|
940
|
-
for (const scheduleName of newScheduleNames) {
|
|
941
|
-
if (!oldScheduleNames.has(scheduleName)) {
|
|
942
|
-
changes.push({
|
|
943
|
-
type: "added",
|
|
944
|
-
category: "schedule",
|
|
945
|
-
name: `${newAgent.name}/${scheduleName}`,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Removed schedules
|
|
950
|
-
for (const scheduleName of oldScheduleNames) {
|
|
951
|
-
if (!newScheduleNames.has(scheduleName)) {
|
|
952
|
-
changes.push({
|
|
953
|
-
type: "removed",
|
|
954
|
-
category: "schedule",
|
|
955
|
-
name: `${newAgent.name}/${scheduleName}`,
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
// Modified schedules
|
|
960
|
-
for (const scheduleName of newScheduleNames) {
|
|
961
|
-
if (oldScheduleNames.has(scheduleName)) {
|
|
962
|
-
const oldSchedule = oldAgent.schedules[scheduleName];
|
|
963
|
-
const newSchedule = newAgent.schedules[scheduleName];
|
|
964
|
-
if (this.isScheduleModified(oldSchedule, newSchedule)) {
|
|
965
|
-
changes.push({
|
|
966
|
-
type: "modified",
|
|
967
|
-
category: "schedule",
|
|
968
|
-
name: `${newAgent.name}/${scheduleName}`,
|
|
969
|
-
details: this.getScheduleModificationDetails(oldSchedule, newSchedule),
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return changes;
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Check if an agent configuration has been modified
|
|
979
|
-
* Returns a description of what changed, or null if not modified
|
|
980
|
-
*/
|
|
981
|
-
isAgentModified(oldAgent, newAgent) {
|
|
982
|
-
const modifications = [];
|
|
983
|
-
// Check key properties
|
|
984
|
-
if (oldAgent.description !== newAgent.description) {
|
|
985
|
-
modifications.push("description");
|
|
986
|
-
}
|
|
987
|
-
if (oldAgent.model !== newAgent.model) {
|
|
988
|
-
modifications.push("model");
|
|
989
|
-
}
|
|
990
|
-
if (oldAgent.max_turns !== newAgent.max_turns) {
|
|
991
|
-
modifications.push("max_turns");
|
|
992
|
-
}
|
|
993
|
-
if (oldAgent.system_prompt !== newAgent.system_prompt) {
|
|
994
|
-
modifications.push("system_prompt");
|
|
995
|
-
}
|
|
996
|
-
// Check workspace
|
|
997
|
-
const oldWorkspace = typeof oldAgent.workspace === "string"
|
|
998
|
-
? oldAgent.workspace
|
|
999
|
-
: oldAgent.workspace?.root;
|
|
1000
|
-
const newWorkspace = typeof newAgent.workspace === "string"
|
|
1001
|
-
? newAgent.workspace
|
|
1002
|
-
: newAgent.workspace?.root;
|
|
1003
|
-
if (oldWorkspace !== newWorkspace) {
|
|
1004
|
-
modifications.push("workspace");
|
|
1005
|
-
}
|
|
1006
|
-
// Check instances
|
|
1007
|
-
const oldMaxConcurrent = oldAgent.instances?.max_concurrent ?? 1;
|
|
1008
|
-
const newMaxConcurrent = newAgent.instances?.max_concurrent ?? 1;
|
|
1009
|
-
if (oldMaxConcurrent !== newMaxConcurrent) {
|
|
1010
|
-
modifications.push("max_concurrent");
|
|
1011
|
-
}
|
|
1012
|
-
return modifications.length > 0 ? modifications.join(", ") : null;
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Check if a schedule configuration has been modified
|
|
1016
|
-
*/
|
|
1017
|
-
isScheduleModified(oldSchedule, newSchedule) {
|
|
1018
|
-
return (oldSchedule.type !== newSchedule.type ||
|
|
1019
|
-
oldSchedule.interval !== newSchedule.interval ||
|
|
1020
|
-
oldSchedule.expression !== newSchedule.expression ||
|
|
1021
|
-
oldSchedule.prompt !== newSchedule.prompt);
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Get a description of what changed in a schedule
|
|
1025
|
-
*/
|
|
1026
|
-
getScheduleModificationDetails(oldSchedule, newSchedule) {
|
|
1027
|
-
const details = [];
|
|
1028
|
-
if (oldSchedule.type !== newSchedule.type) {
|
|
1029
|
-
details.push(`type: ${oldSchedule.type} → ${newSchedule.type}`);
|
|
1030
|
-
}
|
|
1031
|
-
if (oldSchedule.interval !== newSchedule.interval) {
|
|
1032
|
-
details.push(`interval: ${oldSchedule.interval ?? "none"} → ${newSchedule.interval ?? "none"}`);
|
|
1033
|
-
}
|
|
1034
|
-
if (oldSchedule.expression !== newSchedule.expression) {
|
|
1035
|
-
details.push(`expression: ${oldSchedule.expression ?? "none"} → ${newSchedule.expression ?? "none"}`);
|
|
1036
|
-
}
|
|
1037
|
-
if (oldSchedule.prompt !== newSchedule.prompt) {
|
|
1038
|
-
details.push("prompt changed");
|
|
1039
|
-
}
|
|
1040
|
-
return details.join("; ");
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Cancel all running jobs during shutdown
|
|
1044
|
-
*
|
|
1045
|
-
* @param cancelTimeout - Timeout for each job cancellation
|
|
1046
|
-
*/
|
|
1047
|
-
async cancelRunningJobs(cancelTimeout) {
|
|
1048
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1049
|
-
// Get all running jobs from the fleet status
|
|
1050
|
-
const agentInfoList = await this.getAgentInfo();
|
|
1051
|
-
const runningJobIds = [];
|
|
1052
|
-
for (const agent of agentInfoList) {
|
|
1053
|
-
if (agent.currentJobId) {
|
|
1054
|
-
runningJobIds.push(agent.currentJobId);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
if (runningJobIds.length === 0) {
|
|
1058
|
-
this.logger.debug("No running jobs to cancel");
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
this.logger.info(`Cancelling ${runningJobIds.length} running job(s)...`);
|
|
1062
|
-
// Cancel all jobs in parallel
|
|
1063
|
-
const cancelPromises = runningJobIds.map(async (jobId) => {
|
|
1064
|
-
try {
|
|
1065
|
-
const result = await this.cancelJob(jobId, { timeout: cancelTimeout });
|
|
1066
|
-
this.logger.debug(`Cancelled job ${jobId}: ${result.terminationType}`);
|
|
1067
|
-
}
|
|
1068
|
-
catch (error) {
|
|
1069
|
-
this.logger.warn(`Failed to cancel job ${jobId}: ${error.message}`);
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
await Promise.all(cancelPromises);
|
|
1073
|
-
this.logger.info("All jobs cancelled");
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Persist shutdown state to ensure all state is saved before completing
|
|
1077
|
-
*/
|
|
1078
|
-
async persistShutdownState() {
|
|
1079
|
-
if (!this.stateDirInfo) {
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
// Persist fleet state
|
|
1083
|
-
const { writeFleetState } = await import("../state/fleet-state.js");
|
|
1084
|
-
// Read current state and update with stopped status
|
|
1085
|
-
const currentState = await this.readFleetStateSnapshot();
|
|
1086
|
-
// Update fleet-level state
|
|
1087
|
-
const updatedState = {
|
|
1088
|
-
...currentState,
|
|
1089
|
-
fleet: {
|
|
1090
|
-
...currentState.fleet,
|
|
1091
|
-
stoppedAt: new Date().toISOString(),
|
|
1092
|
-
},
|
|
1093
|
-
};
|
|
1094
|
-
try {
|
|
1095
|
-
await writeFleetState(this.stateDirInfo.stateFile, updatedState);
|
|
1096
|
-
this.logger.debug("Fleet state persisted");
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
this.logger.warn(`Failed to persist fleet state: ${error.message}`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
197
|
// ===========================================================================
|
|
1103
|
-
//
|
|
198
|
+
// Public API - One-liner delegations to module classes
|
|
1104
199
|
// ===========================================================================
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
* const job = await manager.trigger('my-agent');
|
|
1126
|
-
*
|
|
1127
|
-
* // Trigger a specific schedule
|
|
1128
|
-
* const job = await manager.trigger('my-agent', 'hourly');
|
|
1129
|
-
*
|
|
1130
|
-
* // Trigger with custom prompt
|
|
1131
|
-
* const job = await manager.trigger('my-agent', undefined, {
|
|
1132
|
-
* prompt: 'Review the latest security updates',
|
|
1133
|
-
* });
|
|
1134
|
-
*
|
|
1135
|
-
* // Force trigger even at capacity
|
|
1136
|
-
* const job = await manager.trigger('my-agent', undefined, {
|
|
1137
|
-
* bypassConcurrencyLimit: true,
|
|
1138
|
-
* });
|
|
1139
|
-
* ```
|
|
1140
|
-
*/
|
|
1141
|
-
async trigger(agentName, scheduleName, options) {
|
|
1142
|
-
// Validate state - must be at least initialized
|
|
1143
|
-
if (this.status === "uninitialized") {
|
|
1144
|
-
throw new InvalidStateError("trigger", this.status, ["initialized", "running", "stopped"]);
|
|
1145
|
-
}
|
|
1146
|
-
// Find the agent
|
|
1147
|
-
const agents = this.config?.agents ?? [];
|
|
1148
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
1149
|
-
if (!agent) {
|
|
1150
|
-
throw new AgentNotFoundError(agentName, {
|
|
1151
|
-
availableAgents: agents.map((a) => a.name),
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
// If a schedule name is provided, validate it exists
|
|
1155
|
-
let schedule;
|
|
1156
|
-
if (scheduleName) {
|
|
1157
|
-
if (!agent.schedules || !(scheduleName in agent.schedules)) {
|
|
1158
|
-
const availableSchedules = agent.schedules
|
|
1159
|
-
? Object.keys(agent.schedules)
|
|
1160
|
-
: [];
|
|
1161
|
-
throw new ScheduleNotFoundError(agentName, scheduleName, {
|
|
1162
|
-
availableSchedules,
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
schedule = agent.schedules[scheduleName];
|
|
1166
|
-
}
|
|
1167
|
-
// Check concurrency limits unless bypassed
|
|
1168
|
-
if (!options?.bypassConcurrencyLimit) {
|
|
1169
|
-
const maxConcurrent = agent.instances?.max_concurrent ?? 1;
|
|
1170
|
-
const runningCount = this.scheduler?.getRunningJobCount(agentName) ?? 0;
|
|
1171
|
-
if (runningCount >= maxConcurrent) {
|
|
1172
|
-
throw new ConcurrencyLimitError(agentName, runningCount, maxConcurrent);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
// Determine the prompt to use (priority: options > schedule > agent default)
|
|
1176
|
-
const prompt = options?.prompt ?? schedule?.prompt ?? undefined;
|
|
1177
|
-
// Create the job
|
|
1178
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1179
|
-
const job = await createJob(jobsDir, {
|
|
1180
|
-
agent: agentName,
|
|
1181
|
-
trigger_type: "manual",
|
|
1182
|
-
schedule: scheduleName ?? null,
|
|
1183
|
-
prompt: prompt ?? null,
|
|
1184
|
-
});
|
|
1185
|
-
const timestamp = new Date().toISOString();
|
|
1186
|
-
this.logger.info(`Manually triggered ${agentName}${scheduleName ? `/${scheduleName}` : ""} - job ${job.id}`);
|
|
1187
|
-
// Emit job:created event
|
|
1188
|
-
this.emit("job:created", {
|
|
1189
|
-
job,
|
|
1190
|
-
agentName,
|
|
1191
|
-
scheduleName: scheduleName ?? null,
|
|
1192
|
-
timestamp,
|
|
1193
|
-
});
|
|
1194
|
-
// Build and return the result
|
|
1195
|
-
const result = {
|
|
1196
|
-
jobId: job.id,
|
|
1197
|
-
agentName,
|
|
1198
|
-
scheduleName: scheduleName ?? null,
|
|
1199
|
-
startedAt: job.started_at,
|
|
1200
|
-
prompt,
|
|
1201
|
-
};
|
|
1202
|
-
return result;
|
|
1203
|
-
}
|
|
200
|
+
// Status Queries
|
|
201
|
+
async getFleetStatus() { return this.statusQueries.getFleetStatus(); }
|
|
202
|
+
async getAgentInfo() { return this.statusQueries.getAgentInfo(); }
|
|
203
|
+
async getAgentInfoByName(name) { return this.statusQueries.getAgentInfoByName(name); }
|
|
204
|
+
// Schedule Management
|
|
205
|
+
async getSchedules() { return this.scheduleManagement.getSchedules(); }
|
|
206
|
+
async getSchedule(agentName, scheduleName) { return this.scheduleManagement.getSchedule(agentName, scheduleName); }
|
|
207
|
+
async enableSchedule(agentName, scheduleName) { return this.scheduleManagement.enableSchedule(agentName, scheduleName); }
|
|
208
|
+
async disableSchedule(agentName, scheduleName) { return this.scheduleManagement.disableSchedule(agentName, scheduleName); }
|
|
209
|
+
// Config Reload
|
|
210
|
+
async reload() { return this.configReloadModule.reload(); }
|
|
211
|
+
computeConfigChanges(oldConfig, newConfig) { return computeConfigChanges(oldConfig, newConfig); }
|
|
212
|
+
// Job Control
|
|
213
|
+
async trigger(agentName, scheduleName, options) { return this.jobControl.trigger(agentName, scheduleName, options); }
|
|
214
|
+
async cancelJob(jobId, options) { return this.jobControl.cancelJob(jobId, options); }
|
|
215
|
+
async forkJob(jobId, modifications) { return this.jobControl.forkJob(jobId, modifications); }
|
|
216
|
+
// Log Streaming
|
|
217
|
+
async *streamLogs(options) { yield* this.logStreaming.streamLogs(options); }
|
|
218
|
+
async *streamJobOutput(jobId) { yield* this.logStreaming.streamJobOutput(jobId); }
|
|
219
|
+
async *streamAgentLogs(agentName) { yield* this.logStreaming.streamAgentLogs(agentName); }
|
|
1204
220
|
// ===========================================================================
|
|
1205
|
-
//
|
|
221
|
+
// Private Methods
|
|
1206
222
|
// ===========================================================================
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
* @param jobId - ID of the job to cancel
|
|
1215
|
-
* @param options - Optional cancellation options
|
|
1216
|
-
* @param options.timeout - Time in ms to wait for graceful shutdown (default: 10000)
|
|
1217
|
-
* @returns Result of the cancellation operation
|
|
1218
|
-
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
1219
|
-
* @throws {JobNotFoundError} If the job doesn't exist
|
|
1220
|
-
*
|
|
1221
|
-
* @example
|
|
1222
|
-
* ```typescript
|
|
1223
|
-
* // Cancel with default timeout
|
|
1224
|
-
* const result = await manager.cancelJob('job-2024-01-15-abc123');
|
|
1225
|
-
* console.log(`Job cancelled: ${result.terminationType}`);
|
|
1226
|
-
*
|
|
1227
|
-
* // Cancel with custom timeout
|
|
1228
|
-
* const result = await manager.cancelJob('job-2024-01-15-abc123', {
|
|
1229
|
-
* timeout: 30000, // 30 seconds
|
|
1230
|
-
* });
|
|
1231
|
-
* ```
|
|
1232
|
-
*/
|
|
1233
|
-
async cancelJob(jobId, options) {
|
|
1234
|
-
// Validate state - must be at least initialized
|
|
1235
|
-
if (this.status === "uninitialized") {
|
|
1236
|
-
throw new InvalidStateError("cancelJob", this.status, ["initialized", "running", "stopped"]);
|
|
1237
|
-
}
|
|
1238
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1239
|
-
const timeout = options?.timeout ?? 10000; // Default 10 seconds
|
|
1240
|
-
// Get the job to verify it exists and check its status
|
|
1241
|
-
const job = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1242
|
-
if (!job) {
|
|
1243
|
-
throw new JobNotFoundError(jobId);
|
|
1244
|
-
}
|
|
1245
|
-
const timestamp = new Date().toISOString();
|
|
1246
|
-
let terminationType;
|
|
1247
|
-
let durationSeconds;
|
|
1248
|
-
// If job is already not running, return early
|
|
1249
|
-
if (job.status !== "running" && job.status !== "pending") {
|
|
1250
|
-
this.logger.info(`Job ${jobId} is already ${job.status}, no cancellation needed`);
|
|
1251
|
-
terminationType = 'already_stopped';
|
|
1252
|
-
// Calculate duration if we have finished_at
|
|
1253
|
-
if (job.finished_at) {
|
|
1254
|
-
const startTime = new Date(job.started_at).getTime();
|
|
1255
|
-
const endTime = new Date(job.finished_at).getTime();
|
|
1256
|
-
durationSeconds = Math.round((endTime - startTime) / 1000);
|
|
1257
|
-
}
|
|
1258
|
-
return {
|
|
1259
|
-
jobId,
|
|
1260
|
-
success: true,
|
|
1261
|
-
terminationType,
|
|
1262
|
-
canceledAt: timestamp,
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1265
|
-
// Calculate duration
|
|
1266
|
-
const startTime = new Date(job.started_at).getTime();
|
|
1267
|
-
const endTime = new Date(timestamp).getTime();
|
|
1268
|
-
durationSeconds = Math.round((endTime - startTime) / 1000);
|
|
1269
|
-
// Try to cancel via the scheduler if it has process tracking
|
|
1270
|
-
// For now, we'll update the job status directly since we don't have
|
|
1271
|
-
// direct process control yet. In a full implementation, this would
|
|
1272
|
-
// send SIGTERM to the process, wait, then SIGKILL if needed.
|
|
1273
|
-
// Note: The scheduler/executor would need to track job processes
|
|
1274
|
-
// and provide an API to cancel them. For this implementation,
|
|
1275
|
-
// we'll update the job state and emit events, assuming the executor
|
|
1276
|
-
// monitors the job status and handles cancellation.
|
|
1277
|
-
this.logger.info(`Cancelling job ${jobId} for agent ${job.agent}`);
|
|
1278
|
-
// Update job status to cancelled
|
|
1279
|
-
try {
|
|
1280
|
-
await updateJob(jobsDir, jobId, {
|
|
1281
|
-
status: "cancelled",
|
|
1282
|
-
exit_reason: "cancelled",
|
|
1283
|
-
finished_at: timestamp,
|
|
1284
|
-
});
|
|
1285
|
-
// Assume graceful termination for now
|
|
1286
|
-
// In a full implementation, this would be determined by whether
|
|
1287
|
-
// the process responded to SIGTERM or required SIGKILL
|
|
1288
|
-
terminationType = 'graceful';
|
|
1289
|
-
}
|
|
1290
|
-
catch (error) {
|
|
1291
|
-
this.logger.error(`Failed to update job status: ${error.message}`);
|
|
1292
|
-
throw new JobCancelError(jobId, 'process_error', {
|
|
1293
|
-
cause: error,
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
// Emit job:cancelled event
|
|
1297
|
-
const updatedJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1298
|
-
if (updatedJob) {
|
|
1299
|
-
this.emit("job:cancelled", {
|
|
1300
|
-
job: updatedJob,
|
|
1301
|
-
agentName: job.agent,
|
|
1302
|
-
terminationType,
|
|
1303
|
-
durationSeconds,
|
|
1304
|
-
timestamp,
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
this.logger.info(`Job ${jobId} cancelled (${terminationType}) after ${durationSeconds}s`);
|
|
1308
|
-
return {
|
|
1309
|
-
jobId,
|
|
1310
|
-
success: true,
|
|
1311
|
-
terminationType,
|
|
1312
|
-
canceledAt: timestamp,
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
/**
|
|
1316
|
-
* Fork a job to create a new job based on an existing one
|
|
1317
|
-
*
|
|
1318
|
-
* This method creates a new job that is based on an existing job's
|
|
1319
|
-
* configuration. The new job will have the same agent and can optionally
|
|
1320
|
-
* have modifications applied (different prompt, schedule, etc.).
|
|
1321
|
-
*
|
|
1322
|
-
* If the original job has a session ID, the new job will fork from that
|
|
1323
|
-
* session, preserving conversation context.
|
|
1324
|
-
*
|
|
1325
|
-
* @param jobId - ID of the job to fork
|
|
1326
|
-
* @param modifications - Optional modifications to apply to the forked job
|
|
1327
|
-
* @returns Result of the fork operation including the new job ID
|
|
1328
|
-
* @throws {InvalidStateError} If the fleet manager is not initialized
|
|
1329
|
-
* @throws {JobNotFoundError} If the original job doesn't exist
|
|
1330
|
-
* @throws {JobForkError} If the job cannot be forked (e.g., no session)
|
|
1331
|
-
*
|
|
1332
|
-
* @example
|
|
1333
|
-
* ```typescript
|
|
1334
|
-
* // Fork with same configuration
|
|
1335
|
-
* const result = await manager.forkJob('job-2024-01-15-abc123');
|
|
1336
|
-
* console.log(`Forked to: ${result.jobId}`);
|
|
1337
|
-
*
|
|
1338
|
-
* // Fork with modified prompt
|
|
1339
|
-
* const result = await manager.forkJob('job-2024-01-15-abc123', {
|
|
1340
|
-
* prompt: 'Continue the previous task but focus on testing',
|
|
1341
|
-
* });
|
|
1342
|
-
*
|
|
1343
|
-
* // Fork with different schedule
|
|
1344
|
-
* const result = await manager.forkJob('job-2024-01-15-abc123', {
|
|
1345
|
-
* schedule: 'nightly',
|
|
1346
|
-
* });
|
|
1347
|
-
* ```
|
|
1348
|
-
*/
|
|
1349
|
-
async forkJob(jobId, modifications) {
|
|
1350
|
-
// Validate state - must be at least initialized
|
|
1351
|
-
if (this.status === "uninitialized") {
|
|
1352
|
-
throw new InvalidStateError("forkJob", this.status, ["initialized", "running", "stopped"]);
|
|
1353
|
-
}
|
|
1354
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1355
|
-
// Get the original job
|
|
1356
|
-
const originalJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1357
|
-
if (!originalJob) {
|
|
1358
|
-
throw new JobForkError(jobId, 'job_not_found');
|
|
1359
|
-
}
|
|
1360
|
-
// Verify the agent exists in config
|
|
1361
|
-
const agents = this.config?.agents ?? [];
|
|
1362
|
-
const agent = agents.find((a) => a.name === originalJob.agent);
|
|
1363
|
-
if (!agent) {
|
|
1364
|
-
throw new JobForkError(jobId, 'agent_not_found', {
|
|
1365
|
-
message: `Agent "${originalJob.agent}" for job "${jobId}" not found in current configuration`,
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
// Determine the prompt to use (priority: modifications > original job)
|
|
1369
|
-
const prompt = modifications?.prompt ?? originalJob.prompt ?? undefined;
|
|
1370
|
-
// Determine the schedule to use
|
|
1371
|
-
const scheduleName = modifications?.schedule ?? originalJob.schedule ?? undefined;
|
|
1372
|
-
// Create the new job
|
|
1373
|
-
const timestamp = new Date().toISOString();
|
|
1374
|
-
const newJob = await createJob(jobsDir, {
|
|
1375
|
-
agent: originalJob.agent,
|
|
1376
|
-
trigger_type: "fork",
|
|
1377
|
-
schedule: scheduleName ?? null,
|
|
1378
|
-
prompt: prompt ?? null,
|
|
1379
|
-
forked_from: jobId,
|
|
1380
|
-
});
|
|
1381
|
-
this.logger.info(`Forked job ${jobId} to new job ${newJob.id} for agent ${originalJob.agent}`);
|
|
1382
|
-
// Emit job:created event
|
|
1383
|
-
this.emit("job:created", {
|
|
1384
|
-
job: newJob,
|
|
1385
|
-
agentName: originalJob.agent,
|
|
1386
|
-
scheduleName: scheduleName ?? undefined,
|
|
1387
|
-
timestamp,
|
|
1388
|
-
});
|
|
1389
|
-
// Emit job:forked event
|
|
1390
|
-
this.emit("job:forked", {
|
|
1391
|
-
job: newJob,
|
|
1392
|
-
originalJob,
|
|
1393
|
-
agentName: originalJob.agent,
|
|
1394
|
-
timestamp,
|
|
1395
|
-
});
|
|
1396
|
-
return {
|
|
1397
|
-
jobId: newJob.id,
|
|
1398
|
-
forkedFromJobId: jobId,
|
|
1399
|
-
agentName: originalJob.agent,
|
|
1400
|
-
startedAt: newJob.started_at,
|
|
1401
|
-
prompt,
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
// ===========================================================================
|
|
1405
|
-
// Log Streaming (US-11)
|
|
1406
|
-
// ===========================================================================
|
|
1407
|
-
/**
|
|
1408
|
-
* Stream all fleet logs as an async iterable
|
|
1409
|
-
*
|
|
1410
|
-
* Provides a unified stream of logs from all sources in the fleet including
|
|
1411
|
-
* agents, jobs, and the scheduler. Logs can be filtered by level and optionally
|
|
1412
|
-
* by agent or job.
|
|
1413
|
-
*
|
|
1414
|
-
* For completed jobs, this will replay their history (if includeHistory is true)
|
|
1415
|
-
* before streaming new logs from running jobs.
|
|
1416
|
-
*
|
|
1417
|
-
* @param options - Options for filtering and configuring the stream
|
|
1418
|
-
* @returns An async iterable of LogEntry objects
|
|
1419
|
-
*
|
|
1420
|
-
* @example
|
|
1421
|
-
* ```typescript
|
|
1422
|
-
* // Stream all info+ logs
|
|
1423
|
-
* for await (const log of manager.streamLogs()) {
|
|
1424
|
-
* console.log(`[${log.level}] ${log.message}`);
|
|
1425
|
-
* }
|
|
1426
|
-
*
|
|
1427
|
-
* // Stream only errors for a specific agent
|
|
1428
|
-
* for await (const log of manager.streamLogs({
|
|
1429
|
-
* level: 'error',
|
|
1430
|
-
* agentName: 'my-agent',
|
|
1431
|
-
* })) {
|
|
1432
|
-
* console.error(log.message);
|
|
1433
|
-
* }
|
|
1434
|
-
* ```
|
|
1435
|
-
*/
|
|
1436
|
-
async *streamLogs(options) {
|
|
1437
|
-
const level = options?.level ?? "info";
|
|
1438
|
-
const includeHistory = options?.includeHistory ?? true;
|
|
1439
|
-
const historyLimit = options?.historyLimit ?? 1000;
|
|
1440
|
-
const agentFilter = options?.agentName;
|
|
1441
|
-
const jobFilter = options?.jobId;
|
|
1442
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1443
|
-
const { readJobOutputAll } = await import("../state/job-output.js");
|
|
1444
|
-
// Replay historical logs if requested
|
|
1445
|
-
if (includeHistory) {
|
|
1446
|
-
// Get jobs to replay history from
|
|
1447
|
-
const jobsResult = await listJobs(jobsDir, agentFilter ? { agent: agentFilter } : {}, { logger: this.logger });
|
|
1448
|
-
// Filter by job ID if specified
|
|
1449
|
-
let jobs = jobsResult.jobs;
|
|
1450
|
-
if (jobFilter) {
|
|
1451
|
-
jobs = jobs.filter((j) => j.id === jobFilter);
|
|
1452
|
-
}
|
|
1453
|
-
// Sort by started_at ascending to replay in chronological order
|
|
1454
|
-
jobs.sort((a, b) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime());
|
|
1455
|
-
let yielded = 0;
|
|
1456
|
-
for (const job of jobs) {
|
|
1457
|
-
if (yielded >= historyLimit)
|
|
1458
|
-
break;
|
|
1459
|
-
// Read job output and convert to log entries
|
|
1460
|
-
const output = await readJobOutputAll(jobsDir, job.id, {
|
|
1461
|
-
skipInvalidLines: true,
|
|
1462
|
-
logger: this.logger,
|
|
1463
|
-
});
|
|
1464
|
-
for (const msg of output) {
|
|
1465
|
-
if (yielded >= historyLimit)
|
|
1466
|
-
break;
|
|
1467
|
-
const logEntry = this.jobOutputToLogEntry(job, msg);
|
|
1468
|
-
if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
|
|
1469
|
-
yield logEntry;
|
|
1470
|
-
yielded++;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
// For running jobs, subscribe to job:output events
|
|
1476
|
-
const outputQueue = [];
|
|
1477
|
-
let resolveWait = null;
|
|
1478
|
-
let stopped = false;
|
|
1479
|
-
const outputHandler = (payload) => {
|
|
1480
|
-
if (stopped)
|
|
1481
|
-
return;
|
|
1482
|
-
const logEntry = {
|
|
1483
|
-
timestamp: payload.timestamp,
|
|
1484
|
-
level: "info",
|
|
1485
|
-
source: "job",
|
|
1486
|
-
agentName: payload.agentName,
|
|
1487
|
-
jobId: payload.jobId,
|
|
1488
|
-
message: payload.output,
|
|
1489
|
-
};
|
|
1490
|
-
if (this.shouldYieldLog(logEntry, level, agentFilter, jobFilter)) {
|
|
1491
|
-
outputQueue.push(logEntry);
|
|
1492
|
-
if (resolveWait) {
|
|
1493
|
-
resolveWait();
|
|
1494
|
-
resolveWait = null;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
};
|
|
1498
|
-
this.on("job:output", outputHandler);
|
|
1499
|
-
try {
|
|
1500
|
-
// Yield queued entries as they arrive
|
|
1501
|
-
while (!stopped) {
|
|
1502
|
-
while (outputQueue.length > 0) {
|
|
1503
|
-
const entry = outputQueue.shift();
|
|
1504
|
-
yield entry;
|
|
1505
|
-
}
|
|
1506
|
-
// Wait for more entries
|
|
1507
|
-
await new Promise((resolve) => {
|
|
1508
|
-
resolveWait = resolve;
|
|
1509
|
-
// Add timeout to prevent hanging forever
|
|
1510
|
-
setTimeout(resolve, 1000);
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
finally {
|
|
1515
|
-
stopped = true;
|
|
1516
|
-
this.off("job:output", outputHandler);
|
|
1517
|
-
}
|
|
223
|
+
initializeModules() {
|
|
224
|
+
this.statusQueries = new StatusQueries(this);
|
|
225
|
+
this.scheduleManagement = new ScheduleManagement(this, () => this.statusQueries.readFleetStateSnapshot());
|
|
226
|
+
this.configReloadModule = new ConfigReload(this, () => this.loadConfiguration(), (config) => { this.config = config; });
|
|
227
|
+
this.jobControl = new JobControl(this, () => this.statusQueries.getAgentInfo());
|
|
228
|
+
this.logStreaming = new LogStreaming(this);
|
|
229
|
+
this.scheduleExecutor = new ScheduleExecutor(this);
|
|
1518
230
|
}
|
|
1519
|
-
/**
|
|
1520
|
-
* Stream output from a specific job as an async iterable
|
|
1521
|
-
*
|
|
1522
|
-
* Provides a stream of log entries for a specific job. For completed jobs,
|
|
1523
|
-
* this will replay the job's history and then complete. For running jobs,
|
|
1524
|
-
* it will continue streaming until the job completes.
|
|
1525
|
-
*
|
|
1526
|
-
* @param jobId - The ID of the job to stream output from
|
|
1527
|
-
* @returns An async iterable of LogEntry objects
|
|
1528
|
-
* @throws {JobNotFoundError} If the job doesn't exist
|
|
1529
|
-
*
|
|
1530
|
-
* @example
|
|
1531
|
-
* ```typescript
|
|
1532
|
-
* // Stream job output
|
|
1533
|
-
* for await (const log of manager.streamJobOutput('job-2024-01-15-abc123')) {
|
|
1534
|
-
* console.log(`[${log.level}] ${log.message}`);
|
|
1535
|
-
* }
|
|
1536
|
-
* ```
|
|
1537
|
-
*/
|
|
1538
|
-
async *streamJobOutput(jobId) {
|
|
1539
|
-
const jobsDir = join(this.stateDir, "jobs");
|
|
1540
|
-
// Verify job exists
|
|
1541
|
-
const job = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1542
|
-
if (!job) {
|
|
1543
|
-
throw new JobNotFoundError(jobId);
|
|
1544
|
-
}
|
|
1545
|
-
const { readJobOutputAll, getJobOutputPath } = await import("../state/job-output.js");
|
|
1546
|
-
const { watch } = await import("node:fs");
|
|
1547
|
-
const { stat } = await import("node:fs/promises");
|
|
1548
|
-
const { createReadStream } = await import("node:fs");
|
|
1549
|
-
const { createInterface } = await import("node:readline");
|
|
1550
|
-
const outputPath = getJobOutputPath(jobsDir, jobId);
|
|
1551
|
-
// First, replay all existing output
|
|
1552
|
-
const existingOutput = await readJobOutputAll(jobsDir, jobId, {
|
|
1553
|
-
skipInvalidLines: true,
|
|
1554
|
-
logger: this.logger,
|
|
1555
|
-
});
|
|
1556
|
-
for (const msg of existingOutput) {
|
|
1557
|
-
yield this.jobOutputToLogEntry(job, msg);
|
|
1558
|
-
}
|
|
1559
|
-
// If job is already completed, we're done
|
|
1560
|
-
if (job.status !== "running" && job.status !== "pending") {
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
// For running jobs, watch for new output
|
|
1564
|
-
const outputQueue = [];
|
|
1565
|
-
let resolveWait = null;
|
|
1566
|
-
let stopped = false;
|
|
1567
|
-
let lastReadPosition = 0;
|
|
1568
|
-
// Get current file position
|
|
1569
|
-
try {
|
|
1570
|
-
const stats = await stat(outputPath);
|
|
1571
|
-
lastReadPosition = stats.size;
|
|
1572
|
-
}
|
|
1573
|
-
catch {
|
|
1574
|
-
// File doesn't exist yet
|
|
1575
|
-
}
|
|
1576
|
-
// Watch for file changes
|
|
1577
|
-
let watcher = null;
|
|
1578
|
-
try {
|
|
1579
|
-
watcher = watch(outputPath, async (eventType) => {
|
|
1580
|
-
if (stopped || eventType !== "change")
|
|
1581
|
-
return;
|
|
1582
|
-
try {
|
|
1583
|
-
const currentStats = await stat(outputPath);
|
|
1584
|
-
if (currentStats.size > lastReadPosition) {
|
|
1585
|
-
// Read new content
|
|
1586
|
-
const fileStream = createReadStream(outputPath, {
|
|
1587
|
-
encoding: "utf-8",
|
|
1588
|
-
start: lastReadPosition,
|
|
1589
|
-
});
|
|
1590
|
-
const rl = createInterface({
|
|
1591
|
-
input: fileStream,
|
|
1592
|
-
crlfDelay: Infinity,
|
|
1593
|
-
});
|
|
1594
|
-
for await (const line of rl) {
|
|
1595
|
-
if (stopped)
|
|
1596
|
-
break;
|
|
1597
|
-
const trimmedLine = line.trim();
|
|
1598
|
-
if (trimmedLine === "")
|
|
1599
|
-
continue;
|
|
1600
|
-
try {
|
|
1601
|
-
const parsed = JSON.parse(trimmedLine);
|
|
1602
|
-
const logEntry = this.jobOutputToLogEntry(job, parsed);
|
|
1603
|
-
outputQueue.push(logEntry);
|
|
1604
|
-
if (resolveWait) {
|
|
1605
|
-
resolveWait();
|
|
1606
|
-
resolveWait = null;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
catch {
|
|
1610
|
-
// Skip malformed lines
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
rl.close();
|
|
1614
|
-
fileStream.destroy();
|
|
1615
|
-
lastReadPosition = currentStats.size;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
catch (err) {
|
|
1619
|
-
this.logger.warn(`Error reading output file: ${err.message}`);
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
catch {
|
|
1624
|
-
// Can't watch file - might not exist yet
|
|
1625
|
-
}
|
|
1626
|
-
// Poll for job completion
|
|
1627
|
-
const checkJobComplete = async () => {
|
|
1628
|
-
const currentJob = await getJob(jobsDir, jobId, { logger: this.logger });
|
|
1629
|
-
return (!currentJob ||
|
|
1630
|
-
(currentJob.status !== "running" && currentJob.status !== "pending"));
|
|
1631
|
-
};
|
|
1632
|
-
try {
|
|
1633
|
-
// Yield queued entries as they arrive
|
|
1634
|
-
while (!stopped) {
|
|
1635
|
-
while (outputQueue.length > 0) {
|
|
1636
|
-
const entry = outputQueue.shift();
|
|
1637
|
-
yield entry;
|
|
1638
|
-
}
|
|
1639
|
-
// Check if job is complete
|
|
1640
|
-
if (await checkJobComplete()) {
|
|
1641
|
-
stopped = true;
|
|
1642
|
-
break;
|
|
1643
|
-
}
|
|
1644
|
-
// Wait for more entries
|
|
1645
|
-
await new Promise((resolve) => {
|
|
1646
|
-
resolveWait = resolve;
|
|
1647
|
-
setTimeout(resolve, 1000);
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
finally {
|
|
1652
|
-
stopped = true;
|
|
1653
|
-
if (watcher) {
|
|
1654
|
-
watcher.close();
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
/**
|
|
1659
|
-
* Stream logs for a specific agent as an async iterable
|
|
1660
|
-
*
|
|
1661
|
-
* Provides a stream of log entries for all jobs belonging to a specific agent.
|
|
1662
|
-
* For completed jobs, this will replay their history. For running jobs, it
|
|
1663
|
-
* will continue streaming until the iterator is stopped.
|
|
1664
|
-
*
|
|
1665
|
-
* @param agentName - The name of the agent to stream logs for
|
|
1666
|
-
* @returns An async iterable of LogEntry objects
|
|
1667
|
-
* @throws {AgentNotFoundError} If the agent doesn't exist in the configuration
|
|
1668
|
-
*
|
|
1669
|
-
* @example
|
|
1670
|
-
* ```typescript
|
|
1671
|
-
* // Stream all logs for an agent
|
|
1672
|
-
* for await (const log of manager.streamAgentLogs('my-agent')) {
|
|
1673
|
-
* console.log(`[${log.jobId}] ${log.message}`);
|
|
1674
|
-
* }
|
|
1675
|
-
* ```
|
|
1676
|
-
*/
|
|
1677
|
-
async *streamAgentLogs(agentName) {
|
|
1678
|
-
// Verify agent exists
|
|
1679
|
-
const agents = this.config?.agents ?? [];
|
|
1680
|
-
const agent = agents.find((a) => a.name === agentName);
|
|
1681
|
-
if (!agent) {
|
|
1682
|
-
throw new AgentNotFoundError(agentName, {
|
|
1683
|
-
availableAgents: agents.map((a) => a.name),
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
// Delegate to streamLogs with agent filter
|
|
1687
|
-
yield* this.streamLogs({
|
|
1688
|
-
agentName,
|
|
1689
|
-
includeHistory: true,
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
// ===========================================================================
|
|
1693
|
-
// Log Streaming Helpers (US-11)
|
|
1694
|
-
// ===========================================================================
|
|
1695
|
-
/**
|
|
1696
|
-
* Convert a job output message to a LogEntry
|
|
1697
|
-
*/
|
|
1698
|
-
jobOutputToLogEntry(job, msg) {
|
|
1699
|
-
// Determine log level based on message type
|
|
1700
|
-
let level = "info";
|
|
1701
|
-
if (msg.type === "error") {
|
|
1702
|
-
level = "error";
|
|
1703
|
-
}
|
|
1704
|
-
else if (msg.type === "system") {
|
|
1705
|
-
level = "debug";
|
|
1706
|
-
}
|
|
1707
|
-
return {
|
|
1708
|
-
timestamp: msg.timestamp ?? new Date().toISOString(),
|
|
1709
|
-
level,
|
|
1710
|
-
source: "job",
|
|
1711
|
-
agentName: job.agent,
|
|
1712
|
-
jobId: job.id,
|
|
1713
|
-
scheduleName: job.schedule ?? undefined,
|
|
1714
|
-
message: msg.content ?? "",
|
|
1715
|
-
data: { type: msg.type },
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
/**
|
|
1719
|
-
* Determine if a log entry should be yielded based on filters
|
|
1720
|
-
*/
|
|
1721
|
-
shouldYieldLog(entry, minLevel, agentFilter, jobFilter) {
|
|
1722
|
-
// Check log level
|
|
1723
|
-
const levelOrder = {
|
|
1724
|
-
debug: 0,
|
|
1725
|
-
info: 1,
|
|
1726
|
-
warn: 2,
|
|
1727
|
-
error: 3,
|
|
1728
|
-
};
|
|
1729
|
-
if (levelOrder[entry.level] < levelOrder[minLevel]) {
|
|
1730
|
-
return false;
|
|
1731
|
-
}
|
|
1732
|
-
// Check agent filter
|
|
1733
|
-
if (agentFilter && entry.agentName !== agentFilter) {
|
|
1734
|
-
return false;
|
|
1735
|
-
}
|
|
1736
|
-
// Check job filter
|
|
1737
|
-
if (jobFilter && entry.jobId !== jobFilter) {
|
|
1738
|
-
return false;
|
|
1739
|
-
}
|
|
1740
|
-
return true;
|
|
1741
|
-
}
|
|
1742
|
-
// ===========================================================================
|
|
1743
|
-
// Private Helper Methods
|
|
1744
|
-
// ===========================================================================
|
|
1745
|
-
/**
|
|
1746
|
-
* Load configuration with proper error handling
|
|
1747
|
-
*/
|
|
1748
231
|
async loadConfiguration() {
|
|
1749
232
|
try {
|
|
1750
233
|
return await loadConfig(this.configPath);
|
|
1751
234
|
}
|
|
1752
235
|
catch (error) {
|
|
1753
236
|
if (error instanceof ConfigNotFoundError) {
|
|
1754
|
-
throw new
|
|
237
|
+
throw new ConfigurationError(`Configuration file not found. ${error.message}`, { configPath: this.configPath, cause: error });
|
|
1755
238
|
}
|
|
1756
239
|
if (error instanceof ConfigError) {
|
|
1757
|
-
throw new
|
|
240
|
+
throw new ConfigurationError(`Invalid configuration: ${error.message}`, { configPath: this.configPath, cause: error });
|
|
1758
241
|
}
|
|
1759
|
-
throw new
|
|
242
|
+
throw new ConfigurationError(`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`, { configPath: this.configPath, cause: error instanceof Error ? error : undefined });
|
|
1760
243
|
}
|
|
1761
244
|
}
|
|
1762
|
-
/**
|
|
1763
|
-
* Initialize state directory with proper error handling
|
|
1764
|
-
*/
|
|
1765
245
|
async initializeStateDir() {
|
|
1766
246
|
try {
|
|
1767
247
|
return await initStateDirectory({ path: this.stateDir });
|
|
@@ -1770,14 +250,8 @@ export class FleetManager extends EventEmitter {
|
|
|
1770
250
|
throw new FleetManagerStateDirError(`Failed to initialize state directory: ${error instanceof Error ? error.message : String(error)}`, this.stateDir, { cause: error instanceof Error ? error : undefined });
|
|
1771
251
|
}
|
|
1772
252
|
}
|
|
1773
|
-
/**
|
|
1774
|
-
* Start the scheduler asynchronously (don't block on the loop)
|
|
1775
|
-
*/
|
|
1776
253
|
startSchedulerAsync(agents) {
|
|
1777
|
-
// Start the scheduler loop in the background
|
|
1778
|
-
// The scheduler.start() method runs the loop and returns when stopped
|
|
1779
254
|
this.scheduler.start(agents).catch((error) => {
|
|
1780
|
-
// Only handle errors if we're still supposed to be running
|
|
1781
255
|
if (this.status === "running" || this.status === "starting") {
|
|
1782
256
|
this.logger.error(`Scheduler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1783
257
|
this.status = "error";
|
|
@@ -1786,121 +260,22 @@ export class FleetManager extends EventEmitter {
|
|
|
1786
260
|
}
|
|
1787
261
|
});
|
|
1788
262
|
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Handle schedule trigger callback from scheduler
|
|
1791
|
-
*/
|
|
1792
263
|
async handleScheduleTrigger(info) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
this.
|
|
1800
|
-
|
|
1801
|
-
scheduleName,
|
|
1802
|
-
schedule,
|
|
1803
|
-
timestamp,
|
|
1804
|
-
});
|
|
264
|
+
await this.scheduleExecutor.executeSchedule(info);
|
|
265
|
+
}
|
|
266
|
+
async persistShutdownState() {
|
|
267
|
+
if (!this.stateDirInfo)
|
|
268
|
+
return;
|
|
269
|
+
const { writeFleetState } = await import("../state/fleet-state.js");
|
|
270
|
+
const currentState = await this.statusQueries.readFleetStateSnapshot();
|
|
271
|
+
const updatedState = { ...currentState, fleet: { ...currentState.fleet, stoppedAt: new Date().toISOString() } };
|
|
1805
272
|
try {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
this.logger.debug(`Schedule ${scheduleName} triggered for agent ${agent.name} ` +
|
|
1809
|
-
`(type: ${schedule.type}, prompt: ${schedule.prompt ?? "default"})`);
|
|
1810
|
-
// Emit legacy completion event for backwards compatibility
|
|
1811
|
-
this.emit("schedule:complete", agent.name, scheduleName);
|
|
273
|
+
await writeFleetState(this.stateDirInfo.stateFile, updatedState);
|
|
274
|
+
this.logger.debug("Fleet state persisted");
|
|
1812
275
|
}
|
|
1813
276
|
catch (error) {
|
|
1814
|
-
|
|
1815
|
-
this.logger.error(`Error in ${agent.name}/${scheduleName}: ${err.message}`);
|
|
1816
|
-
// Emit legacy error event for backwards compatibility
|
|
1817
|
-
this.emit("schedule:error", agent.name, scheduleName, err);
|
|
1818
|
-
throw error;
|
|
277
|
+
this.logger.warn(`Failed to persist fleet state: ${error.message}`);
|
|
1819
278
|
}
|
|
1820
279
|
}
|
|
1821
|
-
// ===========================================================================
|
|
1822
|
-
// Event Emission Helpers (US-2)
|
|
1823
|
-
// ===========================================================================
|
|
1824
|
-
/**
|
|
1825
|
-
* Emit a config:reloaded event
|
|
1826
|
-
*
|
|
1827
|
-
* Called when configuration is hot-reloaded.
|
|
1828
|
-
*/
|
|
1829
|
-
emitConfigReloaded(payload) {
|
|
1830
|
-
this.emit("config:reloaded", payload);
|
|
1831
|
-
}
|
|
1832
|
-
/**
|
|
1833
|
-
* Emit an agent:started event
|
|
1834
|
-
*
|
|
1835
|
-
* Called when an agent is started/registered with the fleet.
|
|
1836
|
-
*/
|
|
1837
|
-
emitAgentStarted(payload) {
|
|
1838
|
-
this.emit("agent:started", payload);
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Emit an agent:stopped event
|
|
1842
|
-
*
|
|
1843
|
-
* Called when an agent is stopped/unregistered from the fleet.
|
|
1844
|
-
*/
|
|
1845
|
-
emitAgentStopped(payload) {
|
|
1846
|
-
this.emit("agent:stopped", payload);
|
|
1847
|
-
}
|
|
1848
|
-
/**
|
|
1849
|
-
* Emit a schedule:skipped event
|
|
1850
|
-
*
|
|
1851
|
-
* Called when a schedule check is skipped (already running, disabled, etc.).
|
|
1852
|
-
*/
|
|
1853
|
-
emitScheduleSkipped(payload) {
|
|
1854
|
-
this.emit("schedule:skipped", payload);
|
|
1855
|
-
}
|
|
1856
|
-
/**
|
|
1857
|
-
* Emit a job:created event
|
|
1858
|
-
*
|
|
1859
|
-
* Called when a new job is created.
|
|
1860
|
-
*/
|
|
1861
|
-
emitJobCreated(payload) {
|
|
1862
|
-
this.emit("job:created", payload);
|
|
1863
|
-
}
|
|
1864
|
-
/**
|
|
1865
|
-
* Emit a job:output event
|
|
1866
|
-
*
|
|
1867
|
-
* Called when a job produces output during execution.
|
|
1868
|
-
* This enables real-time streaming of output to UIs.
|
|
1869
|
-
*/
|
|
1870
|
-
emitJobOutput(payload) {
|
|
1871
|
-
this.emit("job:output", payload);
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* Emit a job:completed event
|
|
1875
|
-
*
|
|
1876
|
-
* Called when a job completes successfully.
|
|
1877
|
-
*/
|
|
1878
|
-
emitJobCompleted(payload) {
|
|
1879
|
-
this.emit("job:completed", payload);
|
|
1880
|
-
}
|
|
1881
|
-
/**
|
|
1882
|
-
* Emit a job:failed event
|
|
1883
|
-
*
|
|
1884
|
-
* Called when a job fails.
|
|
1885
|
-
*/
|
|
1886
|
-
emitJobFailed(payload) {
|
|
1887
|
-
this.emit("job:failed", payload);
|
|
1888
|
-
}
|
|
1889
|
-
/**
|
|
1890
|
-
* Emit a job:cancelled event (US-6)
|
|
1891
|
-
*
|
|
1892
|
-
* Called when a job is cancelled.
|
|
1893
|
-
*/
|
|
1894
|
-
emitJobCancelled(payload) {
|
|
1895
|
-
this.emit("job:cancelled", payload);
|
|
1896
|
-
}
|
|
1897
|
-
/**
|
|
1898
|
-
* Emit a job:forked event (US-6)
|
|
1899
|
-
*
|
|
1900
|
-
* Called when a job is forked to create a new job.
|
|
1901
|
-
*/
|
|
1902
|
-
emitJobForked(payload) {
|
|
1903
|
-
this.emit("job:forked", payload);
|
|
1904
|
-
}
|
|
1905
280
|
}
|
|
1906
281
|
//# sourceMappingURL=fleet-manager.js.map
|