@hheei/pi-subagents 0.1.2 → 0.2.2
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 +42 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -52
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/runner.js
DELETED
|
@@ -1,930 +0,0 @@
|
|
|
1
|
-
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { chmod, lstat, unlink } from "node:fs/promises";
|
|
3
|
-
import { createConnection, createServer } from "node:net";
|
|
4
|
-
import { abortError, errorMessage, toError, writeDiagnostic } from "./diagnostics.js";
|
|
5
|
-
import { isRecord, PROTOCOL_VERSION } from "./domain.js";
|
|
6
|
-
import { attachJsonLineReader, writeJsonLine } from "./json-lines.js";
|
|
7
|
-
import { CANCEL_PAUSE_EVENT, CHILD_LIFECYCLE_EVENT, DEFAULT_MAX_BUFFERED_EVENTS, DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING_REQUESTS, eventFrame, failureResponse, isChildLifecyclePayload, isContactReportPayload, isHelloFrame, isPauseReportPayload, isRequestFrame, PAUSE_EVENT, RUNNER_EVENTS_DROPPED_EVENT, RUNNER_EXIT_EVENT, successResponse, WRITER_EXIT_EVENT, } from "./protocol.js";
|
|
8
|
-
import { PiRpcAdapter, PiRpcTimeoutError } from "./rpc-adapter.js";
|
|
9
|
-
import { isIdlePiState, shouldForwardPiEvent } from "./rpc-events.js";
|
|
10
|
-
/** Bounded memory of finished request IDs, used to answer duplicate requests. */
|
|
11
|
-
const MAX_RETIRED_REQUEST_IDS = 256;
|
|
12
|
-
/** Unauthenticated connections accepted at once before new ones are refused. */
|
|
13
|
-
const MAX_PENDING_HANDSHAKES = 16;
|
|
14
|
-
/**
|
|
15
|
-
* Starts the reconnectable runner endpoint for one Pi RPC child.
|
|
16
|
-
*
|
|
17
|
-
* The runner owns the child stdio, accepts exactly one authenticated controller
|
|
18
|
-
* at a time, and stays alive across controller disconnects: only an explicit
|
|
19
|
-
* `shutdown` request, the end of the Pi child, or process termination stops it.
|
|
20
|
-
*/
|
|
21
|
-
export async function startRunner(options) {
|
|
22
|
-
const runner = new Runner(options);
|
|
23
|
-
const readiness = await runner.start();
|
|
24
|
-
return {
|
|
25
|
-
endpoint: runner.endpoint,
|
|
26
|
-
identity: options.identity,
|
|
27
|
-
readiness,
|
|
28
|
-
closed: () => runner.closed(),
|
|
29
|
-
shutdown: (reason) => runner.shutdown(reason),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
class Runner {
|
|
33
|
-
endpoint;
|
|
34
|
-
identity;
|
|
35
|
-
#process;
|
|
36
|
-
#adapter;
|
|
37
|
-
#spawnWriter;
|
|
38
|
-
#diagnose;
|
|
39
|
-
#maxFrameBytes;
|
|
40
|
-
#maxPendingRequests;
|
|
41
|
-
#maxBufferedEvents;
|
|
42
|
-
#maxControllerBufferBytes;
|
|
43
|
-
#requestTimeoutMs;
|
|
44
|
-
#readyTimeoutMs;
|
|
45
|
-
#handshakeTimeoutMs;
|
|
46
|
-
#shutdownGraceMs;
|
|
47
|
-
#killGraceMs;
|
|
48
|
-
#pendingHandshakes = new Map();
|
|
49
|
-
#authenticating = new Set();
|
|
50
|
-
#bufferedEvents = [];
|
|
51
|
-
#detachEvents;
|
|
52
|
-
#reporters = new Map();
|
|
53
|
-
#pendingReports = [];
|
|
54
|
-
#controller;
|
|
55
|
-
#server;
|
|
56
|
-
#boundEndpoint = false;
|
|
57
|
-
#droppedEvents = 0;
|
|
58
|
-
#readyState = undefined;
|
|
59
|
-
#shutdownRequested = false;
|
|
60
|
-
#writerClosing = false;
|
|
61
|
-
#writerExit;
|
|
62
|
-
#exit;
|
|
63
|
-
#closing;
|
|
64
|
-
#authorizeRecovery;
|
|
65
|
-
#bridgeBound = true;
|
|
66
|
-
#boundSessionId;
|
|
67
|
-
#bridge;
|
|
68
|
-
#pauseGeneration = 0;
|
|
69
|
-
#pauseWaiters = [];
|
|
70
|
-
#resolveClosed;
|
|
71
|
-
constructor(options) {
|
|
72
|
-
this.endpoint = options.identity.endpoint;
|
|
73
|
-
this.identity = options.identity;
|
|
74
|
-
this.#process = options.process;
|
|
75
|
-
this.#spawnWriter = options.spawnWriter;
|
|
76
|
-
this.#diagnose = options.onDiagnostic ?? ((line) => writeDiagnostic("runner", line));
|
|
77
|
-
this.#maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
|
|
78
|
-
this.#maxPendingRequests = options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS;
|
|
79
|
-
this.#maxBufferedEvents = options.maxBufferedEvents ?? DEFAULT_MAX_BUFFERED_EVENTS;
|
|
80
|
-
this.#maxControllerBufferBytes = options.maxControllerBufferBytes ?? 4 * this.#maxFrameBytes;
|
|
81
|
-
this.#requestTimeoutMs = options.requestTimeoutMs ?? 120_000;
|
|
82
|
-
this.#readyTimeoutMs = options.readyTimeoutMs ?? 60_000;
|
|
83
|
-
this.#handshakeTimeoutMs = options.handshakeTimeoutMs ?? 10_000;
|
|
84
|
-
this.#shutdownGraceMs = options.shutdownGraceMs ?? 2_000;
|
|
85
|
-
this.#killGraceMs = options.killGraceMs ?? 1_000;
|
|
86
|
-
this.#authorizeRecovery = options.authorizeRecovery;
|
|
87
|
-
this.#boundSessionId = options.sessionId;
|
|
88
|
-
this.#bindWriter(options.process);
|
|
89
|
-
}
|
|
90
|
-
#bindWriter(child) {
|
|
91
|
-
this.#process = child;
|
|
92
|
-
this.#writerExit = undefined;
|
|
93
|
-
this.#adapter = new PiRpcAdapter({
|
|
94
|
-
process: child,
|
|
95
|
-
maxFrameBytes: this.#maxFrameBytes,
|
|
96
|
-
maxPendingRequests: this.#maxPendingRequests,
|
|
97
|
-
requestTimeoutMs: this.#requestTimeoutMs,
|
|
98
|
-
onListenerError: (error) => this.#diagnose(`event listener failed: ${errorMessage(error)}`),
|
|
99
|
-
});
|
|
100
|
-
this.#detachEvents = this.#adapter.onEvent((event) => {
|
|
101
|
-
if (shouldForwardPiEvent(event))
|
|
102
|
-
this.#broadcast(event);
|
|
103
|
-
});
|
|
104
|
-
child.once("exit", (code, signal) => this.#onChildExit(code, signal));
|
|
105
|
-
}
|
|
106
|
-
/** Confirms a real Pi RPC round trip, then publishes the endpoint. */
|
|
107
|
-
async start() {
|
|
108
|
-
const adapter = this.#adapter;
|
|
109
|
-
if (adapter === undefined)
|
|
110
|
-
throw new Error("Pi RPC writer is not bound");
|
|
111
|
-
try {
|
|
112
|
-
this.#readyState = await adapter.ready(AbortSignal.timeout(this.#readyTimeoutMs));
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
this.#teardownWriter(toError(error));
|
|
116
|
-
throw new Error(`Pi RPC did not become ready: ${errorMessage(error)}`);
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
await this.#listen();
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
await this.shutdown(toError(error));
|
|
123
|
-
throw toError(error);
|
|
124
|
-
}
|
|
125
|
-
return this.#readyState;
|
|
126
|
-
}
|
|
127
|
-
closed() {
|
|
128
|
-
if (this.#exit !== undefined)
|
|
129
|
-
return Promise.resolve(this.#exit);
|
|
130
|
-
return new Promise((resolve) => {
|
|
131
|
-
this.#resolveClosed = resolve;
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
shutdown(reason = new Error("Runner shutdown requested")) {
|
|
135
|
-
if (this.#closing !== undefined)
|
|
136
|
-
return this.#closing;
|
|
137
|
-
this.#closing = this.#runShutdown(reason);
|
|
138
|
-
return this.#closing;
|
|
139
|
-
}
|
|
140
|
-
async #runShutdown(reason) {
|
|
141
|
-
this.#shutdownRequested = true;
|
|
142
|
-
this.#diagnose(`shutting down: ${reason.message}`);
|
|
143
|
-
try {
|
|
144
|
-
return await this.#cleanup(reason);
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
this.#diagnose(`shutdown cleanup failed: ${errorMessage(error)}`);
|
|
148
|
-
const exit = this.#exit ?? {
|
|
149
|
-
code: this.#process?.exitCode ?? this.#writerExit?.code ?? null,
|
|
150
|
-
signal: this.#process?.signalCode ?? this.#writerExit?.signal ?? null,
|
|
151
|
-
reason,
|
|
152
|
-
requested: true,
|
|
153
|
-
};
|
|
154
|
-
this.#finish(exit);
|
|
155
|
-
return exit;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async #cleanup(reason) {
|
|
159
|
-
const controller = this.#controller;
|
|
160
|
-
this.#controller = undefined;
|
|
161
|
-
if (controller !== undefined) {
|
|
162
|
-
// Let the final events (child exit, gap notice) reach a live controller.
|
|
163
|
-
await drainSocket(controller.socket, 250);
|
|
164
|
-
this.#destroyController(controller, reason);
|
|
165
|
-
}
|
|
166
|
-
for (const [socket, handshake] of this.#pendingHandshakes) {
|
|
167
|
-
clearTimeout(handshake.timer);
|
|
168
|
-
handshake.detach();
|
|
169
|
-
socket.destroy();
|
|
170
|
-
}
|
|
171
|
-
this.#pendingHandshakes.clear();
|
|
172
|
-
for (const reporter of this.#reporters.values())
|
|
173
|
-
this.#dropReporter(reporter);
|
|
174
|
-
this.#reporters.clear();
|
|
175
|
-
if (this.#bridge !== undefined)
|
|
176
|
-
this.#dropBridge(this.#bridge);
|
|
177
|
-
this.#failPauseWaiters(new Error("Runner is shutting down"));
|
|
178
|
-
await this.#closeServer();
|
|
179
|
-
await this.#releaseChild();
|
|
180
|
-
this.#teardownWriter(reason);
|
|
181
|
-
const exit = this.#exit ?? {
|
|
182
|
-
code: this.#process?.exitCode ?? this.#writerExit?.code ?? null,
|
|
183
|
-
signal: this.#process?.signalCode ?? this.#writerExit?.signal ?? null,
|
|
184
|
-
reason,
|
|
185
|
-
requested: true,
|
|
186
|
-
};
|
|
187
|
-
this.#finish(exit);
|
|
188
|
-
return exit;
|
|
189
|
-
}
|
|
190
|
-
async #listen() {
|
|
191
|
-
await this.#clearStaleEndpoint();
|
|
192
|
-
const server = createServer({ allowHalfOpen: false });
|
|
193
|
-
this.#server = server;
|
|
194
|
-
server.on("connection", (socket) => this.#accept(socket));
|
|
195
|
-
server.on("error", (error) => this.#diagnose(`socket server error: ${errorMessage(error)}`));
|
|
196
|
-
const listening = new Promise((resolve, reject) => {
|
|
197
|
-
server.once("error", reject);
|
|
198
|
-
server.listen(this.endpoint, () => {
|
|
199
|
-
server.off("error", reject);
|
|
200
|
-
resolve();
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
try {
|
|
204
|
-
await listening;
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
this.#server = undefined;
|
|
208
|
-
server.close();
|
|
209
|
-
throw toError(error);
|
|
210
|
-
}
|
|
211
|
-
this.#boundEndpoint = true;
|
|
212
|
-
await this.#restrictEndpoint();
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Refuses to steal an endpoint owned by a live runner. A socket that exists
|
|
216
|
-
* but cannot be connected to is a leftover from a dead runner and is removed.
|
|
217
|
-
*/
|
|
218
|
-
async #clearStaleEndpoint() {
|
|
219
|
-
try {
|
|
220
|
-
const stats = await lstat(this.endpoint);
|
|
221
|
-
if (!stats.isSocket()) {
|
|
222
|
-
throw new Error(`runner endpoint path exists and is not a Unix socket: ${this.endpoint}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
if (isErrno(error, "ENOENT"))
|
|
227
|
-
return;
|
|
228
|
-
throw toError(error);
|
|
229
|
-
}
|
|
230
|
-
if (await isEndpointLive(this.endpoint)) {
|
|
231
|
-
throw new Error(`a live runner already owns endpoint ${this.endpoint}`);
|
|
232
|
-
}
|
|
233
|
-
await unlink(this.endpoint);
|
|
234
|
-
this.#diagnose(`removed stale endpoint ${this.endpoint}`);
|
|
235
|
-
}
|
|
236
|
-
async #restrictEndpoint() {
|
|
237
|
-
try {
|
|
238
|
-
await chmod(this.endpoint, 0o600);
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
this.#diagnose(`could not restrict endpoint permissions: ${errorMessage(error)}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
async #closeServer() {
|
|
245
|
-
const server = this.#server;
|
|
246
|
-
this.#server = undefined;
|
|
247
|
-
if (server !== undefined) {
|
|
248
|
-
await new Promise((resolve) => {
|
|
249
|
-
server.close(() => resolve());
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
// Only remove a socket this runner created; another live runner may own it.
|
|
253
|
-
if (!this.#boundEndpoint)
|
|
254
|
-
return;
|
|
255
|
-
this.#boundEndpoint = false;
|
|
256
|
-
try {
|
|
257
|
-
await unlink(this.endpoint);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
if (!isErrno(error, "ENOENT"))
|
|
261
|
-
this.#diagnose(`could not remove endpoint: ${errorMessage(error)}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
#accept(socket) {
|
|
265
|
-
if (this.#pendingHandshakes.size >= MAX_PENDING_HANDSHAKES) {
|
|
266
|
-
this.#diagnose("refused connection: too many unauthenticated connections");
|
|
267
|
-
socket.destroy();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
socket.setNoDelay(true);
|
|
271
|
-
const detach = attachJsonLineReader(socket, {
|
|
272
|
-
maxFrameBytes: this.#maxFrameBytes,
|
|
273
|
-
onError: (error) => {
|
|
274
|
-
this.#diagnose(`controller stream error: ${error.message}`);
|
|
275
|
-
this.#dropSocket(socket);
|
|
276
|
-
},
|
|
277
|
-
onValue: (value) => {
|
|
278
|
-
const reporter = this.#reporters.get(socket) ?? this.#bridgeFor(socket);
|
|
279
|
-
if (reporter !== undefined) {
|
|
280
|
-
this.#handleReporterRequest(reporter, value);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const controller = this.#controller;
|
|
284
|
-
if (controller !== undefined && controller.socket === socket) {
|
|
285
|
-
this.#handleRequest(controller, value);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (this.#authenticating.has(socket)) {
|
|
289
|
-
this.#diagnose("rejected connection: overlapping handshake frames");
|
|
290
|
-
this.#dropSocket(socket);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
void this.#authenticate(socket, value);
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
const timer = setTimeout(() => {
|
|
297
|
-
this.#diagnose("refused connection: handshake deadline exceeded");
|
|
298
|
-
this.#dropSocket(socket);
|
|
299
|
-
}, this.#handshakeTimeoutMs);
|
|
300
|
-
this.#pendingHandshakes.set(socket, { detach, timer });
|
|
301
|
-
socket.once("close", () => {
|
|
302
|
-
const handshake = this.#pendingHandshakes.get(socket);
|
|
303
|
-
if (handshake !== undefined) {
|
|
304
|
-
clearTimeout(handshake.timer);
|
|
305
|
-
this.#pendingHandshakes.delete(socket);
|
|
306
|
-
handshake.detach();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const reporter = this.#reporters.get(socket);
|
|
310
|
-
if (reporter !== undefined) {
|
|
311
|
-
this.#reporters.delete(socket);
|
|
312
|
-
reporter.detach();
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (this.#bridge?.socket === socket) {
|
|
316
|
-
this.#bridge.detach();
|
|
317
|
-
this.#bridge = undefined;
|
|
318
|
-
this.#diagnose("child bridge disconnected; runner keeps running");
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const controller = this.#controller;
|
|
322
|
-
if (controller === undefined || controller.socket !== socket)
|
|
323
|
-
return;
|
|
324
|
-
this.#controller = undefined;
|
|
325
|
-
controller.detach();
|
|
326
|
-
this.#diagnose("controller disconnected; runner keeps running");
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
/** Only a connection that proves identity and token may control the runner. */
|
|
330
|
-
async #authenticate(socket, value) {
|
|
331
|
-
const handshake = this.#pendingHandshakes.get(socket);
|
|
332
|
-
if (handshake === undefined) {
|
|
333
|
-
socket.destroy();
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (!isHelloFrame(value) || !this.#matchesStaticIdentity(value)) {
|
|
337
|
-
this.#diagnose("rejected connection: handshake did not authenticate");
|
|
338
|
-
this.#dropSocket(socket);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
this.#authenticating.add(socket);
|
|
342
|
-
try {
|
|
343
|
-
const authenticated = value.role === "recovery"
|
|
344
|
-
? value.claimId !== undefined &&
|
|
345
|
-
this.#authorizeRecovery !== undefined &&
|
|
346
|
-
(await this.#authorizeRecovery(value.claimId, value.token))
|
|
347
|
-
: tokensMatch(value.token, this.identity.token);
|
|
348
|
-
if (!authenticated || socket.destroyed || !this.#pendingHandshakes.has(socket)) {
|
|
349
|
-
this.#diagnose("rejected connection: handshake did not authenticate");
|
|
350
|
-
this.#dropSocket(socket);
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
clearTimeout(handshake.timer);
|
|
354
|
-
this.#pendingHandshakes.delete(socket);
|
|
355
|
-
if (value.role === "reporter") {
|
|
356
|
-
const reporter = { socket, detach: handshake.detach };
|
|
357
|
-
this.#reporters.set(socket, reporter);
|
|
358
|
-
void writeJsonLine(socket, { version: PROTOCOL_VERSION, type: "hello_ack" }, this.#maxFrameBytes).catch((error) => {
|
|
359
|
-
this.#diagnose(`reporter handshake write failed: ${errorMessage(error)}`);
|
|
360
|
-
this.#dropReporter(reporter);
|
|
361
|
-
});
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (value.role === "bridge") {
|
|
365
|
-
const previous = this.#bridge;
|
|
366
|
-
if (previous !== undefined)
|
|
367
|
-
this.#dropBridge(previous);
|
|
368
|
-
const bridge = { socket, detach: handshake.detach };
|
|
369
|
-
this.#bridge = bridge;
|
|
370
|
-
void writeJsonLine(socket, { version: PROTOCOL_VERSION, type: "hello_ack" }, this.#maxFrameBytes).catch((error) => {
|
|
371
|
-
this.#diagnose(`bridge handshake write failed: ${errorMessage(error)}`);
|
|
372
|
-
this.#dropBridge(bridge);
|
|
373
|
-
});
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const previous = this.#controller;
|
|
377
|
-
if (previous !== undefined) {
|
|
378
|
-
this.#diagnose("revoking previous controller");
|
|
379
|
-
this.#controller = undefined;
|
|
380
|
-
this.#destroyController(previous, new Error("Controller replaced by a new authenticated connection"));
|
|
381
|
-
}
|
|
382
|
-
const link = {
|
|
383
|
-
socket,
|
|
384
|
-
detach: handshake.detach,
|
|
385
|
-
requests: new Map(),
|
|
386
|
-
retired: new Set(),
|
|
387
|
-
};
|
|
388
|
-
this.#controller = link;
|
|
389
|
-
void writeJsonLine(socket, { version: PROTOCOL_VERSION, type: "hello_ack" }, this.#maxFrameBytes)
|
|
390
|
-
.then(() => {
|
|
391
|
-
this.#flushPendingReports(link);
|
|
392
|
-
this.#flushBufferedEvents(link);
|
|
393
|
-
})
|
|
394
|
-
.catch((error) => {
|
|
395
|
-
this.#diagnose(`controller handshake write failed: ${errorMessage(error)}`);
|
|
396
|
-
this.#dropController(link, toError(error));
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
this.#diagnose(`recovery authorization failed: ${errorMessage(error)}`);
|
|
401
|
-
this.#dropSocket(socket);
|
|
402
|
-
}
|
|
403
|
-
finally {
|
|
404
|
-
this.#authenticating.delete(socket);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
#matchesStaticIdentity(frame) {
|
|
408
|
-
const identity = this.identity;
|
|
409
|
-
return (frame.parentSessionId === identity.parentSessionId &&
|
|
410
|
-
frame.subagentId === identity.subagentId &&
|
|
411
|
-
frame.runtimeIdentity === identity.runtimeIdentity &&
|
|
412
|
-
frame.endpoint === identity.endpoint);
|
|
413
|
-
}
|
|
414
|
-
#handleReporterRequest(link, value) {
|
|
415
|
-
if (!isRequestFrame(value)) {
|
|
416
|
-
void this.#respondReporter(link, failureResponse("report", "invalid_report", "Reporter sent an invalid report"));
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (value.operation === "report_lifecycle" && isChildLifecyclePayload(value.payload)) {
|
|
420
|
-
if (!this.#matchesReporterIdentity(value.payload)) {
|
|
421
|
-
void this.#respondReporter(link, failureResponse(value.id, "invalid_report", "Reporter sent an invalid report"));
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
if (value.payload.kind === "left_session")
|
|
425
|
-
this.#bridgeBound = false;
|
|
426
|
-
this.#broadcast({ ...value.payload, type: CHILD_LIFECYCLE_EVENT });
|
|
427
|
-
void this.#respondReporter(link, successResponse(value.id));
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
if (value.operation === "report_paused" && isPauseReportPayload(value.payload)) {
|
|
431
|
-
if (!this.#matchesReporterIdentity(value.payload)) {
|
|
432
|
-
void this.#respondReporter(link, failureResponse(value.id, "invalid_report", "Reporter sent an invalid report"));
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
this.#acceptPauseAck(value.payload.generation);
|
|
436
|
-
void this.#respondReporter(link, successResponse(value.id));
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
if (value.operation !== "contact_parent" ||
|
|
440
|
-
!isContactReportPayload(value.payload) ||
|
|
441
|
-
!this.#matchesReporterIdentity(value.payload)) {
|
|
442
|
-
void this.#respondReporter(link, failureResponse(value.id, "invalid_report", "Reporter sent an invalid report"));
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
if (!this.#bridgeBound) {
|
|
446
|
-
void this.#respondReporter(link, failureResponse(value.id, "bridge_unbound", "Child bridge is no longer bound to this session"));
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (value.payload.sessionId !== undefined &&
|
|
450
|
-
this.#boundSessionId !== undefined &&
|
|
451
|
-
value.payload.sessionId !== this.#boundSessionId) {
|
|
452
|
-
void this.#respondReporter(link, failureResponse(value.id, "bridge_unbound", "Report session is not the bound child session"));
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const accepted = this.#deliverOrQueueReport(value.payload);
|
|
456
|
-
void this.#respondReporter(link, accepted
|
|
457
|
-
? successResponse(value.id)
|
|
458
|
-
: failureResponse(value.id, "report_queue_full", "Parent report queue is full"));
|
|
459
|
-
}
|
|
460
|
-
#matchesReporterIdentity(payload) {
|
|
461
|
-
return (payload.parentSessionId === this.identity.parentSessionId &&
|
|
462
|
-
payload.childId === this.identity.subagentId &&
|
|
463
|
-
payload.runtimeIdentity === this.identity.runtimeIdentity);
|
|
464
|
-
}
|
|
465
|
-
async #respondReporter(link, frame) {
|
|
466
|
-
try {
|
|
467
|
-
await writeJsonLine(link.socket, frame, this.#maxFrameBytes);
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
this.#diagnose(`reporter response write failed: ${errorMessage(error)}`);
|
|
471
|
-
if (this.#bridge === link)
|
|
472
|
-
this.#dropBridge(link);
|
|
473
|
-
else
|
|
474
|
-
this.#dropReporter(link);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (this.#bridge !== link)
|
|
478
|
-
this.#dropReporter(link);
|
|
479
|
-
}
|
|
480
|
-
#dropReporter(link) {
|
|
481
|
-
if (this.#reporters.get(link.socket) === link)
|
|
482
|
-
this.#reporters.delete(link.socket);
|
|
483
|
-
link.detach();
|
|
484
|
-
if (!link.socket.destroyed)
|
|
485
|
-
link.socket.destroy();
|
|
486
|
-
}
|
|
487
|
-
#bridgeFor(socket) {
|
|
488
|
-
return this.#bridge?.socket === socket ? this.#bridge : undefined;
|
|
489
|
-
}
|
|
490
|
-
#dropBridge(link) {
|
|
491
|
-
if (this.#bridge === link)
|
|
492
|
-
this.#bridge = undefined;
|
|
493
|
-
link.detach();
|
|
494
|
-
if (!link.socket.destroyed)
|
|
495
|
-
link.socket.destroy();
|
|
496
|
-
}
|
|
497
|
-
#deliverOrQueueReport(report) {
|
|
498
|
-
if (this.#pendingReports.length >= this.#maxBufferedEvents)
|
|
499
|
-
return false;
|
|
500
|
-
const link = this.#controller;
|
|
501
|
-
if (link !== undefined && link.socket.writableLength <= this.#maxControllerBufferBytes) {
|
|
502
|
-
void writeJsonLine(link.socket, eventFrame({ type: "subagent_report", report }), this.#maxFrameBytes).catch((error) => {
|
|
503
|
-
this.#diagnose(`parent report delivery failed: ${errorMessage(error)}`);
|
|
504
|
-
this.#dropController(link, toError(error));
|
|
505
|
-
if (this.#pendingReports.length < this.#maxBufferedEvents)
|
|
506
|
-
this.#pendingReports.push(report);
|
|
507
|
-
});
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
if (this.#pendingReports.length >= this.#maxBufferedEvents)
|
|
511
|
-
return false;
|
|
512
|
-
this.#pendingReports.push(report);
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
#flushPendingReports(link) {
|
|
516
|
-
if (this.#controller !== link)
|
|
517
|
-
return;
|
|
518
|
-
const reports = this.#pendingReports.splice(0, this.#pendingReports.length);
|
|
519
|
-
for (const report of reports) {
|
|
520
|
-
void writeJsonLine(link.socket, eventFrame({ type: "subagent_report", report }), this.#maxFrameBytes).catch((error) => {
|
|
521
|
-
this.#pendingReports.unshift(report);
|
|
522
|
-
this.#diagnose(`parent report catch-up failed: ${errorMessage(error)}`);
|
|
523
|
-
this.#dropController(link, toError(error));
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
#handleRequest(link, value) {
|
|
528
|
-
if (!isRequestFrame(value)) {
|
|
529
|
-
this.#diagnose("controller sent a malformed frame");
|
|
530
|
-
this.#dropController(link, new Error("Controller sent a malformed frame"));
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
const { id, operation } = value;
|
|
534
|
-
if (link.retired.has(id) || link.requests.has(id)) {
|
|
535
|
-
rememberRequestId(link.retired, id);
|
|
536
|
-
void this.#respond(link, failureResponse(id, "duplicate_request_id", `Request ${id} is already known`));
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if (link.requests.size >= this.#maxPendingRequests) {
|
|
540
|
-
void this.#respond(link, failureResponse(id, "too_many_pending_requests", "Runner pending request limit exceeded"));
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (operation === "shutdown") {
|
|
544
|
-
rememberRequestId(link.retired, id);
|
|
545
|
-
void this.#respond(link, successResponse(id)).then(() => this.shutdown(new Error("Controller requested shutdown")));
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
if (operation === "close_writer") {
|
|
549
|
-
rememberRequestId(link.retired, id);
|
|
550
|
-
void this.#closeWriter()
|
|
551
|
-
.then(() => this.#respond(link, successResponse(id, { closed: true })))
|
|
552
|
-
.catch((error) => this.#respond(link, failureResponse(id, errorCode(error), errorMessage(error))));
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (operation === "start_rpc") {
|
|
556
|
-
rememberRequestId(link.retired, id);
|
|
557
|
-
void this.#startRpc()
|
|
558
|
-
.then((data) => this.#respond(link, successResponse(id, data)))
|
|
559
|
-
.catch((error) => this.#respond(link, failureResponse(id, errorCode(error), errorMessage(error))));
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (operation === "pause") {
|
|
563
|
-
const abort = new AbortController();
|
|
564
|
-
link.requests.set(id, abort);
|
|
565
|
-
void this.#runPause(abort.signal)
|
|
566
|
-
.then((data) => this.#respond(link, successResponse(id, data)))
|
|
567
|
-
.catch((error) => this.#respond(link, failureResponse(id, errorCode(error), errorMessage(error))))
|
|
568
|
-
.finally(() => {
|
|
569
|
-
link.requests.delete(id);
|
|
570
|
-
rememberRequestId(link.retired, id);
|
|
571
|
-
});
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (operation === "cancel_pause") {
|
|
575
|
-
rememberRequestId(link.retired, id);
|
|
576
|
-
this.#cancelPause("Controller cancelled pause");
|
|
577
|
-
void this.#respond(link, successResponse(id, { cancelled: true }));
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
if (operation === "contact_parent" ||
|
|
581
|
-
operation === "report_lifecycle" ||
|
|
582
|
-
operation === "report_paused") {
|
|
583
|
-
rememberRequestId(link.retired, id);
|
|
584
|
-
void this.#respond(link, failureResponse(id, "reporter_required", `${operation} requires a reporter connection`));
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
const abort = new AbortController();
|
|
588
|
-
link.requests.set(id, abort);
|
|
589
|
-
void this.#forward(link, id, operation, value.payload, abort);
|
|
590
|
-
}
|
|
591
|
-
async #forward(link, id, operation, payload, abort) {
|
|
592
|
-
try {
|
|
593
|
-
const adapter = this.#adapter;
|
|
594
|
-
if (adapter === undefined) {
|
|
595
|
-
throw new Error("RPC writer is not bound");
|
|
596
|
-
}
|
|
597
|
-
const data = await adapter.request(operation, payload, {
|
|
598
|
-
signal: abort.signal,
|
|
599
|
-
timeoutMs: this.#requestTimeoutMs,
|
|
600
|
-
});
|
|
601
|
-
await this.#respond(link, successResponse(id, data));
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
await this.#respond(link, failureResponse(id, errorCode(error), errorMessage(error)));
|
|
605
|
-
}
|
|
606
|
-
finally {
|
|
607
|
-
link.requests.delete(id);
|
|
608
|
-
rememberRequestId(link.retired, id);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/** Answers only the controller that asked; a revoked controller gets nothing. */
|
|
612
|
-
#respond(link, frame) {
|
|
613
|
-
if (this.#controller !== link || link.socket.destroyed)
|
|
614
|
-
return Promise.resolve();
|
|
615
|
-
return writeJsonLine(link.socket, frame, this.#maxFrameBytes).catch((error) => {
|
|
616
|
-
this.#diagnose(`controller response write failed: ${errorMessage(error)}`);
|
|
617
|
-
this.#dropController(link, toError(error));
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
#broadcast(event) {
|
|
621
|
-
if (this.#exit !== undefined)
|
|
622
|
-
return;
|
|
623
|
-
const link = this.#controller;
|
|
624
|
-
if (link !== undefined) {
|
|
625
|
-
if (link.socket.writableLength > this.#maxControllerBufferBytes) {
|
|
626
|
-
this.#diagnose(`controller is not reading events (${link.socket.writableLength} bytes queued)`);
|
|
627
|
-
this.#dropController(link, new Error("Controller is not reading runner events"));
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
void writeJsonLine(link.socket, eventFrame(event), this.#maxFrameBytes).catch((error) => {
|
|
631
|
-
this.#diagnose(`controller event write failed: ${errorMessage(error)}`);
|
|
632
|
-
this.#dropController(link, toError(error));
|
|
633
|
-
this.#bufferEvent(event);
|
|
634
|
-
});
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
this.#bufferEvent(event);
|
|
639
|
-
}
|
|
640
|
-
#bufferEvent(event) {
|
|
641
|
-
if (this.#bufferedEvents.length >= this.#maxBufferedEvents) {
|
|
642
|
-
this.#bufferedEvents.shift();
|
|
643
|
-
this.#droppedEvents += 1;
|
|
644
|
-
}
|
|
645
|
-
this.#bufferedEvents.push(event);
|
|
646
|
-
}
|
|
647
|
-
/** Hands a reconnected controller the events it missed, or a gap notice. */
|
|
648
|
-
#flushBufferedEvents(link) {
|
|
649
|
-
if (this.#controller !== link)
|
|
650
|
-
return;
|
|
651
|
-
const pending = [...this.#bufferedEvents];
|
|
652
|
-
this.#bufferedEvents.length = 0;
|
|
653
|
-
const dropped = this.#droppedEvents;
|
|
654
|
-
this.#droppedEvents = 0;
|
|
655
|
-
if (dropped > 0) {
|
|
656
|
-
pending.unshift({ type: RUNNER_EVENTS_DROPPED_EVENT, count: dropped });
|
|
657
|
-
}
|
|
658
|
-
for (const event of pending) {
|
|
659
|
-
void writeJsonLine(link.socket, eventFrame(event), this.#maxFrameBytes).catch((error) => {
|
|
660
|
-
this.#bufferEvent(event);
|
|
661
|
-
this.#diagnose(`controller catch-up write failed: ${errorMessage(error)}`);
|
|
662
|
-
this.#dropController(link, toError(error));
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
#onChildExit(code, signal) {
|
|
667
|
-
this.#diagnose(`Pi child exited (code=${String(code)}, signal=${String(signal)})`);
|
|
668
|
-
this.#failPauseWaiters(new Error("Pi child exited"));
|
|
669
|
-
this.#writerExit = { code, signal };
|
|
670
|
-
if (this.#writerClosing && !this.#shutdownRequested) {
|
|
671
|
-
this.#broadcast({ type: WRITER_EXIT_EVENT, code, signal });
|
|
672
|
-
this.#teardownWriter(new Error(`Pi RPC writer exited (code=${String(code)}, signal=${String(signal)})`));
|
|
673
|
-
this.#process = undefined;
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
this.#broadcast({ type: RUNNER_EXIT_EVENT, code, signal });
|
|
677
|
-
this.#exit = {
|
|
678
|
-
code,
|
|
679
|
-
signal,
|
|
680
|
-
reason: new Error(`Pi child exited (code=${String(code)}, signal=${String(signal)})`),
|
|
681
|
-
requested: this.#shutdownRequested,
|
|
682
|
-
};
|
|
683
|
-
void this.shutdown(this.#exit.reason);
|
|
684
|
-
}
|
|
685
|
-
#notifyBridge(event) {
|
|
686
|
-
const bridge = this.#bridge;
|
|
687
|
-
if (bridge === undefined)
|
|
688
|
-
return;
|
|
689
|
-
void writeJsonLine(bridge.socket, eventFrame(event), this.#maxFrameBytes).catch((error) => {
|
|
690
|
-
this.#diagnose(`bridge event write failed: ${errorMessage(error)}`);
|
|
691
|
-
this.#dropBridge(bridge);
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
async #runPause(signal) {
|
|
695
|
-
if (signal.aborted)
|
|
696
|
-
throw abortError();
|
|
697
|
-
const adapter = this.#adapter;
|
|
698
|
-
if (adapter !== undefined) {
|
|
699
|
-
try {
|
|
700
|
-
const state = await adapter.request("get_state", undefined, {
|
|
701
|
-
signal,
|
|
702
|
-
timeoutMs: this.#requestTimeoutMs,
|
|
703
|
-
});
|
|
704
|
-
if (isIdlePiState(state)) {
|
|
705
|
-
return { paused: true, idle: true, generation: this.#pauseGeneration };
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
catch (error) {
|
|
709
|
-
if (signal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
710
|
-
throw toError(error);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return this.#waitForPauseAck(signal);
|
|
715
|
-
}
|
|
716
|
-
#waitForPauseAck(signal) {
|
|
717
|
-
if (signal.aborted)
|
|
718
|
-
return Promise.reject(abortError());
|
|
719
|
-
this.#pauseGeneration += 1;
|
|
720
|
-
const generation = this.#pauseGeneration;
|
|
721
|
-
this.#notifyBridge({ type: PAUSE_EVENT, generation });
|
|
722
|
-
return new Promise((resolve, reject) => {
|
|
723
|
-
const timer = setTimeout(() => {
|
|
724
|
-
this.#cancelPause("Pause handshake timed out");
|
|
725
|
-
}, this.#requestTimeoutMs);
|
|
726
|
-
const onAbort = () => {
|
|
727
|
-
this.#cancelPause("Pause was cancelled");
|
|
728
|
-
};
|
|
729
|
-
this.#pauseWaiters.push({ generation, resolve, reject, timer, onAbort, signal });
|
|
730
|
-
if (signal.aborted) {
|
|
731
|
-
onAbort();
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
#acceptPauseAck(generation) {
|
|
738
|
-
const waiters = this.#pauseWaiters.filter((waiter) => waiter.generation === generation);
|
|
739
|
-
if (waiters.length === 0)
|
|
740
|
-
return;
|
|
741
|
-
this.#pauseWaiters.splice(0, this.#pauseWaiters.length, ...this.#pauseWaiters.filter((waiter) => waiter.generation !== generation));
|
|
742
|
-
for (const waiter of waiters) {
|
|
743
|
-
clearTimeout(waiter.timer);
|
|
744
|
-
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
745
|
-
waiter.resolve({ paused: true, idle: false, generation });
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
#cancelPause(reason) {
|
|
749
|
-
this.#pauseGeneration += 1;
|
|
750
|
-
this.#notifyBridge({ type: CANCEL_PAUSE_EVENT, generation: this.#pauseGeneration });
|
|
751
|
-
this.#failPauseWaiters(new Error(reason));
|
|
752
|
-
}
|
|
753
|
-
#failPauseWaiters(error) {
|
|
754
|
-
const waiters = this.#pauseWaiters.splice(0, this.#pauseWaiters.length);
|
|
755
|
-
for (const waiter of waiters) {
|
|
756
|
-
clearTimeout(waiter.timer);
|
|
757
|
-
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
758
|
-
waiter.reject(error);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
async #closeWriter() {
|
|
762
|
-
if (this.#process === undefined || this.#writerExit !== undefined)
|
|
763
|
-
return;
|
|
764
|
-
this.#writerClosing = true;
|
|
765
|
-
try {
|
|
766
|
-
await this.#releaseChild();
|
|
767
|
-
if (!(await this.#waitForExit(this.#shutdownGraceMs + this.#killGraceMs))) {
|
|
768
|
-
throw new Error("RPC writer did not exit after close_writer");
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
finally {
|
|
772
|
-
this.#writerClosing = false;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
async #startRpc() {
|
|
776
|
-
if (this.#process !== undefined && this.#writerExit === undefined) {
|
|
777
|
-
throw new Error("RPC writer is still bound");
|
|
778
|
-
}
|
|
779
|
-
if (this.#spawnWriter === undefined) {
|
|
780
|
-
throw new Error("RPC writer respawn is not configured");
|
|
781
|
-
}
|
|
782
|
-
this.#teardownWriter(new Error("Replacing RPC writer"));
|
|
783
|
-
this.#bindWriter(this.#spawnWriter());
|
|
784
|
-
this.#bridgeBound = true;
|
|
785
|
-
const adapter = this.#adapter;
|
|
786
|
-
if (adapter === undefined)
|
|
787
|
-
throw new Error("RPC writer is not bound");
|
|
788
|
-
this.#readyState = await adapter.ready(AbortSignal.timeout(this.#readyTimeoutMs));
|
|
789
|
-
return this.#readyState;
|
|
790
|
-
}
|
|
791
|
-
#teardownWriter(reason) {
|
|
792
|
-
this.#detachEvents?.();
|
|
793
|
-
this.#detachEvents = undefined;
|
|
794
|
-
this.#adapter?.close(reason);
|
|
795
|
-
this.#adapter = undefined;
|
|
796
|
-
}
|
|
797
|
-
#dropSocket(socket) {
|
|
798
|
-
const handshake = this.#pendingHandshakes.get(socket);
|
|
799
|
-
if (handshake !== undefined) {
|
|
800
|
-
clearTimeout(handshake.timer);
|
|
801
|
-
this.#pendingHandshakes.delete(socket);
|
|
802
|
-
handshake.detach();
|
|
803
|
-
}
|
|
804
|
-
socket.destroy();
|
|
805
|
-
}
|
|
806
|
-
#dropController(link, error) {
|
|
807
|
-
if (this.#controller === link)
|
|
808
|
-
this.#controller = undefined;
|
|
809
|
-
this.#destroyController(link, error);
|
|
810
|
-
}
|
|
811
|
-
/** Fails every in-flight request of a controller that lost its authority. */
|
|
812
|
-
#destroyController(link, error) {
|
|
813
|
-
link.detach();
|
|
814
|
-
const requests = [...link.requests.values()];
|
|
815
|
-
link.requests.clear();
|
|
816
|
-
for (const request of requests)
|
|
817
|
-
request.abort();
|
|
818
|
-
if (!link.socket.destroyed)
|
|
819
|
-
link.socket.destroy();
|
|
820
|
-
this.#diagnose(`controller released: ${error.message}`);
|
|
821
|
-
}
|
|
822
|
-
/** Aborts the current turn, then ends the Pi child within fixed deadlines. */
|
|
823
|
-
async #releaseChild() {
|
|
824
|
-
const child = this.#process;
|
|
825
|
-
if (child === undefined)
|
|
826
|
-
return;
|
|
827
|
-
if (this.#writerExit === undefined && child.exitCode === null && child.signalCode === null) {
|
|
828
|
-
try {
|
|
829
|
-
await this.#adapter?.request("abort", undefined, { timeoutMs: 1_000 });
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
this.#diagnose(`abort before shutdown failed: ${errorMessage(error)}`);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
if (await this.#waitForExit(0))
|
|
836
|
-
return;
|
|
837
|
-
child.kill("SIGTERM");
|
|
838
|
-
if (await this.#waitForExit(this.#shutdownGraceMs))
|
|
839
|
-
return;
|
|
840
|
-
this.#diagnose("Pi child ignored SIGTERM; sending SIGKILL");
|
|
841
|
-
child.kill("SIGKILL");
|
|
842
|
-
if (await this.#waitForExit(this.#killGraceMs))
|
|
843
|
-
return;
|
|
844
|
-
this.#diagnose("Pi child did not exit after SIGKILL");
|
|
845
|
-
}
|
|
846
|
-
#waitForExit(timeoutMs) {
|
|
847
|
-
if (this.#writerExit !== undefined || this.#process === undefined)
|
|
848
|
-
return Promise.resolve(true);
|
|
849
|
-
if (this.#process.exitCode !== null || this.#process.signalCode !== null) {
|
|
850
|
-
return Promise.resolve(true);
|
|
851
|
-
}
|
|
852
|
-
const child = this.#process;
|
|
853
|
-
return new Promise((resolve) => {
|
|
854
|
-
const timer = setTimeout(() => {
|
|
855
|
-
child.off("exit", onExit);
|
|
856
|
-
resolve(false);
|
|
857
|
-
}, timeoutMs);
|
|
858
|
-
const onExit = () => {
|
|
859
|
-
clearTimeout(timer);
|
|
860
|
-
resolve(true);
|
|
861
|
-
};
|
|
862
|
-
child.once("exit", onExit);
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
#finish(exit) {
|
|
866
|
-
if (this.#exit === undefined)
|
|
867
|
-
this.#exit = exit;
|
|
868
|
-
const resolve = this.#resolveClosed;
|
|
869
|
-
this.#resolveClosed = undefined;
|
|
870
|
-
resolve?.(exit);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
/** Bounded FIFO memory of finished request IDs for one controller. */
|
|
874
|
-
function rememberRequestId(retired, id) {
|
|
875
|
-
retired.add(id);
|
|
876
|
-
if (retired.size <= MAX_RETIRED_REQUEST_IDS)
|
|
877
|
-
return;
|
|
878
|
-
const oldest = retired.values().next();
|
|
879
|
-
if (!oldest.done)
|
|
880
|
-
retired.delete(oldest.value);
|
|
881
|
-
}
|
|
882
|
-
function errorCode(error) {
|
|
883
|
-
if (error instanceof Error && error.name === "AbortError")
|
|
884
|
-
return "aborted";
|
|
885
|
-
if (error instanceof PiRpcTimeoutError)
|
|
886
|
-
return "timeout";
|
|
887
|
-
return "runner_error";
|
|
888
|
-
}
|
|
889
|
-
function tokensMatch(candidate, expected) {
|
|
890
|
-
const left = Buffer.from(candidate, "utf8");
|
|
891
|
-
const right = Buffer.from(expected, "utf8");
|
|
892
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
893
|
-
}
|
|
894
|
-
function isErrno(error, code) {
|
|
895
|
-
return isRecord(error) && error.code === code;
|
|
896
|
-
}
|
|
897
|
-
/** Waits, within a fixed deadline, for queued socket writes to flush. */
|
|
898
|
-
function drainSocket(socket, timeoutMs) {
|
|
899
|
-
if (socket.destroyed || socket.writableLength === 0)
|
|
900
|
-
return Promise.resolve();
|
|
901
|
-
return new Promise((resolve) => {
|
|
902
|
-
const timer = setTimeout(() => {
|
|
903
|
-
socket.off("drain", onDrain);
|
|
904
|
-
resolve();
|
|
905
|
-
}, timeoutMs);
|
|
906
|
-
const onDrain = () => {
|
|
907
|
-
clearTimeout(timer);
|
|
908
|
-
resolve();
|
|
909
|
-
};
|
|
910
|
-
socket.once("drain", onDrain);
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
/** True when a runner answers on this endpoint; connect failures mean stale. */
|
|
914
|
-
function isEndpointLive(endpoint) {
|
|
915
|
-
const socket = createConnection(endpoint);
|
|
916
|
-
return new Promise((resolve) => {
|
|
917
|
-
let settled = false;
|
|
918
|
-
const finish = (live) => {
|
|
919
|
-
if (settled)
|
|
920
|
-
return;
|
|
921
|
-
settled = true;
|
|
922
|
-
clearTimeout(timer);
|
|
923
|
-
socket.destroy();
|
|
924
|
-
resolve(live);
|
|
925
|
-
};
|
|
926
|
-
const timer = setTimeout(() => finish(true), 1_000);
|
|
927
|
-
socket.once("connect", () => finish(true));
|
|
928
|
-
socket.once("error", () => finish(false));
|
|
929
|
-
});
|
|
930
|
-
}
|