@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/package.json
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
2
|
+
"name": "@hheei/pi-subagents",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Independent background Pi subagent sessions with durable identity and RPC control.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"dist/extension.js"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"pi",
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi-extension",
|
|
25
|
+
"hepi",
|
|
26
|
+
"subagents"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/hheei/hepi-mono.git",
|
|
32
|
+
"directory": "packages/pi-subagents"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@hheei/pi-ext-core": "^0.2.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=1.0.0",
|
|
43
|
+
"typebox": "*"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
47
|
+
"@earendil-works/pi-tui": "1.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/child-nudge.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child-side completion reminder.
|
|
3
|
-
*
|
|
4
|
-
* Adapted from HazAT/maplezzk pi-interactive-subagents `subagent-done.ts`
|
|
5
|
-
* (MIT, upstream 7a5c96be138158a5cf6413ac819509eff12b1ccd). This version
|
|
6
|
-
* reminds the child to call `contact_parent` and never exits the session.
|
|
7
|
-
*/
|
|
8
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
/** Return true only when the latest assistant message ended by the model stopping normally. */
|
|
10
|
-
export declare function shouldScheduleAgentEndNudge(messages: readonly {
|
|
11
|
-
role?: string;
|
|
12
|
-
stopReason?: string;
|
|
13
|
-
}[] | undefined): boolean;
|
|
14
|
-
export interface ChildNudgeController {
|
|
15
|
-
markReported(): void;
|
|
16
|
-
listen(pi: ExtensionAPI): void;
|
|
17
|
-
dispose(): void;
|
|
18
|
-
}
|
|
19
|
-
/** Owns the delay timer and the “already reported / user took over” gates. */
|
|
20
|
-
export declare function createChildNudgeController(options?: {
|
|
21
|
-
readonly delayMs?: number;
|
|
22
|
-
readonly disabled?: boolean;
|
|
23
|
-
}): ChildNudgeController;
|
|
24
|
-
export declare function registerChildNudge(pi: ExtensionAPI): ChildNudgeController;
|
package/dist/child-nudge.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { isRecord } from "./domain.js";
|
|
2
|
-
const ASSISTANT_ROLE = "assistant";
|
|
3
|
-
const NORMAL_STOP_REASON = "stop";
|
|
4
|
-
const DEFAULT_NUDGE_DELAY_MS = 5_000;
|
|
5
|
-
const MIN_NUDGE_DELAY_MS = 1_000;
|
|
6
|
-
const NUDGE_TEXT = "You finished a turn without calling contact_parent. If the parent needs a progress update, important finding, decision, or blocker, call contact_parent now. Do not wait for the parent to poll you.";
|
|
7
|
-
/** Return true only when the latest assistant message ended by the model stopping normally. */
|
|
8
|
-
export function shouldScheduleAgentEndNudge(messages) {
|
|
9
|
-
if (messages === undefined || messages.length === 0)
|
|
10
|
-
return false;
|
|
11
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
12
|
-
const message = messages[index];
|
|
13
|
-
if (message?.role !== ASSISTANT_ROLE)
|
|
14
|
-
continue;
|
|
15
|
-
return message.stopReason === NORMAL_STOP_REASON;
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
function parseNudgeDelayMs(rawValue) {
|
|
20
|
-
const parsed = Number.parseInt(rawValue ?? "", 10);
|
|
21
|
-
if (!Number.isFinite(parsed))
|
|
22
|
-
return DEFAULT_NUDGE_DELAY_MS;
|
|
23
|
-
return Math.max(MIN_NUDGE_DELAY_MS, parsed);
|
|
24
|
-
}
|
|
25
|
-
function agentEndMessages(event) {
|
|
26
|
-
if (!isRecord(event) || !Array.isArray(event.messages))
|
|
27
|
-
return undefined;
|
|
28
|
-
const messages = [];
|
|
29
|
-
for (const item of event.messages) {
|
|
30
|
-
if (!isRecord(item))
|
|
31
|
-
continue;
|
|
32
|
-
messages.push({
|
|
33
|
-
...(typeof item.role === "string" ? { role: item.role } : {}),
|
|
34
|
-
...(typeof item.stopReason === "string" ? { stopReason: item.stopReason } : {}),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return messages;
|
|
38
|
-
}
|
|
39
|
-
/** Owns the delay timer and the “already reported / user took over” gates. */
|
|
40
|
-
export function createChildNudgeController(options = {}) {
|
|
41
|
-
const delayMs = options.delayMs ?? parseNudgeDelayMs(process.env.PI_SUBAGENTS_NUDGE_DELAY_MS);
|
|
42
|
-
const disabled = options.disabled ?? process.env.PI_SUBAGENTS_NUDGE_DISABLE === "1";
|
|
43
|
-
let reported = false;
|
|
44
|
-
let userInputAfterAgentEnd = false;
|
|
45
|
-
let agentStarted = false;
|
|
46
|
-
let userTookOver = false;
|
|
47
|
-
let timer = null;
|
|
48
|
-
const clearTimer = () => {
|
|
49
|
-
if (timer === null)
|
|
50
|
-
return;
|
|
51
|
-
clearTimeout(timer);
|
|
52
|
-
timer = null;
|
|
53
|
-
};
|
|
54
|
-
return {
|
|
55
|
-
markReported() {
|
|
56
|
-
reported = true;
|
|
57
|
-
clearTimer();
|
|
58
|
-
},
|
|
59
|
-
listen(pi) {
|
|
60
|
-
pi.on("input", () => {
|
|
61
|
-
userInputAfterAgentEnd = true;
|
|
62
|
-
clearTimer();
|
|
63
|
-
if (agentStarted)
|
|
64
|
-
userTookOver = true;
|
|
65
|
-
});
|
|
66
|
-
pi.on("before_agent_start", () => {
|
|
67
|
-
clearTimer();
|
|
68
|
-
});
|
|
69
|
-
pi.on("agent_start", () => {
|
|
70
|
-
agentStarted = true;
|
|
71
|
-
reported = false;
|
|
72
|
-
userInputAfterAgentEnd = false;
|
|
73
|
-
clearTimer();
|
|
74
|
-
});
|
|
75
|
-
pi.on("agent_end", (event) => {
|
|
76
|
-
if (disabled || reported || userTookOver) {
|
|
77
|
-
clearTimer();
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (!shouldScheduleAgentEndNudge(agentEndMessages(event))) {
|
|
81
|
-
clearTimer();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
clearTimer();
|
|
85
|
-
timer = setTimeout(() => {
|
|
86
|
-
timer = null;
|
|
87
|
-
if (reported || userInputAfterAgentEnd || userTookOver)
|
|
88
|
-
return;
|
|
89
|
-
pi.sendUserMessage(NUDGE_TEXT, { deliverAs: "followUp" });
|
|
90
|
-
}, delayMs);
|
|
91
|
-
});
|
|
92
|
-
},
|
|
93
|
-
dispose() {
|
|
94
|
-
clearTimer();
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export function registerChildNudge(pi) {
|
|
99
|
-
const controller = createChildNudgeController();
|
|
100
|
-
controller.listen(pi);
|
|
101
|
-
return controller;
|
|
102
|
-
}
|
package/dist/connector.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { type ChildIdentity } from "./domain.js";
|
|
2
|
-
import { type RunnerOperation } from "./protocol.js";
|
|
3
|
-
export interface RunnerConnectionOptions {
|
|
4
|
-
/** Unix socket path published by the runner. */
|
|
5
|
-
readonly endpoint: string;
|
|
6
|
-
readonly identity: ChildIdentity;
|
|
7
|
-
readonly token: string;
|
|
8
|
-
readonly maxFrameBytes?: number;
|
|
9
|
-
readonly maxPendingRequests?: number;
|
|
10
|
-
readonly requestTimeoutMs?: number;
|
|
11
|
-
readonly connectTimeoutMs?: number;
|
|
12
|
-
readonly role?: "controller" | "reporter" | "recovery" | "bridge";
|
|
13
|
-
readonly claimId?: string;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Parent-side client of the runner IPC endpoint.
|
|
17
|
-
*
|
|
18
|
-
* The connection is re-connectable by design: `close()` only releases this
|
|
19
|
-
* parent's local socket and in-flight requests. It never terminates the runner
|
|
20
|
-
* or the Pi child that the runner owns.
|
|
21
|
-
*/
|
|
22
|
-
export declare class RunnerConnection {
|
|
23
|
-
#private;
|
|
24
|
-
constructor(options: RunnerConnectionOptions);
|
|
25
|
-
connect(signal?: AbortSignal): Promise<void>;
|
|
26
|
-
get connected(): boolean;
|
|
27
|
-
request(operation: RunnerOperation, payload?: unknown, options?: {
|
|
28
|
-
readonly signal?: AbortSignal;
|
|
29
|
-
readonly timeoutMs?: number;
|
|
30
|
-
}): Promise<unknown>;
|
|
31
|
-
onEvent(listener: (event: unknown) => void): () => void;
|
|
32
|
-
/**
|
|
33
|
-
* Releases the local socket, reader, listeners and in-flight requests. The
|
|
34
|
-
* runner keeps running and can be reconnected from the same identity.
|
|
35
|
-
*/
|
|
36
|
-
close(): void;
|
|
37
|
-
}
|
|
38
|
-
/** Sends one child report through a short-lived authenticated reporter connection. */
|
|
39
|
-
export declare function sendReportToRunner(identity: ChildIdentity, report: unknown, signal?: AbortSignal): Promise<void>;
|
|
40
|
-
/** Sends one child lifecycle event through a short-lived authenticated reporter connection. */
|
|
41
|
-
export declare function sendLifecycleToRunner(identity: ChildIdentity, payload: unknown, signal?: AbortSignal): Promise<void>;
|
|
42
|
-
/**
|
|
43
|
-
* Connects to a runner endpoint that may still be starting up.
|
|
44
|
-
*
|
|
45
|
-
* Readiness is never a fixed sleep: each attempt is a real connect plus
|
|
46
|
-
* identity handshake, so a resolved promise means the runner accepted this
|
|
47
|
-
* controller. Only connection-level failures inside the deadline are retried.
|
|
48
|
-
*/
|
|
49
|
-
export declare function connectWithRetry(connection: Pick<RunnerConnection, "connect">, options?: {
|
|
50
|
-
readonly attempts?: number;
|
|
51
|
-
readonly delayMs?: number;
|
|
52
|
-
readonly signal?: AbortSignal;
|
|
53
|
-
}): Promise<void>;
|
package/dist/connector.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { createConnection } from "node:net";
|
|
2
|
-
import { abortError, errorMessage, toError, writeDiagnostic } from "./diagnostics.js";
|
|
3
|
-
import { PROTOCOL_VERSION } from "./domain.js";
|
|
4
|
-
import { attachJsonLineReader, writeJsonLine } from "./json-lines.js";
|
|
5
|
-
import { DEFAULT_MAX_FRAME_BYTES, DEFAULT_MAX_PENDING_REQUESTS, isEventFrame, isHelloAckFrame, isResponseFrame, } from "./protocol.js";
|
|
6
|
-
/** Bounded memory of locally finished request IDs, used to ignore late responses. */
|
|
7
|
-
const MAX_RETIRED_REQUEST_IDS = 256;
|
|
8
|
-
/**
|
|
9
|
-
* Parent-side client of the runner IPC endpoint.
|
|
10
|
-
*
|
|
11
|
-
* The connection is re-connectable by design: `close()` only releases this
|
|
12
|
-
* parent's local socket and in-flight requests. It never terminates the runner
|
|
13
|
-
* or the Pi child that the runner owns.
|
|
14
|
-
*/
|
|
15
|
-
export class RunnerConnection {
|
|
16
|
-
#options;
|
|
17
|
-
#pending = new Map();
|
|
18
|
-
#retired = new Set();
|
|
19
|
-
#eventListeners = new Set();
|
|
20
|
-
#socket;
|
|
21
|
-
#detachReader;
|
|
22
|
-
#nextId = 0;
|
|
23
|
-
#connectPromise;
|
|
24
|
-
constructor(options) {
|
|
25
|
-
this.#options = options;
|
|
26
|
-
}
|
|
27
|
-
connect(signal) {
|
|
28
|
-
if (this.#socket && !this.#socket.destroyed)
|
|
29
|
-
return Promise.resolve();
|
|
30
|
-
if (this.#connectPromise !== undefined)
|
|
31
|
-
return this.#connectPromise;
|
|
32
|
-
const promise = this.#open(signal);
|
|
33
|
-
this.#connectPromise = promise;
|
|
34
|
-
void promise
|
|
35
|
-
.finally(() => {
|
|
36
|
-
if (this.#connectPromise === promise)
|
|
37
|
-
this.#connectPromise = undefined;
|
|
38
|
-
})
|
|
39
|
-
.catch(() => undefined);
|
|
40
|
-
return promise;
|
|
41
|
-
}
|
|
42
|
-
get connected() {
|
|
43
|
-
return this.#socket !== undefined && !this.#socket.destroyed;
|
|
44
|
-
}
|
|
45
|
-
request(operation, payload, options = {}) {
|
|
46
|
-
const socket = this.#socket;
|
|
47
|
-
if (!socket || socket.destroyed)
|
|
48
|
-
return Promise.reject(new Error("Runner is not connected"));
|
|
49
|
-
if (options.signal?.aborted)
|
|
50
|
-
return Promise.reject(abortError());
|
|
51
|
-
const limit = this.#options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS;
|
|
52
|
-
if (this.#pending.size >= limit) {
|
|
53
|
-
return Promise.reject(new Error("Runner pending request limit exceeded"));
|
|
54
|
-
}
|
|
55
|
-
const id = `controller-${++this.#nextId}`;
|
|
56
|
-
// `Promise.withResolvers` needs lib ES2024; this workspace targets ES2022.
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const settle = (error, value) => {
|
|
59
|
-
const pending = this.#pending.get(id);
|
|
60
|
-
if (!pending)
|
|
61
|
-
return;
|
|
62
|
-
this.#pending.delete(id);
|
|
63
|
-
this.#retire(id);
|
|
64
|
-
pending.cleanup();
|
|
65
|
-
if (error)
|
|
66
|
-
pending.reject(error);
|
|
67
|
-
else
|
|
68
|
-
pending.resolve(value);
|
|
69
|
-
};
|
|
70
|
-
const timeout = setTimeout(() => settle(new Error(`Runner ${operation} request timed out`)), options.timeoutMs ?? this.#options.requestTimeoutMs ?? 30_000);
|
|
71
|
-
const onAbort = () => settle(abortError());
|
|
72
|
-
const cleanup = () => {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
75
|
-
};
|
|
76
|
-
this.#pending.set(id, { resolve, reject, cleanup });
|
|
77
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
78
|
-
void writeJsonLine(socket, { version: PROTOCOL_VERSION, type: "request", id, operation, payload }, this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES).catch((error) => {
|
|
79
|
-
settle(toError(error));
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
onEvent(listener) {
|
|
84
|
-
this.#eventListeners.add(listener);
|
|
85
|
-
return () => {
|
|
86
|
-
this.#eventListeners.delete(listener);
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Releases the local socket, reader, listeners and in-flight requests. The
|
|
91
|
-
* runner keeps running and can be reconnected from the same identity.
|
|
92
|
-
*/
|
|
93
|
-
close() {
|
|
94
|
-
const socket = this.#socket;
|
|
95
|
-
this.#disconnectFor(socket, new Error("Runner connection closed"));
|
|
96
|
-
}
|
|
97
|
-
async #open(signal) {
|
|
98
|
-
if (signal?.aborted)
|
|
99
|
-
throw abortError();
|
|
100
|
-
const socket = createConnection(this.#options.endpoint);
|
|
101
|
-
this.#socket = socket;
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
let handshakeSettled = false;
|
|
104
|
-
const settleHandshake = (error) => {
|
|
105
|
-
if (handshakeSettled)
|
|
106
|
-
return;
|
|
107
|
-
handshakeSettled = true;
|
|
108
|
-
clearTimeout(timeout);
|
|
109
|
-
signal?.removeEventListener("abort", onAbort);
|
|
110
|
-
if (error === undefined)
|
|
111
|
-
resolve();
|
|
112
|
-
else {
|
|
113
|
-
this.#disconnectFor(socket, error);
|
|
114
|
-
reject(error);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
const streamError = (error) => {
|
|
118
|
-
if (!handshakeSettled)
|
|
119
|
-
settleHandshake(error);
|
|
120
|
-
else if (this.#socket === socket)
|
|
121
|
-
this.#disconnectFor(socket, error);
|
|
122
|
-
};
|
|
123
|
-
const timeout = setTimeout(() => settleHandshake(new Error("Runner connection timed out")), this.#options.connectTimeoutMs ?? 10_000);
|
|
124
|
-
const onAbort = () => settleHandshake(abortError());
|
|
125
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
126
|
-
socket.on("error", streamError);
|
|
127
|
-
socket.on("close", () => {
|
|
128
|
-
streamError(new Error("Runner disconnected"));
|
|
129
|
-
});
|
|
130
|
-
const detach = attachJsonLineReader(socket, {
|
|
131
|
-
maxFrameBytes: this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES,
|
|
132
|
-
onError: streamError,
|
|
133
|
-
onValue: (value) => {
|
|
134
|
-
if (!handshakeSettled) {
|
|
135
|
-
if (!isHelloAckFrame(value)) {
|
|
136
|
-
settleHandshake(new Error("Runner sent an invalid handshake response"));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
settleHandshake();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (this.#socket !== socket)
|
|
143
|
-
return;
|
|
144
|
-
this.#handleFrame(value);
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
this.#detachReader = detach;
|
|
148
|
-
socket.once("connect", () => {
|
|
149
|
-
void writeJsonLine(socket, {
|
|
150
|
-
version: PROTOCOL_VERSION,
|
|
151
|
-
type: "hello",
|
|
152
|
-
...(this.#options.role === undefined ? {} : { role: this.#options.role }),
|
|
153
|
-
...(this.#options.claimId === undefined ? {} : { claimId: this.#options.claimId }),
|
|
154
|
-
...this.#options.identity,
|
|
155
|
-
token: this.#options.token,
|
|
156
|
-
}, this.#options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES).catch((error) => settleHandshake(toError(error)));
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
#handleFrame(value) {
|
|
161
|
-
if (isEventFrame(value)) {
|
|
162
|
-
for (const listener of this.#eventListeners) {
|
|
163
|
-
try {
|
|
164
|
-
listener(value.event);
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
writeDiagnostic("controller", `event listener failed: ${errorMessage(error)}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const socket = this.#socket;
|
|
173
|
-
if (!isResponseFrame(value)) {
|
|
174
|
-
this.#disconnectFor(socket, new Error("Runner sent a malformed frame"));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const pending = this.#pending.get(value.id);
|
|
178
|
-
if (pending === undefined) {
|
|
179
|
-
if (this.#retired.has(value.id))
|
|
180
|
-
return;
|
|
181
|
-
this.#disconnectFor(socket, new Error(`Runner sent unknown response ID: ${value.id}`));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
this.#pending.delete(value.id);
|
|
185
|
-
this.#retire(value.id);
|
|
186
|
-
pending.cleanup();
|
|
187
|
-
if (value.ok)
|
|
188
|
-
pending.resolve(value.data);
|
|
189
|
-
else
|
|
190
|
-
pending.reject(new Error(`${value.error.code}: ${value.error.message}`));
|
|
191
|
-
}
|
|
192
|
-
/** Remembers an ID that finished locally so a late response is ignored. */
|
|
193
|
-
#retire(id) {
|
|
194
|
-
this.#retired.add(id);
|
|
195
|
-
if (this.#retired.size <= MAX_RETIRED_REQUEST_IDS)
|
|
196
|
-
return;
|
|
197
|
-
const oldest = this.#retired.values().next();
|
|
198
|
-
if (!oldest.done)
|
|
199
|
-
this.#retired.delete(oldest.value);
|
|
200
|
-
}
|
|
201
|
-
#disconnectFor(socket, error) {
|
|
202
|
-
if (socket !== undefined && this.#socket !== socket) {
|
|
203
|
-
socket.destroy();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
this.#socket = undefined;
|
|
207
|
-
this.#detachReader?.();
|
|
208
|
-
this.#detachReader = undefined;
|
|
209
|
-
if (socket !== undefined && !socket.destroyed)
|
|
210
|
-
socket.destroy();
|
|
211
|
-
const pending = [...this.#pending.values()];
|
|
212
|
-
this.#pending.clear();
|
|
213
|
-
for (const entry of pending) {
|
|
214
|
-
entry.cleanup();
|
|
215
|
-
entry.reject(error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/** Sends one child report through a short-lived authenticated reporter connection. */
|
|
220
|
-
export async function sendReportToRunner(identity, report, signal) {
|
|
221
|
-
await sendReporterRequest(identity, "contact_parent", report, signal);
|
|
222
|
-
}
|
|
223
|
-
/** Sends one child lifecycle event through a short-lived authenticated reporter connection. */
|
|
224
|
-
export async function sendLifecycleToRunner(identity, payload, signal) {
|
|
225
|
-
await sendReporterRequest(identity, "report_lifecycle", payload, signal);
|
|
226
|
-
}
|
|
227
|
-
async function sendReporterRequest(identity, operation, payload, signal) {
|
|
228
|
-
const connection = new RunnerConnection({
|
|
229
|
-
endpoint: identity.endpoint,
|
|
230
|
-
identity,
|
|
231
|
-
token: identity.token,
|
|
232
|
-
role: "reporter",
|
|
233
|
-
});
|
|
234
|
-
try {
|
|
235
|
-
await connection.connect(signal);
|
|
236
|
-
await connection.request(operation, payload, signal === undefined ? {} : { signal });
|
|
237
|
-
}
|
|
238
|
-
finally {
|
|
239
|
-
connection.close();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Connects to a runner endpoint that may still be starting up.
|
|
244
|
-
*
|
|
245
|
-
* Readiness is never a fixed sleep: each attempt is a real connect plus
|
|
246
|
-
* identity handshake, so a resolved promise means the runner accepted this
|
|
247
|
-
* controller. Only connection-level failures inside the deadline are retried.
|
|
248
|
-
*/
|
|
249
|
-
export async function connectWithRetry(connection, options = {}) {
|
|
250
|
-
const attempts = options.attempts ?? 40;
|
|
251
|
-
const delayMs = options.delayMs ?? 50;
|
|
252
|
-
let lastError = new Error("Runner connection was not attempted");
|
|
253
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
254
|
-
if (options.signal?.aborted)
|
|
255
|
-
throw abortError();
|
|
256
|
-
try {
|
|
257
|
-
await connection.connect(options.signal);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
lastError = toError(error);
|
|
262
|
-
if (lastError.name === "AbortError")
|
|
263
|
-
throw lastError;
|
|
264
|
-
if (attempt === attempts - 1)
|
|
265
|
-
break;
|
|
266
|
-
await sleep(delayMs, options.signal);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
throw lastError;
|
|
270
|
-
}
|
|
271
|
-
function sleep(ms, signal) {
|
|
272
|
-
return new Promise((resolve, reject) => {
|
|
273
|
-
const timer = setTimeout(() => {
|
|
274
|
-
signal?.removeEventListener("abort", onAbort);
|
|
275
|
-
resolve();
|
|
276
|
-
}, ms);
|
|
277
|
-
const onAbort = () => {
|
|
278
|
-
clearTimeout(timer);
|
|
279
|
-
reject(abortError());
|
|
280
|
-
};
|
|
281
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
282
|
-
});
|
|
283
|
-
}
|
package/dist/pause-gate.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child-side hold used by `turn_end`. Parent pause events arm the gate;
|
|
3
|
-
* `cancel_pause` or process abort releases it. Returning from `turn_end`
|
|
4
|
-
* is what lets Pi start the next provider request, so the hook must await
|
|
5
|
-
* this while a matching generation is holding.
|
|
6
|
-
*/
|
|
7
|
-
export declare class PauseGate {
|
|
8
|
-
#private;
|
|
9
|
-
get generation(): number;
|
|
10
|
-
get holding(): boolean;
|
|
11
|
-
request(generation: number): void;
|
|
12
|
-
cancel(generation?: number): void;
|
|
13
|
-
wait(signal?: AbortSignal): Promise<void>;
|
|
14
|
-
}
|
package/dist/pause-gate.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child-side hold used by `turn_end`. Parent pause events arm the gate;
|
|
3
|
-
* `cancel_pause` or process abort releases it. Returning from `turn_end`
|
|
4
|
-
* is what lets Pi start the next provider request, so the hook must await
|
|
5
|
-
* this while a matching generation is holding.
|
|
6
|
-
*/
|
|
7
|
-
export class PauseGate {
|
|
8
|
-
#generation = 0;
|
|
9
|
-
#holding = false;
|
|
10
|
-
#waiters = new Set();
|
|
11
|
-
get generation() {
|
|
12
|
-
return this.#generation;
|
|
13
|
-
}
|
|
14
|
-
get holding() {
|
|
15
|
-
return this.#holding;
|
|
16
|
-
}
|
|
17
|
-
request(generation) {
|
|
18
|
-
if (!Number.isInteger(generation) || generation < this.#generation)
|
|
19
|
-
return;
|
|
20
|
-
this.#generation = generation;
|
|
21
|
-
this.#holding = true;
|
|
22
|
-
}
|
|
23
|
-
cancel(generation) {
|
|
24
|
-
if (generation !== undefined && generation < this.#generation)
|
|
25
|
-
return;
|
|
26
|
-
if (generation !== undefined)
|
|
27
|
-
this.#generation = generation;
|
|
28
|
-
this.#holding = false;
|
|
29
|
-
this.#release();
|
|
30
|
-
}
|
|
31
|
-
wait(signal) {
|
|
32
|
-
if (!this.#holding)
|
|
33
|
-
return Promise.resolve();
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const finish = (error) => {
|
|
36
|
-
this.#waiters.delete(onRelease);
|
|
37
|
-
signal?.removeEventListener("abort", onAbort);
|
|
38
|
-
if (error !== undefined)
|
|
39
|
-
reject(error);
|
|
40
|
-
else
|
|
41
|
-
resolve();
|
|
42
|
-
};
|
|
43
|
-
const onRelease = (error) => {
|
|
44
|
-
finish(error);
|
|
45
|
-
};
|
|
46
|
-
const onAbort = () => {
|
|
47
|
-
this.#holding = false;
|
|
48
|
-
this.#release();
|
|
49
|
-
};
|
|
50
|
-
this.#waiters.add(onRelease);
|
|
51
|
-
if (signal?.aborted) {
|
|
52
|
-
onAbort();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
#release(error) {
|
|
59
|
-
const waiters = [...this.#waiters];
|
|
60
|
-
this.#waiters.clear();
|
|
61
|
-
for (const waiter of waiters)
|
|
62
|
-
waiter(error);
|
|
63
|
-
}
|
|
64
|
-
}
|