@adaptic/maestro 1.9.2 → 1.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,18 +3,22 @@
3
3
  // Maestro Daemon — Reactive event-driven message processor
4
4
  // =============================================================================
5
5
  //
6
- // Generic entry point that loads agent identity from config/agent.ts and
6
+ // Generic entry point that loads agent identity from config/agent.json and
7
7
  // delegates to the core daemon logic. This file is agent-name-agnostic.
8
8
  //
9
+ // Lifecycle:
10
+ // 1. Honour .emergency-stop BEFORE doing anything (don't acquire singleton
11
+ // lock, don't start consumer, don't import sophie-daemon). Stops the
12
+ // launchd restart treadmill cold.
13
+ // 2. Acquire the daemon singleton lock so only one instance runs.
14
+ // 3. Start the cadence consumer (state/cadence-bus/ drain loop).
15
+ // 4. Import the core daemon (sophie-daemon.mjs or legacy <firstName>-daemon.mjs).
16
+ //
9
17
  // Run: node scripts/daemon/maestro-daemon.mjs
10
- // Install: launchd plist with KeepAlive: true
18
+ // Install: launchd plist with KeepAlive.SuccessfulExit: false (clean exits
19
+ // during emergency-stop should NOT trigger restart; non-zero exits should).
11
20
  // =============================================================================
12
21
 
13
- // The core daemon implementation is in sophie-daemon.mjs by default, but
14
- // older agent repos may have a renamed copy at <firstname>-daemon.mjs (a
15
- // pre-1.7 convention). This entry-point auto-detects the correct file so
16
- // the launchd plist stays stable regardless of agent identity history.
17
-
18
22
  import { resolve, dirname, join } from "node:path";
19
23
  import { fileURLToPath } from "node:url";
20
24
  import { existsSync, readFileSync, readdirSync } from "node:fs";
@@ -22,25 +26,44 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
22
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
27
  const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
24
28
 
25
- // Set AGENT_DIR as env var so all modules can use it
26
29
  process.env.AGENT_DIR = AGENT_DIR;
27
30
  process.env.AGENT_ROOT = AGENT_DIR;
28
31
 
29
- // Singleton guard — prevent duplicate daemon instances
32
+ // ---------------------------------------------------------------------------
33
+ // 1. Emergency-stop gate
34
+ // ---------------------------------------------------------------------------
35
+ // If the operator dropped .emergency-stop, we exit cleanly (code 0). With
36
+ // KeepAlive.SuccessfulExit:false this means launchd will NOT restart us
37
+ // until the flag is lifted + the plist is reloaded by resume-operations.sh.
38
+ // Older plists with plain KeepAlive:true will still restart, so we hold
39
+ // the process for 30s before exiting to give launchd's ThrottleInterval
40
+ // something to throttle.
41
+ if (existsSync(join(AGENT_DIR, ".emergency-stop"))) {
42
+ console.error("[DAEMON] .emergency-stop flag present — refusing to start.");
43
+ console.error("[DAEMON] Lift with: scripts/resume-operations.sh");
44
+ await new Promise((r) => setTimeout(r, 30_000));
45
+ process.exit(0);
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // 2. Singleton guard
50
+ // ---------------------------------------------------------------------------
30
51
  try {
31
52
  const { acquireLock } = await import(resolve(process.env.HOME, "maestro/lib/singleton.js"));
32
53
  if (!acquireLock("daemon")) {
33
54
  console.log("[DAEMON] Already running — exiting");
34
55
  process.exit(0);
35
56
  }
36
- } catch { /* maestro singleton not available */ }
57
+ } catch { /* maestro singleton not available — proceed without lock */ }
37
58
 
38
- // Start the cadence consumer alongside the reactive event loop. This is the
39
- // single persistent owner of cadence housekeeping launchd plists enqueue
40
- // cadence ticks onto state/cadence-bus/ and the consumer drains them here,
41
- // either handling them inline or escalating to a sub-session when warranted.
42
- // Failure to start the consumer must NOT take the reactive daemon down, so
43
- // we isolate startup errors.
59
+ // ---------------------------------------------------------------------------
60
+ // 3. Cadence consumer (cadence bus drain loop)
61
+ // ---------------------------------------------------------------------------
62
+ // Start alongside the reactive event loop. This is the single persistent
63
+ // owner of cadence housekeeping launchd plists enqueue cadence ticks
64
+ // onto state/cadence-bus/ and the consumer drains them here, either
65
+ // handling them inline or escalating to a sub-session when warranted.
66
+ // Failure to start the consumer must NOT take the reactive daemon down.
44
67
  try {
45
68
  const { startConsumer } = await import("./cadence-consumer.mjs");
46
69
  const consumer = startConsumer({ agentRoot: AGENT_DIR });
@@ -55,7 +78,10 @@ try {
55
78
  // Reactive daemon continues. Doctor / healthcheck will surface this.
56
79
  }
57
80
 
58
- // Locate the core daemon module. Try, in order:
81
+ // ---------------------------------------------------------------------------
82
+ // 4. Core daemon import
83
+ // ---------------------------------------------------------------------------
84
+ // Resolve the core daemon module. Try, in order:
59
85
  // 1. ./sophie-daemon.mjs — canonical filename (post-Phase-2.5 SOT)
60
86
  // 2. ./<firstName>-daemon.mjs — legacy rename from init-maestro Phase 1
61
87
  // 3. The first scripts/daemon/*-daemon.mjs that isn't this file
@@ -129,13 +129,18 @@ PLIST_MID
129
129
  cat >> "$FILE" << 'PLIST_KEEPALIVE'
130
130
 
131
131
  <key>KeepAlive</key>
132
- <true/>
132
+ <dict>
133
+ <key>SuccessfulExit</key>
134
+ <false/>
135
+ <key>Crashed</key>
136
+ <true/>
137
+ </dict>
133
138
 
134
139
  <key>RunAtLoad</key>
135
140
  <true/>
136
141
 
137
142
  <key>ThrottleInterval</key>
138
- <integer>5</integer>
143
+ <integer>30</integer>
139
144
  PLIST_KEEPALIVE
140
145
  fi
141
146
 
@@ -167,7 +167,12 @@ test("daemon plist remains a KeepAlive job (not a cadence enqueue)", async () =>
167
167
  runGenerator(root);
168
168
  const path = join(root, "scripts/local-triggers/plists/ai.adaptic.erin-daemon.plist");
169
169
  const body = readFileSync(path, "utf-8");
170
- assert.match(body, /<key>KeepAlive<\/key>\s*<true\/>/);
170
+ // KeepAlive switched 1.9.2 from plain `<true/>` to a dict with
171
+ // SuccessfulExit:false / Crashed:true so emergency-stop doesn't trigger
172
+ // a restart treadmill but real crashes still do.
173
+ assert.match(body, /<key>KeepAlive<\/key>/);
174
+ assert.match(body, /<key>SuccessfulExit<\/key>\s*<false\/>/);
175
+ assert.match(body, /<key>Crashed<\/key>\s*<true\/>/);
171
176
  assert.match(body, /launchd-wrapper\.sh/);
172
177
  assert.ok(!body.includes("enqueue-cadence-tick.mjs"),
173
178
  "daemon plist should not enqueue ticks — it IS the consumer");