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