@flowdot.ai/daemon 1.0.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 (85) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +51 -0
  3. package/dist/goals/DependencyResolver.d.ts +54 -0
  4. package/dist/goals/DependencyResolver.js +329 -0
  5. package/dist/goals/ErrorRecovery.d.ts +133 -0
  6. package/dist/goals/ErrorRecovery.js +489 -0
  7. package/dist/goals/GoalApiClient.d.ts +81 -0
  8. package/dist/goals/GoalApiClient.js +743 -0
  9. package/dist/goals/GoalCache.d.ts +65 -0
  10. package/dist/goals/GoalCache.js +243 -0
  11. package/dist/goals/GoalCommsHandler.d.ts +150 -0
  12. package/dist/goals/GoalCommsHandler.js +378 -0
  13. package/dist/goals/GoalExporter.d.ts +164 -0
  14. package/dist/goals/GoalExporter.js +318 -0
  15. package/dist/goals/GoalImporter.d.ts +107 -0
  16. package/dist/goals/GoalImporter.js +345 -0
  17. package/dist/goals/GoalManager.d.ts +110 -0
  18. package/dist/goals/GoalManager.js +535 -0
  19. package/dist/goals/GoalReporter.d.ts +105 -0
  20. package/dist/goals/GoalReporter.js +534 -0
  21. package/dist/goals/GoalScheduler.d.ts +102 -0
  22. package/dist/goals/GoalScheduler.js +209 -0
  23. package/dist/goals/GoalValidator.d.ts +72 -0
  24. package/dist/goals/GoalValidator.js +657 -0
  25. package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
  26. package/dist/goals/MetaGoalEnforcer.js +536 -0
  27. package/dist/goals/MilestoneBreaker.d.ts +74 -0
  28. package/dist/goals/MilestoneBreaker.js +348 -0
  29. package/dist/goals/PermissionBridge.d.ts +109 -0
  30. package/dist/goals/PermissionBridge.js +326 -0
  31. package/dist/goals/ProgressTracker.d.ts +113 -0
  32. package/dist/goals/ProgressTracker.js +324 -0
  33. package/dist/goals/ReviewScheduler.d.ts +106 -0
  34. package/dist/goals/ReviewScheduler.js +360 -0
  35. package/dist/goals/TaskExecutor.d.ts +116 -0
  36. package/dist/goals/TaskExecutor.js +370 -0
  37. package/dist/goals/TaskFeedback.d.ts +126 -0
  38. package/dist/goals/TaskFeedback.js +402 -0
  39. package/dist/goals/TaskGenerator.d.ts +75 -0
  40. package/dist/goals/TaskGenerator.js +329 -0
  41. package/dist/goals/TaskQueue.d.ts +84 -0
  42. package/dist/goals/TaskQueue.js +331 -0
  43. package/dist/goals/TaskSanitizer.d.ts +61 -0
  44. package/dist/goals/TaskSanitizer.js +464 -0
  45. package/dist/goals/errors.d.ts +116 -0
  46. package/dist/goals/errors.js +299 -0
  47. package/dist/goals/index.d.ts +24 -0
  48. package/dist/goals/index.js +23 -0
  49. package/dist/goals/types.d.ts +395 -0
  50. package/dist/goals/types.js +230 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.js +3 -0
  53. package/dist/loop/DaemonIPC.d.ts +67 -0
  54. package/dist/loop/DaemonIPC.js +358 -0
  55. package/dist/loop/IntervalParser.d.ts +39 -0
  56. package/dist/loop/IntervalParser.js +217 -0
  57. package/dist/loop/LoopDaemon.d.ts +123 -0
  58. package/dist/loop/LoopDaemon.js +1821 -0
  59. package/dist/loop/LoopExecutor.d.ts +93 -0
  60. package/dist/loop/LoopExecutor.js +326 -0
  61. package/dist/loop/LoopManager.d.ts +79 -0
  62. package/dist/loop/LoopManager.js +476 -0
  63. package/dist/loop/LoopScheduler.d.ts +69 -0
  64. package/dist/loop/LoopScheduler.js +329 -0
  65. package/dist/loop/LoopStore.d.ts +57 -0
  66. package/dist/loop/LoopStore.js +406 -0
  67. package/dist/loop/LoopValidator.d.ts +55 -0
  68. package/dist/loop/LoopValidator.js +603 -0
  69. package/dist/loop/errors.d.ts +115 -0
  70. package/dist/loop/errors.js +312 -0
  71. package/dist/loop/index.d.ts +11 -0
  72. package/dist/loop/index.js +10 -0
  73. package/dist/loop/notifications/Notifier.d.ts +28 -0
  74. package/dist/loop/notifications/Notifier.js +78 -0
  75. package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
  76. package/dist/loop/notifications/SlackNotifier.js +203 -0
  77. package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
  78. package/dist/loop/notifications/TerminalNotifier.js +72 -0
  79. package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
  80. package/dist/loop/notifications/WebhookNotifier.js +123 -0
  81. package/dist/loop/notifications/index.d.ts +24 -0
  82. package/dist/loop/notifications/index.js +109 -0
  83. package/dist/loop/types.d.ts +280 -0
  84. package/dist/loop/types.js +222 -0
  85. package/package.json +92 -0
