@kmmao/happy-agent 0.4.1 → 0.5.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.
package/dist/index.cjs CHANGED
@@ -18,7 +18,7 @@ var path = require('path');
18
18
  var fs = require('fs');
19
19
  var os = require('os');
20
20
 
21
- var version = "0.4.1";
21
+ var version = "0.5.0";
22
22
 
23
23
  function loadConfig() {
24
24
  const serverUrl = (process.env.HAPPY_SERVER_URL ?? "https://happyserve.xycloud.info").replace(/\/+$/, "");
@@ -2025,11 +2025,11 @@ function stopSession(pid) {
2025
2025
  }
2026
2026
  }
2027
2027
 
2028
- const PROMPT_DIR = path.join(os.tmpdir(), "happy", "agent-prompts");
2028
+ const PROMPT_DIR$1 = path.join(os.tmpdir(), "happy", "agent-prompts");
2029
2029
  async function writePromptFile(prefix, content) {
2030
- await promises.mkdir(PROMPT_DIR, { recursive: true });
2030
+ await promises.mkdir(PROMPT_DIR$1, { recursive: true });
2031
2031
  const filename = `${prefix}-${Date.now()}.md`;
2032
- const filepath = path.join(PROMPT_DIR, filename);
2032
+ const filepath = path.join(PROMPT_DIR$1, filename);
2033
2033
  await promises.writeFile(filepath, content, "utf-8");
2034
2034
  return filepath;
2035
2035
  }
@@ -2213,6 +2213,8 @@ class MachineClient {
2213
2213
  automationServerUrl = "";
2214
2214
  automationAuthToken = "";
2215
2215
  scheduler = null;
2216
+ loopCoordinator = null;
2217
+ auditStore = null;
2216
2218
  constructor(opts) {
2217
2219
  this.token = opts.token;
2218
2220
  this.machine = opts.machine;
@@ -2254,6 +2256,34 @@ class MachineClient {
2254
2256
  return { sessions };
2255
2257
  });
2256
2258
  }
2259
+ registerLoopHandlers() {
2260
+ const coord = this.loopCoordinator;
2261
+ this.rpcHandlerManager.registerHandler("create-loop", async (data) => {
2262
+ const loop = coord.createLoop(data);
2263
+ return { loop: { id: loop.id, name: loop.name, state: loop.state } };
2264
+ });
2265
+ this.rpcHandlerManager.registerHandler("list-loops", async () => {
2266
+ return { loops: coord.listLoops() };
2267
+ });
2268
+ this.rpcHandlerManager.registerHandler("pause-loop", async (data) => {
2269
+ return { success: coord.pauseLoop(data.loopId) };
2270
+ });
2271
+ this.rpcHandlerManager.registerHandler("resume-loop", async (data) => {
2272
+ return { success: coord.resumeLoop(data.loopId) };
2273
+ });
2274
+ this.rpcHandlerManager.registerHandler("delete-loop", async (data) => {
2275
+ return { success: coord.deleteLoop(data.loopId) };
2276
+ });
2277
+ }
2278
+ registerAuditHandlers() {
2279
+ const audit = this.auditStore;
2280
+ this.rpcHandlerManager.registerHandler("query-audit-log", async (data) => {
2281
+ return { events: audit.query(data) };
2282
+ });
2283
+ this.rpcHandlerManager.registerHandler("audit-summary", async () => {
2284
+ return { summary: audit.summarize() };
2285
+ });
2286
+ }
2257
2287
  // -----------------------------------------------------------------------
2258
2288
  // Connection
2259
2289
  // -----------------------------------------------------------------------
