@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.
Files changed (275) hide show
  1. package/dist/config/__tests__/agent.test.js +31 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +9 -2
  4. package/dist/config/__tests__/merge.test.js.map +1 -1
  5. package/dist/config/__tests__/schema.test.js +350 -1
  6. package/dist/config/__tests__/schema.test.js.map +1 -1
  7. package/dist/config/index.d.ts +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +3 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/schema.d.ts +828 -24
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +118 -6
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
  16. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  17. package/dist/fleet-manager/__tests__/errors.test.js +1 -49
  18. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  19. package/dist/fleet-manager/__tests__/integration.test.js +109 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/reload.test.js +1 -1
  22. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  23. package/dist/fleet-manager/config-reload.d.ts +164 -0
  24. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  25. package/dist/fleet-manager/config-reload.js +445 -0
  26. package/dist/fleet-manager/config-reload.js.map +1 -0
  27. package/dist/fleet-manager/context.d.ts +76 -0
  28. package/dist/fleet-manager/context.d.ts.map +1 -0
  29. package/dist/fleet-manager/context.js +11 -0
  30. package/dist/fleet-manager/context.js.map +1 -0
  31. package/dist/fleet-manager/errors.d.ts +0 -25
  32. package/dist/fleet-manager/errors.d.ts.map +1 -1
  33. package/dist/fleet-manager/errors.js +0 -38
  34. package/dist/fleet-manager/errors.js.map +1 -1
  35. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  36. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  37. package/dist/fleet-manager/event-emitters.js +136 -0
  38. package/dist/fleet-manager/event-emitters.js.map +1 -0
  39. package/dist/fleet-manager/event-types.d.ts +0 -15
  40. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  41. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  42. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  43. package/dist/fleet-manager/fleet-manager.js +95 -1720
  44. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  45. package/dist/fleet-manager/index.d.ts +13 -2
  46. package/dist/fleet-manager/index.d.ts.map +1 -1
  47. package/dist/fleet-manager/index.js +19 -6
  48. package/dist/fleet-manager/index.js.map +1 -1
  49. package/dist/fleet-manager/job-control.d.ts +64 -0
  50. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  51. package/dist/fleet-manager/job-control.js +296 -0
  52. package/dist/fleet-manager/job-control.js.map +1 -0
  53. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  54. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  55. package/dist/fleet-manager/log-streaming.js +503 -0
  56. package/dist/fleet-manager/log-streaming.js.map +1 -0
  57. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  58. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  59. package/dist/fleet-manager/schedule-executor.js +209 -0
  60. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  61. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  62. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  63. package/dist/fleet-manager/schedule-management.js +171 -0
  64. package/dist/fleet-manager/schedule-management.js.map +1 -0
  65. package/dist/fleet-manager/status-queries.d.ts +105 -0
  66. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  67. package/dist/fleet-manager/status-queries.js +247 -0
  68. package/dist/fleet-manager/status-queries.js.map +1 -0
  69. package/dist/fleet-manager/types.d.ts +0 -39
  70. package/dist/fleet-manager/types.d.ts.map +1 -1
  71. package/dist/runner/__tests__/job-executor.test.js +206 -1
  72. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  73. package/dist/runner/job-executor.d.ts +9 -0
  74. package/dist/runner/job-executor.d.ts.map +1 -1
  75. package/dist/runner/job-executor.js +78 -4
  76. package/dist/runner/job-executor.js.map +1 -1
  77. package/dist/runner/types.d.ts +2 -0
  78. package/dist/runner/types.d.ts.map +1 -1
  79. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  80. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  81. package/dist/scheduler/__tests__/cron.test.js +867 -0
  82. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  83. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  84. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  85. package/dist/scheduler/cron.d.ts +126 -0
  86. package/dist/scheduler/cron.d.ts.map +1 -0
  87. package/dist/scheduler/cron.js +390 -0
  88. package/dist/scheduler/cron.js.map +1 -0
  89. package/dist/scheduler/errors.d.ts +81 -1
  90. package/dist/scheduler/errors.d.ts.map +1 -1
  91. package/dist/scheduler/errors.js +81 -6
  92. package/dist/scheduler/errors.js.map +1 -1
  93. package/dist/scheduler/index.d.ts +1 -0
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/scheduler/index.js +2 -0
  96. package/dist/scheduler/index.js.map +1 -1
  97. package/dist/scheduler/schedule-runner.d.ts +2 -2
  98. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  99. package/dist/scheduler/schedule-runner.js +20 -8
  100. package/dist/scheduler/schedule-runner.js.map +1 -1
  101. package/dist/scheduler/scheduler.d.ts +4 -4
  102. package/dist/scheduler/scheduler.d.ts.map +1 -1
  103. package/dist/scheduler/scheduler.js +86 -20
  104. package/dist/scheduler/scheduler.js.map +1 -1
  105. package/dist/scheduler/types.d.ts +1 -1
  106. package/dist/scheduler/types.d.ts.map +1 -1
  107. package/dist/state/schemas/job-metadata.d.ts +2 -2
  108. package/package.json +33 -8
  109. package/.turbo/turbo-build.log +0 -4
  110. package/.turbo/turbo-test.log +0 -219
  111. package/.turbo/turbo-typecheck.log +0 -4
  112. package/coverage/base.css +0 -224
  113. package/coverage/block-navigation.js +0 -87
  114. package/coverage/coverage-final.json +0 -51
  115. package/coverage/favicon.png +0 -0
  116. package/coverage/index.html +0 -251
  117. package/coverage/prettify.css +0 -1
  118. package/coverage/prettify.js +0 -2
  119. package/coverage/sort-arrow-sprite.png +0 -0
  120. package/coverage/sorter.js +0 -210
  121. package/coverage/src/config/index.html +0 -191
  122. package/coverage/src/config/index.ts.html +0 -442
  123. package/coverage/src/config/interpolate.ts.html +0 -652
  124. package/coverage/src/config/loader.ts.html +0 -1501
  125. package/coverage/src/config/merge.ts.html +0 -823
  126. package/coverage/src/config/parser.ts.html +0 -1213
  127. package/coverage/src/config/schema.ts.html +0 -1123
  128. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  129. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  130. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  131. package/coverage/src/fleet-manager/index.html +0 -206
  132. package/coverage/src/fleet-manager/index.ts.html +0 -469
  133. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  134. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  135. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  136. package/coverage/src/index.html +0 -116
  137. package/coverage/src/index.ts.html +0 -181
  138. package/coverage/src/runner/errors.ts.html +0 -1006
  139. package/coverage/src/runner/index.html +0 -191
  140. package/coverage/src/runner/index.ts.html +0 -256
  141. package/coverage/src/runner/job-executor.ts.html +0 -1429
  142. package/coverage/src/runner/message-processor.ts.html +0 -1150
  143. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  144. package/coverage/src/runner/types.ts.html +0 -559
  145. package/coverage/src/scheduler/errors.ts.html +0 -388
  146. package/coverage/src/scheduler/index.html +0 -206
  147. package/coverage/src/scheduler/index.ts.html +0 -244
  148. package/coverage/src/scheduler/interval.ts.html +0 -652
  149. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  150. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  151. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  152. package/coverage/src/scheduler/types.ts.html +0 -733
  153. package/coverage/src/state/directory.ts.html +0 -736
  154. package/coverage/src/state/errors.ts.html +0 -376
  155. package/coverage/src/state/fleet-state.ts.html +0 -937
  156. package/coverage/src/state/index.html +0 -221
  157. package/coverage/src/state/index.ts.html +0 -322
  158. package/coverage/src/state/job-metadata.ts.html +0 -1420
  159. package/coverage/src/state/job-output.ts.html +0 -1033
  160. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  161. package/coverage/src/state/schemas/index.html +0 -176
  162. package/coverage/src/state/schemas/index.ts.html +0 -286
  163. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  164. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  165. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  166. package/coverage/src/state/session.ts.html +0 -844
  167. package/coverage/src/state/types.ts.html +0 -262
  168. package/coverage/src/state/utils/atomic.ts.html +0 -748
  169. package/coverage/src/state/utils/index.html +0 -146
  170. package/coverage/src/state/utils/index.ts.html +0 -103
  171. package/coverage/src/state/utils/reads.ts.html +0 -1621
  172. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  173. package/coverage/src/work-sources/adapters/index.html +0 -131
  174. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  175. package/coverage/src/work-sources/errors.ts.html +0 -298
  176. package/coverage/src/work-sources/index.html +0 -176
  177. package/coverage/src/work-sources/index.ts.html +0 -529
  178. package/coverage/src/work-sources/manager.ts.html +0 -1324
  179. package/coverage/src/work-sources/registry.ts.html +0 -619
  180. package/coverage/src/work-sources/types.ts.html +0 -568
  181. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  182. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  183. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  184. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  185. package/src/config/__tests__/agent.test.ts +0 -864
  186. package/src/config/__tests__/interpolate.test.ts +0 -644
  187. package/src/config/__tests__/loader.test.ts +0 -784
  188. package/src/config/__tests__/merge.test.ts +0 -751
  189. package/src/config/__tests__/parser.test.ts +0 -533
  190. package/src/config/__tests__/schema.test.ts +0 -873
  191. package/src/config/index.ts +0 -119
  192. package/src/config/interpolate.ts +0 -189
  193. package/src/config/loader.ts +0 -472
  194. package/src/config/merge.ts +0 -246
  195. package/src/config/parser.ts +0 -376
  196. package/src/config/schema.ts +0 -346
  197. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  198. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  199. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  200. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  201. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  202. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  203. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  204. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  205. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  206. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  207. package/src/fleet-manager/errors.ts +0 -747
  208. package/src/fleet-manager/event-types.ts +0 -378
  209. package/src/fleet-manager/fleet-manager.ts +0 -2315
  210. package/src/fleet-manager/index.ts +0 -128
  211. package/src/fleet-manager/job-manager.ts +0 -663
  212. package/src/fleet-manager/job-queue.ts +0 -798
  213. package/src/fleet-manager/types.ts +0 -839
  214. package/src/index.ts +0 -32
  215. package/src/runner/__tests__/errors.test.ts +0 -382
  216. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  217. package/src/runner/__tests__/message-processor.test.ts +0 -960
  218. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  219. package/src/runner/errors.ts +0 -307
  220. package/src/runner/index.ts +0 -57
  221. package/src/runner/job-executor.ts +0 -448
  222. package/src/runner/message-processor.ts +0 -355
  223. package/src/runner/sdk-adapter.ts +0 -191
  224. package/src/runner/types.ts +0 -158
  225. package/src/scheduler/__tests__/errors.test.ts +0 -159
  226. package/src/scheduler/__tests__/interval.test.ts +0 -515
  227. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  228. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  229. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  230. package/src/scheduler/errors.ts +0 -101
  231. package/src/scheduler/index.ts +0 -53
  232. package/src/scheduler/interval.ts +0 -189
  233. package/src/scheduler/schedule-runner.ts +0 -442
  234. package/src/scheduler/schedule-state.ts +0 -211
  235. package/src/scheduler/scheduler.ts +0 -570
  236. package/src/scheduler/types.ts +0 -216
  237. package/src/state/__tests__/directory.test.ts +0 -595
  238. package/src/state/__tests__/fleet-state.test.ts +0 -868
  239. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  240. package/src/state/__tests__/job-metadata.test.ts +0 -831
  241. package/src/state/__tests__/job-output.test.ts +0 -856
  242. package/src/state/__tests__/session-schema.test.ts +0 -378
  243. package/src/state/__tests__/session.test.ts +0 -604
  244. package/src/state/directory.ts +0 -217
  245. package/src/state/errors.ts +0 -97
  246. package/src/state/fleet-state.ts +0 -284
  247. package/src/state/index.ts +0 -79
  248. package/src/state/job-metadata.ts +0 -445
  249. package/src/state/job-output.ts +0 -316
  250. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  251. package/src/state/schemas/fleet-state.ts +0 -120
  252. package/src/state/schemas/index.ts +0 -67
  253. package/src/state/schemas/job-metadata.ts +0 -181
  254. package/src/state/schemas/job-output.ts +0 -177
  255. package/src/state/schemas/session-info.ts +0 -92
  256. package/src/state/session.ts +0 -253
  257. package/src/state/types.ts +0 -59
  258. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  259. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  260. package/src/state/utils/atomic.ts +0 -221
  261. package/src/state/utils/index.ts +0 -6
  262. package/src/state/utils/reads.ts +0 -512
  263. package/src/work-sources/__tests__/github.test.ts +0 -1800
  264. package/src/work-sources/__tests__/manager.test.ts +0 -529
  265. package/src/work-sources/__tests__/registry.test.ts +0 -477
  266. package/src/work-sources/__tests__/types.test.ts +0 -479
  267. package/src/work-sources/adapters/github.ts +0 -1166
  268. package/src/work-sources/adapters/index.ts +0 -64
  269. package/src/work-sources/errors.ts +0 -71
  270. package/src/work-sources/index.ts +0 -148
  271. package/src/work-sources/manager.ts +0 -413
  272. package/src/work-sources/registry.ts +0 -178
  273. package/src/work-sources/types.ts +0 -161
  274. package/tsconfig.json +0 -9
  275. package/vitest.config.ts +0 -19
