mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 17:11:42 -06:00
78502eda6d
* feat(workers): run device sessions from Gateway bundles Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice. * docs(runners): record Gateway bundle cutover * test(ci): repair runner validation fixtures # Conflicts: # src/scripts/test-projects.test.ts * fix(workers): surface outdated node recovery Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces. * fix(workers): reject legacy inventory with recovery * fix(workers): bundle worker deploy closure * test(workers): close bundle cutover gates * fix(workers): compose browser runtime at build * fix(workers): satisfy bundle cutover gates * fix(workers): route temp runtime through infra * docs(workers): align bundle host guidance * fix(ui): fence outdated session destinations
126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import { signalProcessTree } from "../process/kill-tree.js";
|
|
import type { WorkerBrowserRuntime } from "./browser-runtime.js";
|
|
import {
|
|
NODE_WORKER_CONNECTION_FAILURE_MESSAGE_TYPE,
|
|
type NodeWorkerConnectionFailureMessage,
|
|
} from "./node-supervisor-protocol.js";
|
|
import { runWorkerCommand, type WorkerCommandLifetime } from "./worker-command.runtime.js";
|
|
|
|
const WORKER_START_MESSAGE_TYPE = "openclaw-worker-start-v1";
|
|
|
|
function isWorkerStartMessage(value: unknown): boolean {
|
|
return (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
!Array.isArray(value) &&
|
|
Object.keys(value).length === 1 &&
|
|
(value as { type?: unknown }).type === WORKER_START_MESSAGE_TYPE
|
|
);
|
|
}
|
|
|
|
function createWorkerIpcLifetime(): WorkerCommandLifetime {
|
|
if (!process.connected || !process.channel || typeof process.send !== "function") {
|
|
throw new Error("internal worker IPC mode requires a connected Node IPC channel");
|
|
}
|
|
const abortController = new AbortController();
|
|
let disposed = false;
|
|
let started = false;
|
|
let settled = false;
|
|
let resolveStarted!: (started: boolean) => void;
|
|
let rejectStarted!: (error: Error) => void;
|
|
const startedPromise = new Promise<boolean>((resolve, reject) => {
|
|
resolveStarted = resolve;
|
|
rejectStarted = reject;
|
|
});
|
|
const rejectOrAbort = (error: Error) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
rejectStarted(error);
|
|
return;
|
|
}
|
|
abortController.abort(error);
|
|
};
|
|
const onMessage = (message: unknown) => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
if (!isWorkerStartMessage(message) || settled) {
|
|
rejectOrAbort(new Error("invalid internal worker IPC start message"));
|
|
return;
|
|
}
|
|
started = true;
|
|
settled = true;
|
|
resolveStarted(true);
|
|
};
|
|
const onDisconnect = () => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
if (!settled) {
|
|
settled = true;
|
|
resolveStarted(false);
|
|
return;
|
|
}
|
|
if (started) {
|
|
abortController.abort(new Error("worker supervisor lifetime ended"));
|
|
}
|
|
};
|
|
process.on("message", onMessage);
|
|
process.once("disconnect", onDisconnect);
|
|
return {
|
|
started: startedPromise,
|
|
signal: abortController.signal,
|
|
reportConnectionFailure: (cause) => {
|
|
if (disposed || !process.connected || typeof process.send !== "function") {
|
|
return;
|
|
}
|
|
const message: NodeWorkerConnectionFailureMessage = {
|
|
type: NODE_WORKER_CONNECTION_FAILURE_MESSAGE_TYPE,
|
|
cause: cause ?? null,
|
|
};
|
|
try {
|
|
process.send(message, () => {});
|
|
} catch {
|
|
// The disconnect handler owns worker shutdown when the supervisor is gone.
|
|
}
|
|
},
|
|
terminateOwnedTree: () => {
|
|
signalProcessTree(process.pid, "SIGKILL", {
|
|
detached: process.platform !== "win32",
|
|
});
|
|
},
|
|
dispose: () => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
disposed = true;
|
|
process.off("message", onMessage);
|
|
process.off("disconnect", onDisconnect);
|
|
if (process.connected) {
|
|
try {
|
|
process.disconnect?.();
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ERR_IPC_DISCONNECTED") {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Runs the worker-only process entry without loading the general CLI command tree. */
|
|
export async function runWorkerProcess(
|
|
options: {
|
|
internalWorkerIpc?: boolean;
|
|
browserRuntime?: WorkerBrowserRuntime;
|
|
} = {},
|
|
): Promise<void> {
|
|
await runWorkerCommand({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
...(options.internalWorkerIpc ? { lifetime: createWorkerIpcLifetime() } : {}),
|
|
...(options.browserRuntime ? { browserRuntime: options.browserRuntime } : {}),
|
|
});
|
|
}
|