@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
|
@@ -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.
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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
|
-
//
|
|
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
|
-
<
|
|
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>
|
|
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
|
-
|
|
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");
|