@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
package/dist/daemon/rpc_child.js
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { EventEmitter } from "node:events";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { defaultAgentName, loadLocalConfig } from "../session/local_config.js";
|
|
6
7
|
export const EXIT_DAEMON_FRESH_SESSION = 42;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* cmd.exe). An extensionless `pi` is a Unix shell script Node CANNOT spawn on
|
|
10
|
-
* Windows — picking it yields `spawn … ENOENT` and the daemon shows "crashed". */
|
|
8
|
+
export const DEFAULT_READINESS_TIMEOUT_MS = 10_000;
|
|
9
|
+
export const DEFAULT_PROMPT_TIMEOUT_MS = 5_000;
|
|
11
10
|
const WIN_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".com"];
|
|
12
|
-
/**
|
|
13
|
-
* Resolve the `pi` executable for `spawn` (plan/40, decision C). On Windows a
|
|
14
|
-
* bare `pi` is actually `pi.cmd`/`pi.ps1`, and `spawn` won't find it without an
|
|
15
|
-
* extension → resolve the real path via `where` (rather than `shell:true`,
|
|
16
|
-
* which risks shell injection). An explicit path or an already-suffixed name is
|
|
17
|
-
* used as-is. POSIX returns the name unchanged. Best-effort: if `where` fails,
|
|
18
|
-
* fall back to the bare name (spawn will surface ENOENT honestly).
|
|
19
|
-
*
|
|
20
|
-
* `where` lists every match in PATH order, which on pi-node installs puts the
|
|
21
|
-
* extensionless `pi` (a shell script) BEFORE `pi.cmd`. Naively taking the first
|
|
22
|
-
* line spawns the unrunnable script → ENOENT → "crashed". So we prefer a result
|
|
23
|
-
* with a real Windows executable extension and only fall back to the first line.
|
|
24
|
-
*/
|
|
25
11
|
export function resolvePiBin(piBin, plat = process.platform) {
|
|
26
12
|
if (plat !== "win32")
|
|
27
13
|
return piBin;
|
|
@@ -29,29 +15,13 @@ export function resolvePiBin(piBin, plat = process.platform) {
|
|
|
29
15
|
return piBin;
|
|
30
16
|
try {
|
|
31
17
|
const out = execFileSync("where", [piBin], { encoding: "utf8" });
|
|
32
|
-
const lines = out.split(/\r?\n/).map((
|
|
33
|
-
|
|
34
|
-
const chosen = runnable ?? lines[0];
|
|
35
|
-
if (chosen)
|
|
36
|
-
return chosen;
|
|
18
|
+
const lines = out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
19
|
+
return lines.find((line) => WIN_EXECUTABLE_EXTS.some((ext) => line.toLowerCase().endsWith(ext))) ?? lines[0] ?? piBin;
|
|
37
20
|
}
|
|
38
21
|
catch {
|
|
39
|
-
|
|
22
|
+
return piBin;
|
|
40
23
|
}
|
|
41
|
-
return piBin;
|
|
42
24
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Resolve `pi` to a directly-spawnable target (plan/40). On Windows `pi` is an
|
|
45
|
-
* npm `.cmd` shim, which modern Node refuses to `spawn` without `shell:true`
|
|
46
|
-
* (EINVAL, post CVE-2024-27980). Wrapping it in `cmd.exe` would (a) add a layer
|
|
47
|
-
* that orphans the real `node` on kill and (b) complicate stdio — both fatal for
|
|
48
|
-
* a long-lived RPC daemon the supervisor must stop/restart cleanly. Instead we
|
|
49
|
-
* parse the shim to recover the underlying `node <cli.js>` and spawn that
|
|
50
|
-
* directly: one process, clean signals, clean stdio.
|
|
51
|
-
*
|
|
52
|
-
* Falls back to the bare resolved path when not a parseable shim (e.g. a real
|
|
53
|
-
* `pi.exe`, or POSIX) so `spawn` surfaces any real error honestly.
|
|
54
|
-
*/
|
|
55
25
|
export function resolvePiSpawn(piBin, plat = process.platform, nodeExe = process.execPath) {
|
|
56
26
|
const resolved = resolvePiBin(piBin, plat);
|
|
57
27
|
if (plat !== "win32")
|
|
@@ -63,12 +33,6 @@ export function resolvePiSpawn(piBin, plat = process.platform, nodeExe = process
|
|
|
63
33
|
}
|
|
64
34
|
return { command: resolved, prefixArgs: [] };
|
|
65
35
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Extract the JS entry an npm `.cmd` shim launches. npm shims invoke
|
|
68
|
-
* `"%_prog%" "%dp0%\<relative>\entry.js" %*`; we recover `<dir>\<relative>\entry.js`
|
|
69
|
-
* (dir = the shim's own folder, i.e. `%dp0%`). Returns null when the shim
|
|
70
|
-
* doesn't match the expected shape or the target is missing.
|
|
71
|
-
*/
|
|
72
36
|
export function _npmShimTarget(cmdPath) {
|
|
73
37
|
let content;
|
|
74
38
|
try {
|
|
@@ -77,313 +41,422 @@ export function _npmShimTarget(cmdPath) {
|
|
|
77
41
|
catch {
|
|
78
42
|
return null;
|
|
79
43
|
}
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
44
|
+
const match = content.match(/"%dp0%\\([^\"]+\.[cm]?js)"/i);
|
|
45
|
+
if (!match)
|
|
82
46
|
return null;
|
|
83
|
-
const target = join(dirname(cmdPath),
|
|
47
|
+
const target = join(dirname(cmdPath), match[1]);
|
|
84
48
|
return existsSync(target) ? target : null;
|
|
85
49
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Maps an RPC stdout line to a busy-state transition: `message_start` → true
|
|
88
|
-
* (a message is streaming), `message_end` → false. Other lines → null (no
|
|
89
|
-
* change). Pure + exported for tests.
|
|
90
|
-
*
|
|
91
|
-
* NOTE (plan/39 detail 1): the Pi RPC stream has NO turn-level event — only
|
|
92
|
-
* per-message start/end (and `response{command:"prompt"}` is emitted at
|
|
93
|
-
* PREFLIGHT, i.e. turn START, not end). So this passive flag reflects
|
|
94
|
-
* "a message is being produced right now"; the authoritative turn-busy signal
|
|
95
|
-
* is `get_state.isStreaming` (see `RpcChild.refreshBusy`).
|
|
96
|
-
*/
|
|
97
50
|
export function busyTransition(line) {
|
|
98
|
-
let
|
|
51
|
+
let value;
|
|
99
52
|
try {
|
|
100
|
-
|
|
53
|
+
value = JSON.parse(line);
|
|
101
54
|
}
|
|
102
55
|
catch {
|
|
103
56
|
return null;
|
|
104
57
|
}
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
58
|
+
const type = value?.type;
|
|
59
|
+
if (type === "message_start")
|
|
107
60
|
return true;
|
|
108
|
-
if (
|
|
61
|
+
if (type === "message_end")
|
|
109
62
|
return false;
|
|
110
63
|
return null;
|
|
111
64
|
}
|
|
112
|
-
/** Parses a `get_state` RPC response line, returning its id + isStreaming. */
|
|
113
|
-
function parseGetStateResponse(line) {
|
|
114
|
-
let obj;
|
|
115
|
-
try {
|
|
116
|
-
obj = JSON.parse(line);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
const o = obj;
|
|
122
|
-
if (o.type !== "response" || o.command !== "get_state")
|
|
123
|
-
return null;
|
|
124
|
-
const data = o.data;
|
|
125
|
-
return {
|
|
126
|
-
id: typeof o.id === "string" ? o.id : undefined,
|
|
127
|
-
isStreaming: typeof data?.isStreaming === "boolean" ? data.isStreaming : undefined,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
65
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* `--continue` is the key bit: it resumes the **most recent** session for the
|
|
134
|
-
* cwd (`SessionManager.continueRecent`, non-interactive — unlike `--resume`
|
|
135
|
-
* which opens a picker). Without it every supervisor restart spun up a brand
|
|
136
|
-
* new session file, piling up thousands of JSONLs per folder. With it, a
|
|
137
|
-
* restart REUSES the one session; the app's `/new` (session_new) still rolls
|
|
138
|
-
* it over to a fresh one, which the next restart then continues. First boot
|
|
139
|
-
* (no prior session) just creates the first one.
|
|
140
|
-
*
|
|
141
|
-
* `--name <sessionName>`, when given, pins the session's display name to the
|
|
142
|
-
* daemon's identity (its `agent_name`) so every restart shows up under the
|
|
143
|
-
* same stable name in the picker/app instead of an auto-generated one. The
|
|
144
|
-
* daemon's name is set at registration (`remote-pi create <cwd> --name "…"`).
|
|
145
|
-
* Omitted when no name resolves, so the arg list stays minimal.
|
|
146
|
-
*
|
|
147
|
-
* `--approve` is mandatory for a daemon (pi ≥0.79 project trust): RPC mode is
|
|
148
|
-
* non-interactive, so without an override Pi resolves an untrusted project
|
|
149
|
-
* folder (any folder with `.pi/` or CLAUDE.md/AGENTS.md) to NOT trusted and
|
|
150
|
-
* silently skips its `.pi/settings.json` (model/provider/keys), instructions,
|
|
151
|
-
* resources and project extensions — the daemon then comes up with no model
|
|
152
|
-
* and fails on the first turn. The operator already authorized this folder by
|
|
153
|
-
* registering/launching a daemon in it, so `--approve` (trust-for-this-run) is
|
|
154
|
-
* the correct non-interactive stance. (Does NOT affect the separate "extension
|
|
155
|
-
* loaded twice" conflict, which comes from the extension being BOTH installed
|
|
156
|
-
* in ~/.pi/agent/extensions or cwd/.pi/extensions AND passed via `-e`.)
|
|
66
|
+
* The installed Pi RPC starts configured extensions itself. In particular this
|
|
67
|
+
* must not attach Remote Pi with `-e`, which could load it a second time.
|
|
157
68
|
*/
|
|
158
|
-
export function rpcSpawnArgs(
|
|
69
|
+
export function rpcSpawnArgs(_extensionPath, sessionName, useContinue = true) {
|
|
159
70
|
return [
|
|
160
71
|
"--mode", "rpc",
|
|
161
72
|
"--approve",
|
|
162
73
|
...(useContinue ? ["--continue"] : []),
|
|
163
74
|
...(sessionName ? ["--name", sessionName] : []),
|
|
164
|
-
"-e", extensionPath,
|
|
165
75
|
];
|
|
166
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Parses direct daemon-control events and the current extension's custom
|
|
79
|
+
* relay-state message shape. Natural-language stderr is intentionally absent.
|
|
80
|
+
*/
|
|
81
|
+
export function parseRuntimeEvent(line) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = JSON.parse(line);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!raw || typeof raw !== "object")
|
|
90
|
+
return null;
|
|
91
|
+
const obj = raw;
|
|
92
|
+
let type = obj["type"];
|
|
93
|
+
let payload = obj;
|
|
94
|
+
if (type === "entry_appended" && obj["entry"] && typeof obj["entry"] === "object") {
|
|
95
|
+
const entry = obj["entry"];
|
|
96
|
+
const customType = entry["customType"];
|
|
97
|
+
const details = entry["details"];
|
|
98
|
+
if (typeof customType === "string" && details && typeof details === "object") {
|
|
99
|
+
type = customType.replace(/^remote-pi:/, "").replace(/-/g, "_");
|
|
100
|
+
payload = details;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (type === "runtime_ready") {
|
|
104
|
+
return {
|
|
105
|
+
type,
|
|
106
|
+
event: {
|
|
107
|
+
control_protocol_version: typeof payload["control_protocol_version"] === "number" ? payload["control_protocol_version"] : undefined,
|
|
108
|
+
extension_version: typeof payload["extension_version"] === "string" ? payload["extension_version"] : undefined,
|
|
109
|
+
endpoint_id: typeof payload["endpoint_id"] === "string" ? payload["endpoint_id"] : undefined,
|
|
110
|
+
runtime_instance_id: typeof payload["runtime_instance_id"] === "string" ? payload["runtime_instance_id"] : undefined,
|
|
111
|
+
session_id: typeof payload["session_id"] === "string" ? payload["session_id"] : undefined,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (type === "runtime_failed") {
|
|
116
|
+
if (typeof payload["code"] !== "string" || typeof payload["retryable"] !== "boolean")
|
|
117
|
+
return null;
|
|
118
|
+
return { type, event: {
|
|
119
|
+
stage: typeof payload["stage"] === "string" ? payload["stage"] : undefined,
|
|
120
|
+
code: payload["code"],
|
|
121
|
+
message: typeof payload["message"] === "string" ? payload["message"] : undefined,
|
|
122
|
+
retryable: payload["retryable"],
|
|
123
|
+
} };
|
|
124
|
+
}
|
|
125
|
+
if (type === "relay_state_changed" || type === "relay_state") {
|
|
126
|
+
const state = payload["state"] ?? payload["status"];
|
|
127
|
+
if (state === "disconnected" || state === "connecting" || state === "connected" || state === "reconnecting") {
|
|
128
|
+
return { type: "relay_state_changed", event: { state } };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (type === "session_changed") {
|
|
132
|
+
return { type, event: { session_id: typeof payload["session_id"] === "string" ? payload["session_id"] : undefined } };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
167
136
|
export class RpcChild extends EventEmitter {
|
|
168
137
|
opts;
|
|
169
138
|
child = null;
|
|
170
139
|
_state = "stopped";
|
|
171
|
-
|
|
140
|
+
_process = "absent";
|
|
141
|
+
_runtime = "pending";
|
|
142
|
+
_relay = "disconnected";
|
|
143
|
+
_startedAt;
|
|
144
|
+
_runtimeInstanceId;
|
|
145
|
+
_sessionId;
|
|
172
146
|
_restartCount = 0;
|
|
173
|
-
/** True while a deliberate `stop()` is in flight. The process dies by
|
|
174
|
-
* signal (SIGTERM/SIGKILL), which would otherwise look like a crash and
|
|
175
|
-
* trip the supervisor's auto-restart — re-spawning a daemon the operator
|
|
176
|
-
* just stopped/removed. Gating `isCrash` on this makes a deliberate stop a
|
|
177
|
-
* clean exit (no restart). Reset on every `spawn()`. */
|
|
178
147
|
_stopping = false;
|
|
179
|
-
/** Next spawn should create a fresh session instead of --continue. */
|
|
180
148
|
forceFreshSessionOnNextSpawn = false;
|
|
181
|
-
/** Accumulates partial stdout lines while waiting for `\n`. */
|
|
182
149
|
stdoutBuf = "";
|
|
183
|
-
/** Passive busy flag derived from the RPC stream (message_start/end). Hint
|
|
184
|
-
* only; `refreshBusy` syncs it authoritatively via get_state.isStreaming. */
|
|
185
150
|
_busy = false;
|
|
186
|
-
|
|
187
|
-
|
|
151
|
+
rpcReady = false;
|
|
152
|
+
extensionReady = false;
|
|
153
|
+
extensionReadyEvent = null;
|
|
154
|
+
readinessTimer = null;
|
|
155
|
+
readyPromise = Promise.resolve(false);
|
|
156
|
+
readyResolve = null;
|
|
157
|
+
statePending = new Map();
|
|
158
|
+
promptPending = new Map();
|
|
188
159
|
constructor(opts) {
|
|
189
160
|
super();
|
|
190
161
|
this.opts = opts;
|
|
191
162
|
}
|
|
192
163
|
get state() { return this._state; }
|
|
193
|
-
|
|
194
|
-
|
|
164
|
+
get processState() { return this._process; }
|
|
165
|
+
get runtimeState() { return this._runtime; }
|
|
166
|
+
get relayState() { return this._relay; }
|
|
195
167
|
get isBusy() { return this._busy; }
|
|
196
168
|
get pid() { return this.child?.pid; }
|
|
197
169
|
get restartCount() { return this._restartCount; }
|
|
198
|
-
get
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
170
|
+
get startedAt() { return this._startedAt; }
|
|
171
|
+
get runtimeInstanceId() { return this._runtimeInstanceId; }
|
|
172
|
+
get sessionId() { return this._sessionId; }
|
|
173
|
+
get uptimeMs() { return this._startedAt === undefined ? undefined : Date.now() - this._startedAt; }
|
|
174
|
+
/** Resolves true only after the readiness probe/event, false on timeout/exit. */
|
|
175
|
+
ready() { return this.readyPromise; }
|
|
205
176
|
spawn() {
|
|
206
177
|
if (this.child)
|
|
207
178
|
return;
|
|
208
|
-
this._stopping = false;
|
|
179
|
+
this._stopping = false;
|
|
209
180
|
this._busy = false;
|
|
210
181
|
this._state = "starting";
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
182
|
+
this._process = "spawning";
|
|
183
|
+
this._runtime = "pending";
|
|
184
|
+
this._relay = "connecting";
|
|
185
|
+
this._sessionId = undefined;
|
|
186
|
+
this.rpcReady = false;
|
|
187
|
+
this.extensionReady = false;
|
|
188
|
+
this.extensionReadyEvent = null;
|
|
189
|
+
this._runtimeInstanceId = randomUUID();
|
|
190
|
+
this.readyPromise = new Promise((resolve) => { this.readyResolve = resolve; });
|
|
191
|
+
const target = resolvePiSpawn(this.opts.piBin ?? "pi");
|
|
192
|
+
const config = this.opts.config ?? loadLocalConfig(this.opts.cwd);
|
|
193
|
+
const sessionName = config.agent_name ?? defaultAgentName(this.opts.cwd);
|
|
217
194
|
const useContinue = !this.forceFreshSessionOnNextSpawn;
|
|
218
195
|
this.forceFreshSessionOnNextSpawn = false;
|
|
219
|
-
|
|
220
|
-
// POSIX it's empty and `command` is `pi` itself.
|
|
221
|
-
const args = [...piTarget.prefixArgs, ...rpcSpawnArgs(this.opts.extensionPath, sessionName, useContinue)];
|
|
196
|
+
const args = [...target.prefixArgs, ...rpcSpawnArgs(this.opts.extensionPath, sessionName, useContinue)];
|
|
222
197
|
const env = {
|
|
223
198
|
...process.env,
|
|
224
199
|
...this.opts.env,
|
|
225
|
-
// Mandatory daemon marker — `_cmdRoot` in index.ts can use this to
|
|
226
|
-
// bail early if local config is missing (no wizard in RPC mode).
|
|
227
200
|
REMOTE_PI_DAEMON: "1",
|
|
228
|
-
|
|
201
|
+
REMOTE_PI_ENDPOINT_ID: this.opts.endpointId ?? "",
|
|
202
|
+
REMOTE_PI_RUNTIME_INSTANCE_ID: this._runtimeInstanceId,
|
|
229
203
|
...(this.opts.config ? { REMOTE_PI_DIRECT_CONFIG: JSON.stringify(this.opts.config) } : {}),
|
|
230
204
|
};
|
|
231
|
-
const child = spawn(
|
|
232
|
-
cwd: this.opts.cwd,
|
|
233
|
-
env,
|
|
234
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
235
|
-
// Windows: suppress any console window the child might allocate. With the
|
|
236
|
-
// node+cli.js path (resolvePiSpawn) there's no cmd.exe layer, but keep this
|
|
237
|
-
// as belt-and-suspenders alongside the supervisor's hidden wscript launcher
|
|
238
|
-
// (service-templates/task-launcher.vbs.template). No-op on POSIX.
|
|
239
|
-
windowsHide: process.platform === "win32",
|
|
240
|
-
});
|
|
205
|
+
const child = spawn(target.command, args, { cwd: this.opts.cwd, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: process.platform === "win32" });
|
|
241
206
|
this.child = child;
|
|
242
207
|
this._startedAt = Date.now();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
child.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
process.stderr.write(`[${this.opts.cwd}] ${chunk.toString()}`);
|
|
208
|
+
// A spawned ChildProcess object is not evidence its executable completed
|
|
209
|
+
// startup. Preserve `spawning` until Node reports the OS spawn event.
|
|
210
|
+
child.once("spawn", () => {
|
|
211
|
+
if (this.child === child && this._process === "spawning")
|
|
212
|
+
this._process = "running";
|
|
249
213
|
});
|
|
250
|
-
child.on("
|
|
251
|
-
child.on("
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
214
|
+
child.stdout?.on("data", (chunk) => this.onStdout(chunk));
|
|
215
|
+
child.stderr?.on("data", (chunk) => process.stderr.write(`[${this.opts.cwd}] ${chunk.toString()}`));
|
|
216
|
+
child.on("exit", (code, signal) => this.onExit(code, signal));
|
|
217
|
+
child.on("error", (error) => {
|
|
218
|
+
this.failRuntime({ stage: "spawn", code: "spawn_failed", message: String(error), retryable: true });
|
|
219
|
+
this._process = "exited";
|
|
256
220
|
});
|
|
257
|
-
this.
|
|
221
|
+
this.startReadinessProbe();
|
|
222
|
+
this.emit("spawn", { pid: child.pid, runtime_instance_id: this._runtimeInstanceId });
|
|
258
223
|
}
|
|
259
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
* stdout).
|
|
264
|
-
*
|
|
265
|
-
* Returns false if the child isn't running (caller decides how to report).
|
|
266
|
-
*/
|
|
267
|
-
sendPrompt(text, requestId) {
|
|
268
|
-
if (!this.child || !this.child.stdin || this._state !== "running")
|
|
269
|
-
return false;
|
|
270
|
-
const cmd = { id: requestId ?? `sv-${Date.now()}`, type: "prompt", message: text };
|
|
271
|
-
try {
|
|
272
|
-
this.child.stdin.write(JSON.stringify(cmd) + "\n");
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
return false;
|
|
224
|
+
/** Waits for Pi's correlated prompt preflight response, not merely stdin write. */
|
|
225
|
+
sendPrompt(text, requestId = `sv-${randomUUID()}`, timeoutMs = this.opts.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS) {
|
|
226
|
+
if (!this.child?.stdin || this._process !== "running" || this._runtime !== "ready") {
|
|
227
|
+
return Promise.resolve({ accepted: false, code: "runtime_not_ready", error: "daemon RPC runtime is not ready" });
|
|
277
228
|
}
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
const timer = setTimeout(() => {
|
|
231
|
+
this.promptPending.delete(requestId);
|
|
232
|
+
resolve({ accepted: false, code: "prompt_response_timeout", error: "Pi did not accept the prompt before timeout" });
|
|
233
|
+
}, timeoutMs);
|
|
234
|
+
this.promptPending.set(requestId, { resolve, timer });
|
|
235
|
+
try {
|
|
236
|
+
this.child.stdin.write(JSON.stringify({ id: requestId, type: "prompt", message: text }) + "\n");
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
this.promptPending.delete(requestId);
|
|
241
|
+
resolve({ accepted: false, code: "stdin_write_failed", error: String(error) });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
278
244
|
}
|
|
279
|
-
|
|
280
|
-
* Asks the child to exit gracefully. Sends SIGTERM; if the child doesn't
|
|
281
|
-
* exit within `timeoutMs`, escalates to SIGKILL. Resolves when the
|
|
282
|
-
* `exit` event fires.
|
|
283
|
-
*/
|
|
284
|
-
async stop(timeoutMs = 5000) {
|
|
245
|
+
async stop(timeoutMs = 5_000) {
|
|
285
246
|
if (!this.child)
|
|
286
247
|
return;
|
|
287
|
-
this._stopping = true;
|
|
248
|
+
this._stopping = true;
|
|
288
249
|
const child = this.child;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
this.once("exit", onExit);
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
this.once("exit", resolve);
|
|
292
252
|
try {
|
|
293
253
|
child.kill("SIGTERM");
|
|
294
254
|
}
|
|
295
|
-
catch {
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
child.kill("SIGKILL");
|
|
299
|
-
}
|
|
300
|
-
catch { /* race — already dead */ }
|
|
301
|
-
}, timeoutMs);
|
|
302
|
-
this.once("exit", () => clearTimeout(t));
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
_onStdout(chunk) {
|
|
306
|
-
this.stdoutBuf += chunk.toString();
|
|
307
|
-
let nl;
|
|
308
|
-
while ((nl = this.stdoutBuf.indexOf("\n")) >= 0) {
|
|
309
|
-
const line = this.stdoutBuf.slice(0, nl);
|
|
310
|
-
this.stdoutBuf = this.stdoutBuf.slice(nl + 1);
|
|
311
|
-
if (!line.trim())
|
|
312
|
-
continue;
|
|
313
|
-
this._handleStdoutLine(line);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
_handleStdoutLine(line) {
|
|
317
|
-
const t = busyTransition(line);
|
|
318
|
-
if (t !== null)
|
|
319
|
-
this._busy = t;
|
|
320
|
-
const gs = parseGetStateResponse(line);
|
|
321
|
-
if (gs && gs.id) {
|
|
322
|
-
const pending = this._statePending.get(gs.id);
|
|
323
|
-
if (pending) {
|
|
324
|
-
clearTimeout(pending.timer);
|
|
325
|
-
this._statePending.delete(gs.id);
|
|
326
|
-
if (typeof gs.isStreaming === "boolean")
|
|
327
|
-
this._busy = gs.isStreaming;
|
|
328
|
-
pending.resolve(this._busy);
|
|
255
|
+
catch {
|
|
256
|
+
resolve();
|
|
329
257
|
}
|
|
330
|
-
|
|
331
|
-
|
|
258
|
+
const timer = setTimeout(() => { try {
|
|
259
|
+
child.kill("SIGKILL");
|
|
260
|
+
}
|
|
261
|
+
catch { /* process already gone */ } }, timeoutMs);
|
|
262
|
+
this.once("exit", () => clearTimeout(timer));
|
|
263
|
+
});
|
|
332
264
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
* `_busy` to `isStreaming`. Resolves the passive flag on timeout (no
|
|
336
|
-
* response) or when the child isn't running. Used by the cron `skip_if_busy`
|
|
337
|
-
* gate, where a false "not busy" would pile a prompt onto a live turn.
|
|
338
|
-
*/
|
|
339
|
-
async refreshBusy(timeoutMs = 1500) {
|
|
340
|
-
if (this._state !== "running" || !this.child?.stdin)
|
|
265
|
+
async refreshBusy(timeoutMs = 1_500) {
|
|
266
|
+
if (this._process !== "running" || !this.child?.stdin)
|
|
341
267
|
return this._busy;
|
|
342
|
-
const id = `gs-${
|
|
268
|
+
const id = `gs-${randomUUID()}`;
|
|
343
269
|
return new Promise((resolve) => {
|
|
344
|
-
const timer = setTimeout(() => {
|
|
345
|
-
|
|
346
|
-
resolve(this._busy);
|
|
347
|
-
}, timeoutMs);
|
|
348
|
-
this._statePending.set(id, { resolve, timer });
|
|
270
|
+
const timer = setTimeout(() => { this.statePending.delete(id); resolve(this._busy); }, timeoutMs);
|
|
271
|
+
this.statePending.set(id, { resolve, timer });
|
|
349
272
|
try {
|
|
350
273
|
this.child.stdin.write(JSON.stringify({ id, type: "get_state" }) + "\n");
|
|
351
274
|
}
|
|
352
275
|
catch {
|
|
353
276
|
clearTimeout(timer);
|
|
354
|
-
this.
|
|
277
|
+
this.statePending.delete(id);
|
|
355
278
|
resolve(this._busy);
|
|
356
279
|
}
|
|
357
280
|
});
|
|
358
281
|
}
|
|
359
|
-
/** Test
|
|
360
|
-
_ingestStdoutForTest(line) {
|
|
361
|
-
|
|
282
|
+
/** Test injection point for deterministic RPC event/response coverage. */
|
|
283
|
+
_ingestStdoutForTest(line) { this.handleStdoutLine(line); }
|
|
284
|
+
startReadinessProbe() {
|
|
285
|
+
const timeoutMs = this.opts.readinessTimeoutMs ?? DEFAULT_READINESS_TIMEOUT_MS;
|
|
286
|
+
this.readinessTimer = setTimeout(() => {
|
|
287
|
+
if (this.rpcReady && !this.extensionReady) {
|
|
288
|
+
this.failRuntime({ stage: "extension", code: "extension_not_ready", message: "Pi RPC is ready but the configured Remote Pi extension did not report readiness", retryable: false });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.failRuntime({ stage: "rpc_probe", code: "readiness_timeout", message: "Pi RPC get_state did not respond before timeout", retryable: true });
|
|
292
|
+
}, timeoutMs);
|
|
293
|
+
const id = `ready-${randomUUID()}`;
|
|
294
|
+
try {
|
|
295
|
+
this.child?.stdin?.write(JSON.stringify({ id, type: "get_state" }) + "\n");
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
this.failRuntime({ stage: "rpc_probe", code: "readiness_probe_write_failed", message: String(error), retryable: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
onStdout(chunk) {
|
|
302
|
+
this.stdoutBuf += chunk.toString();
|
|
303
|
+
let newline;
|
|
304
|
+
while ((newline = this.stdoutBuf.indexOf("\n")) >= 0) {
|
|
305
|
+
const line = this.stdoutBuf.slice(0, newline);
|
|
306
|
+
this.stdoutBuf = this.stdoutBuf.slice(newline + 1);
|
|
307
|
+
if (line.trim())
|
|
308
|
+
this.handleStdoutLine(line);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
handleStdoutLine(line) {
|
|
312
|
+
const busy = busyTransition(line);
|
|
313
|
+
if (busy !== null)
|
|
314
|
+
this._busy = busy;
|
|
315
|
+
const response = this.parseResponse(line);
|
|
316
|
+
if (response)
|
|
317
|
+
this.handleResponse(response);
|
|
318
|
+
const event = parseRuntimeEvent(line);
|
|
319
|
+
if (event)
|
|
320
|
+
this.handleRuntimeEvent(event);
|
|
321
|
+
this.emit("stdout", line);
|
|
322
|
+
}
|
|
323
|
+
parseResponse(line) {
|
|
324
|
+
try {
|
|
325
|
+
const value = JSON.parse(line);
|
|
326
|
+
return value.type === "response" ? value : null;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
handleResponse(response) {
|
|
333
|
+
if (typeof response.id !== "string" || typeof response.command !== "string")
|
|
334
|
+
return;
|
|
335
|
+
if (response.command === "get_state") {
|
|
336
|
+
const pending = this.statePending.get(response.id);
|
|
337
|
+
if (pending) {
|
|
338
|
+
clearTimeout(pending.timer);
|
|
339
|
+
this.statePending.delete(response.id);
|
|
340
|
+
const streaming = response.data?.isStreaming;
|
|
341
|
+
if (typeof streaming === "boolean")
|
|
342
|
+
this._busy = streaming;
|
|
343
|
+
pending.resolve(this._busy);
|
|
344
|
+
}
|
|
345
|
+
if (response.success === true) {
|
|
346
|
+
const data = response.data;
|
|
347
|
+
this.rpcReady = true;
|
|
348
|
+
if (typeof data?.sessionId === "string")
|
|
349
|
+
this._sessionId = data.sessionId;
|
|
350
|
+
this.maybeMarkReady();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.failRuntime({ stage: "rpc_probe", code: "readiness_probe_rejected", message: typeof response.error === "string" ? response.error : "Pi rejected get_state", retryable: true });
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (response.command === "prompt") {
|
|
358
|
+
const pending = this.promptPending.get(response.id);
|
|
359
|
+
if (!pending)
|
|
360
|
+
return;
|
|
361
|
+
clearTimeout(pending.timer);
|
|
362
|
+
this.promptPending.delete(response.id);
|
|
363
|
+
pending.resolve(response.success === true
|
|
364
|
+
? { accepted: true }
|
|
365
|
+
: { accepted: false, code: "prompt_rejected", error: typeof response.error === "string" ? response.error : "Pi rejected prompt" });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
handleRuntimeEvent(event) {
|
|
369
|
+
switch (event.type) {
|
|
370
|
+
case "runtime_ready":
|
|
371
|
+
this.acceptExtensionReady(event.event);
|
|
372
|
+
break;
|
|
373
|
+
case "runtime_failed":
|
|
374
|
+
this.failRuntime(event.event);
|
|
375
|
+
break;
|
|
376
|
+
case "relay_state_changed":
|
|
377
|
+
this._relay = event.event.state;
|
|
378
|
+
this.emit("relay_state_changed", event.event);
|
|
379
|
+
break;
|
|
380
|
+
case "session_changed":
|
|
381
|
+
this._sessionId = event.event.session_id;
|
|
382
|
+
this.emit("session_changed", event.event);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
362
385
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
386
|
+
acceptExtensionReady(event) {
|
|
387
|
+
if (this._runtime === "failed" || !this.child)
|
|
388
|
+
return;
|
|
389
|
+
if (event.control_protocol_version !== 2) {
|
|
390
|
+
this.failRuntime({ stage: "extension", code: "control_protocol_mismatch", message: "Remote Pi extension control protocol is incompatible", retryable: false });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (event.endpoint_id !== this.opts.endpointId || event.runtime_instance_id !== this._runtimeInstanceId) {
|
|
394
|
+
this.failRuntime({ stage: "extension", code: "runtime_identity_mismatch", message: "Remote Pi extension reported a different endpoint runtime identity", retryable: false });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.extensionReady = true;
|
|
398
|
+
this.extensionReadyEvent = event;
|
|
399
|
+
if (event.session_id)
|
|
400
|
+
this._sessionId = event.session_id;
|
|
401
|
+
this.maybeMarkReady();
|
|
402
|
+
}
|
|
403
|
+
maybeMarkReady() {
|
|
404
|
+
if (!this.rpcReady || !this.extensionReady || this._runtime === "failed" || !this.child)
|
|
405
|
+
return;
|
|
406
|
+
this._process = "running";
|
|
407
|
+
if (this.readinessTimer) {
|
|
408
|
+
clearTimeout(this.readinessTimer);
|
|
409
|
+
this.readinessTimer = null;
|
|
410
|
+
}
|
|
411
|
+
this._runtime = "ready";
|
|
412
|
+
this._state = "running";
|
|
413
|
+
this.readyResolve?.(true);
|
|
414
|
+
this.readyResolve = null;
|
|
415
|
+
this.emit("runtime_ready", { ...this.extensionReadyEvent, session_id: this._sessionId, endpoint_id: this.opts.endpointId, runtime_instance_id: this._runtimeInstanceId });
|
|
416
|
+
}
|
|
417
|
+
failRuntime(event) {
|
|
418
|
+
if (this._runtime === "failed")
|
|
419
|
+
return;
|
|
420
|
+
if (this.readinessTimer) {
|
|
421
|
+
clearTimeout(this.readinessTimer);
|
|
422
|
+
this.readinessTimer = null;
|
|
423
|
+
}
|
|
424
|
+
this._runtime = "failed";
|
|
425
|
+
this._state = event.retryable ? "crashed" : "blocked";
|
|
426
|
+
this.readyResolve?.(false);
|
|
427
|
+
this.readyResolve = null;
|
|
428
|
+
this.emit("runtime_failed", event);
|
|
429
|
+
}
|
|
430
|
+
onExit(code, signal) {
|
|
367
431
|
if (code === EXIT_DAEMON_FRESH_SESSION)
|
|
368
432
|
this.forceFreshSessionOnNextSpawn = true;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
433
|
+
if (this.readinessTimer) {
|
|
434
|
+
clearTimeout(this.readinessTimer);
|
|
435
|
+
this.readinessTimer = null;
|
|
436
|
+
}
|
|
437
|
+
const isCrash = !this._stopping;
|
|
438
|
+
this._process = "exited";
|
|
439
|
+
if (this._runtime !== "failed")
|
|
440
|
+
this._runtime = "pending";
|
|
372
441
|
this._state = isCrash ? "crashed" : "stopped";
|
|
373
442
|
this.child = null;
|
|
374
|
-
this._startedAt =
|
|
443
|
+
this._startedAt = undefined;
|
|
375
444
|
this._busy = false;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
445
|
+
this._relay = "disconnected";
|
|
446
|
+
this.readyResolve?.(false);
|
|
447
|
+
this.readyResolve = null;
|
|
448
|
+
for (const pending of this.statePending.values()) {
|
|
449
|
+
clearTimeout(pending.timer);
|
|
450
|
+
pending.resolve(false);
|
|
451
|
+
}
|
|
452
|
+
this.statePending.clear();
|
|
453
|
+
for (const pending of this.promptPending.values()) {
|
|
454
|
+
clearTimeout(pending.timer);
|
|
455
|
+
pending.resolve({ accepted: false, code: "child_exited", error: "daemon exited before Pi accepted the prompt" });
|
|
379
456
|
}
|
|
380
|
-
this.
|
|
457
|
+
this.promptPending.clear();
|
|
381
458
|
this.emit("exit", { code, signal, isCrash });
|
|
382
459
|
}
|
|
383
|
-
|
|
384
|
-
* decides to re-spawn after a crash. Exposed so tests can drive it. */
|
|
385
|
-
noteRestart() {
|
|
386
|
-
this._restartCount += 1;
|
|
387
|
-
}
|
|
460
|
+
noteRestart() { this._restartCount += 1; }
|
|
388
461
|
}
|
|
389
462
|
//# sourceMappingURL=rpc_child.js.map
|