@psnext/slingcli 2.5.20260623-1 → 2.5.20260629-1
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/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +5 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +2 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +51 -47
- package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +9 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +9 -6
- package/node_modules/@earendil-works/pi-ai/dist/api/cloudflare.js +0 -18
- package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +6 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +6 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +15 -25
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +30 -26
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +189 -204
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +24 -21
- package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -5
- package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +11 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +19 -4
- package/node_modules/@earendil-works/pi-ai/dist/compat.js +81 -7
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +45 -0
- package/node_modules/@earendil-works/pi-ai/dist/images-models.js +7 -3
- package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/legacy-api-aliases.js +49 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.js +7 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +25 -8
- package/node_modules/@earendil-works/pi-ai/dist/providers/ant-ling.models.js +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +0 -34
- package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +21 -9
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +85 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.models.js +13 -13
- package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +1 -20
- package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +19 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/huggingface.models.js +72 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/minimax-cn.models.js +4 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/minimax.models.js +4 -4
- package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +19 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +11 -11
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +15 -15
- package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +310 -39
- package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +37 -18
- package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +349 -77
- package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js +7 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +6 -6
- package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +6 -6
- package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +95 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/headers.js +10 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +86 -0
- package/node_modules/@earendil-works/pi-ai/package.json +1 -33
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/file-processor.js +13 -25
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +15 -30
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +6 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +2 -4
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +2 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/timings.js +24 -14
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +12 -25
- package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +20 -4
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +9 -9
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +12 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +14 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +37 -25
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +14 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +16 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/rpc-entry.js +10 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-convert.js +21 -15
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-process.js +83 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/mime.js +41 -0
- package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +12 -12
- package/node_modules/@earendil-works/pi-coding-agent/package.json +8 -5
- package/node_modules/@earendil-works/pi-orchestrator/dist/cli.js +136 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/config.js +55 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/handler.js +105 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/index.js +11 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/client.js +56 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/protocol.js +12 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/server.js +145 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/radius.js +365 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/rpc-process.js +168 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/serve.js +70 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/storage.js +58 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/supervisor.js +290 -0
- package/node_modules/@earendil-works/pi-orchestrator/dist/types.js +2 -0
- package/node_modules/@earendil-works/pi-orchestrator/package.json +45 -0
- package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +3 -0
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/diagnostics_channel.d.ts +17 -20
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
- package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
- package/node_modules/brace-expansion/dist/esm/index.js +24 -14
- package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
- package/node_modules/brace-expansion/package.json +2 -2
- package/node_modules/google-auth-library/build/src/auth/googleauth.js +1 -1
- package/node_modules/google-auth-library/package.json +4 -4
- package/package.json +7 -6
- package/slingshot/index.js +302 -304
- package/node_modules/@earendil-works/pi-ai/dist/api-registry.js +0 -44
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function encodeMessage(message) {
|
|
2
|
+
return `${JSON.stringify(message)}\n`;
|
|
3
|
+
}
|
|
4
|
+
export function parseRequestLine(line) {
|
|
5
|
+
const value = JSON.parse(line);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function parseResponseLine(line) {
|
|
9
|
+
const value = JSON.parse(line);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createConnection, createServer } from "node:net";
|
|
3
|
+
import { getSocketPath } from "../config.js";
|
|
4
|
+
import { encodeMessage, parseRequestLine, } from "./protocol.js";
|
|
5
|
+
export async function startIpcServer(handler) {
|
|
6
|
+
const socketPath = getSocketPath();
|
|
7
|
+
await removeStaleSocketIfNeeded(socketPath);
|
|
8
|
+
const server = createServer((socket) => {
|
|
9
|
+
let buffer = "";
|
|
10
|
+
socket.on("data", async (chunk) => {
|
|
11
|
+
buffer += chunk.toString();
|
|
12
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
13
|
+
if (newlineIndex === -1) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
17
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
18
|
+
if (!line) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const request = parseRequestLine(line);
|
|
23
|
+
if (request.type === "rpc_stream") {
|
|
24
|
+
const response = await handler(request);
|
|
25
|
+
if (!response.ok || response.type !== "rpc_ready" || !response.instance) {
|
|
26
|
+
socket.end(encodeMessage(response));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
socket.removeAllListeners("data");
|
|
30
|
+
const rpcStream = handler.openRpcStream(request.instanceId, (response) => {
|
|
31
|
+
socket.write(encodeMessage(response));
|
|
32
|
+
}, (event) => {
|
|
33
|
+
socket.write(encodeMessage(event));
|
|
34
|
+
}, (request) => {
|
|
35
|
+
socket.write(encodeMessage(request));
|
|
36
|
+
});
|
|
37
|
+
if (!rpcStream) {
|
|
38
|
+
socket.end(encodeMessage({ type: "error", ok: false, error: `Unknown instance: ${request.instanceId}` }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
socket.write(encodeMessage(response));
|
|
42
|
+
let rpcRequestQueue = Promise.resolve();
|
|
43
|
+
socket.on("data", (rpcChunk) => {
|
|
44
|
+
buffer += rpcChunk.toString();
|
|
45
|
+
for (;;) {
|
|
46
|
+
const rpcNewlineIndex = buffer.indexOf("\n");
|
|
47
|
+
if (rpcNewlineIndex === -1) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const rpcLine = buffer.slice(0, rpcNewlineIndex).trim();
|
|
51
|
+
buffer = buffer.slice(rpcNewlineIndex + 1);
|
|
52
|
+
if (!rpcLine) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
rpcRequestQueue = rpcRequestQueue
|
|
56
|
+
.then(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await rpcStream.handleRequest(JSON.parse(rpcLine));
|
|
59
|
+
}
|
|
60
|
+
catch (rpcError) {
|
|
61
|
+
socket.write(encodeMessage({
|
|
62
|
+
type: "error",
|
|
63
|
+
ok: false,
|
|
64
|
+
error: rpcError instanceof Error ? rpcError.message : String(rpcError),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.catch((rpcError) => {
|
|
69
|
+
socket.write(encodeMessage({
|
|
70
|
+
type: "error",
|
|
71
|
+
ok: false,
|
|
72
|
+
error: rpcError.message,
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
socket.once("close", () => rpcStream.close());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const response = await handler(request);
|
|
81
|
+
socket.end(encodeMessage(response));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const response = {
|
|
85
|
+
type: "error",
|
|
86
|
+
ok: false,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
};
|
|
89
|
+
socket.end(encodeMessage(response));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
server.once("error", reject);
|
|
95
|
+
server.listen(socketPath, () => {
|
|
96
|
+
server.off("error", reject);
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return server;
|
|
101
|
+
}
|
|
102
|
+
async function removeStaleSocketIfNeeded(socketPath) {
|
|
103
|
+
if (!existsSync(socketPath)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const isLive = await isSocketLive(socketPath);
|
|
107
|
+
if (isLive) {
|
|
108
|
+
throw new Error(`orchestrator is already running: ${socketPath}`);
|
|
109
|
+
}
|
|
110
|
+
unlinkSync(socketPath);
|
|
111
|
+
}
|
|
112
|
+
async function isSocketLive(socketPath) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const socket = createConnection(socketPath);
|
|
115
|
+
let settled = false;
|
|
116
|
+
const finish = (result) => {
|
|
117
|
+
if (settled) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
settled = true;
|
|
121
|
+
socket.removeAllListeners();
|
|
122
|
+
socket.destroy();
|
|
123
|
+
resolve(result);
|
|
124
|
+
};
|
|
125
|
+
socket.on("connect", () => finish(true));
|
|
126
|
+
socket.on("error", (error) => {
|
|
127
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT") {
|
|
128
|
+
finish(false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (error.code === "EPIPE" || error.code === "ECONNRESET") {
|
|
132
|
+
finish(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (settled) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
settled = true;
|
|
139
|
+
socket.removeAllListeners();
|
|
140
|
+
socket.destroy();
|
|
141
|
+
reject(error);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { hostname, platform } from "node:os";
|
|
2
|
+
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { getOrchestratorDir, getSocketPath, VERSION } from "./config.js";
|
|
4
|
+
import { loadMachine, saveMachine } from "./storage.js";
|
|
5
|
+
const DEFAULT_RADIUS_URL = "https://radius.pi.dev/";
|
|
6
|
+
const DEFAULT_ORCHESTRATOR_BASE_PATH = "/v1/";
|
|
7
|
+
const NOT_FOUND_RETRY_THRESHOLD = 3;
|
|
8
|
+
const HEARTBEAT_BACKOFF_BASE_MS = 1_000;
|
|
9
|
+
const HEARTBEAT_BACKOFF_MAX_MS = 30_000;
|
|
10
|
+
const RADIUS_PROVIDER = "radius";
|
|
11
|
+
class RadiusHttpError extends Error {
|
|
12
|
+
status;
|
|
13
|
+
constructor(status, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "RadiusHttpError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function post(path, body) {
|
|
20
|
+
const response = await fetch(new URL(path, getRadiusOrchestratorBaseUrl()), {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${getRadiusAccessToken()}`,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new RadiusHttpError(response.status, `Radius request failed: ${response.status} ${await response.text()}`);
|
|
30
|
+
}
|
|
31
|
+
return (await response.json());
|
|
32
|
+
}
|
|
33
|
+
async function maybePost(path, body) {
|
|
34
|
+
const response = await fetch(new URL(path, getRadiusOrchestratorBaseUrl()), {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${getRadiusAccessToken()}`,
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new RadiusHttpError(response.status, `Radius request failed: ${response.status} ${await response.text()}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isNotFoundError(error) {
|
|
47
|
+
return error instanceof RadiusHttpError && error.status === 404;
|
|
48
|
+
}
|
|
49
|
+
function computeBackoffDelayMs(failureCount) {
|
|
50
|
+
const exponentialDelay = Math.min(HEARTBEAT_BACKOFF_MAX_MS, HEARTBEAT_BACKOFF_BASE_MS * 2 ** Math.max(0, failureCount - 1));
|
|
51
|
+
const jitterMs = Math.floor(Math.random() * Math.max(250, exponentialDelay / 4));
|
|
52
|
+
return Math.min(HEARTBEAT_BACKOFF_MAX_MS, exponentialDelay + jitterMs);
|
|
53
|
+
}
|
|
54
|
+
function formatRadiusError(error) {
|
|
55
|
+
if (error instanceof RadiusHttpError) {
|
|
56
|
+
return `HTTP ${error.status}: ${error.message}`;
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
return error.message;
|
|
60
|
+
}
|
|
61
|
+
return String(error);
|
|
62
|
+
}
|
|
63
|
+
function logRadiusRetry(scope, action, delayMs, failureCount, error) {
|
|
64
|
+
console.error(`${scope} ${action} failed (attempt ${failureCount}); retrying in ${delayMs}ms: ${formatRadiusError(error)}`);
|
|
65
|
+
}
|
|
66
|
+
export function getRadiusUrl() {
|
|
67
|
+
return process.env.PI_RADIUS_URL || DEFAULT_RADIUS_URL;
|
|
68
|
+
}
|
|
69
|
+
export function getRadiusOrchestratorBaseUrl() {
|
|
70
|
+
const explicitUrl = process.env.PI_RADIUS_ORCHESTRATOR_URL;
|
|
71
|
+
if (explicitUrl) {
|
|
72
|
+
return explicitUrl;
|
|
73
|
+
}
|
|
74
|
+
return new URL(DEFAULT_ORCHESTRATOR_BASE_PATH, getRadiusUrl()).toString();
|
|
75
|
+
}
|
|
76
|
+
const radiusAuthStorage = AuthStorage.create();
|
|
77
|
+
function getStoredRadiusCredential() {
|
|
78
|
+
radiusAuthStorage.reload();
|
|
79
|
+
const credential = radiusAuthStorage.get(RADIUS_PROVIDER);
|
|
80
|
+
if (!credential || credential.type !== "oauth") {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return credential;
|
|
84
|
+
}
|
|
85
|
+
export function getRadiusAccessToken() {
|
|
86
|
+
const storedCredential = getStoredRadiusCredential();
|
|
87
|
+
if (typeof storedCredential?.access === "string" && storedCredential.access) {
|
|
88
|
+
return storedCredential.access;
|
|
89
|
+
}
|
|
90
|
+
const apiKey = process.env.PI_RADIUS_API_KEY;
|
|
91
|
+
if (apiKey) {
|
|
92
|
+
return apiKey;
|
|
93
|
+
}
|
|
94
|
+
throw new Error("Radius credentials are required in ~/.pi/agent/auth.json or PI_RADIUS_API_KEY");
|
|
95
|
+
}
|
|
96
|
+
export function isRadiusEnabled() {
|
|
97
|
+
return !!getStoredRadiusCredential()?.access || !!process.env.PI_RADIUS_API_KEY;
|
|
98
|
+
}
|
|
99
|
+
export class RadiusPresence {
|
|
100
|
+
machineHeartbeatTimer;
|
|
101
|
+
machineHeartbeatIntervalMs = 0;
|
|
102
|
+
machineConsecutiveNotFoundCount = 0;
|
|
103
|
+
machineTransientFailureCount = 0;
|
|
104
|
+
piHeartbeatStates = new Map();
|
|
105
|
+
machine;
|
|
106
|
+
coordinator;
|
|
107
|
+
setCoordinator(coordinator) {
|
|
108
|
+
this.coordinator = coordinator;
|
|
109
|
+
}
|
|
110
|
+
async start(label) {
|
|
111
|
+
if (!isRadiusEnabled()) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const registered = await this.registerMachine(label);
|
|
115
|
+
this.startMachineHeartbeat(registered.heartbeatIntervalMs);
|
|
116
|
+
return this.machine;
|
|
117
|
+
}
|
|
118
|
+
async stop() {
|
|
119
|
+
if (this.machineHeartbeatTimer) {
|
|
120
|
+
clearTimeout(this.machineHeartbeatTimer);
|
|
121
|
+
this.machineHeartbeatTimer = undefined;
|
|
122
|
+
}
|
|
123
|
+
for (const [instanceId, state] of this.piHeartbeatStates) {
|
|
124
|
+
if (state.timer) {
|
|
125
|
+
clearTimeout(state.timer);
|
|
126
|
+
}
|
|
127
|
+
this.piHeartbeatStates.delete(instanceId);
|
|
128
|
+
}
|
|
129
|
+
if (!this.machine || !isRadiusEnabled()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await maybePost(`machines/${this.machine.id}/disconnect`, {});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!isNotFoundError(error)) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async registerPi(instance) {
|
|
142
|
+
if (!isRadiusEnabled()) {
|
|
143
|
+
return instance;
|
|
144
|
+
}
|
|
145
|
+
const machine = this.machine ?? loadMachine();
|
|
146
|
+
if (!machine) {
|
|
147
|
+
throw new Error("No registered machine available for Pi registration");
|
|
148
|
+
}
|
|
149
|
+
const registered = await post("pis/register", {
|
|
150
|
+
machineId: machine.id,
|
|
151
|
+
label: instance.label,
|
|
152
|
+
cwd: instance.cwd,
|
|
153
|
+
hostname: hostname(),
|
|
154
|
+
pid: process.pid,
|
|
155
|
+
transport: "local-rpc",
|
|
156
|
+
capabilities: { rpc: true, relay: false, iroh: false },
|
|
157
|
+
sessionId: instance.sessionId,
|
|
158
|
+
});
|
|
159
|
+
const registeredInstance = { ...instance, radiusPiId: registered.id };
|
|
160
|
+
this.startPiHeartbeat(instance.id, registered.heartbeatIntervalMs, registered.id);
|
|
161
|
+
return registeredInstance;
|
|
162
|
+
}
|
|
163
|
+
async disconnectPi(instance) {
|
|
164
|
+
const state = this.piHeartbeatStates.get(instance.id);
|
|
165
|
+
if (state) {
|
|
166
|
+
if (state.timer) {
|
|
167
|
+
clearTimeout(state.timer);
|
|
168
|
+
}
|
|
169
|
+
this.piHeartbeatStates.delete(instance.id);
|
|
170
|
+
}
|
|
171
|
+
if (!isRadiusEnabled() || !instance.radiusPiId) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await maybePost(`pis/${instance.radiusPiId}/disconnect`, {});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (!isNotFoundError(error)) {
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async registerMachine(label) {
|
|
184
|
+
const existingMachine = this.machine ?? loadMachine();
|
|
185
|
+
const registered = await post("machines/register", {
|
|
186
|
+
machineId: existingMachine?.id,
|
|
187
|
+
label,
|
|
188
|
+
hostname: hostname(),
|
|
189
|
+
platform: platform(),
|
|
190
|
+
arch: process.arch,
|
|
191
|
+
version: VERSION,
|
|
192
|
+
capabilities: { spawn: true, relay: false, iroh: false },
|
|
193
|
+
});
|
|
194
|
+
const timestamp = new Date().toISOString();
|
|
195
|
+
this.machine = {
|
|
196
|
+
id: registered.id,
|
|
197
|
+
createdAt: existingMachine?.createdAt ?? timestamp,
|
|
198
|
+
lastSeenAt: timestamp,
|
|
199
|
+
label,
|
|
200
|
+
};
|
|
201
|
+
saveMachine(this.machine);
|
|
202
|
+
this.machineConsecutiveNotFoundCount = 0;
|
|
203
|
+
this.machineTransientFailureCount = 0;
|
|
204
|
+
return registered;
|
|
205
|
+
}
|
|
206
|
+
startMachineHeartbeat(intervalMs) {
|
|
207
|
+
this.machineHeartbeatIntervalMs = intervalMs;
|
|
208
|
+
this.scheduleMachineHeartbeat(intervalMs);
|
|
209
|
+
}
|
|
210
|
+
scheduleMachineHeartbeat(delayMs) {
|
|
211
|
+
if (this.machineHeartbeatTimer) {
|
|
212
|
+
clearTimeout(this.machineHeartbeatTimer);
|
|
213
|
+
}
|
|
214
|
+
this.machineHeartbeatTimer = setTimeout(() => {
|
|
215
|
+
void this.heartbeatMachine();
|
|
216
|
+
}, delayMs);
|
|
217
|
+
}
|
|
218
|
+
startPiHeartbeat(instanceId, intervalMs, radiusPiId) {
|
|
219
|
+
const existingState = this.piHeartbeatStates.get(instanceId);
|
|
220
|
+
if (existingState?.timer) {
|
|
221
|
+
clearTimeout(existingState.timer);
|
|
222
|
+
}
|
|
223
|
+
const state = existingState ?? {
|
|
224
|
+
intervalMs,
|
|
225
|
+
radiusPiId,
|
|
226
|
+
consecutiveNotFoundCount: 0,
|
|
227
|
+
transientFailureCount: 0,
|
|
228
|
+
};
|
|
229
|
+
state.intervalMs = intervalMs;
|
|
230
|
+
state.radiusPiId = radiusPiId;
|
|
231
|
+
state.consecutiveNotFoundCount = 0;
|
|
232
|
+
state.transientFailureCount = 0;
|
|
233
|
+
this.piHeartbeatStates.set(instanceId, state);
|
|
234
|
+
this.schedulePiHeartbeat(instanceId, intervalMs);
|
|
235
|
+
}
|
|
236
|
+
schedulePiHeartbeat(instanceId, delayMs) {
|
|
237
|
+
const state = this.piHeartbeatStates.get(instanceId);
|
|
238
|
+
if (!state) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (state.timer) {
|
|
242
|
+
clearTimeout(state.timer);
|
|
243
|
+
}
|
|
244
|
+
state.timer = setTimeout(() => {
|
|
245
|
+
void this.heartbeatPi(instanceId);
|
|
246
|
+
}, delayMs);
|
|
247
|
+
}
|
|
248
|
+
async heartbeatMachine() {
|
|
249
|
+
if (!this.machine || !isRadiusEnabled()) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await maybePost(`machines/${this.machine.id}/heartbeat`, {
|
|
254
|
+
cwd: getOrchestratorDir(),
|
|
255
|
+
socketPath: getSocketPath(),
|
|
256
|
+
});
|
|
257
|
+
this.machineConsecutiveNotFoundCount = 0;
|
|
258
|
+
this.machineTransientFailureCount = 0;
|
|
259
|
+
this.scheduleMachineHeartbeat(this.machineHeartbeatIntervalMs);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (!isNotFoundError(error)) {
|
|
263
|
+
this.machineTransientFailureCount += 1;
|
|
264
|
+
const delayMs = computeBackoffDelayMs(this.machineTransientFailureCount);
|
|
265
|
+
logRadiusRetry("Radius machine", "heartbeat", delayMs, this.machineTransientFailureCount, error);
|
|
266
|
+
this.scheduleMachineHeartbeat(delayMs);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.machineTransientFailureCount = 0;
|
|
270
|
+
this.machineConsecutiveNotFoundCount += 1;
|
|
271
|
+
if (this.machineConsecutiveNotFoundCount < NOT_FOUND_RETRY_THRESHOLD) {
|
|
272
|
+
this.scheduleMachineHeartbeat(this.machineHeartbeatIntervalMs);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await this.reRegisterMachineAndPis();
|
|
277
|
+
}
|
|
278
|
+
catch (recoveryError) {
|
|
279
|
+
this.machineTransientFailureCount += 1;
|
|
280
|
+
const delayMs = computeBackoffDelayMs(this.machineTransientFailureCount);
|
|
281
|
+
logRadiusRetry("Radius machine", "re-registration", delayMs, this.machineTransientFailureCount, recoveryError);
|
|
282
|
+
this.scheduleMachineHeartbeat(delayMs);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async heartbeatPi(instanceId) {
|
|
287
|
+
if (!isRadiusEnabled()) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const state = this.piHeartbeatStates.get(instanceId);
|
|
291
|
+
if (!state) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
await maybePost(`pis/${state.radiusPiId}/heartbeat`, {});
|
|
296
|
+
state.consecutiveNotFoundCount = 0;
|
|
297
|
+
state.transientFailureCount = 0;
|
|
298
|
+
this.schedulePiHeartbeat(instanceId, state.intervalMs);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (!isNotFoundError(error)) {
|
|
302
|
+
state.transientFailureCount += 1;
|
|
303
|
+
const delayMs = computeBackoffDelayMs(state.transientFailureCount);
|
|
304
|
+
logRadiusRetry(`Radius Pi ${instanceId}`, "heartbeat", delayMs, state.transientFailureCount, error);
|
|
305
|
+
this.schedulePiHeartbeat(instanceId, delayMs);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
state.transientFailureCount = 0;
|
|
309
|
+
state.consecutiveNotFoundCount += 1;
|
|
310
|
+
if (state.consecutiveNotFoundCount < NOT_FOUND_RETRY_THRESHOLD) {
|
|
311
|
+
this.schedulePiHeartbeat(instanceId, state.intervalMs);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const recovered = await this.reRegisterPi(instanceId);
|
|
316
|
+
if (!recovered) {
|
|
317
|
+
const delayMs = computeBackoffDelayMs(1);
|
|
318
|
+
console.error(`Radius Pi ${instanceId} re-registration skipped; retrying in ${delayMs}ms`);
|
|
319
|
+
this.schedulePiHeartbeat(instanceId, delayMs);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (recoveryError) {
|
|
323
|
+
state.transientFailureCount += 1;
|
|
324
|
+
const delayMs = computeBackoffDelayMs(state.transientFailureCount);
|
|
325
|
+
logRadiusRetry(`Radius Pi ${instanceId}`, "re-registration", delayMs, state.transientFailureCount, recoveryError);
|
|
326
|
+
this.schedulePiHeartbeat(instanceId, delayMs);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async reRegisterMachineAndPis() {
|
|
331
|
+
const registered = await this.registerMachine(this.machine?.label);
|
|
332
|
+
this.startMachineHeartbeat(registered.heartbeatIntervalMs);
|
|
333
|
+
const instances = this.coordinator?.listLiveInstances() ?? [];
|
|
334
|
+
for (const instance of instances) {
|
|
335
|
+
try {
|
|
336
|
+
await this.reRegisterPi(instance.id);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error(`Radius Pi ${instance.id} re-registration failed: ${formatRadiusError(error)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async reRegisterPi(instanceId) {
|
|
344
|
+
const instance = this.coordinator?.getLiveInstance(instanceId);
|
|
345
|
+
if (!instance) {
|
|
346
|
+
const state = this.piHeartbeatStates.get(instanceId);
|
|
347
|
+
if (state) {
|
|
348
|
+
if (state.timer) {
|
|
349
|
+
clearTimeout(state.timer);
|
|
350
|
+
}
|
|
351
|
+
this.piHeartbeatStates.delete(instanceId);
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (!this.machine) {
|
|
356
|
+
await this.reRegisterMachineAndPis();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
const registeredInstance = await this.registerPi(instance);
|
|
360
|
+
this.coordinator?.updateInstance(registeredInstance);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
export const radiusPresence = new RadiusPresence();
|
|
365
|
+
//# sourceMappingURL=radius.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { isBunBinary } from "./config.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
function toError(error) {
|
|
8
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
9
|
+
}
|
|
10
|
+
export class RpcProcessInstance {
|
|
11
|
+
process;
|
|
12
|
+
exited = false;
|
|
13
|
+
nextRequestId = 0;
|
|
14
|
+
stdoutBuffer = "";
|
|
15
|
+
stderrBuffer = "";
|
|
16
|
+
pendingRequests = new Map();
|
|
17
|
+
eventListeners = new Set();
|
|
18
|
+
exitListeners = new Set();
|
|
19
|
+
uiRequestHandler;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
const rpcCommand = this.getSpawnCommand();
|
|
22
|
+
this.process = spawn(rpcCommand.command, rpcCommand.args, {
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
env: process.env,
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
+
});
|
|
27
|
+
if (!this.process.stdin || !this.process.stdout) {
|
|
28
|
+
throw new Error("Failed to create RPC process stdio");
|
|
29
|
+
}
|
|
30
|
+
this.attachListeners();
|
|
31
|
+
}
|
|
32
|
+
getSpawnCommand() {
|
|
33
|
+
if (isBunBinary) {
|
|
34
|
+
return {
|
|
35
|
+
command: join(dirname(process.execPath), process.platform === "win32" ? "pi.exe" : "pi"),
|
|
36
|
+
args: ["--mode", "rpc"],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: process.execPath,
|
|
41
|
+
args: [require.resolve("@earendil-works/pi-coding-agent/rpc-entry")],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
attachListeners() {
|
|
45
|
+
this.process.stdout?.setEncoding("utf8");
|
|
46
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
47
|
+
this.stdoutBuffer += chunk;
|
|
48
|
+
while (true) {
|
|
49
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
50
|
+
if (newlineIndex === -1) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
54
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
55
|
+
if (!line) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
this.handleLine(line);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.process.stderr?.setEncoding("utf8");
|
|
62
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
63
|
+
this.stderrBuffer += chunk;
|
|
64
|
+
});
|
|
65
|
+
this.process.once("error", (error) => {
|
|
66
|
+
this.exited = true;
|
|
67
|
+
const wrapped = new Error(`RPC process error: ${error.message}. Stderr: ${this.stderrBuffer}`);
|
|
68
|
+
this.rejectAllPending(wrapped);
|
|
69
|
+
this.notifyExit(wrapped);
|
|
70
|
+
});
|
|
71
|
+
this.process.once("exit", (code, signal) => {
|
|
72
|
+
this.exited = true;
|
|
73
|
+
const error = new Error(`RPC process exited (code=${code} signal=${signal}). Stderr: ${this.stderrBuffer}`);
|
|
74
|
+
this.rejectAllPending(error);
|
|
75
|
+
this.notifyExit(error);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
handleLine(line) {
|
|
79
|
+
const parsed = JSON.parse(line);
|
|
80
|
+
switch (parsed.type) {
|
|
81
|
+
case "response": {
|
|
82
|
+
if (!parsed.id) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const pending = this.pendingRequests.get(parsed.id);
|
|
86
|
+
if (!pending) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.pendingRequests.delete(parsed.id);
|
|
90
|
+
pending.resolve(parsed);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "extension_ui_request": {
|
|
94
|
+
this.uiRequestHandler?.(parsed);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
default: {
|
|
98
|
+
for (const listener of this.eventListeners) {
|
|
99
|
+
listener(parsed);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
rejectAllPending(error) {
|
|
105
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
106
|
+
this.pendingRequests.delete(id);
|
|
107
|
+
pending.reject(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
notifyExit(error) {
|
|
111
|
+
for (const listener of this.exitListeners) {
|
|
112
|
+
listener(error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
send(command) {
|
|
116
|
+
if (this.exited) {
|
|
117
|
+
throw new Error(`RPC process is not running. Stderr: ${this.stderrBuffer}`);
|
|
118
|
+
}
|
|
119
|
+
const id = command.id ?? `orchestrator_${++this.nextRequestId}_${randomUUID()}`;
|
|
120
|
+
const fullCommand = { ...command, id };
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
123
|
+
this.process.stdin?.write(`${JSON.stringify(fullCommand)}\n`, (error) => {
|
|
124
|
+
if (!error) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.pendingRequests.delete(id);
|
|
128
|
+
reject(toError(error));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
handleUiResponse(response) {
|
|
133
|
+
if (this.exited) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.process.stdin?.write(`${JSON.stringify(response)}\n`);
|
|
137
|
+
}
|
|
138
|
+
setUiRequestHandler(handler) {
|
|
139
|
+
this.uiRequestHandler = handler;
|
|
140
|
+
}
|
|
141
|
+
onEvent(listener) {
|
|
142
|
+
this.eventListeners.add(listener);
|
|
143
|
+
return () => {
|
|
144
|
+
this.eventListeners.delete(listener);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
onExit(listener) {
|
|
148
|
+
this.exitListeners.add(listener);
|
|
149
|
+
return () => {
|
|
150
|
+
this.exitListeners.delete(listener);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async dispose() {
|
|
154
|
+
this.uiRequestHandler = undefined;
|
|
155
|
+
this.rejectAllPending(new Error("RPC process disposed"));
|
|
156
|
+
if (this.exited) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.process.kill("SIGTERM");
|
|
160
|
+
await new Promise((resolve) => {
|
|
161
|
+
this.process.once("exit", () => resolve());
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function createRpcProcessInstance(options) {
|
|
166
|
+
return new RpcProcessInstance(options);
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=rpc-process.js.map
|