@yefengr/remote-pi 0.7.4 → 0.7.6
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/README.md +81 -586
- package/dist/bin/supervisord.js +4 -5
- package/dist/bin/supervisord.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/daemon/cli-links.d.ts +30 -0
- package/dist/daemon/cli-links.js +134 -0
- package/dist/daemon/cli-links.js.map +1 -0
- package/dist/daemon/commands.d.ts +27 -0
- package/dist/daemon/commands.js +295 -0
- package/dist/daemon/commands.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +53 -31
- package/dist/daemon/control_protocol.js +8 -31
- package/dist/daemon/control_protocol.js.map +1 -1
- package/dist/daemon/cron_log.d.ts +2 -23
- package/dist/daemon/cron_log.js +10 -25
- package/dist/daemon/cron_log.js.map +1 -1
- package/dist/daemon/cron_registry.d.ts +3 -40
- package/dist/daemon/cron_registry.js +58 -103
- package/dist/daemon/cron_registry.js.map +1 -1
- package/dist/daemon/install-runtime.d.ts +10 -0
- package/dist/daemon/install-runtime.js +81 -0
- package/dist/daemon/install-runtime.js.map +1 -0
- package/dist/daemon/install.d.ts +3 -63
- package/dist/daemon/install.js +5 -289
- package/dist/daemon/install.js.map +1 -1
- package/dist/daemon/preflight.d.ts +51 -0
- package/dist/daemon/preflight.js +136 -0
- package/dist/daemon/preflight.js.map +1 -0
- package/dist/daemon/registry.d.ts +33 -45
- package/dist/daemon/registry.js +171 -117
- package/dist/daemon/registry.js.map +1 -1
- package/dist/daemon/rpc_child.d.ts +92 -146
- package/dist/daemon/rpc_child.js +336 -263
- package/dist/daemon/rpc_child.js.map +1 -1
- package/dist/daemon/status.d.ts +45 -0
- package/dist/daemon/status.js +87 -0
- package/dist/daemon/status.js.map +1 -0
- package/dist/daemon/supervisor-ipc.d.ts +2 -0
- package/dist/daemon/supervisor-ipc.js +20 -0
- package/dist/daemon/supervisor-ipc.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +48 -71
- package/dist/daemon/supervisor.js +430 -420
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/extension_ui_bridge.d.ts +4 -3
- package/dist/extension_ui_bridge.js +10 -0
- package/dist/extension_ui_bridge.js.map +1 -1
- package/dist/index.d.ts +25 -230
- package/dist/index.js +515 -5553
- package/dist/index.js.map +1 -1
- package/dist/pairing/crypto.d.ts +8 -1
- package/dist/pairing/crypto.js +56 -6
- package/dist/pairing/crypto.js.map +1 -1
- package/dist/pairing/owner_storage.d.ts +42 -0
- package/dist/pairing/owner_storage.js +207 -0
- package/dist/pairing/owner_storage.js.map +1 -0
- package/dist/pairing/qr.d.ts +3 -42
- package/dist/pairing/qr.js +9 -59
- package/dist/pairing/qr.js.map +1 -1
- package/dist/pairing/storage.d.ts +3 -45
- package/dist/pairing/storage.js +10 -223
- package/dist/pairing/storage.js.map +1 -1
- package/dist/protocol/types.d.ts +1 -1
- package/dist/protocol/v2/schemas.d.ts +1 -1
- package/dist/protocol/v2/schemas.js +1 -1
- package/dist/protocol/v2/schemas.js.map +1 -1
- package/dist/runtime/owner_router.d.ts +16 -0
- package/dist/runtime/owner_router.js +37 -0
- package/dist/runtime/owner_router.js.map +1 -0
- package/dist/session/ipc.d.ts +1 -22
- package/dist/session/ipc.js +4 -11
- package/dist/session/ipc.js.map +1 -1
- package/dist/session/local_config.d.ts +4 -47
- package/dist/session/local_config.js +19 -101
- package/dist/session/local_config.js.map +1 -1
- package/dist/transport/peer_channel.d.ts +25 -39
- package/dist/transport/peer_channel.js +74 -131
- package/dist/transport/peer_channel.js.map +1 -1
- package/dist/transport/relay_client.d.ts +26 -70
- package/dist/transport/relay_client.js +37 -106
- package/dist/transport/relay_client.js.map +1 -1
- package/docs/daemon.md +91 -207
- package/package.json +21 -17
- package/dist/daemon/id.d.ts +0 -24
- package/dist/daemon/id.js +0 -36
- package/dist/daemon/id.js.map +0 -1
- package/dist/mcp/mesh_result.d.ts +0 -10
- package/dist/mcp/mesh_result.js +0 -42
- package/dist/mcp/mesh_result.js.map +0 -1
- package/dist/mcp/mesh_server.d.ts +0 -16
- package/dist/mcp/mesh_server.js +0 -327
- package/dist/mcp/mesh_server.js.map +0 -1
- package/dist/mesh/canonical.d.ts +0 -30
- package/dist/mesh/canonical.js +0 -61
- package/dist/mesh/canonical.js.map +0 -1
- package/dist/mesh/client.d.ts +0 -18
- package/dist/mesh/client.js +0 -125
- package/dist/mesh/client.js.map +0 -1
- package/dist/mesh/encoding.d.ts +0 -38
- package/dist/mesh/encoding.js +0 -216
- package/dist/mesh/encoding.js.map +0 -1
- package/dist/mesh/self_revoke.d.ts +0 -73
- package/dist/mesh/self_revoke.js +0 -401
- package/dist/mesh/self_revoke.js.map +0 -1
- package/dist/mesh/siblings.d.ts +0 -49
- package/dist/mesh/siblings.js +0 -159
- package/dist/mesh/siblings.js.map +0 -1
- package/dist/mesh/types.d.ts +0 -34
- package/dist/mesh/types.js +0 -11
- package/dist/mesh/types.js.map +0 -1
- package/dist/mesh/verify.d.ts +0 -17
- package/dist/mesh/verify.js +0 -86
- package/dist/mesh/verify.js.map +0 -1
- package/dist/rooms.d.ts +0 -29
- package/dist/rooms.js +0 -57
- package/dist/rooms.js.map +0 -1
- package/dist/session/bridge.d.ts +0 -37
- package/dist/session/bridge.js +0 -130
- package/dist/session/bridge.js.map +0 -1
- package/dist/session/broker.d.ts +0 -189
- package/dist/session/broker.js +0 -507
- package/dist/session/broker.js.map +0 -1
- package/dist/session/broker_remote.d.ts +0 -74
- package/dist/session/broker_remote.js +0 -583
- package/dist/session/broker_remote.js.map +0 -1
- package/dist/session/cwd_lock.d.ts +0 -33
- package/dist/session/cwd_lock.js +0 -126
- package/dist/session/cwd_lock.js.map +0 -1
- package/dist/session/envelope.d.ts +0 -33
- package/dist/session/envelope.js +0 -117
- package/dist/session/envelope.js.map +0 -1
- package/dist/session/global_config.d.ts +0 -33
- package/dist/session/global_config.js +0 -73
- package/dist/session/global_config.js.map +0 -1
- package/dist/session/leader_election.d.ts +0 -32
- package/dist/session/leader_election.js +0 -106
- package/dist/session/leader_election.js.map +0 -1
- package/dist/session/mesh_node.d.ts +0 -116
- package/dist/session/mesh_node.js +0 -573
- package/dist/session/mesh_node.js.map +0 -1
- package/dist/session/peer.d.ts +0 -139
- package/dist/session/peer.js +0 -451
- package/dist/session/peer.js.map +0 -1
- package/dist/session/peer_inventory.d.ts +0 -13
- package/dist/session/peer_inventory.js +0 -48
- package/dist/session/peer_inventory.js.map +0 -1
- package/dist/session/peer_limits.d.ts +0 -9
- package/dist/session/peer_limits.js +0 -21
- package/dist/session/peer_limits.js.map +0 -1
- package/dist/session/setup_wizard.d.ts +0 -39
- package/dist/session/setup_wizard.js +0 -63
- package/dist/session/setup_wizard.js.map +0 -1
- package/dist/session/tools.d.ts +0 -24
- package/dist/session/tools.js +0 -252
- package/dist/session/tools.js.map +0 -1
- package/dist/session/wizard.d.ts +0 -13
- package/dist/session/wizard.js +0 -20
- package/dist/session/wizard.js.map +0 -1
- package/dist/transport/pi_forward_client.d.ts +0 -29
- package/dist/transport/pi_forward_client.js +0 -62
- package/dist/transport/pi_forward_client.js.map +0 -1
- package/dist/ui/footer.d.ts +0 -32
- package/dist/ui/footer.js +0 -34
- package/dist/ui/footer.js.map +0 -1
- package/skills/agent-network/SKILL.md +0 -310
|
@@ -1,515 +1,426 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Cron } from "croner";
|
|
6
|
+
import { addDaemon, findDaemonByCwd, listDaemons, removeDaemon, setDaemonDesiredState, } from "./registry.js";
|
|
7
7
|
import { defaultAgentName } from "../session/local_config.js";
|
|
8
8
|
import { ipcAddress, usesNamedPipe } from "../session/ipc.js";
|
|
9
|
-
import { EXIT_DAEMON_FRESH_SESSION, RpcChild } from "./rpc_child.js";
|
|
9
|
+
import { EXIT_DAEMON_FRESH_SESSION, RpcChild, } from "./rpc_child.js";
|
|
10
10
|
import { encodeReply, parseRequest, } from "./control_protocol.js";
|
|
11
|
-
import { Cron } from "croner";
|
|
12
11
|
import { addJob as addCronJob, getJob as getCronJob, listJobs as listCronJobs, nextRunFor, recordRun, removeJob as removeCronJob, setJobEnabled, validateSchedule, } from "./cron_registry.js";
|
|
13
12
|
import { appendCronLog, readCronLog } from "./cron_log.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* Responsibilities:
|
|
18
|
-
* - Spawn one `pi --mode rpc` child per registry entry. Track them in
|
|
19
|
-
* `_children: Map<id, RpcChild>`.
|
|
20
|
-
* - Auto-restart crashed children with exponential backoff
|
|
21
|
-
* (1s, 5s, 30s, 5min). Give up after 4 attempts to avoid log spam
|
|
22
|
-
* when the agent is misconfigured.
|
|
23
|
-
* - Listen on `~/.pi/remote/supervisor.sock` for `ControlRequest`s from
|
|
24
|
-
* the `remote-pi` CLI. Each connection: 1 request → 1 reply → close.
|
|
25
|
-
* - Graceful shutdown on SIGTERM/SIGINT: stop all children + unlink
|
|
26
|
-
* the UDS file so a next supervisor can bind cleanly.
|
|
27
|
-
*
|
|
28
|
-
* The supervisor itself is the only long-running process the user
|
|
29
|
-
* installs as a system service (plan/26 W3 will generate the unit/plist).
|
|
30
|
-
* If it crashes, systemd/launchd restarts it; on restart it re-reads
|
|
31
|
-
* the registry and re-spawns everything.
|
|
32
|
-
*/
|
|
13
|
+
import { decideFireAction, infoFor } from "./status.js";
|
|
14
|
+
import { runDaemonPreflight } from "./preflight.js";
|
|
15
|
+
import { probeSupervisor } from "./supervisor-ipc.js";
|
|
33
16
|
const SUPERVISOR_SOCK_NAME = "supervisor.sock";
|
|
34
|
-
|
|
35
|
-
* child stays in `crashed` state until manual `restart_all` or fresh
|
|
36
|
-
* registry add. Keeps logs sane when the agent dies on every boot. */
|
|
37
|
-
const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000, 5 * 60_000];
|
|
17
|
+
const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000, 5 * 60_000], RESTART_STABILITY_MS = 30_000, RECONCILE_INTERVAL_MS = 15_000, MISSING_CWD_CONFIRMATIONS = 2;
|
|
38
18
|
function supervisorSockPath() {
|
|
39
19
|
const root = process.env["REMOTE_PI_HOME"] || homedir();
|
|
40
|
-
// POSIX → ~/.pi/remote/supervisor.sock; Windows → per-user named pipe (plan/40).
|
|
41
20
|
return ipcAddress("supervisor", join(root, ".pi", "remote", SUPERVISOR_SOCK_NAME));
|
|
42
21
|
}
|
|
43
|
-
/** Thrown by `start()` when another live supervisor already holds the UDS.
|
|
44
|
-
* Prevents a second supervisor from orphaning the first's children. */
|
|
45
22
|
export class SupervisorAlreadyRunningError extends Error {
|
|
46
23
|
sockPath;
|
|
47
24
|
constructor(sockPath) {
|
|
48
|
-
super(`Another pi-supervisord is already running (UDS held at ${sockPath})
|
|
49
|
-
"Refusing to start a second instance. Use `remote-pi daemon …` to control it, " +
|
|
50
|
-
"or stop the running one first.");
|
|
25
|
+
super(`Another pi-supervisord is already running (UDS held at ${sockPath}).`);
|
|
51
26
|
this.sockPath = sockPath;
|
|
52
27
|
this.name = "SupervisorAlreadyRunningError";
|
|
53
28
|
}
|
|
54
29
|
}
|
|
55
|
-
|
|
56
|
-
* Resolves true if the connect succeeds (a listener is there), false on
|
|
57
|
-
* ECONNREFUSED / ENOENT (stale socket file from a crashed supervisor). */
|
|
58
|
-
function _probeSupervisor(path) {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
const sock = createConnection({ path });
|
|
61
|
-
const done = (alive) => {
|
|
62
|
-
sock.removeAllListeners();
|
|
63
|
-
sock.destroy();
|
|
64
|
-
resolve(alive);
|
|
65
|
-
};
|
|
66
|
-
const timer = setTimeout(() => done(false), 1_000);
|
|
67
|
-
sock.once("connect", () => { clearTimeout(timer); done(true); });
|
|
68
|
-
sock.once("error", () => { clearTimeout(timer); done(false); });
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
export function decideFireAction(o) {
|
|
72
|
-
if (!o.running)
|
|
73
|
-
return o.wake ? "wake_and_send" : "skip_down";
|
|
74
|
-
if (o.skipIfBusy && o.busy)
|
|
75
|
-
return "skip_busy";
|
|
76
|
-
return "send";
|
|
77
|
-
}
|
|
30
|
+
export { decideFireAction } from "./status.js";
|
|
78
31
|
export class Supervisor {
|
|
79
32
|
opts;
|
|
80
33
|
server = null;
|
|
81
34
|
children = new Map();
|
|
82
|
-
/** Live croner schedules, keyed by cron job id (plan/39). */
|
|
83
35
|
cronJobs = new Map();
|
|
84
36
|
shuttingDown = false;
|
|
37
|
+
reconcileTimer = null;
|
|
38
|
+
reconcileRunning = false;
|
|
39
|
+
preflightFor;
|
|
85
40
|
constructor(opts) {
|
|
86
41
|
this.opts = opts;
|
|
42
|
+
this.preflightFor = opts.preflight ?? ((entry) => runDaemonPreflight({ cwd: entry.cwd }));
|
|
87
43
|
}
|
|
88
|
-
/** Bind the control UDS + spawn all registered daemons. */
|
|
89
44
|
async start() {
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
this.
|
|
99
|
-
}
|
|
100
|
-
/** Graceful shutdown: stop all children, close UDS. */
|
|
45
|
+
this.mkdirParent();
|
|
46
|
+
await this.bindUds();
|
|
47
|
+
await this.reconcileRegistryCwds();
|
|
48
|
+
this.spawnDesiredEntries();
|
|
49
|
+
this.reconcileCron();
|
|
50
|
+
this.runCatchup();
|
|
51
|
+
const interval = this.opts.reconcileIntervalMs ?? RECONCILE_INTERVAL_MS;
|
|
52
|
+
this.reconcileTimer = setInterval(() => { void this.reconcileRegistryCwds(); }, interval);
|
|
53
|
+
this.reconcileTimer.unref();
|
|
54
|
+
}
|
|
101
55
|
async stop() {
|
|
102
56
|
this.shuttingDown = true;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
57
|
+
if (this.reconcileTimer)
|
|
58
|
+
clearInterval(this.reconcileTimer);
|
|
59
|
+
this.reconcileTimer = null;
|
|
60
|
+
for (const cron of this.cronJobs.values())
|
|
61
|
+
cron.stop();
|
|
106
62
|
this.cronJobs.clear();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
clearTimeout(slot.restartTimer);
|
|
111
|
-
slot.restartTimer = null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
await Promise.all([...this.children.values()].map((s) => s.child.stop()));
|
|
63
|
+
for (const slot of this.children.values())
|
|
64
|
+
this.cancelRetry(slot);
|
|
65
|
+
await Promise.all([...this.children.values()].map(async (slot) => slot.child.stop()));
|
|
115
66
|
this.children.clear();
|
|
116
|
-
await new Promise((resolve) =>
|
|
117
|
-
if (!this.server)
|
|
118
|
-
return resolve();
|
|
119
|
-
this.server.close(() => resolve());
|
|
120
|
-
});
|
|
67
|
+
await new Promise((resolve) => this.server ? this.server.close(() => resolve()) : resolve());
|
|
121
68
|
this.server = null;
|
|
122
|
-
// Best-effort: clear the socket file so a next supervisor bind succeeds.
|
|
123
|
-
// Windows named pipes have no file (auto-removed on exit) → nothing to do.
|
|
124
69
|
if (!usesNamedPipe()) {
|
|
125
70
|
try {
|
|
126
71
|
unlinkSync(supervisorSockPath());
|
|
127
72
|
}
|
|
128
|
-
catch { /*
|
|
73
|
+
catch { /* socket already absent */ }
|
|
129
74
|
}
|
|
130
75
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (usesNamedPipe())
|
|
135
|
-
return;
|
|
136
|
-
mkdirSync(dirname(supervisorSockPath()), { recursive: true });
|
|
76
|
+
mkdirParent() {
|
|
77
|
+
if (!usesNamedPipe())
|
|
78
|
+
mkdirSync(dirname(supervisorSockPath()), { recursive: true });
|
|
137
79
|
}
|
|
138
|
-
async
|
|
80
|
+
async bindUds() {
|
|
139
81
|
const path = supervisorSockPath();
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// (unlink + bind) would orphan the running supervisor's children — they'd
|
|
144
|
-
// keep running, unreachable by the CLI. Only on POSIX, when the probe
|
|
145
|
-
// fails (stale socket from a crash), do we unlink + bind. On Windows there
|
|
146
|
-
// is no file: always probe, never unlink (the pipe self-cleans on exit).
|
|
147
|
-
if (pipe || existsSync(path)) {
|
|
148
|
-
const alive = await _probeSupervisor(path);
|
|
149
|
-
if (alive) {
|
|
82
|
+
const namedPipe = usesNamedPipe();
|
|
83
|
+
if (namedPipe || existsSync(path)) {
|
|
84
|
+
if (await probeSupervisor(path))
|
|
150
85
|
throw new SupervisorAlreadyRunningError(path);
|
|
151
|
-
|
|
152
|
-
if (!pipe) {
|
|
86
|
+
if (!namedPipe) {
|
|
153
87
|
try {
|
|
154
88
|
unlinkSync(path);
|
|
155
89
|
}
|
|
156
|
-
catch { /*
|
|
90
|
+
catch { /* bind reports an actual race */ }
|
|
157
91
|
}
|
|
158
92
|
}
|
|
159
|
-
const server = createServer((socket) => this.
|
|
93
|
+
const server = createServer((socket) => this.onConnection(socket));
|
|
160
94
|
await new Promise((resolve, reject) => {
|
|
161
95
|
server.once("error", reject);
|
|
162
96
|
server.listen(path, () => resolve());
|
|
163
97
|
});
|
|
164
98
|
this.server = server;
|
|
165
99
|
}
|
|
166
|
-
|
|
167
|
-
let
|
|
100
|
+
onConnection(socket) {
|
|
101
|
+
let buffer = "";
|
|
168
102
|
socket.setEncoding("utf8");
|
|
169
103
|
socket.on("data", (chunk) => {
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
104
|
+
buffer += chunk;
|
|
105
|
+
const newline = buffer.indexOf("\n");
|
|
106
|
+
if (newline < 0)
|
|
173
107
|
return;
|
|
174
|
-
|
|
175
|
-
// Single request per connection; ignore anything past the newline.
|
|
176
|
-
void this._handleRequest(line)
|
|
177
|
-
.then((reply) => socket.end(encodeReply(reply)))
|
|
178
|
-
.catch((err) => socket.end(encodeReply({ ok: false, error: String(err) })));
|
|
108
|
+
void this.handleRequest(buffer.slice(0, newline)).then((reply) => socket.end(encodeReply(reply)), (error) => socket.end(encodeReply({ ok: false, error: String(error) })));
|
|
179
109
|
});
|
|
180
110
|
socket.on("error", () => { });
|
|
181
111
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let req;
|
|
112
|
+
async handleRequest(line) {
|
|
113
|
+
let request;
|
|
185
114
|
try {
|
|
186
|
-
|
|
115
|
+
request = parseRequest(line);
|
|
187
116
|
}
|
|
188
|
-
catch (
|
|
189
|
-
return { ok: false, error:
|
|
117
|
+
catch (error) {
|
|
118
|
+
return { ok: false, error: error.message };
|
|
190
119
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
case "
|
|
194
|
-
case "
|
|
195
|
-
case "
|
|
196
|
-
case "
|
|
197
|
-
case "
|
|
198
|
-
case "
|
|
199
|
-
case "
|
|
200
|
-
case "
|
|
201
|
-
case "
|
|
202
|
-
case "
|
|
203
|
-
case "
|
|
204
|
-
case "
|
|
205
|
-
case "
|
|
206
|
-
case "
|
|
207
|
-
case "
|
|
208
|
-
case "
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
120
|
+
await this.reconcileRegistryCwds();
|
|
121
|
+
switch (request.op) {
|
|
122
|
+
case "list": return { ok: true, data: { daemons: this.listInfo() } };
|
|
123
|
+
case "status": return { ok: true, data: { daemons: this.listInfo() } };
|
|
124
|
+
case "start_all": return this.startAll();
|
|
125
|
+
case "start": return this.startOne(request.id);
|
|
126
|
+
case "stop_all": return this.stopAll();
|
|
127
|
+
case "stop": return this.stopOne(request.id);
|
|
128
|
+
case "restart_all": return this.restartAll();
|
|
129
|
+
case "restart": return this.restartOne(request.id);
|
|
130
|
+
case "send": return this.send(request.id, request.text);
|
|
131
|
+
case "register": return this.register(request.cwd);
|
|
132
|
+
case "unregister": return this.unregister(request.id);
|
|
133
|
+
case "unregister_cwd": return this.unregisterCwd(request.cwd);
|
|
134
|
+
case "cron_add": return this.cronAdd(request);
|
|
135
|
+
case "cron_list": return { ok: true, data: { jobs: listCronJobs().map((job) => this.jobView(job)) } };
|
|
136
|
+
case "cron_remove": return this.cronRemove(request.job_id);
|
|
137
|
+
case "cron_enable": return this.cronEnable(request.job_id, request.enabled);
|
|
138
|
+
case "cron_run": return this.cronRun(request.job_id);
|
|
139
|
+
case "cron_log": return { ok: true, data: { entries: readCronLog({ jobId: request.job_id, tail: request.tail }) } };
|
|
140
|
+
default: return { ok: false, error: `unknown op: ${request.op}` };
|
|
213
141
|
}
|
|
214
142
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
143
|
+
listInfo() {
|
|
144
|
+
return listDaemons().map((entry) => this.infoFor(entry));
|
|
145
|
+
}
|
|
146
|
+
infoFor(entry) {
|
|
147
|
+
const slot = this.children.get(entry.id);
|
|
148
|
+
const snapshot = {
|
|
149
|
+
id: entry.id,
|
|
150
|
+
cwd: entry.cwd,
|
|
151
|
+
name: entry.name,
|
|
152
|
+
registration: existsSync(entry.cwd) ? "registered" : "missing",
|
|
153
|
+
desired: entry.desired_state,
|
|
154
|
+
process: slot?.child.processState ?? "absent",
|
|
155
|
+
runtime: slot?.child.runtimeState ?? "pending",
|
|
156
|
+
relay: slot?.child.relayState ?? "disconnected",
|
|
157
|
+
...(slot?.child.runtimeInstanceId ? { runtimeInstanceId: slot.child.runtimeInstanceId } : {}),
|
|
158
|
+
...(slot?.child.pid !== undefined ? { pid: slot.child.pid } : {}),
|
|
159
|
+
...(slot?.child.startedAt !== undefined ? { startedAt: slot.child.startedAt } : {}),
|
|
160
|
+
...(slot?.child.uptimeMs !== undefined ? { uptimeMs: slot.child.uptimeMs } : {}),
|
|
161
|
+
restartCount: slot?.child.restartCount ?? 0,
|
|
162
|
+
retrying: !!slot?.restartTimer,
|
|
163
|
+
...(slot?.nextRetryAt ? { nextRetryAt: slot.nextRetryAt } : {}),
|
|
164
|
+
blocked: !!slot?.blocked || slot?.child.state === "blocked",
|
|
165
|
+
...(slot?.error ? { error: slot.error } : {}),
|
|
166
|
+
};
|
|
167
|
+
return infoFor(snapshot);
|
|
236
168
|
}
|
|
237
|
-
|
|
169
|
+
startAll() {
|
|
238
170
|
const started = [];
|
|
239
|
-
const
|
|
171
|
+
const alreadyRunning = [];
|
|
240
172
|
for (const entry of listDaemons()) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
already.push(entry.id);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
this._spawnEntry(entry.id, entry.cwd);
|
|
247
|
-
started.push(entry.id);
|
|
173
|
+
const result = this.startEntry(entry);
|
|
174
|
+
(result ? alreadyRunning : started).push(entry.id);
|
|
248
175
|
}
|
|
249
|
-
return { ok: true, data: { started, already_running:
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
* `/remote-pi create` calls so a freshly-registered folder boots its Pi
|
|
254
|
-
* immediately instead of waiting for the next supervisor restart. */
|
|
255
|
-
_opStart(id) {
|
|
256
|
-
const entry = listDaemons().find((d) => d.id === id);
|
|
176
|
+
return { ok: true, data: { started, already_running: alreadyRunning } };
|
|
177
|
+
}
|
|
178
|
+
startOne(id) {
|
|
179
|
+
const entry = listDaemons().find((item) => item.id === id);
|
|
257
180
|
if (!entry)
|
|
258
181
|
return { ok: false, error: `no daemon with id ${id}` };
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (slot.
|
|
276
|
-
|
|
277
|
-
|
|
182
|
+
const alreadyRunning = this.startEntry(entry);
|
|
183
|
+
const info = this.infoFor(setDaemonDesiredState(id, "running"));
|
|
184
|
+
return { ok: true, data: { id, state: info.state, started: !alreadyRunning, desired_state: "running" } };
|
|
185
|
+
}
|
|
186
|
+
startEntry(entry) {
|
|
187
|
+
const updated = setDaemonDesiredState(entry.id, "running");
|
|
188
|
+
const slot = this.children.get(entry.id);
|
|
189
|
+
if (slot)
|
|
190
|
+
slot.entry = updated;
|
|
191
|
+
if (slot && (slot.child.processState === "running" || slot.child.processState === "spawning") && !slot.blocked)
|
|
192
|
+
return true;
|
|
193
|
+
if (slot) {
|
|
194
|
+
this.cancelRetry(slot);
|
|
195
|
+
slot.blocked = false;
|
|
196
|
+
slot.error = undefined;
|
|
197
|
+
slot.entry = updated;
|
|
198
|
+
if (slot.child.processState === "exited" || slot.child.pid === undefined) {
|
|
199
|
+
void this.preflightAndSpawn(slot);
|
|
200
|
+
return false;
|
|
278
201
|
}
|
|
279
|
-
|
|
280
|
-
|
|
202
|
+
void slot.child.stop().then(() => this.preflightAndSpawn(slot));
|
|
203
|
+
return false;
|
|
281
204
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
205
|
+
this.createSlot(updated);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
async stopAll() {
|
|
209
|
+
const stopped = [], alreadyStopped = [];
|
|
210
|
+
for (const entry of listDaemons())
|
|
211
|
+
(await this.stopEntry(entry) ? stopped : alreadyStopped).push(entry.id);
|
|
212
|
+
return { ok: true, data: { stopped, already_stopped: alreadyStopped } };
|
|
213
|
+
}
|
|
214
|
+
async stopOne(id) {
|
|
215
|
+
const entry = listDaemons().find((item) => item.id === id);
|
|
290
216
|
if (!entry)
|
|
291
217
|
return { ok: false, error: `no daemon with id ${id}` };
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
218
|
+
const stopped = await this.stopEntry(entry);
|
|
219
|
+
const info = this.infoFor(setDaemonDesiredState(id, "stopped"));
|
|
220
|
+
return { ok: true, data: { id, state: info.state, stopped, desired_state: "stopped" } };
|
|
221
|
+
}
|
|
222
|
+
async stopEntry(entry) {
|
|
223
|
+
const updated = setDaemonDesiredState(entry.id, "stopped");
|
|
224
|
+
const slot = this.children.get(entry.id);
|
|
225
|
+
if (slot)
|
|
226
|
+
slot.entry = updated;
|
|
227
|
+
if (!slot)
|
|
228
|
+
return false;
|
|
229
|
+
this.cancelRetry(slot);
|
|
230
|
+
this.cancelStabilityReset(slot);
|
|
231
|
+
const wasLive = slot.child.processState === "running" || slot.child.processState === "spawning";
|
|
300
232
|
await slot.child.stop();
|
|
301
|
-
return
|
|
233
|
+
return wasLive;
|
|
302
234
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
async _opRestart(id) {
|
|
306
|
-
const entry = listDaemons().find((d) => d.id === id);
|
|
235
|
+
async restartOne(id) {
|
|
236
|
+
const entry = listDaemons().find((item) => item.id === id);
|
|
307
237
|
if (!entry)
|
|
308
238
|
return { ok: false, error: `no daemon with id ${id}` };
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
239
|
+
await this.restartEntry(entry);
|
|
240
|
+
const info = this.infoFor(setDaemonDesiredState(id, "running"));
|
|
241
|
+
return { ok: true, data: { id, state: info.state, restarted: true, desired_state: "running" } };
|
|
242
|
+
}
|
|
243
|
+
async restartAll() {
|
|
244
|
+
const restarted = [];
|
|
245
|
+
for (const entry of listDaemons()) {
|
|
246
|
+
await this.restartEntry(entry);
|
|
247
|
+
restarted.push(entry.id);
|
|
316
248
|
}
|
|
317
|
-
this._spawnEntry(entry.id, entry.cwd, entry.name);
|
|
318
|
-
const state = this.children.get(id)?.child.state ?? "starting";
|
|
319
|
-
return { ok: true, data: { id, state, restarted: true } };
|
|
320
|
-
}
|
|
321
|
-
async _opRestartAll() {
|
|
322
|
-
const stopReply = await this._opStopAll();
|
|
323
|
-
if (!stopReply.ok)
|
|
324
|
-
return stopReply;
|
|
325
|
-
const startReply = this._opStartAll();
|
|
326
|
-
if (!startReply.ok)
|
|
327
|
-
return startReply;
|
|
328
|
-
const restarted = startReply.data.started;
|
|
329
249
|
return { ok: true, data: { restarted } };
|
|
330
250
|
}
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
251
|
+
async restartEntry(entry) {
|
|
252
|
+
const updated = setDaemonDesiredState(entry.id, "running");
|
|
253
|
+
const existing = this.children.get(entry.id);
|
|
254
|
+
if (existing) {
|
|
255
|
+
this.cancelRetry(existing);
|
|
256
|
+
this.cancelStabilityReset(existing);
|
|
257
|
+
existing.blocked = false;
|
|
258
|
+
existing.error = undefined;
|
|
259
|
+
await existing.child.stop();
|
|
260
|
+
this.children.delete(entry.id);
|
|
337
261
|
}
|
|
338
|
-
|
|
339
|
-
return { ok: true, data: { id, delivered: ok } };
|
|
262
|
+
this.createSlot(updated);
|
|
340
263
|
}
|
|
341
|
-
|
|
264
|
+
async send(id, text) {
|
|
265
|
+
const entry = listDaemons().find((item) => item.id === id);
|
|
266
|
+
if (!entry)
|
|
267
|
+
return { ok: false, error: `no daemon with id ${id}` };
|
|
268
|
+
if (!existsSync(entry.cwd)) {
|
|
269
|
+
return { ok: true, data: { id, accepted: false, delivered: false, code: "cwd_missing", error: "daemon cwd no longer exists" } };
|
|
270
|
+
}
|
|
271
|
+
const info = this.infoFor(entry);
|
|
272
|
+
if (info.desired !== "running" || info.runtime !== "ready") {
|
|
273
|
+
return { ok: true, data: { id, accepted: false, delivered: false, code: "runtime_not_ready", error: `daemon is ${info.health}` } };
|
|
274
|
+
}
|
|
275
|
+
const slot = this.children.get(id);
|
|
276
|
+
if (!slot)
|
|
277
|
+
return { ok: true, data: { id, accepted: false, delivered: false, code: "runtime_missing" } };
|
|
278
|
+
const acceptance = await slot.child.sendPrompt(text);
|
|
279
|
+
return { ok: true, data: {
|
|
280
|
+
id,
|
|
281
|
+
accepted: acceptance.accepted,
|
|
282
|
+
delivered: acceptance.accepted,
|
|
283
|
+
...(acceptance.code ? { code: acceptance.code } : {}),
|
|
284
|
+
...(acceptance.error ? { error: acceptance.error } : {}),
|
|
285
|
+
} };
|
|
286
|
+
}
|
|
287
|
+
register(cwd) {
|
|
342
288
|
try {
|
|
343
|
-
const
|
|
344
|
-
|
|
289
|
+
const entry = addDaemon(cwd);
|
|
290
|
+
this.startEntry(entry);
|
|
291
|
+
return { ok: true, data: { id: entry.id, cwd: entry.cwd, name: entry.name, desired_state: entry.desired_state } };
|
|
345
292
|
}
|
|
346
|
-
catch (
|
|
347
|
-
return { ok: false, error:
|
|
293
|
+
catch (error) {
|
|
294
|
+
return { ok: false, error: error.message };
|
|
348
295
|
}
|
|
349
296
|
}
|
|
350
|
-
async
|
|
351
|
-
// Stop the child first so we don't leave an orphan when the registry
|
|
352
|
-
// entry is gone.
|
|
297
|
+
async unregister(id) {
|
|
353
298
|
const slot = this.children.get(id);
|
|
354
299
|
if (slot) {
|
|
355
|
-
|
|
356
|
-
clearTimeout(slot.restartTimer);
|
|
357
|
-
slot.restartTimer = null;
|
|
358
|
-
}
|
|
300
|
+
this.cancelRetry(slot);
|
|
359
301
|
await slot.child.stop();
|
|
360
302
|
this.children.delete(id);
|
|
361
303
|
}
|
|
362
304
|
try {
|
|
363
|
-
|
|
364
|
-
return { ok: true, data: result };
|
|
305
|
+
return { ok: true, data: removeDaemon(id) };
|
|
365
306
|
}
|
|
366
|
-
catch (
|
|
367
|
-
return { ok: false, error:
|
|
307
|
+
catch (error) {
|
|
308
|
+
return { ok: false, error: error.message };
|
|
368
309
|
}
|
|
369
310
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
311
|
+
async unregisterCwd(cwd) {
|
|
312
|
+
const entry = findDaemonByCwd(cwd);
|
|
313
|
+
if (!entry)
|
|
314
|
+
return { ok: true, data: { removed: false } };
|
|
315
|
+
return this.unregister(entry.id);
|
|
316
|
+
}
|
|
317
|
+
cronAdd(request) {
|
|
318
|
+
const validation = validateSchedule(request.schedule, request.tz);
|
|
319
|
+
if (!validation.ok)
|
|
320
|
+
return { ok: false, error: validation.error ?? "invalid schedule" };
|
|
321
|
+
const input = { daemon_id: request.daemon_id, schedule: request.schedule, prompt: request.prompt };
|
|
322
|
+
if (request.tz !== undefined)
|
|
323
|
+
input.tz = request.tz;
|
|
324
|
+
if (request.skip_if_busy !== undefined)
|
|
325
|
+
input.skip_if_busy = request.skip_if_busy;
|
|
326
|
+
if (request.catchup !== undefined)
|
|
327
|
+
input.catchup = request.catchup;
|
|
384
328
|
const job = addCronJob(input);
|
|
385
|
-
this.
|
|
386
|
-
return { ok: true, data: { job: this.
|
|
387
|
-
}
|
|
388
|
-
_opCronList() {
|
|
389
|
-
const jobs = listCronJobs().map((j) => this._jobView(j));
|
|
390
|
-
return { ok: true, data: { jobs } };
|
|
329
|
+
this.scheduleCron(job);
|
|
330
|
+
return { ok: true, data: { job: this.jobView(job) } };
|
|
391
331
|
}
|
|
392
|
-
|
|
332
|
+
cronRemove(jobId) {
|
|
393
333
|
const removed = removeCronJob(jobId);
|
|
394
|
-
this.
|
|
334
|
+
this.stopCron(jobId);
|
|
395
335
|
return { ok: true, data: { removed } };
|
|
396
336
|
}
|
|
397
|
-
|
|
337
|
+
cronEnable(jobId, enabled) {
|
|
398
338
|
const updated = setJobEnabled(jobId, enabled);
|
|
399
339
|
if (updated) {
|
|
400
|
-
this.
|
|
340
|
+
this.stopCron(jobId);
|
|
401
341
|
const job = getCronJob(jobId);
|
|
402
342
|
if (enabled && job)
|
|
403
|
-
this.
|
|
343
|
+
this.scheduleCron(job);
|
|
404
344
|
}
|
|
405
345
|
return { ok: true, data: { job_id: jobId, enabled, updated } };
|
|
406
346
|
}
|
|
407
|
-
async
|
|
347
|
+
async cronRun(jobId) {
|
|
408
348
|
if (!getCronJob(jobId))
|
|
409
349
|
return { ok: false, error: `no cron job with id ${jobId}` };
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
_opCronLog(jobId, tail) {
|
|
414
|
-
const opts = {};
|
|
415
|
-
if (jobId !== undefined)
|
|
416
|
-
opts.jobId = jobId;
|
|
417
|
-
if (tail !== undefined)
|
|
418
|
-
opts.tail = tail;
|
|
419
|
-
return { ok: true, data: { entries: readCronLog(opts) } };
|
|
420
|
-
}
|
|
421
|
-
_jobView(job) {
|
|
350
|
+
return { ok: true, data: { job_id: jobId, result: await this.fireJob(jobId, { manual: true }) } };
|
|
351
|
+
}
|
|
352
|
+
jobView(job) {
|
|
422
353
|
const next = nextRunFor(job);
|
|
423
|
-
return { ...job, next_run: next
|
|
354
|
+
return { ...job, next_run: next?.toISOString() ?? null };
|
|
424
355
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
for (const c of this.cronJobs.values())
|
|
429
|
-
c.stop();
|
|
356
|
+
reconcileCron() {
|
|
357
|
+
for (const cron of this.cronJobs.values())
|
|
358
|
+
cron.stop();
|
|
430
359
|
this.cronJobs.clear();
|
|
431
|
-
for (const job of listCronJobs())
|
|
360
|
+
for (const job of listCronJobs())
|
|
432
361
|
if (job.enabled)
|
|
433
|
-
this.
|
|
434
|
-
}
|
|
362
|
+
this.scheduleCron(job);
|
|
435
363
|
}
|
|
436
|
-
|
|
437
|
-
this.
|
|
364
|
+
scheduleCron(job) {
|
|
365
|
+
this.stopCron(job.id);
|
|
438
366
|
try {
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
this.cronJobs.set(job.id, cron);
|
|
367
|
+
const options = job.tz ? { timezone: job.tz, name: job.id } : { name: job.id };
|
|
368
|
+
this.cronJobs.set(job.id, new Cron(job.schedule, options, () => { void this.fireJob(job.id); }));
|
|
442
369
|
}
|
|
443
|
-
catch (
|
|
444
|
-
process.stderr.write(`[remote-pi-supervisord] cron schedule failed for ${job.id}: ${String(
|
|
370
|
+
catch (error) {
|
|
371
|
+
process.stderr.write(`[remote-pi-supervisord] cron schedule failed for ${job.id}: ${String(error)}\n`);
|
|
445
372
|
}
|
|
446
373
|
}
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
|
|
374
|
+
stopCron(jobId) {
|
|
375
|
+
const cron = this.cronJobs.get(jobId);
|
|
376
|
+
if (cron) {
|
|
377
|
+
cron.stop();
|
|
451
378
|
this.cronJobs.delete(jobId);
|
|
452
379
|
}
|
|
453
380
|
}
|
|
454
|
-
|
|
455
|
-
* was missed while the supervisor was down. Opt-in (`catchup`), at most 1×. */
|
|
456
|
-
_runCatchup() {
|
|
381
|
+
runCatchup() {
|
|
457
382
|
for (const job of listCronJobs()) {
|
|
458
383
|
if (!job.enabled || !job.catchup)
|
|
459
384
|
continue;
|
|
460
385
|
try {
|
|
461
386
|
const cron = new Cron(job.schedule, job.tz ? { timezone: job.tz } : {});
|
|
462
|
-
const
|
|
387
|
+
const previous = cron.previousRun();
|
|
463
388
|
cron.stop();
|
|
464
|
-
if (
|
|
465
|
-
continue;
|
|
466
|
-
const lastRunMs = job.last_run ? Date.parse(job.last_run) : 0;
|
|
467
|
-
if (prev.getTime() > lastRunMs)
|
|
389
|
+
if (previous && previous.getTime() > (job.last_run ? Date.parse(job.last_run) : 0)) {
|
|
468
390
|
void this.fireJob(job.id, { manual: true });
|
|
391
|
+
}
|
|
469
392
|
}
|
|
470
|
-
catch { /*
|
|
393
|
+
catch { /* schedule was checked when added; corrupt persisted data simply skips */ }
|
|
471
394
|
}
|
|
472
395
|
}
|
|
473
|
-
|
|
474
|
-
* Fires a cron job: resolves the daemon, decides the action (decideFireAction),
|
|
475
|
-
* acts, and records the outcome — ALWAYS one `last_status` update + one JSONL
|
|
476
|
-
* line, for both fires and skips. Returns the result. `manual` bypasses the
|
|
477
|
-
* disabled-skip (used by `cron run` + catchup).
|
|
478
|
-
*/
|
|
479
|
-
async fireJob(jobId, opts = {}) {
|
|
396
|
+
async fireJob(jobId, options = {}) {
|
|
480
397
|
const job = getCronJob(jobId);
|
|
481
398
|
if (!job)
|
|
482
399
|
return "missing";
|
|
483
400
|
let result;
|
|
484
|
-
if (!job.enabled && !
|
|
401
|
+
if (!job.enabled && !options.manual) {
|
|
485
402
|
result = "skipped_disabled";
|
|
486
403
|
}
|
|
487
404
|
else {
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const action = decideFireAction({
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
this._spawnEntry(entry.id, entry.cwd, entry.name);
|
|
507
|
-
const woke = this.children.get(job.daemon_id);
|
|
508
|
-
result = woke && woke.child.sendPrompt(job.prompt) ? "woke_and_delivered" : "deliver_failed";
|
|
509
|
-
}
|
|
405
|
+
const entry = listDaemons().find((item) => item.id === job.daemon_id);
|
|
406
|
+
const cwdExists = !!entry && existsSync(entry.cwd);
|
|
407
|
+
const slot = cwdExists && entry ? this.children.get(entry.id) : undefined;
|
|
408
|
+
const info = cwdExists && entry ? this.infoFor(entry) : undefined;
|
|
409
|
+
const busy = !!slot && info?.runtime === "ready" && job.skip_if_busy ? await slot.child.refreshBusy() : false;
|
|
410
|
+
const action = decideFireAction({
|
|
411
|
+
exists: cwdExists,
|
|
412
|
+
desired: entry?.desired_state ?? "stopped",
|
|
413
|
+
runtime: info?.runtime ?? "pending",
|
|
414
|
+
health: info?.health ?? "failed",
|
|
415
|
+
busy,
|
|
416
|
+
skipIfBusy: job.skip_if_busy,
|
|
417
|
+
retrying: info?.retrying ?? false,
|
|
418
|
+
});
|
|
419
|
+
if (action === "send") {
|
|
420
|
+
result = slot && (await slot.child.sendPrompt(job.prompt)).accepted ? "accepted" : "rejected";
|
|
510
421
|
}
|
|
511
422
|
else {
|
|
512
|
-
result =
|
|
423
|
+
result = this.cronResultFor(action);
|
|
513
424
|
}
|
|
514
425
|
}
|
|
515
426
|
const at = new Date().toISOString();
|
|
@@ -517,80 +428,179 @@ export class Supervisor {
|
|
|
517
428
|
appendCronLog({ job_id: job.id, daemon_id: job.daemon_id, schedule: job.schedule, result, prompt: job.prompt });
|
|
518
429
|
return result;
|
|
519
430
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
431
|
+
cronResultFor(action) {
|
|
432
|
+
switch (action) {
|
|
433
|
+
case "skip_busy": return "skipped_busy";
|
|
434
|
+
case "skip_desired_stopped": return "skipped_desired_stopped";
|
|
435
|
+
case "skip_starting": return "skipped_starting";
|
|
436
|
+
case "skip_retrying": return "skipped_retrying";
|
|
437
|
+
case "skip_failed": return "skipped_failed";
|
|
438
|
+
case "skip_blocked": return "skipped_blocked";
|
|
439
|
+
case "skip_missing": return "skipped_missing";
|
|
524
440
|
}
|
|
525
441
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
442
|
+
spawnDesiredEntries() {
|
|
443
|
+
for (const entry of listDaemons())
|
|
444
|
+
if (entry.desired_state === "running")
|
|
445
|
+
this.createSlot(entry);
|
|
446
|
+
}
|
|
447
|
+
createSlot(entry) {
|
|
448
|
+
const config = { agent_name: entry.name || defaultAgentName(entry.cwd), auto_start_relay: true };
|
|
449
|
+
const childOptions = { endpointId: entry.id, extensionPath: this.opts.extensionPath, cwd: entry.cwd, config };
|
|
450
|
+
if (this.opts.piBin)
|
|
451
|
+
childOptions.piBin = this.opts.piBin;
|
|
452
|
+
const child = new RpcChild(childOptions);
|
|
453
|
+
const slot = {
|
|
454
|
+
entry,
|
|
455
|
+
child,
|
|
456
|
+
restartTimer: null,
|
|
457
|
+
stabilityTimer: null,
|
|
458
|
+
restartAttempt: 0,
|
|
459
|
+
blocked: false,
|
|
460
|
+
missingCwdObservations: 0,
|
|
461
|
+
reconcileInProgress: false,
|
|
462
|
+
preflight: null,
|
|
543
463
|
};
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
464
|
+
this.children.set(entry.id, slot);
|
|
465
|
+
child.on("exit", (event) => this.onChildExit(entry.id, event));
|
|
466
|
+
child.on("runtime_ready", () => this.onRuntimeReady(entry.id));
|
|
467
|
+
child.on("runtime_failed", (event) => this.onRuntimeFailed(entry.id, event));
|
|
468
|
+
void this.preflightAndSpawn(slot);
|
|
469
|
+
}
|
|
470
|
+
preflightAndSpawn(slot) {
|
|
471
|
+
if (slot.preflight)
|
|
472
|
+
return slot.preflight;
|
|
473
|
+
const run = async () => {
|
|
474
|
+
const result = await this.preflightFor(slot.entry);
|
|
475
|
+
if (this.children.get(slot.entry.id) !== slot || this.shuttingDown || slot.entry.desired_state !== "running")
|
|
476
|
+
return;
|
|
477
|
+
if (!result.ok) {
|
|
478
|
+
slot.blocked = true;
|
|
479
|
+
slot.error = { code: result.code, message: result.message, at: Date.now(), retryable: false, stage: "preflight" };
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
slot.blocked = false;
|
|
483
|
+
slot.error = undefined;
|
|
484
|
+
slot.child.spawn();
|
|
548
485
|
};
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
486
|
+
const pending = run().catch(() => {
|
|
487
|
+
if (this.children.get(slot.entry.id) !== slot || this.shuttingDown)
|
|
488
|
+
return;
|
|
489
|
+
slot.blocked = true;
|
|
490
|
+
slot.error = { code: "preflight_failed", message: "Pi SDK resource discovery failed", at: Date.now(), retryable: false, stage: "preflight" };
|
|
491
|
+
});
|
|
492
|
+
let tracked;
|
|
493
|
+
tracked = pending.finally(() => {
|
|
494
|
+
if (slot.preflight === tracked)
|
|
495
|
+
slot.preflight = null;
|
|
496
|
+
});
|
|
497
|
+
slot.preflight = tracked;
|
|
498
|
+
return tracked;
|
|
499
|
+
}
|
|
500
|
+
onRuntimeReady(id) {
|
|
501
|
+
const slot = this.children.get(id);
|
|
502
|
+
if (!slot || this.shuttingDown || slot.entry.desired_state !== "running")
|
|
559
503
|
return;
|
|
504
|
+
if (slot.stabilityTimer)
|
|
505
|
+
clearTimeout(slot.stabilityTimer);
|
|
506
|
+
slot.stabilityTimer = setTimeout(() => {
|
|
507
|
+
slot.stabilityTimer = null;
|
|
508
|
+
if (this.children.get(id) === slot && slot.child.runtimeState === "ready" && !slot.blocked)
|
|
509
|
+
slot.restartAttempt = 0;
|
|
510
|
+
}, RESTART_STABILITY_MS);
|
|
511
|
+
slot.stabilityTimer.unref();
|
|
512
|
+
}
|
|
513
|
+
onRuntimeFailed(id, event) {
|
|
560
514
|
const slot = this.children.get(id);
|
|
561
|
-
if (!slot)
|
|
515
|
+
if (!slot || this.shuttingDown || slot.entry.desired_state === "stopped")
|
|
562
516
|
return;
|
|
563
|
-
|
|
564
|
-
|
|
517
|
+
slot.error = { code: event.code, message: event.message, at: Date.now(), retryable: event.retryable, stage: event.stage ?? "runtime" };
|
|
518
|
+
if (!event.retryable) {
|
|
519
|
+
slot.blocked = true;
|
|
520
|
+
this.cancelRetry(slot);
|
|
521
|
+
this.cancelStabilityReset(slot);
|
|
522
|
+
void slot.child.stop();
|
|
565
523
|
return;
|
|
566
524
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
525
|
+
this.scheduleRetry(slot, "runtime failure");
|
|
526
|
+
void slot.child.stop();
|
|
527
|
+
}
|
|
528
|
+
onChildExit(id, event) {
|
|
529
|
+
const slot = this.children.get(id);
|
|
530
|
+
if (!slot || this.shuttingDown || slot.entry.desired_state === "stopped" || slot.blocked)
|
|
531
|
+
return;
|
|
532
|
+
this.cancelStabilityReset(slot);
|
|
533
|
+
if (event.code === EXIT_DAEMON_FRESH_SESSION) {
|
|
570
534
|
slot.restartAttempt = 0;
|
|
571
535
|
slot.child.noteRestart();
|
|
572
|
-
|
|
536
|
+
void this.preflightAndSpawn(slot);
|
|
573
537
|
return;
|
|
574
538
|
}
|
|
575
|
-
|
|
576
|
-
|
|
539
|
+
if (!event.isCrash)
|
|
540
|
+
return;
|
|
541
|
+
slot.error ??= { code: "child_exited", message: `child exited (${String(event.code ?? event.signal)})`, at: Date.now(), retryable: true, stage: "process" };
|
|
542
|
+
this.scheduleRetry(slot, "child exit");
|
|
543
|
+
}
|
|
544
|
+
scheduleRetry(slot, _reason) {
|
|
545
|
+
if (slot.restartTimer || slot.blocked || slot.entry.desired_state !== "running")
|
|
546
|
+
return;
|
|
577
547
|
if (slot.restartAttempt >= RESTART_BACKOFFS_MS.length) {
|
|
578
|
-
|
|
548
|
+
slot.error = { code: "retry_exhausted", message: "transient restart budget exhausted", at: Date.now(), retryable: false, stage: "retry" };
|
|
579
549
|
return;
|
|
580
550
|
}
|
|
581
551
|
const delay = RESTART_BACKOFFS_MS[slot.restartAttempt];
|
|
582
|
-
|
|
552
|
+
slot.nextRetryAt = Date.now() + delay;
|
|
583
553
|
slot.restartTimer = setTimeout(() => {
|
|
584
554
|
slot.restartTimer = null;
|
|
555
|
+
slot.nextRetryAt = undefined;
|
|
556
|
+
if (this.shuttingDown || slot.blocked || slot.entry.desired_state !== "running")
|
|
557
|
+
return;
|
|
585
558
|
slot.restartAttempt += 1;
|
|
586
559
|
slot.child.noteRestart();
|
|
587
|
-
|
|
560
|
+
void this.preflightAndSpawn(slot);
|
|
588
561
|
}, delay);
|
|
589
562
|
}
|
|
563
|
+
cancelRetry(slot) {
|
|
564
|
+
if (slot.restartTimer)
|
|
565
|
+
clearTimeout(slot.restartTimer);
|
|
566
|
+
slot.restartTimer = null;
|
|
567
|
+
slot.nextRetryAt = undefined;
|
|
568
|
+
}
|
|
569
|
+
cancelStabilityReset(slot) {
|
|
570
|
+
if (slot.stabilityTimer)
|
|
571
|
+
clearTimeout(slot.stabilityTimer);
|
|
572
|
+
slot.stabilityTimer = null;
|
|
573
|
+
}
|
|
574
|
+
async reconcileRegistryCwds() {
|
|
575
|
+
if (this.reconcileRunning)
|
|
576
|
+
return;
|
|
577
|
+
this.reconcileRunning = true;
|
|
578
|
+
try {
|
|
579
|
+
for (const entry of listDaemons()) {
|
|
580
|
+
const slot = this.children.get(entry.id);
|
|
581
|
+
if (existsSync(entry.cwd)) {
|
|
582
|
+
if (slot)
|
|
583
|
+
slot.missingCwdObservations = 0;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (!slot) {
|
|
587
|
+
removeDaemon(entry.id);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
slot.missingCwdObservations += 1;
|
|
591
|
+
if (slot.missingCwdObservations < MISSING_CWD_CONFIRMATIONS || slot.reconcileInProgress)
|
|
592
|
+
continue;
|
|
593
|
+
slot.reconcileInProgress = true;
|
|
594
|
+
this.cancelRetry(slot);
|
|
595
|
+
await slot.child.stop();
|
|
596
|
+
this.children.delete(entry.id);
|
|
597
|
+
removeDaemon(entry.id);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
this.reconcileRunning = false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
590
604
|
}
|
|
591
|
-
/** Test helper: derive id from cwd without going through the registry. */
|
|
592
|
-
export function _idForCwdForTest(cwd) { return daemonIdForCwd(cwd); }
|
|
593
|
-
/** Exported for the bin/supervisord entry + tests to know where the
|
|
594
|
-
* supervisor will bind. */
|
|
595
605
|
export function getSupervisorSockPath() { return supervisorSockPath(); }
|
|
596
606
|
//# sourceMappingURL=supervisor.js.map
|