@herdctl/core 0.0.1 → 0.1.0

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 (284) hide show
  1. package/dist/config/__tests__/agent.test.js +61 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +10 -3
  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 +841 -27
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +129 -10
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +14 -331
  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 +114 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/job-control.test.js +13 -14
  22. package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -1
  23. package/dist/fleet-manager/__tests__/reload.test.js +12 -2
  24. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  25. package/dist/fleet-manager/__tests__/status-queries.test.js +6 -0
  26. package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -1
  27. package/dist/fleet-manager/__tests__/trigger.test.js +10 -2
  28. package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -1
  29. package/dist/fleet-manager/config-reload.d.ts +164 -0
  30. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  31. package/dist/fleet-manager/config-reload.js +445 -0
  32. package/dist/fleet-manager/config-reload.js.map +1 -0
  33. package/dist/fleet-manager/context.d.ts +76 -0
  34. package/dist/fleet-manager/context.d.ts.map +1 -0
  35. package/dist/fleet-manager/context.js +11 -0
  36. package/dist/fleet-manager/context.js.map +1 -0
  37. package/dist/fleet-manager/errors.d.ts +0 -25
  38. package/dist/fleet-manager/errors.d.ts.map +1 -1
  39. package/dist/fleet-manager/errors.js +0 -38
  40. package/dist/fleet-manager/errors.js.map +1 -1
  41. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  42. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  43. package/dist/fleet-manager/event-emitters.js +136 -0
  44. package/dist/fleet-manager/event-emitters.js.map +1 -0
  45. package/dist/fleet-manager/event-types.d.ts +0 -15
  46. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  47. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  48. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  49. package/dist/fleet-manager/fleet-manager.js +95 -1720
  50. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  51. package/dist/fleet-manager/index.d.ts +13 -2
  52. package/dist/fleet-manager/index.d.ts.map +1 -1
  53. package/dist/fleet-manager/index.js +19 -6
  54. package/dist/fleet-manager/index.js.map +1 -1
  55. package/dist/fleet-manager/job-control.d.ts +67 -0
  56. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  57. package/dist/fleet-manager/job-control.js +333 -0
  58. package/dist/fleet-manager/job-control.js.map +1 -0
  59. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  60. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  61. package/dist/fleet-manager/log-streaming.js +503 -0
  62. package/dist/fleet-manager/log-streaming.js.map +1 -0
  63. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  64. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  65. package/dist/fleet-manager/schedule-executor.js +209 -0
  66. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  67. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  68. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  69. package/dist/fleet-manager/schedule-management.js +171 -0
  70. package/dist/fleet-manager/schedule-management.js.map +1 -0
  71. package/dist/fleet-manager/status-queries.d.ts +105 -0
  72. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  73. package/dist/fleet-manager/status-queries.js +247 -0
  74. package/dist/fleet-manager/status-queries.js.map +1 -0
  75. package/dist/fleet-manager/types.d.ts +0 -39
  76. package/dist/fleet-manager/types.d.ts.map +1 -1
  77. package/dist/runner/__tests__/job-executor.test.js +206 -1
  78. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  79. package/dist/runner/job-executor.d.ts +9 -0
  80. package/dist/runner/job-executor.d.ts.map +1 -1
  81. package/dist/runner/job-executor.js +78 -4
  82. package/dist/runner/job-executor.js.map +1 -1
  83. package/dist/runner/message-processor.d.ts.map +1 -1
  84. package/dist/runner/message-processor.js +53 -0
  85. package/dist/runner/message-processor.js.map +1 -1
  86. package/dist/runner/types.d.ts +3 -1
  87. package/dist/runner/types.d.ts.map +1 -1
  88. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  89. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  90. package/dist/scheduler/__tests__/cron.test.js +867 -0
  91. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  92. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  93. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  94. package/dist/scheduler/cron.d.ts +126 -0
  95. package/dist/scheduler/cron.d.ts.map +1 -0
  96. package/dist/scheduler/cron.js +390 -0
  97. package/dist/scheduler/cron.js.map +1 -0
  98. package/dist/scheduler/errors.d.ts +81 -1
  99. package/dist/scheduler/errors.d.ts.map +1 -1
  100. package/dist/scheduler/errors.js +81 -6
  101. package/dist/scheduler/errors.js.map +1 -1
  102. package/dist/scheduler/index.d.ts +1 -0
  103. package/dist/scheduler/index.d.ts.map +1 -1
  104. package/dist/scheduler/index.js +2 -0
  105. package/dist/scheduler/index.js.map +1 -1
  106. package/dist/scheduler/schedule-runner.d.ts +2 -2
  107. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  108. package/dist/scheduler/schedule-runner.js +20 -8
  109. package/dist/scheduler/schedule-runner.js.map +1 -1
  110. package/dist/scheduler/scheduler.d.ts +4 -4
  111. package/dist/scheduler/scheduler.d.ts.map +1 -1
  112. package/dist/scheduler/scheduler.js +95 -20
  113. package/dist/scheduler/scheduler.js.map +1 -1
  114. package/dist/scheduler/types.d.ts +1 -1
  115. package/dist/scheduler/types.d.ts.map +1 -1
  116. package/dist/state/schemas/job-metadata.d.ts +2 -2
  117. package/package.json +33 -8
  118. package/.turbo/turbo-build.log +0 -4
  119. package/.turbo/turbo-test.log +0 -219
  120. package/.turbo/turbo-typecheck.log +0 -4
  121. package/coverage/base.css +0 -224
  122. package/coverage/block-navigation.js +0 -87
  123. package/coverage/coverage-final.json +0 -51
  124. package/coverage/favicon.png +0 -0
  125. package/coverage/index.html +0 -251
  126. package/coverage/prettify.css +0 -1
  127. package/coverage/prettify.js +0 -2
  128. package/coverage/sort-arrow-sprite.png +0 -0
  129. package/coverage/sorter.js +0 -210
  130. package/coverage/src/config/index.html +0 -191
  131. package/coverage/src/config/index.ts.html +0 -442
  132. package/coverage/src/config/interpolate.ts.html +0 -652
  133. package/coverage/src/config/loader.ts.html +0 -1501
  134. package/coverage/src/config/merge.ts.html +0 -823
  135. package/coverage/src/config/parser.ts.html +0 -1213
  136. package/coverage/src/config/schema.ts.html +0 -1123
  137. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  138. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  139. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  140. package/coverage/src/fleet-manager/index.html +0 -206
  141. package/coverage/src/fleet-manager/index.ts.html +0 -469
  142. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  143. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  144. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  145. package/coverage/src/index.html +0 -116
  146. package/coverage/src/index.ts.html +0 -181
  147. package/coverage/src/runner/errors.ts.html +0 -1006
  148. package/coverage/src/runner/index.html +0 -191
  149. package/coverage/src/runner/index.ts.html +0 -256
  150. package/coverage/src/runner/job-executor.ts.html +0 -1429
  151. package/coverage/src/runner/message-processor.ts.html +0 -1150
  152. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  153. package/coverage/src/runner/types.ts.html +0 -559
  154. package/coverage/src/scheduler/errors.ts.html +0 -388
  155. package/coverage/src/scheduler/index.html +0 -206
  156. package/coverage/src/scheduler/index.ts.html +0 -244
  157. package/coverage/src/scheduler/interval.ts.html +0 -652
  158. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  159. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  160. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  161. package/coverage/src/scheduler/types.ts.html +0 -733
  162. package/coverage/src/state/directory.ts.html +0 -736
  163. package/coverage/src/state/errors.ts.html +0 -376
  164. package/coverage/src/state/fleet-state.ts.html +0 -937
  165. package/coverage/src/state/index.html +0 -221
  166. package/coverage/src/state/index.ts.html +0 -322
  167. package/coverage/src/state/job-metadata.ts.html +0 -1420
  168. package/coverage/src/state/job-output.ts.html +0 -1033
  169. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  170. package/coverage/src/state/schemas/index.html +0 -176
  171. package/coverage/src/state/schemas/index.ts.html +0 -286
  172. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  173. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  174. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  175. package/coverage/src/state/session.ts.html +0 -844
  176. package/coverage/src/state/types.ts.html +0 -262
  177. package/coverage/src/state/utils/atomic.ts.html +0 -748
  178. package/coverage/src/state/utils/index.html +0 -146
  179. package/coverage/src/state/utils/index.ts.html +0 -103
  180. package/coverage/src/state/utils/reads.ts.html +0 -1621
  181. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  182. package/coverage/src/work-sources/adapters/index.html +0 -131
  183. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  184. package/coverage/src/work-sources/errors.ts.html +0 -298
  185. package/coverage/src/work-sources/index.html +0 -176
  186. package/coverage/src/work-sources/index.ts.html +0 -529
  187. package/coverage/src/work-sources/manager.ts.html +0 -1324
  188. package/coverage/src/work-sources/registry.ts.html +0 -619
  189. package/coverage/src/work-sources/types.ts.html +0 -568
  190. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  191. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  192. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  193. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  194. package/src/config/__tests__/agent.test.ts +0 -864
  195. package/src/config/__tests__/interpolate.test.ts +0 -644
  196. package/src/config/__tests__/loader.test.ts +0 -784
  197. package/src/config/__tests__/merge.test.ts +0 -751
  198. package/src/config/__tests__/parser.test.ts +0 -533
  199. package/src/config/__tests__/schema.test.ts +0 -873
  200. package/src/config/index.ts +0 -119
  201. package/src/config/interpolate.ts +0 -189
  202. package/src/config/loader.ts +0 -472
  203. package/src/config/merge.ts +0 -246
  204. package/src/config/parser.ts +0 -376
  205. package/src/config/schema.ts +0 -346
  206. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  207. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  208. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  209. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  210. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  211. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  212. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  213. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  214. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  215. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  216. package/src/fleet-manager/errors.ts +0 -747
  217. package/src/fleet-manager/event-types.ts +0 -378
  218. package/src/fleet-manager/fleet-manager.ts +0 -2315
  219. package/src/fleet-manager/index.ts +0 -128
  220. package/src/fleet-manager/job-manager.ts +0 -663
  221. package/src/fleet-manager/job-queue.ts +0 -798
  222. package/src/fleet-manager/types.ts +0 -839
  223. package/src/index.ts +0 -32
  224. package/src/runner/__tests__/errors.test.ts +0 -382
  225. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  226. package/src/runner/__tests__/message-processor.test.ts +0 -960
  227. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  228. package/src/runner/errors.ts +0 -307
  229. package/src/runner/index.ts +0 -57
  230. package/src/runner/job-executor.ts +0 -448
  231. package/src/runner/message-processor.ts +0 -355
  232. package/src/runner/sdk-adapter.ts +0 -191
  233. package/src/runner/types.ts +0 -158
  234. package/src/scheduler/__tests__/errors.test.ts +0 -159
  235. package/src/scheduler/__tests__/interval.test.ts +0 -515
  236. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  237. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  238. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  239. package/src/scheduler/errors.ts +0 -101
  240. package/src/scheduler/index.ts +0 -53
  241. package/src/scheduler/interval.ts +0 -189
  242. package/src/scheduler/schedule-runner.ts +0 -442
  243. package/src/scheduler/schedule-state.ts +0 -211
  244. package/src/scheduler/scheduler.ts +0 -570
  245. package/src/scheduler/types.ts +0 -216
  246. package/src/state/__tests__/directory.test.ts +0 -595
  247. package/src/state/__tests__/fleet-state.test.ts +0 -868
  248. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  249. package/src/state/__tests__/job-metadata.test.ts +0 -831
  250. package/src/state/__tests__/job-output.test.ts +0 -856
  251. package/src/state/__tests__/session-schema.test.ts +0 -378
  252. package/src/state/__tests__/session.test.ts +0 -604
  253. package/src/state/directory.ts +0 -217
  254. package/src/state/errors.ts +0 -97
  255. package/src/state/fleet-state.ts +0 -284
  256. package/src/state/index.ts +0 -79
  257. package/src/state/job-metadata.ts +0 -445
  258. package/src/state/job-output.ts +0 -316
  259. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  260. package/src/state/schemas/fleet-state.ts +0 -120
  261. package/src/state/schemas/index.ts +0 -67
  262. package/src/state/schemas/job-metadata.ts +0 -181
  263. package/src/state/schemas/job-output.ts +0 -177
  264. package/src/state/schemas/session-info.ts +0 -92
  265. package/src/state/session.ts +0 -253
  266. package/src/state/types.ts +0 -59
  267. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  268. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  269. package/src/state/utils/atomic.ts +0 -221
  270. package/src/state/utils/index.ts +0 -6
  271. package/src/state/utils/reads.ts +0 -512
  272. package/src/work-sources/__tests__/github.test.ts +0 -1800
  273. package/src/work-sources/__tests__/manager.test.ts +0 -529
  274. package/src/work-sources/__tests__/registry.test.ts +0 -477
  275. package/src/work-sources/__tests__/types.test.ts +0 -479
  276. package/src/work-sources/adapters/github.ts +0 -1166
  277. package/src/work-sources/adapters/index.ts +0 -64
  278. package/src/work-sources/errors.ts +0 -71
  279. package/src/work-sources/index.ts +0 -148
  280. package/src/work-sources/manager.ts +0 -413
  281. package/src/work-sources/registry.ts +0 -178
  282. package/src/work-sources/types.ts +0 -161
  283. package/tsconfig.json +0 -9
  284. 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
- }