@@ -1,570 +0,0 @@
1
- /**
2
- * Scheduler class for managing agent schedule execution
3
- *
4
- * The Scheduler continuously checks all agents' interval schedules and triggers
5
- * due agents according to their configured intervals.
6
- */
7
-
8
- import type { ResolvedAgent } from "../config/index.js";
9
- import { calculateNextTrigger, isScheduleDue } from "./interval.js";
10
- import {
11
- getScheduleState,
12
- updateScheduleState,
13
- type ScheduleStateLogger,
14
- } from "./schedule-state.js";
15
- import type {
16
- SchedulerOptions,
17
- SchedulerStatus,
18
- SchedulerState,
19
- SchedulerLogger,
20
- ScheduleCheckResult,
21
- ScheduleSkipReason,
22
- TriggerInfo,
23
- SchedulerTriggerCallback,
24
- StopOptions,
25
- } from "./types.js";
26
- import { SchedulerShutdownError } from "./errors.js";
27
-
28
- // =============================================================================
29
- // Constants
30
- // =============================================================================
31
-
32
- /**
33
- * Default check interval in milliseconds (1 second)
34
- */
35
- const DEFAULT_CHECK_INTERVAL = 1000;
36
-
37
- /**
38
- * Default shutdown timeout in milliseconds (30 seconds)
39
- */
40
- const DEFAULT_SHUTDOWN_TIMEOUT = 30000;
41
-
42
- // =============================================================================
43
- // Default Logger
44
- // =============================================================================
45
-
46
- /**
47
- * Create a default console-based logger
48
- */
49
- function createDefaultLogger(): SchedulerLogger {
50
- return {
51
- debug: (message: string) => console.debug(`[scheduler] ${message}`),
52
- info: (message: string) => console.info(`[scheduler] ${message}`),
53
- warn: (message: string) => console.warn(`[scheduler] ${message}`),
54
- error: (message: string) => console.error(`[scheduler] ${message}`),
55
- };
56
- }
57
-
58
- // =============================================================================
59
- // Scheduler Class
60
- // =============================================================================
61
-
62
- /**
63
- * Scheduler for managing agent schedule execution
64
- *
65
- * The Scheduler runs a polling loop that:
66
- * 1. Checks all agents' interval schedules on each iteration
67
- * 2. Skips non-interval schedule types (cron, webhook, chat)
68
- * 3. Skips disabled schedules
69
- * 4. Skips agents at max_concurrent capacity
70
- * 5. Triggers due schedules via the onTrigger callback
71
- *
72
- * @example
73
- * ```typescript
74
- * const scheduler = new Scheduler({
75
- * stateDir: '.herdctl',
76
- * checkInterval: 1000, // 1 second
77
- * onTrigger: async (info) => {
78
- * console.log(`Triggering ${info.agent.name}/${info.scheduleName}`);
79
- * await runAgent(info.agent, info.schedule);
80
- * },
81
- * });
82
- *
83
- * // Start the scheduler
84
- * await scheduler.start(agents);
85
- *
86
- * // Later, stop the scheduler
87
- * await scheduler.stop();
88
- * ```
89
- */
90
- export class Scheduler {
91
- private readonly checkInterval: number;
92
- private readonly stateDir: string;
93
- private readonly logger: SchedulerLogger;
94
- private readonly onTrigger?: SchedulerTriggerCallback;
95
-
96
- private status: SchedulerStatus = "stopped";
97
- private abortController: AbortController | null = null;
98
- private agents: ResolvedAgent[] = [];
99
- private runningSchedules: Map<string, Set<string>> = new Map();
100
- private runningJobs: Map<string, Promise<void>> = new Map();
101
-
102
- private startedAt: string | null = null;
103
- private checkCount = 0;
104
- private triggerCount = 0;
105
- private lastCheckAt: string | null = null;
106
-
107
- constructor(options: SchedulerOptions) {
108
- this.checkInterval = options.checkInterval ?? DEFAULT_CHECK_INTERVAL;
109
- this.stateDir = options.stateDir;
110
- this.logger = options.logger ?? createDefaultLogger();
111
- this.onTrigger = options.onTrigger;
112
- }
113
-
114
- /**
115
- * Check if the scheduler is currently running
116
- */
117
- isRunning(): boolean {
118
- return this.status === "running";
119
- }
120
-
121
- /**
122
- * Get the current scheduler status
123
- */
124
- getStatus(): SchedulerStatus {
125
- return this.status;
126
- }
127
-
128
- /**
129
- * Get detailed scheduler state for monitoring
130
- */
131
- getState(): SchedulerState {
132
- return {
133
- status: this.status,
134
- startedAt: this.startedAt,
135
- checkCount: this.checkCount,
136
- triggerCount: this.triggerCount,
137
- lastCheckAt: this.lastCheckAt,
138
- };
139
- }
140
-
141
- /**
142
- * Start the scheduler polling loop
143
- *
144
- * @param agents - The agents to schedule
145
- * @throws Error if scheduler is already running
146
- */
147
- async start(agents: ResolvedAgent[]): Promise<void> {
148
- if (this.status === "running") {
149
- throw new Error("Scheduler is already running");
150
- }
151
-
152
- if (this.status === "stopping") {
153
- throw new Error("Scheduler is stopping, wait for it to complete");
154
- }
155
-
156
- this.agents = agents;
157
- this.status = "running";
158
- this.abortController = new AbortController();
159
- this.startedAt = new Date().toISOString();
160
- this.checkCount = 0;
161
- this.triggerCount = 0;
162
- this.runningSchedules.clear();
163
- this.runningJobs.clear();
164
-
165
- this.logger.info(
166
- `Scheduler started with ${agents.length} agents, check interval: ${this.checkInterval}ms`
167
- );
168
-
169
- // Run the polling loop
170
- await this.runLoop();
171
- }
172
-
173
- /**
174
- * Stop the scheduler gracefully
175
- *
176
- * Signals the polling loop to stop and optionally waits for running jobs to complete.
177
- *
178
- * @param options - Options for shutdown behavior
179
- * @param options.waitForJobs - Whether to wait for running jobs to complete (default: true)
180
- * @param options.timeout - Maximum time to wait for jobs in milliseconds (default: 30000)
181
- * @throws SchedulerShutdownError if timeout is reached while waiting for jobs
182
- */
183
- async stop(options?: StopOptions): Promise<void> {
184
- if (this.status !== "running") {
185
- return;
186
- }
187
-
188
- const waitForJobs = options?.waitForJobs ?? true;
189
- const timeout = options?.timeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
190
-
191
- this.status = "stopping";
192
- this.logger.info("Scheduler stopping...");
193
-
194
- // Signal the loop to stop - this prevents new triggers from starting
195
- this.abortController?.abort();
196
-
197
- // Wait a tick for the loop to recognize the abort
198
- await new Promise((resolve) => setImmediate(resolve));
199
-
200
- // Optionally wait for running jobs to complete
201
- if (waitForJobs && this.runningJobs.size > 0) {
202
- this.logger.info(
203
- `Waiting for ${this.runningJobs.size} running job(s) to complete...`
204
- );
205
-
206
- const runningJobPromises = Array.from(this.runningJobs.values());
207
-
208
- // Create a timeout promise
209
- const timeoutPromise = new Promise<"timeout">((resolve) => {
210
- setTimeout(() => resolve("timeout"), timeout);
211
- });
212
-
213
- // Race all running jobs against the timeout
214
- const result = await Promise.race([
215
- Promise.all(runningJobPromises).then(() => "completed" as const),
216
- timeoutPromise,
217
- ]);
218
-
219
- if (result === "timeout") {
220
- const runningJobCount = this.runningJobs.size;
221
- this.status = "stopped";
222
- this.abortController = null;
223
- this.logger.error(
224
- `Shutdown timed out with ${runningJobCount} job(s) still running`
225
- );
226
- throw new SchedulerShutdownError(
227
- `Scheduler shutdown timed out after ${timeout}ms with ${runningJobCount} job(s) still running`,
228
- { timedOut: true, runningJobCount }
229
- );
230
- }
231
-
232
- this.logger.info("All running jobs completed");
233
- }
234
-
235
- this.status = "stopped";
236
- this.abortController = null;
237
- this.logger.info("Scheduler stopped");
238
- }
239
-
240
- /**
241
- * Update the list of agents to schedule
242
- *
243
- * Can be called while the scheduler is running to add/remove agents.
244
- */
245
- setAgents(agents: ResolvedAgent[]): void {
246
- this.agents = agents;
247
- this.logger.debug(`Updated agents list: ${agents.length} agents`);
248
- }
249
-
250
- /**
251
- * Main polling loop
252
- */
253
- private async runLoop(): Promise<void> {
254
- const signal = this.abortController?.signal;
255
-
256
- while (this.status === "running" && !signal?.aborted) {
257
- try {
258
- await this.checkAllSchedules();
259
- } catch (error) {
260
- this.logger.error(
261
- `Error during schedule check: ${error instanceof Error ? error.message : String(error)}`
262
- );
263
- }
264
-
265
- // Sleep until next check, but allow interruption via AbortController
266
- if (this.status === "running" && !signal?.aborted) {
267
- await this.sleep(this.checkInterval, signal);
268
- }
269
- }
270
- }
271
-
272
- /**
273
- * Sleep for the specified duration, interruptible via AbortSignal
274
- */
275
- private sleep(ms: number, signal?: AbortSignal): Promise<void> {
276
- return new Promise((resolve) => {
277
- const timeout = setTimeout(resolve, ms);
278
-
279
- signal?.addEventListener(
280
- "abort",
281
- () => {
282
- clearTimeout(timeout);
283
- resolve();
284
- },
285
- { once: true }
286
- );
287
- });
288
- }
289
-
290
- /**
291
- * Check all agents' schedules and trigger due ones
292
- */
293
- private async checkAllSchedules(): Promise<void> {
294
- this.checkCount++;
295
- this.lastCheckAt = new Date().toISOString();
296
-
297
- for (const agent of this.agents) {
298
- if (!agent.schedules) {
299
- continue;
300
- }
301
-
302
- for (const [scheduleName, schedule] of Object.entries(agent.schedules)) {
303
- const result = await this.checkSchedule(agent, scheduleName, schedule);
304
-
305
- if (result.shouldTrigger) {
306
- await this.triggerSchedule(agent, scheduleName, schedule);
307
- }
308
- }
309
- }
310
- }
311
-
312
- /**
313
- * Check if a single schedule should be triggered
314
- */
315
- private async checkSchedule(
316
- agent: ResolvedAgent,
317
- scheduleName: string,
318
- schedule: { type: string; interval?: string }
319
- ): Promise<ScheduleCheckResult> {
320
- const baseResult = {
321
- agentName: agent.name,
322
- scheduleName,
323
- };
324
-
325
- // Skip non-interval schedule types (cron, webhook, chat reserved for future)
326
- if (schedule.type !== "interval") {
327
- return {
328
- ...baseResult,
329
- shouldTrigger: false,
330
- skipReason: "not_interval" as ScheduleSkipReason,
331
- };
332
- }
333
-
334
- // Get current schedule state
335
- const stateLogger: ScheduleStateLogger = { warn: this.logger.warn };
336
- const scheduleState = await getScheduleState(
337
- this.stateDir,
338
- agent.name,
339
- scheduleName,
340
- { logger: stateLogger }
341
- );
342
-
343
- // Skip disabled schedules
344
- if (scheduleState.status === "disabled") {
345
- this.logger.debug(
346
- `Skipping ${agent.name}/${scheduleName}: schedule is disabled`
347
- );
348
- return {
349
- ...baseResult,
350
- shouldTrigger: false,
351
- skipReason: "disabled" as ScheduleSkipReason,
352
- };
353
- }
354
-
355
- // Skip if already running (tracked locally)
356
- const agentRunning = this.runningSchedules.get(agent.name);
357
- if (agentRunning?.has(scheduleName)) {
358
- this.logger.debug(
359
- `Skipping ${agent.name}/${scheduleName}: already running`
360
- );
361
- return {
362
- ...baseResult,
363
- shouldTrigger: false,
364
- skipReason: "already_running" as ScheduleSkipReason,
365
- };
366
- }
367
-
368
- // Check max_concurrent capacity
369
- const maxConcurrent = agent.session?.max_turns
370
- ? 1
371
- : this.getMaxConcurrent(agent);
372
- const runningCount = agentRunning?.size ?? 0;
373
-
374
- if (runningCount >= maxConcurrent) {
375
- this.logger.debug(
376
- `Skipping ${agent.name}/${scheduleName}: at max capacity (${runningCount}/${maxConcurrent})`
377
- );
378
- return {
379
- ...baseResult,
380
- shouldTrigger: false,
381
- skipReason: "at_capacity" as ScheduleSkipReason,
382
- };
383
- }
384
-
385
- // Calculate next trigger time
386
- const lastRunAt = scheduleState.last_run_at
387
- ? new Date(scheduleState.last_run_at)
388
- : null;
389
-
390
- if (!schedule.interval) {
391
- this.logger.warn(
392
- `Skipping ${agent.name}/${scheduleName}: interval schedule missing interval value`
393
- );
394
- return {
395
- ...baseResult,
396
- shouldTrigger: false,
397
- skipReason: "not_interval" as ScheduleSkipReason,
398
- };
399
- }
400
-
401
- const nextTrigger = calculateNextTrigger(lastRunAt, schedule.interval);
402
-
403
- // Check if schedule is due
404
- if (!isScheduleDue(nextTrigger)) {
405
- return {
406
- ...baseResult,
407
- shouldTrigger: false,
408
- skipReason: "not_due" as ScheduleSkipReason,
409
- };
410
- }
411
-
412
- return {
413
- ...baseResult,
414
- shouldTrigger: true,
415
- };
416
- }
417
-
418
- /**
419
- * Get max_concurrent for an agent, defaulting to 1
420
- *
421
- * Reads from agent.instances.max_concurrent, which may come from:
422
- * - Agent-specific config
423
- * - Fleet defaults (merged during config loading)
424
- */
425
- private getMaxConcurrent(agent: ResolvedAgent): number {
426
- return agent.instances?.max_concurrent ?? 1;
427
- }
428
-
429
- /**
430
- * Get the count of currently running jobs for a specific agent
431
- *
432
- * This is useful for monitoring and debugging concurrency behavior.
433
- *
434
- * @param agentName - The name of the agent to check
435
- * @returns The number of currently running jobs for this agent
436
- */
437
- getRunningJobCount(agentName: string): number {
438
- return this.runningSchedules.get(agentName)?.size ?? 0;
439
- }
440
-
441
- /**
442
- * Get the total count of running jobs across all agents
443
- *
444
- * This is useful for monitoring overall scheduler load.
445
- *
446
- * @returns The total number of currently running jobs
447
- */
448
- getTotalRunningJobCount(): number {
449
- return this.runningJobs.size;
450
- }
451
-
452
- /**
453
- * Trigger a schedule and update state
454
- */
455
- private async triggerSchedule(
456
- agent: ResolvedAgent,
457
- scheduleName: string,
458
- schedule: { type: string; interval?: string; prompt?: string }
459
- ): Promise<void> {
460
- this.logger.info(`Triggering ${agent.name}/${scheduleName}`);
461
- this.triggerCount++;
462
-
463
- // Create a unique key for this job
464
- const jobKey = `${agent.name}/${scheduleName}`;
465
-
466
- // Mark schedule as running
467
- if (!this.runningSchedules.has(agent.name)) {
468
- this.runningSchedules.set(agent.name, new Set());
469
- }
470
- this.runningSchedules.get(agent.name)!.add(scheduleName);
471
-
472
- const stateLogger: ScheduleStateLogger = { warn: this.logger.warn };
473
-
474
- // Update schedule state to running
475
- await updateScheduleState(
476
- this.stateDir,
477
- agent.name,
478
- scheduleName,
479
- {
480
- status: "running",
481
- last_run_at: new Date().toISOString(),
482
- },
483
- { logger: stateLogger }
484
- );
485
-
486
- // Create and track the job promise
487
- const jobPromise = this.executeJob(agent, scheduleName, schedule);
488
- this.runningJobs.set(jobKey, jobPromise);
489
-
490
- try {
491
- await jobPromise;
492
- } finally {
493
- // Remove from running jobs tracking
494
- this.runningJobs.delete(jobKey);
495
- }
496
- }
497
-
498
- /**
499
- * Execute the actual job logic
500
- */
501
- private async executeJob(
502
- agent: ResolvedAgent,
503
- scheduleName: string,
504
- schedule: { type: string; interval?: string; prompt?: string }
505
- ): Promise<void> {
506
- const stateLogger: ScheduleStateLogger = { warn: this.logger.warn };
507
-
508
- try {
509
- // Get current schedule state for trigger info
510
- const scheduleState = await getScheduleState(
511
- this.stateDir,
512
- agent.name,
513
- scheduleName,
514
- { logger: stateLogger }
515
- );
516
-
517
- // Invoke the trigger callback if provided
518
- if (this.onTrigger) {
519
- const triggerInfo: TriggerInfo = {
520
- agent,
521
- scheduleName,
522
- schedule: schedule as TriggerInfo["schedule"],
523
- scheduleState,
524
- };
525
-
526
- await this.onTrigger(triggerInfo);
527
- }
528
-
529
- // Calculate next trigger time
530
- const nextTrigger = schedule.interval
531
- ? calculateNextTrigger(new Date(), schedule.interval)
532
- : null;
533
-
534
- // Update schedule state to idle with next run time
535
- await updateScheduleState(
536
- this.stateDir,
537
- agent.name,
538
- scheduleName,
539
- {
540
- status: "idle",
541
- next_run_at: nextTrigger?.toISOString() ?? null,
542
- last_error: null,
543
- },
544
- { logger: stateLogger }
545
- );
546
-
547
- this.logger.info(`Completed ${agent.name}/${scheduleName}`);
548
- } catch (error) {
549
- const errorMessage =
550
- error instanceof Error ? error.message : String(error);
551
-
552
- // Update schedule state with error
553
- await updateScheduleState(
554
- this.stateDir,
555
- agent.name,
556
- scheduleName,
557
- {
558
- status: "idle",
559
- last_error: errorMessage,
560
- },
561
- { logger: stateLogger }
562
- );
563
-
564
- this.logger.error(`Error in ${agent.name}/${scheduleName}: ${errorMessage}`);
565
- } finally {
566
- // Mark schedule as no longer running
567
- this.runningSchedules.get(agent.name)?.delete(scheduleName);
568
- }
569
- }
570
- }