@@ -0,0 +1,329 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import cronParser from 'cron-parser';
3
+ import { LoopNotFoundError, LoopAlreadyExistsError, SchedulerError, InvalidLoopStateError, } from './errors.js';
4
+ const MAX_TIMER_DELAY = 2147483647;
5
+ const DEFAULT_DRIFT_THRESHOLD = 1000;
6
+ const noopLogger = {
7
+ debug: () => { },
8
+ info: () => { },
9
+ warn: () => { },
10
+ error: () => { },
11
+ };
12
+ export class LoopScheduler extends EventEmitter {
13
+ scheduledLoops = new Map();
14
+ timezone;
15
+ maxTimerDelay;
16
+ driftThreshold;
17
+ logger;
18
+ started = false;
19
+ startedAt = null;
20
+ executionCount = 0;
21
+ expiredCount = 0;
22
+ lastExecutionAt = null;
23
+ constructor(options = {}) {
24
+ super();
25
+ this.timezone = options.timezone;
26
+ this.maxTimerDelay = options.maxTimerDelay ?? MAX_TIMER_DELAY;
27
+ this.driftThreshold = options.driftThreshold ?? DEFAULT_DRIFT_THRESHOLD;
28
+ this.logger = options.logger ?? noopLogger;
29
+ }
30
+ start() {
31
+ if (this.started) {
32
+ return;
33
+ }
34
+ this.started = true;
35
+ this.startedAt = new Date();
36
+ for (const scheduled of this.scheduledLoops.values()) {
37
+ if (!scheduled.paused && scheduled.loop.status === 'running') {
38
+ this.scheduleNextRun(scheduled);
39
+ }
40
+ }
41
+ }
42
+ stop() {
43
+ if (!this.started) {
44
+ return;
45
+ }
46
+ for (const scheduled of this.scheduledLoops.values()) {
47
+ if (scheduled.timer !== null) {
48
+ clearTimeout(scheduled.timer);
49
+ scheduled.timer = null;
50
+ }
51
+ }
52
+ this.started = false;
53
+ }
54
+ isRunning() {
55
+ return this.started;
56
+ }
57
+ schedule(loop) {
58
+ if (this.scheduledLoops.has(loop.id)) {
59
+ throw new LoopAlreadyExistsError(loop.id);
60
+ }
61
+ if (loop.expiresAt && loop.expiresAt <= new Date()) {
62
+ this.expiredCount++;
63
+ this.emit('expired', loop.id);
64
+ return;
65
+ }
66
+ const nextRunAt = this.calculateNextRunTime(loop.interval);
67
+ const scheduled = {
68
+ loopId: loop.id,
69
+ loop,
70
+ timer: null,
71
+ nextRunAt,
72
+ paused: loop.status === 'paused',
73
+ };
74
+ this.scheduledLoops.set(loop.id, scheduled);
75
+ if (loop.status === 'running' && this.started) {
76
+ this.scheduleNextRun(scheduled);
77
+ }
78
+ this.emit('scheduled', loop.id, nextRunAt);
79
+ }
80
+ unschedule(loopId) {
81
+ const scheduled = this.scheduledLoops.get(loopId);
82
+ if (!scheduled) {
83
+ throw new LoopNotFoundError(loopId);
84
+ }
85
+ if (scheduled.timer !== null) {
86
+ clearTimeout(scheduled.timer);
87
+ }
88
+ this.scheduledLoops.delete(loopId);
89
+ this.emit('unscheduled', loopId);
90
+ }
91
+ update(loop) {
92
+ const scheduled = this.scheduledLoops.get(loop.id);
93
+ if (!scheduled) {
94
+ throw new LoopNotFoundError(loop.id);
95
+ }
96
+ if (scheduled.timer !== null) {
97
+ clearTimeout(scheduled.timer);
98
+ scheduled.timer = null;
99
+ }
100
+ scheduled.loop = loop;
101
+ if (loop.status === 'paused') {
102
+ scheduled.paused = true;
103
+ }
104
+ else if (loop.status === 'running') {
105
+ scheduled.paused = false;
106
+ scheduled.nextRunAt = this.calculateNextRunTime(loop.interval);
107
+ if (this.started) {
108
+ this.scheduleNextRun(scheduled);
109
+ }
110
+ }
111
+ else if (loop.status === 'stopped' || loop.status === 'expired') {
112
+ scheduled.paused = true;
113
+ }
114
+ }
115
+ pause(loopId) {
116
+ const scheduled = this.scheduledLoops.get(loopId);
117
+ if (!scheduled) {
118
+ throw new LoopNotFoundError(loopId);
119
+ }
120
+ if (scheduled.paused) {
121
+ throw new InvalidLoopStateError(loopId, scheduled.loop.status, 'running');
122
+ }
123
+ if (scheduled.timer !== null) {
124
+ clearTimeout(scheduled.timer);
125
+ scheduled.timer = null;
126
+ }
127
+ scheduled.paused = true;
128
+ scheduled.loop = { ...scheduled.loop, status: 'paused' };
129
+ }
130
+ resume(loopId) {
131
+ const scheduled = this.scheduledLoops.get(loopId);
132
+ if (!scheduled) {
133
+ throw new LoopNotFoundError(loopId);
134
+ }
135
+ if (!scheduled.paused) {
136
+ throw new InvalidLoopStateError(loopId, scheduled.loop.status, 'paused');
137
+ }
138
+ if (scheduled.loop.expiresAt && scheduled.loop.expiresAt <= new Date()) {
139
+ this.expiredCount++;
140
+ this.emit('expired', loopId);
141
+ return;
142
+ }
143
+ scheduled.paused = false;
144
+ scheduled.loop = { ...scheduled.loop, status: 'running' };
145
+ scheduled.nextRunAt = this.calculateNextRunTime(scheduled.loop.interval);
146
+ if (this.started) {
147
+ this.scheduleNextRun(scheduled);
148
+ }
149
+ this.emit('scheduled', loopId, scheduled.nextRunAt);
150
+ }
151
+ triggerNow(loopId) {
152
+ const scheduled = this.scheduledLoops.get(loopId);
153
+ if (!scheduled) {
154
+ throw new LoopNotFoundError(loopId);
155
+ }
156
+ if (scheduled.timer !== null) {
157
+ clearTimeout(scheduled.timer);
158
+ scheduled.timer = null;
159
+ }
160
+ this.executeLoop(scheduled);
161
+ }
162
+ isScheduled(loopId) {
163
+ return this.scheduledLoops.has(loopId);
164
+ }
165
+ getScheduled(loopId) {
166
+ const scheduled = this.scheduledLoops.get(loopId);
167
+ return scheduled?.loop ?? null;
168
+ }
169
+ getNextRunTime(loopId) {
170
+ const scheduled = this.scheduledLoops.get(loopId);
171
+ if (!scheduled || scheduled.paused) {
172
+ return null;
173
+ }
174
+ return scheduled.nextRunAt;
175
+ }
176
+ getScheduledLoopIds() {
177
+ return Array.from(this.scheduledLoops.keys());
178
+ }
179
+ getActiveLoopIds() {
180
+ return Array.from(this.scheduledLoops.entries())
181
+ .filter(([, scheduled]) => !scheduled.paused)
182
+ .map(([id]) => id);
183
+ }
184
+ getStats() {
185
+ let pausedCount = 0;
186
+ for (const scheduled of this.scheduledLoops.values()) {
187
+ if (scheduled.paused) {
188
+ pausedCount++;
189
+ }
190
+ }
191
+ return {
192
+ scheduledCount: this.scheduledLoops.size,
193
+ pausedCount,
194
+ executionCount: this.executionCount,
195
+ expiredCount: this.expiredCount,
196
+ startedAt: this.startedAt,
197
+ lastExecutionAt: this.lastExecutionAt,
198
+ };
199
+ }
200
+ scheduleNextRun(scheduled) {
201
+ if (scheduled.paused || !this.started) {
202
+ return;
203
+ }
204
+ if (scheduled.loop.expiresAt && scheduled.loop.expiresAt <= new Date()) {
205
+ this.expiredCount++;
206
+ this.emit('expired', scheduled.loopId);
207
+ return;
208
+ }
209
+ const now = new Date();
210
+ let delay = scheduled.nextRunAt.getTime() - now.getTime();
211
+ if (delay <= 0) {
212
+ delay = 1;
213
+ }
214
+ if (delay > this.maxTimerDelay) {
215
+ scheduled.timer = setTimeout(() => {
216
+ this.scheduleNextRun(scheduled);
217
+ }, this.maxTimerDelay);
218
+ return;
219
+ }
220
+ scheduled.timer = setTimeout(() => {
221
+ this.executeLoop(scheduled);
222
+ }, delay);
223
+ }
224
+ executeLoop(scheduled) {
225
+ scheduled.timer = null;
226
+ if (scheduled.paused || !this.started) {
227
+ return;
228
+ }
229
+ if (scheduled.loop.expiresAt && scheduled.loop.expiresAt <= new Date()) {
230
+ this.expiredCount++;
231
+ this.emit('expired', scheduled.loopId);
232
+ return;
233
+ }
234
+ if (scheduled.loop.options.maxRuns !== null &&
235
+ scheduled.loop.stats.totalRuns >= scheduled.loop.options.maxRuns) {
236
+ }
237
+ this.executionCount++;
238
+ this.lastExecutionAt = new Date();
239
+ try {
240
+ this.emit('execute', scheduled.loop);
241
+ }
242
+ catch (error) {
243
+ this.emit('error', error, scheduled.loopId);
244
+ }
245
+ scheduled.nextRunAt = this.calculateNextRunTime(scheduled.loop.interval);
246
+ this.applyDriftCorrection(scheduled);
247
+ this.scheduleNextRun(scheduled);
248
+ }
249
+ calculateNextRunTime(interval) {
250
+ const now = new Date();
251
+ if (interval.type === 'cron' && interval.cronExpression) {
252
+ try {
253
+ const options = {
254
+ currentDate: now,
255
+ };
256
+ if (this.timezone) {
257
+ options.tz = this.timezone;
258
+ }
259
+ const parsed = cronParser.parseExpression(interval.cronExpression, options);
260
+ return parsed.next().toDate();
261
+ }
262
+ catch (error) {
263
+ this.emit('error', new SchedulerError(`Failed to parse cron expression: ${interval.cronExpression}`, { interval: interval.raw, error: String(error) }));
264
+ return new Date(now.getTime() + interval.milliseconds);
265
+ }
266
+ }
267
+ return new Date(now.getTime() + interval.milliseconds);
268
+ }
269
+ applyDriftCorrection(scheduled) {
270
+ const now = new Date();
271
+ const expected = scheduled.nextRunAt;
272
+ if (expected.getTime() > now.getTime()) {
273
+ return;
274
+ }
275
+ const drift = Math.abs(now.getTime() - expected.getTime());
276
+ if (drift > this.driftThreshold) {
277
+ if (scheduled.loop.interval.type === 'duration') {
278
+ const intervalMs = scheduled.loop.interval.milliseconds;
279
+ const missedIntervals = Math.floor(drift / intervalMs);
280
+ if (missedIntervals > 0) {
281
+ scheduled.nextRunAt = new Date(expected.getTime() + (missedIntervals + 1) * intervalMs);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ updateNextRunTime(loopId, nextRunAt) {
287
+ const scheduled = this.scheduledLoops.get(loopId);
288
+ if (!scheduled) {
289
+ return;
290
+ }
291
+ scheduled.nextRunAt = nextRunAt;
292
+ scheduled.loop = {
293
+ ...scheduled.loop,
294
+ interval: {
295
+ ...scheduled.loop.interval,
296
+ nextRunAt,
297
+ },
298
+ };
299
+ if (this.started && !scheduled.paused) {
300
+ if (scheduled.timer !== null) {
301
+ clearTimeout(scheduled.timer);
302
+ scheduled.timer = null;
303
+ }
304
+ this.scheduleNextRun(scheduled);
305
+ }
306
+ }
307
+ clear() {
308
+ for (const scheduled of this.scheduledLoops.values()) {
309
+ if (scheduled.timer !== null) {
310
+ clearTimeout(scheduled.timer);
311
+ }
312
+ }
313
+ this.scheduledLoops.clear();
314
+ this.executionCount = 0;
315
+ this.expiredCount = 0;
316
+ this.lastExecutionAt = null;
317
+ }
318
+ get size() {
319
+ return this.scheduledLoops.size;
320
+ }
321
+ }
322
+ export function createLoopScheduler(options) {
323
+ return new LoopScheduler(options);
324
+ }
325
+ export function createAndStartScheduler(options) {
326
+ const scheduler = new LoopScheduler(options);
327
+ scheduler.start();
328
+ return scheduler;
329
+ }
@@ -0,0 +1,57 @@
1
+ import type { Loop, LoopId, LoopRun, LoopRunId, LoopRegistry, LoopRegistryEntry, LoopStatus, Logger } from './types.js';
2
+ export interface LoopStoreOptions {
3
+ baseDir?: string;
4
+ flowdotDir?: string;
5
+ maxHistoryEntries?: number;
6
+ logger?: Logger;
7
+ }
8
+ export declare class LoopStore {
9
+ private readonly baseDir;
10
+ private readonly flowdotDir;
11
+ private readonly maxHistoryEntries;
12
+ private readonly loopsPath;
13
+ private readonly registryPath;
14
+ private readonly logger;
15
+ constructor(options?: LoopStoreOptions);
16
+ initialize(): Promise<void>;
17
+ isInitialized(): Promise<boolean>;
18
+ createLoop(loop: Loop): Promise<void>;
19
+ getLoop(loopId: LoopId): Promise<Loop>;
20
+ getLoopByName(name: string): Promise<Loop | null>;
21
+ updateLoop(loop: Loop): Promise<void>;
22
+ deleteLoop(loopId: LoopId): Promise<void>;
23
+ loopExists(loopId: LoopId): Promise<boolean>;
24
+ isNameTaken(name: string, excludeLoopId?: LoopId): Promise<boolean>;
25
+ getAllLoops(): Promise<Loop[]>;
26
+ getLoopsByStatus(status: LoopStatus): Promise<Loop[]>;
27
+ getActiveLoopCount(): Promise<number>;
28
+ createRun(run: LoopRun): Promise<void>;
29
+ updateRun(run: LoopRun): Promise<void>;
30
+ getRun(loopId: LoopId, runId: LoopRunId): Promise<LoopRun>;
31
+ getRunHistory(loopId: LoopId, limit?: number): Promise<LoopRun[]>;
32
+ cleanupRunHistory(loopId: LoopId): Promise<number>;
33
+ readRegistry(): Promise<LoopRegistry>;
34
+ getRegistryEntries(): Promise<LoopRegistryEntry[]>;
35
+ private addToRegistry;
36
+ private updateRegistryEntry;
37
+ private removeFromRegistry;
38
+ private writeRegistry;
39
+ private ensureRegistry;
40
+ private createEmptyRegistry;
41
+ private migrateRegistry;
42
+ writeDaemonPid(pid: number): Promise<void>;
43
+ readDaemonPid(): Promise<{
44
+ pid: number;
45
+ startedAt: Date;
46
+ } | null>;
47
+ deleteDaemonPid(): Promise<void>;
48
+ private getLoopDirectory;
49
+ private getRunPath;
50
+ private ensureDirectory;
51
+ private readJsonFile;
52
+ private writeJsonFile;
53
+ private isNotFoundError;
54
+ getStoragePath(): string;
55
+ }
56
+ export declare function createLoopStore(options?: LoopStoreOptions): LoopStore;
57
+ export declare function initializeLoopStore(options?: LoopStoreOptions): Promise<LoopStore>;