@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,1821 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { EventEmitter } from 'node:events';
5
+ import { DaemonNotRunningError, DaemonAlreadyRunningError, DaemonStartFailedError, DaemonStopFailedError, } from './errors.js';
6
+ import { LoopManager } from './LoopManager.js';
7
+ import { LoopStore } from './LoopStore.js';
8
+ import { GoalManager, TaskGenerator, TaskExecutor, PermissionBridge, GoalCommsHandler, ReviewScheduler, ProgressTracker, ErrorRecovery, GoalScheduler, } from '../goals/index.js';
9
+ import { IPCServer, IPCClient, isDaemonRunning } from './DaemonIPC.js';
10
+ const DAEMON_STARTUP_TIMEOUT_MS = 10000;
11
+ const DAEMON_SHUTDOWN_TIMEOUT_MS = 5000;
12
+ const PROCESS_CHECK_INTERVAL_MS = 100;
13
+ const noopLogger = {
14
+ debug: () => { },
15
+ info: () => { },
16
+ warn: () => { },
17
+ error: () => { },
18
+ };
19
+ export class LoopDaemon extends EventEmitter {
20
+ baseDir;
21
+ flowdotDir;
22
+ config;
23
+ store;
24
+ managerOptions;
25
+ goalConfig;
26
+ logger;
27
+ manager = null;
28
+ goalManager = null;
29
+ taskGenerator = null;
30
+ taskExecutor = null;
31
+ permissionBridge = null;
32
+ goalCommsHandler = null;
33
+ reviewScheduler = null;
34
+ progressTracker = null;
35
+ errorRecovery = null;
36
+ goalScheduler = null;
37
+ ipcServer = null;
38
+ logStream = null;
39
+ startedAt = null;
40
+ isShuttingDown = false;
41
+ constructor(options = {}) {
42
+ super();
43
+ this.baseDir = options.baseDir ?? process.cwd();
44
+ this.flowdotDir = path.join(this.baseDir, '.flowdot');
45
+ this.logger = options.logger ?? noopLogger;
46
+ const defaultConfig = {
47
+ port: 47691,
48
+ logLevel: 'info',
49
+ healthCheckInterval: 30000,
50
+ };
51
+ this.config = { ...defaultConfig, ...options.daemonConfig };
52
+ this.store = new LoopStore({ baseDir: this.baseDir, logger: this.logger });
53
+ this.managerOptions = options.managerOptions ?? {};
54
+ this.goalConfig = options.goalConfig;
55
+ }
56
+ async start() {
57
+ if (this.ipcServer) {
58
+ throw new DaemonAlreadyRunningError(process.pid);
59
+ }
60
+ this.logger.info('LOOP', 'Starting loop daemon...');
61
+ try {
62
+ await this.ensureDirectories();
63
+ await this.setupLogging();
64
+ this.logMessage('info', 'Daemon starting...');
65
+ await this.store.initialize();
66
+ await this.store.writeDaemonPid(process.pid);
67
+ this.manager = new LoopManager({
68
+ baseDir: this.baseDir,
69
+ logger: this.logger,
70
+ ...this.managerOptions,
71
+ });
72
+ await this.manager.initialize();
73
+ this.setupManagerEvents();
74
+ if (this.goalConfig) {
75
+ this.logMessage('info', 'Initializing GoalManager...');
76
+ this.goalManager = new GoalManager({
77
+ baseUrl: this.goalConfig.baseUrl,
78
+ tokenProvider: this.goalConfig.tokenProvider,
79
+ logger: this.logger,
80
+ ...this.goalConfig.managerOptions,
81
+ });
82
+ await this.goalManager.initialize();
83
+ this.setupGoalManagerEvents();
84
+ this.logMessage('info', 'GoalManager initialized');
85
+ if (this.goalConfig.llmFunction) {
86
+ this.logMessage('info', 'Initializing TaskGenerator...');
87
+ this.taskGenerator = new TaskGenerator({
88
+ llmFunction: this.goalConfig.llmFunction,
89
+ logger: this.logger,
90
+ ...this.goalConfig.taskGeneratorOptions,
91
+ });
92
+ this.logMessage('info', 'TaskGenerator initialized');
93
+ }
94
+ if (this.goalConfig.taskHandlers || this.goalConfig.approvalRequestFunction) {
95
+ this.logMessage('info', 'Initializing PermissionBridge...');
96
+ this.permissionBridge = new PermissionBridge({
97
+ logger: this.logger,
98
+ ...this.goalConfig.permissionBridgeOptions,
99
+ });
100
+ this.logMessage('info', 'PermissionBridge initialized');
101
+ this.logMessage('info', 'Initializing TaskExecutor...');
102
+ this.taskExecutor = new TaskExecutor({
103
+ logger: this.logger,
104
+ handlers: this.goalConfig.taskHandlers,
105
+ requestApproval: this.goalConfig.approvalRequestFunction,
106
+ checkPermission: (category, context) => this.permissionBridge.checkPermission(category, context),
107
+ updateTaskStatus: async (taskId, status) => {
108
+ this.logMessage('debug', `Task ${taskId} status update to ${status}`);
109
+ },
110
+ ...this.goalConfig.taskExecutorOptions,
111
+ });
112
+ this.setupTaskExecutorEvents();
113
+ this.logMessage('info', 'TaskExecutor initialized');
114
+ }
115
+ if (this.goalConfig.commsSendFunction) {
116
+ this.logMessage('info', 'Initializing GoalCommsHandler...');
117
+ this.goalCommsHandler = new GoalCommsHandler({
118
+ logger: this.logger,
119
+ sendFunction: this.goalConfig.commsSendFunction,
120
+ ...this.goalConfig.commsHandlerOptions,
121
+ });
122
+ this.setupCommsHandlerEvents();
123
+ this.logMessage('info', 'GoalCommsHandler initialized');
124
+ }
125
+ if (this.goalConfig.enableReviewScheduler) {
126
+ this.logMessage('info', 'Initializing ReviewScheduler...');
127
+ this.reviewScheduler = new ReviewScheduler({
128
+ logger: this.logger,
129
+ ...this.goalConfig.reviewSchedulerOptions,
130
+ });
131
+ this.setupReviewSchedulerEvents();
132
+ this.reviewScheduler.start();
133
+ this.logMessage('info', 'ReviewScheduler initialized and started');
134
+ }
135
+ if (this.goalConfig.enableProgressTracker) {
136
+ this.logMessage('info', 'Initializing ProgressTracker...');
137
+ this.progressTracker = new ProgressTracker({
138
+ logger: this.logger,
139
+ ...this.goalConfig.progressTrackerOptions,
140
+ });
141
+ this.setupProgressTrackerEvents();
142
+ this.logMessage('info', 'ProgressTracker initialized');
143
+ }
144
+ if (this.goalConfig.enableErrorRecovery) {
145
+ this.logMessage('info', 'Initializing ErrorRecovery...');
146
+ this.errorRecovery = new ErrorRecovery({
147
+ logger: this.logger,
148
+ ...this.goalConfig.errorRecoveryOptions,
149
+ });
150
+ this.setupErrorRecoveryEvents();
151
+ this.logMessage('info', 'ErrorRecovery initialized');
152
+ }
153
+ if (this.goalConfig.enableGoalScheduler) {
154
+ this.logMessage('info', 'Initializing GoalScheduler...');
155
+ this.goalScheduler = new GoalScheduler({
156
+ logger: this.logger,
157
+ getDueGoals: this.createGetDueGoalsFunction(),
158
+ markGoalStarted: this.createMarkGoalStartedFunction(),
159
+ markGoalCompleted: this.createMarkGoalCompletedFunction(),
160
+ executeGoal: this.goalConfig.executeGoalFunction ?? this.createDefaultExecuteGoalFunction(),
161
+ ...this.goalConfig.goalSchedulerOptions,
162
+ });
163
+ this.setupGoalSchedulerEvents();
164
+ this.goalScheduler.start();
165
+ this.logMessage('info', 'GoalScheduler initialized and started');
166
+ }
167
+ }
168
+ this.ipcServer = new IPCServer({
169
+ port: this.config.port,
170
+ onRequest: this.handleRequest.bind(this),
171
+ logger: this.logger,
172
+ });
173
+ await this.ipcServer.start();
174
+ this.manager.start();
175
+ this.startedAt = new Date();
176
+ this.setupSignalHandlers();
177
+ this.logMessage('info', `Daemon started on port ${this.config.port} (PID: ${process.pid})`);
178
+ this.logger.info('LOOP', `Daemon started on port ${this.config.port}`);
179
+ this.emit('started');
180
+ }
181
+ catch (error) {
182
+ this.logMessage('error', `Failed to start daemon: ${error instanceof Error ? error.message : String(error)}`);
183
+ await this.cleanup();
184
+ throw new DaemonStartFailedError(error instanceof Error ? error.message : String(error), error instanceof Error ? error : undefined);
185
+ }
186
+ }
187
+ async stop() {
188
+ if (this.isShuttingDown) {
189
+ return;
190
+ }
191
+ this.isShuttingDown = true;
192
+ this.emit('stopping');
193
+ this.logMessage('info', 'Daemon stopping...');
194
+ this.logger.info('LOOP', 'Daemon stopping...');
195
+ const shutdownTimeout = setTimeout(() => {
196
+ this.logMessage('warn', 'Forced shutdown due to timeout');
197
+ process.exit(1);
198
+ }, DAEMON_SHUTDOWN_TIMEOUT_MS);
199
+ try {
200
+ if (this.manager) {
201
+ await this.manager.shutdown();
202
+ this.manager = null;
203
+ }
204
+ if (this.goalManager) {
205
+ this.goalManager.clearCache();
206
+ this.goalManager = null;
207
+ }
208
+ this.taskGenerator = null;
209
+ this.taskExecutor = null;
210
+ this.permissionBridge = null;
211
+ this.goalCommsHandler = null;
212
+ if (this.reviewScheduler) {
213
+ this.reviewScheduler.stop();
214
+ this.reviewScheduler = null;
215
+ }
216
+ this.progressTracker = null;
217
+ this.errorRecovery = null;
218
+ if (this.goalScheduler) {
219
+ this.goalScheduler.stop();
220
+ this.goalScheduler = null;
221
+ }
222
+ if (this.ipcServer) {
223
+ await this.ipcServer.stop();
224
+ this.ipcServer = null;
225
+ }
226
+ await this.store.deleteDaemonPid();
227
+ await this.closeLogStream();
228
+ clearTimeout(shutdownTimeout);
229
+ this.logMessage('info', 'Daemon stopped');
230
+ this.logger.info('LOOP', 'Daemon stopped');
231
+ this.emit('stopped');
232
+ }
233
+ catch (error) {
234
+ clearTimeout(shutdownTimeout);
235
+ this.logMessage('error', `Error during shutdown: ${error instanceof Error ? error.message : String(error)}`);
236
+ await this.cleanup();
237
+ throw new DaemonStopFailedError(process.pid, error instanceof Error ? error.message : String(error));
238
+ }
239
+ }
240
+ async getStatus() {
241
+ const stats = this.manager ? await this.manager.getStats() : null;
242
+ return {
243
+ running: this.ipcServer?.isRunning() ?? false,
244
+ pid: process.pid,
245
+ startedAt: this.startedAt,
246
+ activeLoops: stats?.activeLoops ?? 0,
247
+ uptime: this.startedAt ? Date.now() - this.startedAt.getTime() : null,
248
+ };
249
+ }
250
+ isRunning() {
251
+ return this.ipcServer?.isRunning() ?? false;
252
+ }
253
+ async resolveLoopId(idOrName) {
254
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrName);
255
+ if (isUUID) {
256
+ try {
257
+ const loop = await this.manager.getLoop(idOrName);
258
+ return loop.id;
259
+ }
260
+ catch {
261
+ }
262
+ }
263
+ const loopByName = await this.manager.getLoopByName(idOrName);
264
+ if (loopByName) {
265
+ return loopByName.id;
266
+ }
267
+ const allLoops = await this.manager.getAllLoops();
268
+ const matchingLoop = allLoops.find(l => l.id.startsWith(idOrName));
269
+ if (matchingLoop) {
270
+ return matchingLoop.id;
271
+ }
272
+ throw new Error(`Loop not found: ${idOrName}`);
273
+ }
274
+ assertGoalManager() {
275
+ if (!this.goalManager) {
276
+ throw new Error('Goal tracking is not enabled. Configure goalConfig to use goal features.');
277
+ }
278
+ }
279
+ assertTaskGenerator() {
280
+ if (!this.taskGenerator) {
281
+ throw new Error('Task generation is not enabled. Configure goalConfig.llmFunction to use task generation features.');
282
+ }
283
+ }
284
+ assertTaskExecutor() {
285
+ if (!this.taskExecutor) {
286
+ throw new Error('Task execution is not enabled. Configure goalConfig.taskHandlers or goalConfig.approvalRequestFunction to use task execution features.');
287
+ }
288
+ }
289
+ assertPermissionBridge() {
290
+ if (!this.permissionBridge) {
291
+ throw new Error('Permission bridge is not initialized. This is required for task execution.');
292
+ }
293
+ }
294
+ assertGoalCommsHandler() {
295
+ if (!this.goalCommsHandler) {
296
+ throw new Error('COMMS is not enabled. Configure goalConfig.commsSendFunction to use COMMS features.');
297
+ }
298
+ }
299
+ assertReviewScheduler() {
300
+ if (!this.reviewScheduler) {
301
+ throw new Error('ReviewScheduler is not enabled. Configure goalConfig.enableReviewScheduler to use review features.');
302
+ }
303
+ }
304
+ assertProgressTracker() {
305
+ if (!this.progressTracker) {
306
+ throw new Error('ProgressTracker is not enabled. Configure goalConfig.enableProgressTracker to use progress tracking features.');
307
+ }
308
+ }
309
+ assertErrorRecovery() {
310
+ if (!this.errorRecovery) {
311
+ throw new Error('ErrorRecovery is not enabled. Configure goalConfig.enableErrorRecovery to use error recovery features.');
312
+ }
313
+ }
314
+ async handleRequest(method, params) {
315
+ if (!this.manager && method !== 'ping' && method !== 'status' && method !== 'shutdown') {
316
+ throw new Error('Daemon not fully initialized');
317
+ }
318
+ this.logMessage('debug', `IPC request: ${method}`, params);
319
+ const getLoopId = async () => {
320
+ if (params.loopId) {
321
+ return params.loopId;
322
+ }
323
+ if (params.idOrName) {
324
+ return this.resolveLoopId(params.idOrName);
325
+ }
326
+ throw new Error('Missing loopId or idOrName parameter');
327
+ };
328
+ switch (method) {
329
+ case 'ping':
330
+ return { pong: true, timestamp: Date.now() };
331
+ case 'status':
332
+ return this.getStatus();
333
+ case 'shutdown':
334
+ setImmediate(() => this.stop().catch(() => process.exit(1)));
335
+ return { shutting_down: true };
336
+ case 'createLoop':
337
+ return this.manager.createLoop(params);
338
+ case 'getLoop': {
339
+ const loopId = await getLoopId();
340
+ return this.manager.getLoop(loopId);
341
+ }
342
+ case 'getLoopByName':
343
+ return this.manager.getLoopByName(params.name);
344
+ case 'getAllLoops':
345
+ return this.manager.getAllLoops();
346
+ case 'getLoopsByStatus':
347
+ return this.manager.getLoopsByStatus(params.status);
348
+ case 'pauseLoop': {
349
+ const loopId = await getLoopId();
350
+ return this.manager.pauseLoop(loopId);
351
+ }
352
+ case 'resumeLoop': {
353
+ const loopId = await getLoopId();
354
+ return this.manager.resumeLoop(loopId);
355
+ }
356
+ case 'stopLoop': {
357
+ const loopId = await getLoopId();
358
+ return this.manager.stopLoop(loopId);
359
+ }
360
+ case 'deleteLoop': {
361
+ const loopId = await getLoopId();
362
+ await this.manager.deleteLoop(loopId);
363
+ return { deleted: true };
364
+ }
365
+ case 'runNow':
366
+ case 'runLoopNow': {
367
+ const loopId = await getLoopId();
368
+ const run = await this.manager.runNow(loopId);
369
+ const loop = await this.manager.getLoop(loopId);
370
+ return { loop, run };
371
+ }
372
+ case 'getRunHistory':
373
+ case 'getLoopHistory': {
374
+ const loopId = await getLoopId();
375
+ return this.manager.getRunHistory(loopId, params.limit);
376
+ }
377
+ case 'getRun': {
378
+ const loopId = await getLoopId();
379
+ return this.manager.getRun(loopId, params.runId);
380
+ }
381
+ case 'getStats':
382
+ return this.manager.getStats();
383
+ case 'listGoals':
384
+ this.assertGoalManager();
385
+ return this.goalManager.listGoals(params);
386
+ case 'getGoal':
387
+ this.assertGoalManager();
388
+ return this.goalManager.getGoal(params.goalHash);
389
+ case 'createGoal':
390
+ this.assertGoalManager();
391
+ return this.goalManager.createGoal(params);
392
+ case 'updateGoal':
393
+ this.assertGoalManager();
394
+ return this.goalManager.updateGoal(params.goalHash, params.data);
395
+ case 'deleteGoal':
396
+ this.assertGoalManager();
397
+ await this.goalManager.deleteGoal(params.goalHash);
398
+ return { deleted: true };
399
+ case 'pauseGoal':
400
+ this.assertGoalManager();
401
+ return this.goalManager.pauseGoal(params.goalHash);
402
+ case 'resumeGoal':
403
+ this.assertGoalManager();
404
+ return this.goalManager.resumeGoal(params.goalHash);
405
+ case 'completeGoal':
406
+ this.assertGoalManager();
407
+ return this.goalManager.completeGoal(params.goalHash);
408
+ case 'abandonGoal':
409
+ this.assertGoalManager();
410
+ return this.goalManager.abandonGoal(params.goalHash);
411
+ case 'getGoalProgress':
412
+ this.assertGoalManager();
413
+ return this.goalManager.getGoalProgress(params.goalHash);
414
+ case 'listMilestones':
415
+ this.assertGoalManager();
416
+ return this.goalManager.listMilestones(params.goalHash, params.forceRefresh);
417
+ case 'getMilestone':
418
+ this.assertGoalManager();
419
+ return this.goalManager.getMilestone(params.goalHash, params.milestoneId);
420
+ case 'createMilestone':
421
+ this.assertGoalManager();
422
+ return this.goalManager.createMilestone(params.goalHash, params.data);
423
+ case 'updateMilestone':
424
+ this.assertGoalManager();
425
+ return this.goalManager.updateMilestone(params.goalHash, params.milestoneId, params.data);
426
+ case 'deleteMilestone':
427
+ this.assertGoalManager();
428
+ await this.goalManager.deleteMilestone(params.goalHash, params.milestoneId);
429
+ return { deleted: true };
430
+ case 'completeMilestone':
431
+ this.assertGoalManager();
432
+ return this.goalManager.completeMilestone(params.goalHash, params.milestoneId);
433
+ case 'listTasks':
434
+ this.assertGoalManager();
435
+ return this.goalManager.listTasks(params.goalHash, params);
436
+ case 'getTask':
437
+ this.assertGoalManager();
438
+ return this.goalManager.getTask(params.goalHash, params.taskId);
439
+ case 'createTask':
440
+ this.assertGoalManager();
441
+ return this.goalManager.createTask(params.goalHash, params.data);
442
+ case 'updateTask':
443
+ this.assertGoalManager();
444
+ return this.goalManager.updateTask(params.goalHash, params.taskId, params.data);
445
+ case 'deleteTask':
446
+ this.assertGoalManager();
447
+ await this.goalManager.deleteTask(params.goalHash, params.taskId);
448
+ return { deleted: true };
449
+ case 'approveTask':
450
+ this.assertGoalManager();
451
+ return this.goalManager.approveTask(params.goalHash, params.taskId);
452
+ case 'denyTask':
453
+ this.assertGoalManager();
454
+ return this.goalManager.denyTask(params.goalHash, params.taskId);
455
+ case 'startTask':
456
+ this.assertGoalManager();
457
+ return this.goalManager.startTask(params.goalHash, params.taskId);
458
+ case 'completeTask':
459
+ this.assertGoalManager();
460
+ return this.goalManager.completeTask(params.goalHash, params.taskId, params.result);
461
+ case 'failTask':
462
+ this.assertGoalManager();
463
+ return this.goalManager.failTask(params.goalHash, params.taskId, params.data);
464
+ case 'skipTask':
465
+ this.assertGoalManager();
466
+ return this.goalManager.skipTask(params.goalHash, params.taskId);
467
+ case 'addTaskFeedback':
468
+ this.assertGoalManager();
469
+ return this.goalManager.addTaskFeedback(params.goalHash, params.taskId, params.data);
470
+ case 'getPendingTasks':
471
+ this.assertGoalManager();
472
+ return this.goalManager.getPendingTasks();
473
+ case 'getTasksAwaitingApproval':
474
+ this.assertGoalManager();
475
+ return this.goalManager.getTasksAwaitingApproval();
476
+ case 'listMemories':
477
+ this.assertGoalManager();
478
+ return this.goalManager.listMemories(params.goalHash, params.forceRefresh);
479
+ case 'createMemory':
480
+ this.assertGoalManager();
481
+ return this.goalManager.createMemory(params.goalHash, params.data);
482
+ case 'updateMemory':
483
+ this.assertGoalManager();
484
+ return this.goalManager.updateMemory(params.goalHash, params.memoryId, params.data);
485
+ case 'toggleMemory':
486
+ this.assertGoalManager();
487
+ return this.goalManager.toggleMemory(params.goalHash, params.memoryId);
488
+ case 'deleteMemory':
489
+ this.assertGoalManager();
490
+ await this.goalManager.deleteMemory(params.goalHash, params.memoryId);
491
+ return { deleted: true };
492
+ case 'getEnabledMemories':
493
+ this.assertGoalManager();
494
+ return this.goalManager.getEnabledMemories(params.goalHash);
495
+ case 'getGoalStats':
496
+ this.assertGoalManager();
497
+ return this.goalManager.getStats();
498
+ case 'getActionLogs':
499
+ this.assertGoalManager();
500
+ return this.goalManager.getActionLogs(params.goalHash, params.limit);
501
+ case 'isTaskGenerationEnabled':
502
+ return { enabled: this.taskGenerator !== null };
503
+ case 'generateTasks': {
504
+ this.assertGoalManager();
505
+ this.assertTaskGenerator();
506
+ const goal = await this.goalManager.getGoal(params.goalHash);
507
+ const milestones = await this.goalManager.listMilestones(params.goalHash);
508
+ const existingTasks = await this.goalManager.listTasks(params.goalHash);
509
+ const result = await this.taskGenerator.generateTasks(goal, milestones, existingTasks, params.options);
510
+ return result;
511
+ }
512
+ case 'generateDailyTasks': {
513
+ this.assertGoalManager();
514
+ this.assertTaskGenerator();
515
+ const goal = await this.goalManager.getGoal(params.goalHash);
516
+ const milestones = await this.goalManager.listMilestones(params.goalHash);
517
+ const existingTasks = await this.goalManager.listTasks(params.goalHash);
518
+ const result = await this.taskGenerator.generateDailyTasks(goal, milestones, existingTasks, params.options);
519
+ return result;
520
+ }
521
+ case 'suggestNextTask': {
522
+ this.assertGoalManager();
523
+ this.assertTaskGenerator();
524
+ const goal = await this.goalManager.getGoal(params.goalHash);
525
+ const milestones = await this.goalManager.listMilestones(params.goalHash);
526
+ const pendingTasks = await this.goalManager.listTasks(params.goalHash, {
527
+ status: 'pending',
528
+ });
529
+ const suggestion = await this.taskGenerator.suggestNextTask(goal, milestones, pendingTasks, params.options);
530
+ return suggestion;
531
+ }
532
+ case 'isTaskExecutionEnabled':
533
+ return { enabled: this.taskExecutor !== null };
534
+ case 'getExecutionCapabilities': {
535
+ if (!this.taskExecutor) {
536
+ return {
537
+ enabled: false,
538
+ handlerTypes: [],
539
+ hasApprovalFunction: false,
540
+ hasPermissionBridge: false,
541
+ };
542
+ }
543
+ return {
544
+ enabled: true,
545
+ handlerTypes: Object.keys(this.goalConfig?.taskHandlers ?? {}),
546
+ hasApprovalFunction: !!this.goalConfig?.approvalRequestFunction,
547
+ hasPermissionBridge: !!this.permissionBridge,
548
+ };
549
+ }
550
+ case 'executeTask': {
551
+ this.assertGoalManager();
552
+ this.assertTaskExecutor();
553
+ const goal = await this.goalManager.getGoal(params.goalHash);
554
+ const task = await this.goalManager.getTask(params.goalHash, params.taskId);
555
+ const result = await this.taskExecutor.execute(goal, task);
556
+ if (result.success) {
557
+ await this.goalManager.completeTask(params.goalHash, params.taskId, result.data);
558
+ }
559
+ else if (result.status === 'failed') {
560
+ await this.goalManager.failTask(params.goalHash, params.taskId, { error: result.error ?? 'Task execution failed' });
561
+ }
562
+ else if (result.approvalSkipped) {
563
+ await this.goalManager.skipTask(params.goalHash, params.taskId);
564
+ }
565
+ return result;
566
+ }
567
+ case 'executeTaskSequence': {
568
+ this.assertGoalManager();
569
+ this.assertTaskExecutor();
570
+ const goal = await this.goalManager.getGoal(params.goalHash);
571
+ let tasks;
572
+ if (params.taskIds && Array.isArray(params.taskIds)) {
573
+ tasks = await Promise.all(params.taskIds.map(id => this.goalManager.getTask(params.goalHash, id)));
574
+ }
575
+ else {
576
+ tasks = await this.goalManager.listTasks(params.goalHash, {
577
+ status: 'pending',
578
+ });
579
+ }
580
+ const results = await this.taskExecutor.executeSequence(goal, tasks);
581
+ for (let i = 0; i < results.length; i++) {
582
+ const result = results[i];
583
+ const task = tasks[i];
584
+ if (result.success) {
585
+ await this.goalManager.completeTask(params.goalHash, task.id, result.data);
586
+ }
587
+ else if (result.status === 'failed') {
588
+ await this.goalManager.failTask(params.goalHash, task.id, { error: result.error ?? 'Task execution failed' });
589
+ }
590
+ else if (result.approvalSkipped) {
591
+ await this.goalManager.skipTask(params.goalHash, task.id);
592
+ }
593
+ }
594
+ return { results, executed: results.length, total: tasks.length };
595
+ }
596
+ case 'runGoalAutoMode': {
597
+ this.assertGoalManager();
598
+ this.assertTaskExecutor();
599
+ const goalHash = params.goalHash;
600
+ const maxTasks = params.maxTasks ?? 10;
601
+ const goal = await this.goalManager.getGoal(goalHash);
602
+ const allTasks = await this.goalManager.listTasks(goalHash, { forceRefresh: true });
603
+ const pendingTasks = allTasks.filter(t => t.status === 'pending');
604
+ const tasksToRun = pendingTasks.slice(0, maxTasks);
605
+ let tasksExecuted = 0;
606
+ let tasksCompleted = 0;
607
+ let tasksFailed = 0;
608
+ for (const task of tasksToRun) {
609
+ try {
610
+ const result = await this.taskExecutor.execute(goal, task);
611
+ tasksExecuted++;
612
+ if (result.success) {
613
+ await this.goalManager.completeTask(goalHash, task.id, result.data);
614
+ tasksCompleted++;
615
+ }
616
+ else {
617
+ await this.goalManager.failTask(goalHash, task.id, { error: result.error ?? 'Task execution failed' });
618
+ tasksFailed++;
619
+ }
620
+ }
621
+ catch (error) {
622
+ tasksFailed++;
623
+ try {
624
+ await this.goalManager.failTask(goalHash, task.id, {
625
+ error: error instanceof Error ? error.message : String(error)
626
+ });
627
+ }
628
+ catch { }
629
+ }
630
+ }
631
+ return {
632
+ success: true,
633
+ tasksExecuted,
634
+ tasksCompleted,
635
+ tasksFailed,
636
+ };
637
+ }
638
+ case 'canExecuteTask': {
639
+ this.assertGoalManager();
640
+ this.assertTaskExecutor();
641
+ const goal = await this.goalManager.getGoal(params.goalHash);
642
+ const task = await this.goalManager.getTask(params.goalHash, params.taskId);
643
+ const result = await this.taskExecutor.canExecute(goal, task);
644
+ return result;
645
+ }
646
+ case 'checkTaskPermission': {
647
+ this.assertGoalManager();
648
+ this.assertPermissionBridge();
649
+ const goal = await this.goalManager.getGoal(params.goalHash);
650
+ const task = await this.goalManager.getTask(params.goalHash, params.taskId);
651
+ const context = {
652
+ goal,
653
+ task,
654
+ permissionCategory: params.category,
655
+ approvalMode: goal.approvalMode,
656
+ startTime: new Date(),
657
+ };
658
+ const result = await this.permissionBridge.checkPermission(params.category, context, params.options);
659
+ return result;
660
+ }
661
+ case 'recordPermissionDecision': {
662
+ this.assertPermissionBridge();
663
+ this.permissionBridge.recordDecision(params.category, params.decision, params.options);
664
+ return { recorded: true };
665
+ }
666
+ case 'getPermissionAuditLog': {
667
+ this.assertPermissionBridge();
668
+ const auditLog = this.permissionBridge.getAuditLog({
669
+ goalHash: params.goalHash,
670
+ taskId: params.taskId,
671
+ category: params.category,
672
+ since: params.since ? new Date(params.since) : undefined,
673
+ limit: params.limit,
674
+ });
675
+ return auditLog;
676
+ }
677
+ case 'clearPermissionCache': {
678
+ this.assertPermissionBridge();
679
+ this.permissionBridge.clearCache();
680
+ return { cleared: true };
681
+ }
682
+ case 'isCommsEnabled': {
683
+ return {
684
+ enabled: this.goalCommsHandler !== null,
685
+ };
686
+ }
687
+ case 'sendGoalNotification': {
688
+ this.assertGoalCommsHandler();
689
+ this.assertGoalManager();
690
+ const goal = await this.goalManager.getGoal(params.goalHash);
691
+ await this.goalCommsHandler.sendNotification(params.subtype, goal, params.message, params.details);
692
+ return { sent: true };
693
+ }
694
+ case 'requestGoalApproval': {
695
+ this.assertGoalCommsHandler();
696
+ this.assertGoalManager();
697
+ const goal = await this.goalManager.getGoal(params.goalHash);
698
+ const task = await this.goalManager.getTask(params.goalHash, params.taskId);
699
+ const response = await this.goalCommsHandler.requestApproval(goal, task, params.reason);
700
+ return response;
701
+ }
702
+ case 'handleApprovalResponse': {
703
+ this.assertGoalCommsHandler();
704
+ this.goalCommsHandler.handleApprovalResponse(params.requestId, params.response, params.userId);
705
+ return { handled: true };
706
+ }
707
+ case 'isReviewSchedulerEnabled': {
708
+ return {
709
+ enabled: this.reviewScheduler !== null,
710
+ };
711
+ }
712
+ case 'setReviewSchedule': {
713
+ this.assertReviewScheduler();
714
+ const scheduleConfig = {
715
+ goalHash: params.goalHash,
716
+ frequency: params.frequency,
717
+ enabled: params.enabled !== false,
718
+ customIntervalDays: params.customIntervalDays,
719
+ preferredTime: params.preferredTime,
720
+ preferredDay: params.preferredDay,
721
+ notifyVia: params.notifyVia,
722
+ };
723
+ this.reviewScheduler.setSchedule(scheduleConfig);
724
+ return { scheduled: true };
725
+ }
726
+ case 'removeReviewSchedule': {
727
+ this.assertReviewScheduler();
728
+ this.reviewScheduler.removeSchedule(params.goalHash);
729
+ return { removed: true };
730
+ }
731
+ case 'scheduleReview': {
732
+ this.assertReviewScheduler();
733
+ const scheduledFor = params.scheduledFor
734
+ ? new Date(params.scheduledFor)
735
+ : new Date();
736
+ const review = this.reviewScheduler.scheduleReview(params.goalHash, params.reviewType, scheduledFor, params.frequency, params.metadata);
737
+ return review;
738
+ }
739
+ case 'completeReview': {
740
+ this.assertReviewScheduler();
741
+ this.reviewScheduler.completeReview(params.reviewId, params.result);
742
+ return { completed: true };
743
+ }
744
+ case 'snoozeReview': {
745
+ this.assertReviewScheduler();
746
+ const snoozeHours = params.hours;
747
+ this.reviewScheduler.snoozeReview(params.reviewId, snoozeHours);
748
+ return { snoozed: true };
749
+ }
750
+ case 'getPendingReviews': {
751
+ this.assertReviewScheduler();
752
+ const pending = this.reviewScheduler.getPendingReviews(params.goalHash);
753
+ return pending;
754
+ }
755
+ case 'getDueReviews': {
756
+ this.assertReviewScheduler();
757
+ const due = this.reviewScheduler.getDueReviews();
758
+ return due;
759
+ }
760
+ case 'isProgressTrackerEnabled': {
761
+ return {
762
+ enabled: this.progressTracker !== null,
763
+ };
764
+ }
765
+ case 'calculateGoalProgress': {
766
+ this.assertProgressTracker();
767
+ this.assertGoalManager();
768
+ const goal = await this.goalManager.getGoal(params.goalHash);
769
+ const milestones = await this.goalManager.listMilestones(params.goalHash);
770
+ const tasks = await this.goalManager.listTasks(params.goalHash);
771
+ const progress = this.progressTracker.calculateProgress(goal, milestones, tasks);
772
+ return progress;
773
+ }
774
+ case 'recordTaskCompletion': {
775
+ this.assertProgressTracker();
776
+ this.progressTracker.recordTaskCompletion(params.taskId, params.goalHash, params.durationMs, params.wasSuccessful);
777
+ return { recorded: true };
778
+ }
779
+ case 'createProgressSnapshot': {
780
+ this.assertProgressTracker();
781
+ const snapshot = this.progressTracker.createSnapshot(params.goalHash);
782
+ return snapshot;
783
+ }
784
+ case 'getProgressSnapshots': {
785
+ this.assertProgressTracker();
786
+ const snapshots = this.progressTracker.getSnapshots(params.goalHash);
787
+ return snapshots;
788
+ }
789
+ case 'getVelocityMetrics': {
790
+ this.assertProgressTracker();
791
+ this.assertGoalManager();
792
+ const goal = await this.goalManager.getGoal(params.goalHash);
793
+ const milestones = await this.goalManager.listMilestones(params.goalHash);
794
+ const tasks = await this.goalManager.listTasks(params.goalHash);
795
+ const progress = this.progressTracker.calculateProgress(goal, milestones, tasks);
796
+ return progress.velocity;
797
+ }
798
+ case 'isErrorRecoveryEnabled': {
799
+ return {
800
+ enabled: this.errorRecovery !== null,
801
+ };
802
+ }
803
+ case 'createCheckpoint': {
804
+ this.assertErrorRecovery();
805
+ this.assertGoalManager();
806
+ const goal = await this.goalManager.getGoal(params.goalHash);
807
+ const task = await this.goalManager.getTask(params.goalHash, params.taskId);
808
+ const checkpoint = this.errorRecovery.createCheckpoint(goal, task, params.state, params.description);
809
+ return checkpoint;
810
+ }
811
+ case 'getCheckpoints': {
812
+ this.assertErrorRecovery();
813
+ const checkpoints = this.errorRecovery.getCheckpoints(params.goalHash);
814
+ return checkpoints;
815
+ }
816
+ case 'restoreCheckpoint': {
817
+ this.assertErrorRecovery();
818
+ const checkpoint = this.errorRecovery.getCheckpoint(params.checkpointId);
819
+ if (!checkpoint) {
820
+ throw new Error(`Checkpoint not found: ${params.checkpointId}`);
821
+ }
822
+ return checkpoint.stateData;
823
+ }
824
+ case 'clearCheckpoints': {
825
+ this.assertErrorRecovery();
826
+ this.errorRecovery.clearCheckpoints(params.goalHash);
827
+ return { cleared: true };
828
+ }
829
+ case 'analyzeError': {
830
+ this.assertErrorRecovery();
831
+ const executionResult = {
832
+ success: false,
833
+ taskId: params.taskId,
834
+ goalHash: params.goalHash,
835
+ status: params.status ?? 'failed',
836
+ error: params.error,
837
+ durationMs: params.durationMs ?? 0,
838
+ executedAt: params.executedAt
839
+ ? new Date(params.executedAt)
840
+ : new Date(),
841
+ cost: params.cost,
842
+ };
843
+ const analysis = this.errorRecovery.analyzeError(executionResult);
844
+ return analysis;
845
+ }
846
+ default:
847
+ throw new Error(`Unknown method: ${method}`);
848
+ }
849
+ }
850
+ setupManagerEvents() {
851
+ if (!this.manager || !this.ipcServer) {
852
+ return;
853
+ }
854
+ const events = [
855
+ 'created',
856
+ 'started',
857
+ 'paused',
858
+ 'resumed',
859
+ 'stopped',
860
+ 'deleted',
861
+ 'expired',
862
+ 'runStarted',
863
+ 'runCompleted',
864
+ 'runFailed',
865
+ 'autoPaused',
866
+ 'error',
867
+ ];
868
+ for (const event of events) {
869
+ this.manager.on(event, (...args) => {
870
+ const data = this.serializeEventData(event, args);
871
+ this.ipcServer.broadcast(`loop:${event}`, data);
872
+ this.emit('loopEvent', event, data);
873
+ if (event === 'created' || event === 'stopped' || event === 'error') {
874
+ this.logMessage('info', `Loop event: ${event}`, data);
875
+ }
876
+ });
877
+ }
878
+ }
879
+ serializeEventData(event, args) {
880
+ const data = { event };
881
+ switch (event) {
882
+ case 'created':
883
+ case 'started':
884
+ case 'paused':
885
+ case 'resumed':
886
+ case 'stopped':
887
+ case 'expired':
888
+ if (args[0] && typeof args[0] === 'object' && 'id' in args[0]) {
889
+ const loop = args[0];
890
+ data.loopId = loop.id;
891
+ data.loopName = loop.name;
892
+ data.status = loop.status;
893
+ }
894
+ break;
895
+ case 'deleted':
896
+ data.loopId = args[0];
897
+ break;
898
+ case 'runStarted':
899
+ if (args[0] && typeof args[0] === 'object' && 'id' in args[0]) {
900
+ const loop = args[0];
901
+ data.loopId = loop.id;
902
+ data.runId = args[1];
903
+ }
904
+ break;
905
+ case 'runCompleted':
906
+ case 'runFailed':
907
+ if (args[0] && typeof args[0] === 'object' && 'id' in args[0]) {
908
+ const loop = args[0];
909
+ data.loopId = loop.id;
910
+ data.result = args[1];
911
+ }
912
+ break;
913
+ case 'autoPaused':
914
+ if (args[0] && typeof args[0] === 'object' && 'id' in args[0]) {
915
+ const loop = args[0];
916
+ data.loopId = loop.id;
917
+ data.reason = args[1];
918
+ }
919
+ break;
920
+ case 'error':
921
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
922
+ data.loopId = args[1];
923
+ break;
924
+ default:
925
+ break;
926
+ }
927
+ return data;
928
+ }
929
+ setupGoalManagerEvents() {
930
+ if (!this.goalManager || !this.ipcServer) {
931
+ return;
932
+ }
933
+ const events = [
934
+ 'goalCreated',
935
+ 'goalUpdated',
936
+ 'goalPaused',
937
+ 'goalResumed',
938
+ 'goalCompleted',
939
+ 'goalAbandoned',
940
+ 'goalDeleted',
941
+ 'milestoneCreated',
942
+ 'milestoneUpdated',
943
+ 'milestoneCompleted',
944
+ 'milestoneDeleted',
945
+ 'taskCreated',
946
+ 'taskUpdated',
947
+ 'taskApproved',
948
+ 'taskDenied',
949
+ 'taskStarted',
950
+ 'taskCompleted',
951
+ 'taskFailed',
952
+ 'taskSkipped',
953
+ 'taskDeleted',
954
+ 'memoryCreated',
955
+ 'memoryUpdated',
956
+ 'memoryToggled',
957
+ 'memoryDeleted',
958
+ 'error',
959
+ ];
960
+ for (const event of events) {
961
+ this.goalManager.on(event, (...args) => {
962
+ const data = this.serializeGoalEventData(event, args);
963
+ this.ipcServer.broadcast(`goal:${event}`, data);
964
+ this.emit('goalEvent', event, data);
965
+ if (event.startsWith('goal') || event === 'error') {
966
+ this.logMessage('info', `Goal event: ${event}`, data);
967
+ }
968
+ });
969
+ }
970
+ }
971
+ serializeGoalEventData(event, args) {
972
+ const data = { event };
973
+ if (args[0] && typeof args[0] === 'object') {
974
+ const entity = args[0];
975
+ if ('hash' in entity) {
976
+ data.goalHash = entity.hash;
977
+ }
978
+ if ('id' in entity) {
979
+ data.id = entity.id;
980
+ }
981
+ if ('name' in entity) {
982
+ data.name = entity.name;
983
+ }
984
+ if ('title' in entity) {
985
+ data.title = entity.title;
986
+ }
987
+ if ('status' in entity) {
988
+ data.status = entity.status;
989
+ }
990
+ if (event.startsWith('task')) {
991
+ if ('goal_hash' in entity) {
992
+ data.goalHash = entity.goal_hash;
993
+ }
994
+ if ('milestone_id' in entity) {
995
+ data.milestoneId = entity.milestone_id;
996
+ }
997
+ }
998
+ if (event.startsWith('milestone')) {
999
+ if ('goal_hash' in entity) {
1000
+ data.goalHash = entity.goal_hash;
1001
+ }
1002
+ }
1003
+ }
1004
+ if (event === 'error') {
1005
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1006
+ if (args[1]) {
1007
+ data.context = args[1];
1008
+ }
1009
+ }
1010
+ return data;
1011
+ }
1012
+ setupTaskExecutorEvents() {
1013
+ if (!this.taskExecutor || !this.ipcServer) {
1014
+ return;
1015
+ }
1016
+ const events = [
1017
+ 'execution-started',
1018
+ 'execution-completed',
1019
+ 'permission-checked',
1020
+ 'approval-requested',
1021
+ 'approval-received',
1022
+ 'error',
1023
+ ];
1024
+ for (const event of events) {
1025
+ this.taskExecutor.on(event, (...args) => {
1026
+ const data = this.serializeExecutorEventData(event, args);
1027
+ this.ipcServer.broadcast(`executor:${event}`, data);
1028
+ if (event === 'execution-started' || event === 'execution-completed' || event === 'error') {
1029
+ this.logMessage('info', `Executor event: ${event}`, data);
1030
+ }
1031
+ });
1032
+ }
1033
+ if (this.permissionBridge) {
1034
+ const permissionEvents = [
1035
+ 'permission-granted',
1036
+ 'permission-denied',
1037
+ 'approval-required',
1038
+ 'audit-logged',
1039
+ ];
1040
+ for (const event of permissionEvents) {
1041
+ this.permissionBridge.on(event, (...args) => {
1042
+ const data = this.serializePermissionEventData(event, args);
1043
+ this.ipcServer.broadcast(`permission:${event}`, data);
1044
+ });
1045
+ }
1046
+ }
1047
+ }
1048
+ serializeExecutorEventData(event, args) {
1049
+ const data = { event };
1050
+ switch (event) {
1051
+ case 'execution-started':
1052
+ case 'approval-requested': {
1053
+ const context = args[0];
1054
+ if (context) {
1055
+ data.goalHash = context.goal.hash;
1056
+ data.taskId = context.task.id;
1057
+ data.taskTitle = context.task.title;
1058
+ data.permissionCategory = context.permissionCategory;
1059
+ data.approvalMode = context.approvalMode;
1060
+ }
1061
+ break;
1062
+ }
1063
+ case 'execution-completed': {
1064
+ const result = args[0];
1065
+ const context = args[1];
1066
+ if (result) {
1067
+ data.success = result.success;
1068
+ data.status = result.status;
1069
+ if (result.error)
1070
+ data.error = result.error;
1071
+ if (result.cost !== undefined)
1072
+ data.cost = result.cost;
1073
+ }
1074
+ if (context) {
1075
+ data.goalHash = context.goal.hash;
1076
+ data.taskId = context.task.id;
1077
+ }
1078
+ break;
1079
+ }
1080
+ case 'permission-checked': {
1081
+ const permResult = args[0];
1082
+ const context = args[1];
1083
+ if (permResult) {
1084
+ data.allowed = permResult.allowed;
1085
+ data.requiresApproval = permResult.requiresApproval;
1086
+ data.category = permResult.category;
1087
+ }
1088
+ if (context) {
1089
+ data.goalHash = context.goal.hash;
1090
+ data.taskId = context.task.id;
1091
+ }
1092
+ break;
1093
+ }
1094
+ case 'approval-received': {
1095
+ const approvalResult = args[0];
1096
+ const context = args[1];
1097
+ if (approvalResult) {
1098
+ data.approved = approvalResult.approved;
1099
+ data.response = approvalResult.response;
1100
+ }
1101
+ if (context) {
1102
+ data.goalHash = context.goal.hash;
1103
+ data.taskId = context.task.id;
1104
+ }
1105
+ break;
1106
+ }
1107
+ case 'error':
1108
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1109
+ break;
1110
+ }
1111
+ return data;
1112
+ }
1113
+ serializePermissionEventData(event, args) {
1114
+ const data = { event };
1115
+ if (args[0] && typeof args[0] === 'object') {
1116
+ const eventData = args[0];
1117
+ Object.assign(data, eventData);
1118
+ }
1119
+ if (args[1] && typeof args[1] === 'object') {
1120
+ const context = args[1];
1121
+ data.goalHash = context.goal?.hash;
1122
+ data.taskId = context.task?.id;
1123
+ }
1124
+ return data;
1125
+ }
1126
+ setupCommsHandlerEvents() {
1127
+ if (!this.goalCommsHandler || !this.ipcServer) {
1128
+ return;
1129
+ }
1130
+ const events = [
1131
+ 'notification-sent',
1132
+ 'approval-sent',
1133
+ 'approval-received',
1134
+ 'view-request',
1135
+ 'view-response-sent',
1136
+ 'modification-redirected',
1137
+ 'error',
1138
+ ];
1139
+ for (const event of events) {
1140
+ this.goalCommsHandler.on(event, (...args) => {
1141
+ const data = this.serializeCommsEventData(event, args);
1142
+ this.ipcServer.broadcast(`comms:${event}`, data);
1143
+ if (event === 'approval-received' || event === 'error') {
1144
+ this.logMessage('info', `COMMS event: ${event}`, data);
1145
+ }
1146
+ });
1147
+ }
1148
+ }
1149
+ serializeCommsEventData(event, args) {
1150
+ const data = { event };
1151
+ switch (event) {
1152
+ case 'notification-sent': {
1153
+ const notification = args[0];
1154
+ if (notification) {
1155
+ data.type = notification.type;
1156
+ data.subtype = notification.subtype;
1157
+ data.goalHash = notification.goalHash;
1158
+ data.goalName = notification.goalName;
1159
+ data.message = notification.message;
1160
+ }
1161
+ break;
1162
+ }
1163
+ case 'approval-sent': {
1164
+ const request = args[0];
1165
+ if (request) {
1166
+ data.requestId = request.requestId;
1167
+ data.goalHash = request.goalHash;
1168
+ data.taskId = request.taskId;
1169
+ data.taskTitle = request.taskTitle;
1170
+ data.reason = request.reason;
1171
+ }
1172
+ break;
1173
+ }
1174
+ case 'approval-received': {
1175
+ const response = args[0];
1176
+ if (response) {
1177
+ data.requestId = response.requestId;
1178
+ data.response = response.response;
1179
+ data.userId = response.userId;
1180
+ }
1181
+ break;
1182
+ }
1183
+ case 'view-request': {
1184
+ const viewReq = args[0];
1185
+ if (viewReq) {
1186
+ data.goalHash = viewReq.goalHash;
1187
+ data.viewType = viewReq.viewType;
1188
+ }
1189
+ break;
1190
+ }
1191
+ case 'view-response-sent':
1192
+ case 'modification-redirected': {
1193
+ const payload = args[0];
1194
+ if (payload) {
1195
+ Object.assign(data, payload);
1196
+ }
1197
+ break;
1198
+ }
1199
+ case 'error':
1200
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1201
+ break;
1202
+ }
1203
+ return data;
1204
+ }
1205
+ setupReviewSchedulerEvents() {
1206
+ if (!this.reviewScheduler || !this.ipcServer) {
1207
+ return;
1208
+ }
1209
+ const events = [
1210
+ 'review-scheduled',
1211
+ 'review-due',
1212
+ 'review-completed',
1213
+ 'review-snoozed',
1214
+ 'error',
1215
+ ];
1216
+ for (const event of events) {
1217
+ this.reviewScheduler.on(event, (...args) => {
1218
+ const data = this.serializeReviewSchedulerEventData(event, args);
1219
+ this.ipcServer.broadcast(`review:${event}`, data);
1220
+ if (event === 'review-due' || event === 'review-completed') {
1221
+ this.logMessage('info', `Review event: ${event}`, data);
1222
+ }
1223
+ });
1224
+ }
1225
+ }
1226
+ serializeReviewSchedulerEventData(event, args) {
1227
+ const data = { event };
1228
+ if (args[0] && typeof args[0] === 'object') {
1229
+ const review = args[0];
1230
+ if ('id' in review)
1231
+ data.reviewId = review.id;
1232
+ if ('goalHash' in review)
1233
+ data.goalHash = review.goalHash;
1234
+ if ('type' in review)
1235
+ data.reviewType = review.type;
1236
+ if ('scheduledFor' in review)
1237
+ data.scheduledFor = review.scheduledFor;
1238
+ if ('status' in review)
1239
+ data.status = review.status;
1240
+ }
1241
+ if (event === 'error') {
1242
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1243
+ }
1244
+ return data;
1245
+ }
1246
+ setupProgressTrackerEvents() {
1247
+ if (!this.progressTracker || !this.ipcServer) {
1248
+ return;
1249
+ }
1250
+ const events = [
1251
+ 'task-completed',
1252
+ 'milestone-completed',
1253
+ 'goal-completed',
1254
+ 'velocity-updated',
1255
+ 'snapshot-created',
1256
+ 'error',
1257
+ ];
1258
+ for (const event of events) {
1259
+ this.progressTracker.on(event, (...args) => {
1260
+ const data = this.serializeProgressTrackerEventData(event, args);
1261
+ this.ipcServer.broadcast(`progress:${event}`, data);
1262
+ if (event.includes('completed')) {
1263
+ this.logMessage('info', `Progress event: ${event}`, data);
1264
+ }
1265
+ });
1266
+ }
1267
+ }
1268
+ serializeProgressTrackerEventData(event, args) {
1269
+ const data = { event };
1270
+ if (args[0] && typeof args[0] === 'object') {
1271
+ const entity = args[0];
1272
+ if ('hash' in entity)
1273
+ data.goalHash = entity.hash;
1274
+ if ('goal_hash' in entity)
1275
+ data.goalHash = entity.goal_hash;
1276
+ if ('goalHash' in entity)
1277
+ data.goalHash = entity.goalHash;
1278
+ if ('id' in entity)
1279
+ data.id = entity.id;
1280
+ if ('progress' in entity)
1281
+ data.progress = entity.progress;
1282
+ if ('completedAt' in entity)
1283
+ data.completedAt = entity.completedAt;
1284
+ }
1285
+ if (args[1] && typeof args[1] === 'object') {
1286
+ const context = args[1];
1287
+ if ('velocity' in context)
1288
+ data.velocity = context.velocity;
1289
+ if ('milestone' in context)
1290
+ data.milestoneId = context.milestone?.id;
1291
+ }
1292
+ if (event === 'error') {
1293
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1294
+ }
1295
+ return data;
1296
+ }
1297
+ setupErrorRecoveryEvents() {
1298
+ if (!this.errorRecovery || !this.ipcServer) {
1299
+ return;
1300
+ }
1301
+ const events = [
1302
+ 'checkpoint-created',
1303
+ 'checkpoint-restored',
1304
+ 'checkpoint-cleared',
1305
+ 'error-analyzed',
1306
+ 'recovery-attempted',
1307
+ 'recovery-succeeded',
1308
+ 'recovery-failed',
1309
+ 'error',
1310
+ ];
1311
+ for (const event of events) {
1312
+ this.errorRecovery.on(event, (...args) => {
1313
+ const data = this.serializeErrorRecoveryEventData(event, args);
1314
+ this.ipcServer.broadcast(`recovery:${event}`, data);
1315
+ if (event === 'recovery-succeeded' || event === 'recovery-failed' || event === 'error') {
1316
+ this.logMessage('info', `Recovery event: ${event}`, data);
1317
+ }
1318
+ });
1319
+ }
1320
+ }
1321
+ serializeErrorRecoveryEventData(event, args) {
1322
+ const data = { event };
1323
+ if (args[0] && typeof args[0] === 'object') {
1324
+ const payload = args[0];
1325
+ if ('id' in payload)
1326
+ data.checkpointId = payload.id;
1327
+ if ('goalHash' in payload)
1328
+ data.goalHash = payload.goalHash;
1329
+ if ('taskId' in payload)
1330
+ data.taskId = payload.taskId;
1331
+ if ('createdAt' in payload)
1332
+ data.createdAt = payload.createdAt;
1333
+ if ('category' in payload)
1334
+ data.category = payload.category;
1335
+ if ('recoverable' in payload)
1336
+ data.recoverable = payload.recoverable;
1337
+ if ('suggestedAction' in payload)
1338
+ data.suggestedAction = payload.suggestedAction;
1339
+ if ('message' in payload)
1340
+ data.message = payload.message;
1341
+ }
1342
+ if (event === 'error') {
1343
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1344
+ }
1345
+ return data;
1346
+ }
1347
+ setupGoalSchedulerEvents() {
1348
+ if (!this.goalScheduler || !this.ipcServer) {
1349
+ return;
1350
+ }
1351
+ const events = [
1352
+ 'poll-started',
1353
+ 'poll-completed',
1354
+ 'execution-started',
1355
+ 'execution-completed',
1356
+ 'execution-skipped',
1357
+ 'error',
1358
+ ];
1359
+ for (const event of events) {
1360
+ this.goalScheduler.on(event, (...args) => {
1361
+ const data = this.serializeGoalSchedulerEventData(event, args);
1362
+ this.ipcServer.broadcast(`goal-scheduler:${event}`, data);
1363
+ if (event === 'execution-started' || event === 'execution-completed' || event === 'error') {
1364
+ this.logMessage('info', `GoalScheduler event: ${event}`, data);
1365
+ }
1366
+ });
1367
+ }
1368
+ }
1369
+ serializeGoalSchedulerEventData(event, args) {
1370
+ const data = { event };
1371
+ if (event === 'poll-completed' && typeof args[0] === 'number') {
1372
+ data.dueGoalsCount = args[0];
1373
+ }
1374
+ if (event === 'execution-started' || event === 'execution-completed' || event === 'execution-skipped') {
1375
+ const goal = args[0];
1376
+ if (goal) {
1377
+ data.goalHash = goal.hash;
1378
+ data.goalName = goal.name;
1379
+ data.goalId = goal.id;
1380
+ }
1381
+ if (event === 'execution-completed' && args.length > 1) {
1382
+ data.success = args[1];
1383
+ if (args[2])
1384
+ data.error = args[2];
1385
+ }
1386
+ if (event === 'execution-skipped' && args[1]) {
1387
+ data.reason = args[1];
1388
+ }
1389
+ }
1390
+ if (event === 'error') {
1391
+ data.error = args[0] instanceof Error ? args[0].message : String(args[0]);
1392
+ if (args[1] && typeof args[1] === 'object') {
1393
+ data.context = args[1];
1394
+ }
1395
+ }
1396
+ return data;
1397
+ }
1398
+ createGetDueGoalsFunction() {
1399
+ return async () => {
1400
+ if (!this.goalConfig) {
1401
+ return [];
1402
+ }
1403
+ const os = await import('os');
1404
+ const fs = await import('fs');
1405
+ const pathMod = await import('path');
1406
+ const goalsDir = pathMod.join(os.homedir(), '.flowdot', 'goals');
1407
+ if (!fs.existsSync(goalsDir))
1408
+ return [];
1409
+ const now = new Date();
1410
+ const dueGoals = [];
1411
+ const goalDirs = fs.readdirSync(goalsDir).filter(name => {
1412
+ const p = pathMod.join(goalsDir, name);
1413
+ return fs.statSync(p).isDirectory();
1414
+ });
1415
+ for (const goalHash of goalDirs) {
1416
+ const schedulePath = pathMod.join(goalsDir, goalHash, 'schedule.json');
1417
+ if (!fs.existsSync(schedulePath))
1418
+ continue;
1419
+ let schedule;
1420
+ try {
1421
+ schedule = JSON.parse(fs.readFileSync(schedulePath, 'utf-8'));
1422
+ }
1423
+ catch {
1424
+ continue;
1425
+ }
1426
+ if (!schedule.enabled)
1427
+ continue;
1428
+ if (schedule.nextRunAt) {
1429
+ const nextRun = new Date(schedule.nextRunAt);
1430
+ if (nextRun > now)
1431
+ continue;
1432
+ }
1433
+ let name = goalHash;
1434
+ const statePath = pathMod.join(goalsDir, goalHash, 'state.json');
1435
+ if (fs.existsSync(statePath)) {
1436
+ try {
1437
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
1438
+ name = state.name || goalHash;
1439
+ }
1440
+ catch { }
1441
+ }
1442
+ dueGoals.push({
1443
+ id: 0,
1444
+ hash: goalHash,
1445
+ userId: 0,
1446
+ name,
1447
+ description: null,
1448
+ approvalMode: 'category',
1449
+ scheduleType: schedule.type === 'cron' ? 'cron' : 'interval',
1450
+ cronExpression: schedule.cronExpression || null,
1451
+ intervalMinutes: schedule.intervalMinutes || null,
1452
+ scheduleTimezone: schedule.timezone || 'UTC',
1453
+ executionRecipe: schedule.executionRecipe || null,
1454
+ maxIterations: schedule.maxIterations || 50,
1455
+ maxRuntimeMinutes: schedule.maxRuntimeMinutes || 30,
1456
+ llmTier: (schedule.llmTier || 'capable'),
1457
+ allowedActions: null,
1458
+ blockedActions: null,
1459
+ nextRunAt: schedule.nextRunAt || now.toISOString(),
1460
+ consecutiveFailures: schedule.consecutiveFailures || 0,
1461
+ user: { id: 0, name: '', email: '' },
1462
+ });
1463
+ }
1464
+ return dueGoals;
1465
+ };
1466
+ }
1467
+ createMarkGoalStartedFunction() {
1468
+ return async (goalId, metadata) => {
1469
+ if (!this.goalConfig) {
1470
+ return;
1471
+ }
1472
+ const token = await this.goalConfig.tokenProvider();
1473
+ const response = await fetch(`${this.goalConfig.baseUrl}/api/internal/goals/schedule/${goalId}/started`, {
1474
+ method: 'POST',
1475
+ headers: {
1476
+ 'Authorization': `Bearer ${token}`,
1477
+ 'Content-Type': 'application/json',
1478
+ 'Accept': 'application/json',
1479
+ },
1480
+ body: JSON.stringify({ metadata }),
1481
+ });
1482
+ if (!response.ok) {
1483
+ throw new Error(`Failed to mark goal started: ${response.status}`);
1484
+ }
1485
+ };
1486
+ }
1487
+ createMarkGoalCompletedFunction() {
1488
+ return async (goalId, status, options) => {
1489
+ if (!this.goalConfig) {
1490
+ return { nextRunAt: null, scheduleActive: false };
1491
+ }
1492
+ const token = await this.goalConfig.tokenProvider();
1493
+ const response = await fetch(`${this.goalConfig.baseUrl}/api/internal/goals/schedule/${goalId}/completed`, {
1494
+ method: 'POST',
1495
+ headers: {
1496
+ 'Authorization': `Bearer ${token}`,
1497
+ 'Content-Type': 'application/json',
1498
+ 'Accept': 'application/json',
1499
+ },
1500
+ body: JSON.stringify({
1501
+ status,
1502
+ error: options?.error,
1503
+ progress: options?.progress,
1504
+ }),
1505
+ });
1506
+ if (!response.ok) {
1507
+ throw new Error(`Failed to mark goal completed: ${response.status}`);
1508
+ }
1509
+ const json = await response.json();
1510
+ return {
1511
+ nextRunAt: json.next_run_at ?? null,
1512
+ scheduleActive: json.schedule_active ?? false,
1513
+ };
1514
+ };
1515
+ }
1516
+ createDefaultExecuteGoalFunction() {
1517
+ return async (goal) => {
1518
+ try {
1519
+ this.logMessage('info', `Executing scheduled goal: ${goal.name}`, {
1520
+ goalHash: goal.hash,
1521
+ executionRecipe: goal.executionRecipe,
1522
+ });
1523
+ if (goal.executionRecipe) {
1524
+ this.emit('goal:execute-recipe', goal.hash, goal.executionRecipe);
1525
+ if (this.ipcServer) {
1526
+ this.ipcServer.broadcast('goal:execute-recipe', {
1527
+ goalHash: goal.hash,
1528
+ recipeHash: goal.executionRecipe,
1529
+ goalId: goal.id,
1530
+ userId: goal.userId,
1531
+ approvalMode: goal.approvalMode,
1532
+ llmTier: goal.llmTier,
1533
+ maxIterations: goal.maxIterations,
1534
+ maxRuntimeMinutes: goal.maxRuntimeMinutes,
1535
+ allowedActions: goal.allowedActions,
1536
+ blockedActions: goal.blockedActions,
1537
+ });
1538
+ }
1539
+ return { success: true, data: { recipeStarted: goal.executionRecipe } };
1540
+ }
1541
+ if (this.taskExecutor && this.goalManager) {
1542
+ const fullGoal = await this.goalManager.getGoal(goal.hash);
1543
+ const tasks = await this.goalManager.listTasks(goal.hash, { status: 'pending' });
1544
+ if (tasks.length === 0) {
1545
+ return { success: true, data: { message: 'No pending tasks' } };
1546
+ }
1547
+ let successCount = 0;
1548
+ let failCount = 0;
1549
+ const errors = [];
1550
+ for (const task of tasks.slice(0, goal.maxIterations)) {
1551
+ try {
1552
+ const result = await this.taskExecutor.execute(fullGoal, task);
1553
+ if (result.success) {
1554
+ successCount++;
1555
+ await this.goalManager.completeTask(goal.hash, task.id, result.data);
1556
+ }
1557
+ else {
1558
+ failCount++;
1559
+ errors.push(result.error ?? 'Unknown error');
1560
+ await this.goalManager.failTask(goal.hash, task.id, {
1561
+ error: result.error ?? 'Unknown error',
1562
+ });
1563
+ }
1564
+ }
1565
+ catch (err) {
1566
+ failCount++;
1567
+ const errorMessage = err instanceof Error ? err.message : String(err);
1568
+ errors.push(errorMessage);
1569
+ try {
1570
+ await this.goalManager.failTask(goal.hash, task.id, {
1571
+ error: errorMessage,
1572
+ });
1573
+ }
1574
+ catch {
1575
+ }
1576
+ }
1577
+ }
1578
+ const success = failCount === 0 || successCount > 0;
1579
+ return {
1580
+ success,
1581
+ data: { successCount, failCount, totalTasks: tasks.length },
1582
+ error: errors.length > 0 ? errors.join('; ') : undefined,
1583
+ };
1584
+ }
1585
+ return { success: false, error: 'No task execution capability configured' };
1586
+ }
1587
+ catch (err) {
1588
+ const errorMessage = err instanceof Error ? err.message : String(err);
1589
+ this.logMessage('error', `Goal execution failed: ${errorMessage}`, { goalHash: goal.hash });
1590
+ return { success: false, error: errorMessage };
1591
+ }
1592
+ };
1593
+ }
1594
+ setupSignalHandlers() {
1595
+ const handleShutdown = (signal) => {
1596
+ this.logMessage('info', `Received ${signal}, shutting down...`);
1597
+ this.stop()
1598
+ .then(() => process.exit(0))
1599
+ .catch(() => process.exit(1));
1600
+ };
1601
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
1602
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
1603
+ process.on('uncaughtException', (error) => {
1604
+ this.logMessage('error', `Uncaught exception: ${error.message}`, { stack: error.stack });
1605
+ this.stop()
1606
+ .then(() => process.exit(1))
1607
+ .catch(() => process.exit(1));
1608
+ });
1609
+ process.on('unhandledRejection', (reason) => {
1610
+ this.logMessage('error', `Unhandled rejection: ${reason}`);
1611
+ });
1612
+ }
1613
+ async setupLogging() {
1614
+ const logPath = path.join(this.flowdotDir, 'loops', 'daemon.log');
1615
+ try {
1616
+ this.logStream = await fs.open(logPath, 'a');
1617
+ }
1618
+ catch (error) {
1619
+ this.logger.warn('LOOP', 'Could not open daemon log file', {
1620
+ error: error instanceof Error ? error.message : String(error),
1621
+ });
1622
+ }
1623
+ }
1624
+ async closeLogStream() {
1625
+ if (this.logStream) {
1626
+ try {
1627
+ await this.logStream.close();
1628
+ }
1629
+ catch {
1630
+ }
1631
+ this.logStream = null;
1632
+ }
1633
+ }
1634
+ logMessage(level, message, data) {
1635
+ if (level === 'debug' && this.config.logLevel !== 'debug') {
1636
+ return;
1637
+ }
1638
+ const timestamp = new Date().toISOString();
1639
+ const logLine = JSON.stringify({
1640
+ timestamp,
1641
+ level,
1642
+ message,
1643
+ ...data,
1644
+ }) + '\n';
1645
+ if (this.logStream) {
1646
+ this.logStream.write(logLine).catch(() => {
1647
+ });
1648
+ }
1649
+ }
1650
+ async ensureDirectories() {
1651
+ const loopsDir = path.join(this.flowdotDir, 'loops');
1652
+ await fs.mkdir(loopsDir, { recursive: true });
1653
+ }
1654
+ async cleanup() {
1655
+ try {
1656
+ if (this.manager) {
1657
+ await this.manager.shutdown();
1658
+ this.manager = null;
1659
+ }
1660
+ }
1661
+ catch {
1662
+ }
1663
+ try {
1664
+ if (this.goalManager) {
1665
+ this.goalManager.clearCache();
1666
+ this.goalManager = null;
1667
+ }
1668
+ }
1669
+ catch {
1670
+ }
1671
+ this.taskGenerator = null;
1672
+ this.taskExecutor = null;
1673
+ this.permissionBridge = null;
1674
+ this.goalCommsHandler = null;
1675
+ if (this.reviewScheduler) {
1676
+ this.reviewScheduler.stop();
1677
+ }
1678
+ this.reviewScheduler = null;
1679
+ this.progressTracker = null;
1680
+ this.errorRecovery = null;
1681
+ try {
1682
+ if (this.ipcServer) {
1683
+ await this.ipcServer.stop();
1684
+ this.ipcServer = null;
1685
+ }
1686
+ }
1687
+ catch {
1688
+ }
1689
+ try {
1690
+ await this.store.deleteDaemonPid();
1691
+ }
1692
+ catch {
1693
+ }
1694
+ await this.closeLogStream();
1695
+ }
1696
+ }
1697
+ export async function startDaemon(options) {
1698
+ const baseDir = options?.baseDir ?? process.cwd();
1699
+ const port = options?.daemonConfig?.port ?? options?.port ?? 47691;
1700
+ const logger = options?.logger ?? noopLogger;
1701
+ if (await isDaemonRunning({ port })) {
1702
+ const store = new LoopStore({ baseDir, logger });
1703
+ const pidInfo = await store.readDaemonPid();
1704
+ throw new DaemonAlreadyRunningError(pidInfo?.pid ?? 0);
1705
+ }
1706
+ const daemonScript = options?.daemonScript ?? getDaemonEntryPoint();
1707
+ const args = [
1708
+ '--daemon',
1709
+ '--base-dir', baseDir,
1710
+ '--port', String(port),
1711
+ ];
1712
+ if (options?.daemonConfig?.logLevel) {
1713
+ args.push('--log-level', options.daemonConfig.logLevel);
1714
+ }
1715
+ const child = spawn(process.execPath, [daemonScript, ...args], {
1716
+ detached: true,
1717
+ stdio: 'ignore',
1718
+ cwd: baseDir,
1719
+ env: {
1720
+ ...process.env,
1721
+ FLOWDOT_DAEMON: 'true',
1722
+ },
1723
+ });
1724
+ child.unref();
1725
+ const startTime = Date.now();
1726
+ while (Date.now() - startTime < DAEMON_STARTUP_TIMEOUT_MS) {
1727
+ if (await isDaemonRunning({ port })) {
1728
+ const store = new LoopStore({ baseDir, logger });
1729
+ const pidInfo = await store.readDaemonPid();
1730
+ return { pid: pidInfo?.pid ?? child.pid ?? 0 };
1731
+ }
1732
+ await sleep(PROCESS_CHECK_INTERVAL_MS);
1733
+ }
1734
+ throw new DaemonStartFailedError('Daemon failed to start within timeout');
1735
+ }
1736
+ export async function stopDaemon(options) {
1737
+ const port = options?.port ?? 47691;
1738
+ const baseDir = options?.baseDir ?? process.cwd();
1739
+ const logger = options?.logger ?? noopLogger;
1740
+ if (!(await isDaemonRunning({ port }))) {
1741
+ throw new DaemonNotRunningError();
1742
+ }
1743
+ const client = new IPCClient({ port, logger });
1744
+ try {
1745
+ await client.connect();
1746
+ await client.request('shutdown', {});
1747
+ }
1748
+ finally {
1749
+ client.disconnect();
1750
+ }
1751
+ const startTime = Date.now();
1752
+ while (Date.now() - startTime < DAEMON_SHUTDOWN_TIMEOUT_MS) {
1753
+ if (!(await isDaemonRunning({ port }))) {
1754
+ return;
1755
+ }
1756
+ await sleep(PROCESS_CHECK_INTERVAL_MS);
1757
+ }
1758
+ const store = new LoopStore({ baseDir, logger });
1759
+ const pidInfo = await store.readDaemonPid();
1760
+ if (pidInfo) {
1761
+ try {
1762
+ process.kill(pidInfo.pid, 'SIGKILL');
1763
+ await store.deleteDaemonPid();
1764
+ }
1765
+ catch {
1766
+ }
1767
+ }
1768
+ }
1769
+ export async function getDaemonStatus(options) {
1770
+ const port = options?.port ?? 47691;
1771
+ const baseDir = options?.baseDir ?? process.cwd();
1772
+ const logger = options?.logger ?? noopLogger;
1773
+ if (!(await isDaemonRunning({ port }))) {
1774
+ const store = new LoopStore({ baseDir, logger });
1775
+ const pidInfo = await store.readDaemonPid();
1776
+ return {
1777
+ running: false,
1778
+ pid: pidInfo?.pid ?? null,
1779
+ startedAt: pidInfo?.startedAt ?? null,
1780
+ activeLoops: 0,
1781
+ uptime: null,
1782
+ };
1783
+ }
1784
+ const client = new IPCClient({ port, logger });
1785
+ try {
1786
+ await client.connect();
1787
+ return await client.request('status', {});
1788
+ }
1789
+ finally {
1790
+ client.disconnect();
1791
+ }
1792
+ }
1793
+ export function isProcessRunning(pid) {
1794
+ try {
1795
+ process.kill(pid, 0);
1796
+ return true;
1797
+ }
1798
+ catch {
1799
+ return false;
1800
+ }
1801
+ }
1802
+ function getDaemonEntryPoint() {
1803
+ if (process.env.FLOWDOT_DAEMON_SCRIPT) {
1804
+ return process.env.FLOWDOT_DAEMON_SCRIPT;
1805
+ }
1806
+ const cliPath = path.join(__dirname, 'daemon-runner.js');
1807
+ return cliPath;
1808
+ }
1809
+ function sleep(ms) {
1810
+ return new Promise((resolve) => setTimeout(resolve, ms));
1811
+ }
1812
+ export function createLoopDaemon(options) {
1813
+ return new LoopDaemon(options);
1814
+ }
1815
+ export async function runDaemon(options) {
1816
+ const daemon = createLoopDaemon(options);
1817
+ await daemon.start();
1818
+ await new Promise((resolve) => {
1819
+ daemon.on('stopped', resolve);
1820
+ });
1821
+ }