@@ -2435,11 +2465,19 @@ class MachineClient {
2435
2465
  * Enable automation handling — agent will process webhook, supervisor,
2436
2466
  * and task triggers from the server by spawning Happy CLI sessions.
2437
2467
  */
2438
- enableAutomation(serverUrl, authToken, scheduler) {
2468
+ enableAutomation(serverUrl, authToken, scheduler, loopCoordinator, auditStore) {
2439
2469
  this.automationEnabled = true;
2440
2470
  this.automationServerUrl = serverUrl;
2441
2471
  this.automationAuthToken = authToken;
2442
2472
  this.scheduler = scheduler;
2473
+ this.loopCoordinator = loopCoordinator ?? null;
2474
+ this.auditStore = auditStore ?? null;
2475
+ if (this.loopCoordinator) {
2476
+ this.registerLoopHandlers();
2477
+ }
2478
+ if (this.auditStore) {
2479
+ this.registerAuditHandlers();
2480
+ }
2443
2481
  logger.debug("[MACHINE] Automation enabled");
2444
2482
  }
2445
2483
  /** Internal dispatch for ephemeral events that need automation handling. */
@@ -2560,6 +2598,7 @@ class AutomationScheduler {
2560
2598
  retryDelayMs;
2561
2599
  defaultMaxAttempts;
2562
2600
  maxRecentCompletions;
2601
+ onAudit;
2563
2602
  /** Active jobs indexed by id. */
2564
2603
  jobs = /* @__PURE__ */ new Map();
2565
2604
  /** dedupeKey → jobId for fast dedup lookups. */
@@ -2573,6 +2612,7 @@ class AutomationScheduler {
2573
2612
  this.retryDelayMs = options?.retryDelayMs ?? 5e3;
2574
2613
  this.defaultMaxAttempts = options?.maxAttempts ?? 3;
2575
2614
  this.maxRecentCompletions = options?.maxRecentCompletions ?? 50;
2615
+ this.onAudit = options?.onAudit ?? null;
2576
2616
  this.pumpTimer = setInterval(() => this.pump(), 1e3);
2577
2617
  }
2578
2618
  // -----------------------------------------------------------------------
@@ -2603,6 +2643,7 @@ class AutomationScheduler {
2603
2643
  this.jobs.set(job.id, job);
2604
2644
  this.dedupeIndex.set(job.dedupeKey, job.id);
2605
2645
  logger.debug(`[SCHEDULER] Enqueued: ${job.kind} ${job.dedupeKey} (${job.priority}) id=${job.id}`);
2646
+ this.onAudit?.({ kind: "job_enqueued", jobId: job.id, dedupeKey: job.dedupeKey, message: `${job.kind}:${job.priority}` });
2606
2647
  this.pump();
2607
2648
  return { job, deduped: false };
2608
2649
  }
@@ -2612,6 +2653,7 @@ class AutomationScheduler {
2612
2653
  job.status = "completed";
2613
2654
  job.updatedAt = Date.now();
2614
2655
  logger.debug(`[SCHEDULER] Completed: ${job.kind} ${job.dedupeKey} id=${jobId}`);
2656
+ this.onAudit?.({ kind: "job_completed", jobId, dedupeKey: job.dedupeKey });
2615
2657
  this.finalize(job);
2616
2658
  }
2617
2659
  markFailed(jobId, error) {
@@ -2623,11 +2665,13 @@ class AutomationScheduler {
2623
2665
  job.status = "queued";
2624
2666
  job.nextRunAt = Date.now() + job.attempt * this.retryDelayMs;
2625
2667
  logger.debug(`[SCHEDULER] Retry queued: ${job.dedupeKey} attempt=${job.attempt}/${job.maxAttempts} nextRunAt=+${job.attempt * this.retryDelayMs}ms`);
2668
+ this.onAudit?.({ kind: "job_retried", jobId, dedupeKey: job.dedupeKey, errorMessage: error, message: `attempt ${job.attempt}/${job.maxAttempts}` });
2626
2669
  this.pump();
2627
2670
  return;
2628
2671
  }
2629
2672
  job.status = "failed";
2630
2673
  logger.debug(`[SCHEDULER] Failed (exhausted): ${job.kind} ${job.dedupeKey} id=${jobId}: ${error}`);
2674
+ this.onAudit?.({ kind: "job_failed", jobId, dedupeKey: job.dedupeKey, errorMessage: error });
2631
2675
  this.finalize(job);
2632
2676
  }
2633
2677
  getStatus() {
@@ -2687,6 +2731,7 @@ class AutomationScheduler {
2687
2731
  job.attempt++;
2688
2732
  job.updatedAt = Date.now();
2689
2733
  logger.debug(`[SCHEDULER] Dispatching: ${job.kind} ${job.dedupeKey} attempt=${job.attempt}`);
2734
+ this.onAudit?.({ kind: "job_dispatched", jobId: job.id, dedupeKey: job.dedupeKey, message: `attempt ${job.attempt}` });
2690
2735
  job.run(job.id).then(({ pid }) => {
2691
2736
  if (job.status === "dispatching") {
2692
2737
  job.status = "running";
@@ -2722,6 +2767,364 @@ class AutomationScheduler {
2722
2767
  }
2723
2768
  }
2724
2769
 
2770
+ const PROMPT_DIR = path.join(os.tmpdir(), "happy", "agent-loop-prompts");
2771
+ const DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
2772
+ const DEFAULT_MAX_ITERATIONS = 0;
2773
+ class AgentLoopCoordinator {
2774
+ loops = /* @__PURE__ */ new Map();
2775
+ scheduler;
2776
+ serverUrl;
2777
+ authToken;
2778
+ guardian;
2779
+ tickTimer = null;
2780
+ constructor(scheduler, serverUrl, authToken, guardian) {
2781
+ this.scheduler = scheduler;
2782
+ this.serverUrl = serverUrl;
2783
+ this.authToken = authToken;
2784
+ this.guardian = guardian ?? null;
2785
+ }
2786
+ // -----------------------------------------------------------------------
2787
+ // Lifecycle
2788
+ // -----------------------------------------------------------------------
2789
+ start() {
2790
+ if (this.tickTimer) return;
2791
+ this.tickTimer = setInterval(() => this.tick(), 1e3);
2792
+ logger.debug("[LOOP] Coordinator started");
2793
+ }
2794
+ shutdown() {
2795
+ if (this.tickTimer) {
2796
+ clearInterval(this.tickTimer);
2797
+ this.tickTimer = null;
2798
+ }
2799
+ logger.debug("[LOOP] Coordinator shutdown");
2800
+ }
2801
+ // -----------------------------------------------------------------------
2802
+ // CRUD
2803
+ // -----------------------------------------------------------------------
2804
+ createLoop(input) {
2805
+ const loop = {
2806
+ id: crypto.randomUUID(),
2807
+ name: input.name,
2808
+ prompt: input.prompt,
2809
+ directory: input.directory,
2810
+ intervalMs: Math.max(input.intervalMs, 1e4),
2811
+ // min 10s
2812
+ createdAt: Date.now(),
2813
+ state: "idle",
2814
+ iteration: 0,
2815
+ nextRunAt: Date.now() + Math.max(input.intervalMs, 1e4),
2816
+ consecutiveFailures: 0,
2817
+ maxConsecutiveFailures: input.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES,
2818
+ maxIterations: input.maxIterations ?? DEFAULT_MAX_ITERATIONS
2819
+ };
2820
+ this.loops.set(loop.id, loop);
2821
+ logger.debug(`[LOOP] Created: ${loop.name} (${loop.id}) interval=${loop.intervalMs}ms`);
2822
+ return loop;
2823
+ }
2824
+ getLoop(id) {
2825
+ return this.loops.get(id);
2826
+ }
2827
+ listLoops() {
2828
+ return [...this.loops.values()].map((l) => ({
2829
+ id: l.id,
2830
+ name: l.name,
2831
+ state: l.state,
2832
+ iteration: l.iteration,
2833
+ intervalMs: l.intervalMs,
2834
+ nextRunAt: l.nextRunAt,
2835
+ lastCompletedAt: l.lastCompletedAt
2836
+ }));
2837
+ }
2838
+ pauseLoop(id) {
2839
+ const loop = this.loops.get(id);
2840
+ if (!loop || loop.state === "paused") return false;
2841
+ loop.state = "paused";
2842
+ logger.debug(`[LOOP] Paused: ${loop.name} (${id})`);
2843
+ return true;
2844
+ }
2845
+ resumeLoop(id) {
2846
+ const loop = this.loops.get(id);
2847
+ if (!loop || loop.state !== "paused") return false;
2848
+ loop.state = "idle";
2849
+ loop.nextRunAt = Date.now() + loop.intervalMs;
2850
+ loop.consecutiveFailures = 0;
2851
+ loop.errorMessage = void 0;
2852
+ logger.debug(`[LOOP] Resumed: ${loop.name} (${id})`);
2853
+ return true;
2854
+ }
2855
+ deleteLoop(id) {
2856
+ const deleted = this.loops.delete(id);
2857
+ if (deleted) logger.debug(`[LOOP] Deleted: ${id}`);
2858
+ return deleted;
2859
+ }
2860
+ // -----------------------------------------------------------------------
2861
+ // Scheduler callback
2862
+ // -----------------------------------------------------------------------
2863
+ onJobTerminal(loopId, status, errorMessage) {
2864
+ const loop = this.loops.get(loopId);
2865
+ if (!loop) return;
2866
+ loop.activeJobId = void 0;
2867
+ loop.lastCompletedAt = Date.now();
2868
+ if (status === "completed") {
2869
+ loop.consecutiveFailures = 0;
2870
+ loop.state = "idle";
2871
+ loop.nextRunAt = Date.now() + loop.intervalMs;
2872
+ logger.debug(`[LOOP] Iteration ${loop.iteration} completed: ${loop.name}`);
2873
+ } else {
2874
+ loop.consecutiveFailures++;
2875
+ loop.errorMessage = errorMessage;
2876
+ if (loop.consecutiveFailures >= loop.maxConsecutiveFailures) {
2877
+ loop.state = "blocked";
2878
+ logger.debug(`[LOOP] Blocked after ${loop.consecutiveFailures} failures: ${loop.name}`);
2879
+ } else {
2880
+ loop.state = "idle";
2881
+ loop.nextRunAt = Date.now() + loop.intervalMs;
2882
+ logger.debug(`[LOOP] Iteration ${loop.iteration} failed (${loop.consecutiveFailures}/${loop.maxConsecutiveFailures}): ${loop.name}`);
2883
+ }
2884
+ }
2885
+ }
2886
+ // -----------------------------------------------------------------------
2887
+ // Tick
2888
+ // -----------------------------------------------------------------------
2889
+ tick() {
2890
+ const now = Date.now();
2891
+ for (const loop of this.loops.values()) {
2892
+ if (loop.state !== "idle") continue;
2893
+ if (loop.nextRunAt > now) continue;
2894
+ if (loop.maxIterations > 0 && loop.iteration >= loop.maxIterations) {
2895
+ loop.state = "paused";
2896
+ logger.debug(`[LOOP] Max iterations reached (${loop.maxIterations}): ${loop.name}`);
2897
+ continue;
2898
+ }
2899
+ this.enqueueLoop(loop);
2900
+ }
2901
+ }
2902
+ enqueueLoop(loop) {
2903
+ loop.iteration++;
2904
+ loop.state = "active";
2905
+ loop.lastStartedAt = Date.now();
2906
+ const iterationNum = loop.iteration;
2907
+ const loopId = loop.id;
2908
+ const coordinator = this;
2909
+ const guardianSessionId = this.guardian?.resolve({ loopId: loop.id }) ?? void 0;
2910
+ const { job, deduped } = this.scheduler.enqueue({
2911
+ kind: "task",
2912
+ dedupeKey: `agent-loop:${loop.id}:${loop.iteration}`,
2913
+ priority: "background",
2914
+ run: async (jobId) => {
2915
+ const promptFile = await writeLoopPromptFile(loop.name, loop.prompt, iterationNum);
2916
+ const result = await spawnSession({
2917
+ directory: loop.directory,
2918
+ approvedNewDirectoryCreation: false,
2919
+ happySessionId: guardianSessionId,
2920
+ automationContext: {
2921
+ kind: "agent_loop",
2922
+ trigger: `loop:${loop.name}:iteration-${iterationNum}`
2923
+ },
2924
+ environmentVariables: {
2925
+ HAPPY_INITIAL_PROMPT_FILE: promptFile,
2926
+ HAPPY_LOOP_ID: loopId,
2927
+ HAPPY_LOOP_NAME: loop.name,
2928
+ HAPPY_LOOP_ITERATION: String(iterationNum),
2929
+ HAPPY_SERVER_URL: coordinator.serverUrl,
2930
+ HAPPY_AUTH_TOKEN: coordinator.authToken
2931
+ }
2932
+ });
2933
+ if (result.type !== "success") {
2934
+ throw new Error(result.type === "error" ? result.errorMessage : "Directory not approved");
2935
+ }
2936
+ const tracked = (await Promise.resolve().then(function () { return trackedSessions; })).getTrackedSession(result.pid);
2937
+ if (tracked?.childProcess) {
2938
+ tracked.childProcess.on("exit", (code) => {
2939
+ const status = code === 0 ? "completed" : "failed";
2940
+ coordinator.onJobTerminal(loopId, status, code !== 0 ? `exit code ${code}` : void 0);
2941
+ if (code === 0) coordinator.scheduler.markCompleted(jobId);
2942
+ else coordinator.scheduler.markFailed(jobId, `exit code ${code}`);
2943
+ });
2944
+ }
2945
+ return { pid: result.pid };
2946
+ }
2947
+ });
2948
+ if (deduped) {
2949
+ loop.iteration--;
2950
+ loop.state = "idle";
2951
+ logger.debug(`[LOOP] Enqueue deduped: ${loop.name} iteration ${iterationNum}`);
2952
+ } else {
2953
+ loop.activeJobId = job.id;
2954
+ logger.debug(`[LOOP] Enqueued: ${loop.name} iteration ${iterationNum} job=${job.id}`);
2955
+ }
2956
+ }
2957
+ }
2958
+ async function writeLoopPromptFile(name, prompt, iteration) {
2959
+ await promises.mkdir(PROMPT_DIR, { recursive: true });
2960
+ const filename = `loop-${name.replace(/[^a-zA-Z0-9-]/g, "_")}-${iteration}-${Date.now()}.md`;
2961
+ const filepath = path.join(PROMPT_DIR, filename);
2962
+ const content = [
2963
+ `# Agent Loop: ${name}`,
2964
+ `Iteration: ${iteration}`,
2965
+ "",
2966
+ prompt
2967
+ ].join("\n");
2968
+ await promises.writeFile(filepath, content, "utf-8");
2969
+ return filepath;
2970
+ }
2971
+
2972
+ class GuardianSessionRegistry {
2973
+ entries = /* @__PURE__ */ new Map();
2974
+ /**
2975
+ * Find an existing session to reuse.
2976
+ * Tries loop key first, then project key.
2977
+ */
2978
+ resolve(input) {
2979
+ if (input.loopId) {
2980
+ const entry = this.entries.get(`loop:${input.loopId}`);
2981
+ if (entry) {
2982
+ logger.debug(`[GUARDIAN] Resolved session ${entry.sessionId} for loop:${input.loopId}`);
2983
+ return entry.sessionId;
2984
+ }
2985
+ }
2986
+ if (input.projectId) {
2987
+ const entry = this.entries.get(`project:${input.projectId}`);
2988
+ if (entry) {
2989
+ logger.debug(`[GUARDIAN] Resolved session ${entry.sessionId} for project:${input.projectId}`);
2990
+ return entry.sessionId;
2991
+ }
2992
+ }
2993
+ return null;
2994
+ }
2995
+ /**
2996
+ * Remember a session for future reuse.
2997
+ * Stores under both loop and project keys if available.
2998
+ */
2999
+ remember(sessionId, input) {
3000
+ const now = Date.now();
3001
+ if (input.loopId) {
3002
+ const key = `loop:${input.loopId}`;
3003
+ this.entries.set(key, {
3004
+ key,
3005
+ sessionId,
3006
+ loopId: input.loopId,
3007
+ projectId: input.projectId,
3008
+ updatedAt: now
3009
+ });
3010
+ logger.debug(`[GUARDIAN] Remembered ${sessionId} for ${key}`);
3011
+ }
3012
+ if (input.projectId) {
3013
+ const key = `project:${input.projectId}`;
3014
+ if (!this.entries.has(key)) {
3015
+ this.entries.set(key, {
3016
+ key,
3017
+ sessionId,
3018
+ projectId: input.projectId,
3019
+ updatedAt: now
3020
+ });
3021
+ logger.debug(`[GUARDIAN] Remembered ${sessionId} for ${key}`);
3022
+ }
3023
+ }
3024
+ }
3025
+ /**
3026
+ * Forget a specific session (e.g., after it exits).
3027
+ */
3028
+ forgetSession(sessionId) {
3029
+ let removed = 0;
3030
+ for (const [key, entry] of this.entries) {
3031
+ if (entry.sessionId === sessionId) {
3032
+ this.entries.delete(key);
3033
+ removed++;
3034
+ }
3035
+ }
3036
+ if (removed > 0) {
3037
+ logger.debug(`[GUARDIAN] Forgot session ${sessionId} (${removed} entries)`);
3038
+ }
3039
+ return removed;
3040
+ }
3041
+ /**
3042
+ * Forget all entries for a loop.
3043
+ */
3044
+ forgetLoop(loopId) {
3045
+ return this.entries.delete(`loop:${loopId}`);
3046
+ }
3047
+ /**
3048
+ * Get all entries (for observability).
3049
+ */
3050
+ getSnapshot() {
3051
+ return [...this.entries.values()].sort((a, b) => b.updatedAt - a.updatedAt);
3052
+ }
3053
+ /**
3054
+ * Number of tracked guardian entries.
3055
+ */
3056
+ get size() {
3057
+ return this.entries.size;
3058
+ }
3059
+ }
3060
+
3061
+ class AutomationAuditStore {
3062
+ maxEntries;
3063
+ events = [];
3064
+ nextId = 1;
3065
+ constructor(options) {
3066
+ this.maxEntries = options?.maxEntries ?? 500;
3067
+ }
3068
+ /**
3069
+ * Record an audit event.
3070
+ */
3071
+ record(event) {
3072
+ const entry = {
3073
+ ...event,
3074
+ id: this.nextId++,
3075
+ timestamp: Date.now()
3076
+ };
3077
+ this.events.push(entry);
3078
+ while (this.events.length > this.maxEntries) {
3079
+ this.events.shift();
3080
+ }
3081
+ logger.debug(`[AUDIT] ${entry.kind}: ${entry.message ?? entry.dedupeKey ?? entry.jobId ?? ""}`);
3082
+ return entry;
3083
+ }
3084
+ /**
3085
+ * Query events with optional filters.
3086
+ */
3087
+ query(filter) {
3088
+ const limit = filter?.limit ?? 50;
3089
+ let results = this.events;
3090
+ if (filter?.kind) {
3091
+ results = results.filter((e) => e.kind === filter.kind);
3092
+ }
3093
+ if (filter?.loopId) {
3094
+ results = results.filter((e) => e.loopId === filter.loopId);
3095
+ }
3096
+ if (filter?.since) {
3097
+ results = results.filter((e) => e.timestamp >= filter.since);
3098
+ }
3099
+ return results.slice(-limit).reverse();
3100
+ }
3101
+ /**
3102
+ * Summary counts by kind.
3103
+ */
3104
+ summarize() {
3105
+ const counts = {
3106
+ job_enqueued: 0,
3107
+ job_dispatched: 0,
3108
+ job_completed: 0,
3109
+ job_failed: 0,
3110
+ job_retried: 0,
3111
+ loop_started: 0,
3112
+ loop_blocked: 0,
3113
+ loop_paused: 0
3114
+ };
3115
+ for (const event of this.events) {
3116
+ counts[event.kind]++;
3117
+ }
3118
+ return counts;
3119
+ }
3120
+ /**
3121
+ * Total number of stored events.
3122
+ */
3123
+ get size() {
3124
+ return this.events.length;
3125
+ }
3126
+ }
3127
+
2725
3128
  function pidFilePath(homeDir) {
2726
3129
  return node_path.join(homeDir, "agent-daemon.pid");
2727
3130
  }
@@ -2789,9 +3192,16 @@ async function startDaemon(options) {
2789
3192
  agentVersion: version,
2790
3193
  workingDirectory: workDir
2791
3194
  });
2792
- const scheduler = new AutomationScheduler({ maxConcurrentJobs: 2 });
3195
+ const auditStore = new AutomationAuditStore();
3196
+ const scheduler = new AutomationScheduler({
3197
+ maxConcurrentJobs: 2,
3198
+ onAudit: (event) => auditStore.record(event)
3199
+ });
3200
+ const guardian = new GuardianSessionRegistry();
3201
+ const loopCoordinator = new AgentLoopCoordinator(scheduler, config.serverUrl, creds.token, guardian);
2793
3202
  client.setTailscaleInfo(fullTailscale);
2794
- client.enableAutomation(config.serverUrl, creds.token, scheduler);
3203
+ client.enableAutomation(config.serverUrl, creds.token, scheduler, loopCoordinator, auditStore);
3204
+ loopCoordinator.start();
2795
3205
  client.connect();
2796
3206
  writePidFile(config.homeDir, process.pid);
2797
3207
  console.log(`Daemon started (PID ${process.pid})`);
@@ -2802,6 +3212,7 @@ async function startDaemon(options) {
2802
3212
  logger.debug(`[DAEMON] Received ${signal}, shutting down...`);
2803
3213
  console.log(`
2804
3214
  Received ${signal}, shutting down...`);
3215
+ loopCoordinator.shutdown();
2805
3216
  scheduler.shutdown();
2806
3217
  client.shutdown();
2807
3218
  removePidFile(config.homeDir);