UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

363 lines (362 loc) 15.4 kB
import { n as MAX_TIMER_TIMEOUT_MS } from "./number-coercion-CLj0HTDM.js"; import { n as computeBackoff, s as sleepWithAbort } from "./src-BQ327IOM.js"; import { a as formatNodeRunnerUpdateRequired, n as NODE_RUNNER_UPDATE_REQUIRED_ISSUE } from "./node-runner-inventory-CsPbvshW.js"; import { C as parseNodeWorkerLaunchInput, T as parseNodeWorkerSupervisorReceipt, h as parseWorkerAdmissionDeadlineResult, n as measureWorkerProcessTurnBytes, y as nodeWorkerPlanHash } from "./worker-process-protocol-COVu2ppZ.js"; import { E as NODE_WORKER_SUPERVISOR_STATUS_COMMAND, T as NODE_WORKER_SUPERVISOR_LAUNCH_COMMAND, w as NODE_WORKER_SUPERVISOR_CANCEL_COMMAND } from "./node-commands-BC8PhxqU.js"; import "./backoff-BkMI1WEL.js"; import { n as serializeNodeEvent, t as buildNodeInvokeRequest } from "./node-invoke-request-Cfnz22m2.js"; import { n as WorkerRunnerUnavailableError, t as WorkerRunnerCapacityError } from "./tunnel-contract-CB5bcNwX.js"; import { createHash, randomUUID } from "node:crypto"; //#region src/gateway/worker-environments/node-worker-abort.ts function raceNodeWorkerOperation(operation, signal) { const abortError = () => signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("node worker operation aborted"); if (signal.aborted) return Promise.reject(abortError()); return new Promise((resolve, reject) => { const onAbort = () => reject(abortError()); signal.addEventListener("abort", onAbort, { once: true }); operation.then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error instanceof Error ? error : /* @__PURE__ */ new Error("node worker operation failed")); }); }); } //#endregion //#region src/gateway/worker-environments/node-launch-adapter.ts const DEFAULT_RPC_TIMEOUT_MS = 3e4; const DEFAULT_POLL_INTERVAL_MS = 250; const MAX_RETRY_DELAY_MS = 2e3; const DEFAULT_CANCELLATION_TIMEOUT_MS = 3e4; const DEFAULT_AVAILABILITY_TIMEOUT_MS = 1e4; const MAX_ADMISSION_ATTEMPTS = 5; const ADMISSION_REARM_BACKOFF = { initialMs: 1e3, maxMs: 3e4, factor: 2, jitter: .1 }; const RETRYABLE_TRANSPORT_CODES = /* @__PURE__ */ new Set([ "DISCONNECTED", "NOT_CONNECTED", "PAIRING_CHANGED", "PRIVATE_DIALECT_UNAVAILABLE", "ROUTE_CHANGED", "TIMEOUT", "UNAVAILABLE" ]); var NodeWorkerLaunchTransportError = class extends Error { constructor(code, message) { super(message); this.code = code; } }; function isTerminalReceipt(receipt) { return receipt.state === "completed" || receipt.state === "failed" || receipt.state === "interrupted" || receipt.state === "cancelled"; } function snapshotLaunchInput(input) { let encoded; try { encoded = JSON.stringify(input); } catch { throw new Error("node worker launch input is not serializable"); } return parseNodeWorkerLaunchInput(encoded); } function rearmNodeWorkerLaunchInput(input, attempt) { const launchId = createHash("sha256").update(`${input.launchId}:admission-rearm:${attempt}`).digest("hex"); return { ...input, launchId, descriptor: { ...input.descriptor, assignment: { ...input.descriptor.assignment, turnId: launchId } } }; } function measureNodeWorkerLaunchBytes(nodeId, input) { return Math.max(...[input, rearmNodeWorkerLaunchInput(input, 1)].flatMap((attempt) => [Buffer.byteLength(serializeNodeEvent("node.invoke.request", buildNodeInvokeRequest({ id: randomUUID(), nodeId, command: NODE_WORKER_SUPERVISOR_LAUNCH_COMMAND, params: attempt, timeoutMs: MAX_TIMER_TIMEOUT_MS, idempotencyKey: attempt.launchId })), "utf8"), measureWorkerProcessTurnBytes(attempt.descriptor)])); } function expectedIdentity(input) { if (input.launchId !== input.descriptor.assignment.turnId) throw new Error("node worker launch ID must match the durable turn ID"); return { launchId: input.launchId, planHash: nodeWorkerPlanHash(input), environmentId: input.descriptor.admission.environmentId, sessionId: input.descriptor.admission.sessionId, ownerEpoch: input.descriptor.admission.ownerEpoch, placementGeneration: input.placementGeneration, runId: input.descriptor.assignment.runId }; } function receiptMatchesIdentity(receipt, expected) { return receipt.launchId === expected.launchId && receipt.planHash === expected.planHash && receipt.environmentId === expected.environmentId && receipt.sessionId === expected.sessionId && receipt.ownerEpoch === expected.ownerEpoch && receipt.placementGeneration === expected.placementGeneration && receipt.runId === expected.runId; } function parseInvokeReceipt(payloadJSON) { if (!payloadJSON) throw new Error("node worker supervisor response omitted payload JSON"); let value; try { value = JSON.parse(payloadJSON); } catch { throw new Error("node worker supervisor response contained malformed JSON"); } if (value === null) return null; const receipt = parseNodeWorkerSupervisorReceipt(value); if (!receipt) throw new Error("node worker supervisor response violated the private receipt contract"); return receipt; } function signalError(signal, fallback) { return signal.reason instanceof Error ? signal.reason : new Error(fallback); } function createDeadline(params) { if (!Number.isFinite(params.timeoutMs) || params.timeoutMs <= 0) throw new Error(`${params.label} timeout must be a positive finite number`); const controller = new AbortController(); const expiresAtMs = params.now() + params.timeoutMs; const expire = () => { params.parent?.remainingMs(); controller.abort(/* @__PURE__ */ new Error(`${params.label} timed out`)); }; const timer = setTimeout(expire, params.timeoutMs); timer.unref?.(); const parentSignal = params.parent?.signal ?? params.signal; return { signal: parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal, remainingMs: () => { params.parent?.remainingMs(); const remainingMs = Math.max(0, expiresAtMs - params.now()); if (remainingMs === 0) expire(); return remainingMs; }, dispose: () => clearTimeout(timer) }; } function createNodeWorkerLaunchAdapter(options) { const now = options.now ?? Date.now; const sleep = options.sleep ?? sleepWithAbort; const rpcTimeoutMs = options.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS; const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; const cancellationTimeoutMs = options.cancellationTimeoutMs ?? DEFAULT_CANCELLATION_TIMEOUT_MS; const findNode = async (params) => { let nodes; try { nodes = await raceNodeWorkerOperation(params.transport.listCurrentNodes(), params.signal); } catch (error) { if (params.signal.aborted) throw error; throw new NodeWorkerLaunchTransportError("UNAVAILABLE", "device worker node discovery is unavailable"); } const node = nodes.find((candidate) => candidate.nodeId === params.deviceId); if (!node) throw new NodeWorkerLaunchTransportError("NOT_CONNECTED", "device worker node is not currently connected"); return node; }; const invoke = async (params) => { if (!params.isAuthorized()) throw new NodeWorkerLaunchTransportError("APPROVAL_AUTHORITY_CLOSED", "node worker authority closed"); const remainingMs = params.deadline.remainingMs(); params.deadline.signal.throwIfAborted(); const transport = options.getTransport(); if (!transport) throw new NodeWorkerLaunchTransportError("UNAVAILABLE", "device worker node transport is unavailable"); const rpcBudgetMs = Math.min(rpcTimeoutMs, remainingMs); const rpcController = rpcBudgetMs < remainingMs ? new AbortController() : void 0; const rpcTimer = rpcController ? setTimeout(() => rpcController.abort(/* @__PURE__ */ new Error("node worker RPC timed out")), rpcBudgetMs) : void 0; rpcTimer?.unref?.(); const signal = rpcController ? AbortSignal.any([params.deadline.signal, rpcController.signal]) : params.deadline.signal; try { const node = await findNode({ transport, deviceId: params.deviceId, signal }); if (params.command === "worker.launch.v1" && node.workerHost.environmentSession !== 1) throw new Error(formatNodeRunnerUpdateRequired(node.nodeId, NODE_RUNNER_UPDATE_REQUIRED_ISSUE)); const result = await raceNodeWorkerOperation(transport.invoke({ node, command: params.command, params: params.payload, timeoutMs: rpcBudgetMs, signal, idempotencyKey: params.command === "worker.launch.v1" && typeof params.payload === "object" && params.payload !== null && "launchId" in params.payload && typeof params.payload.launchId === "string" ? params.payload.launchId : void 0, isDispatchAuthorized: params.isAuthorized, ...params.onDispatchReady ? { onDispatchReady: params.onDispatchReady } : {} }), signal); if (!result.ok) { const code = result.error?.code ?? "UNAVAILABLE"; if (code === "WORKER_CAPACITY_EXHAUSTED") throw new WorkerRunnerCapacityError(); throw new NodeWorkerLaunchTransportError(code, `node worker supervisor invocation failed (${code})`); } return parseInvokeReceipt(result.payloadJSON); } catch (error) { if (rpcController?.signal.aborted && !params.deadline.signal.aborted) throw new NodeWorkerLaunchTransportError("TIMEOUT", "node worker RPC timed out"); throw error; } finally { clearTimeout(rpcTimer); } }; const validateReceipt = (receipt, expected) => { if (!receiptMatchesIdentity(receipt, expected)) throw new Error("node worker supervisor receipt identity mismatch"); return receipt; }; const waitBeforeRetry = async (params) => { const remainingMs = params.deadline.remainingMs(); params.deadline.signal.throwIfAborted(); await raceNodeWorkerOperation(sleep(Math.min(params.delayMs, remainingMs), params.deadline.signal), params.deadline.signal); return Math.min(params.delayMs * 2, MAX_RETRY_DELAY_MS); }; const cancelUntilTerminal = async (params) => { const deadline = createDeadline({ now, timeoutMs: cancellationTimeoutMs, label: "node worker cancellation" }); let delayMs = pollIntervalMs; try { while (!deadline.signal.aborted && deadline.remainingMs() > 0) { if (!params.request.isCancellationAuthorized()) throw new Error("node worker cancellation authority closed before terminal settlement"); try { const receipt = await invoke({ deviceId: params.request.deviceId, command: NODE_WORKER_SUPERVISOR_CANCEL_COMMAND, payload: params.expected, isAuthorized: params.request.isCancellationAuthorized, deadline }); if (receipt) { const validated = validateReceipt(receipt, params.expected); if (isTerminalReceipt(validated)) return validated; delayMs = pollIntervalMs; } } catch (error) { if (deadline.signal.aborted) break; if (!(error instanceof NodeWorkerLaunchTransportError) || !RETRYABLE_TRANSPORT_CODES.has(error.code)) throw error; } delayMs = await waitBeforeRetry({ delayMs, deadline }); } } finally { deadline.dispose(); } throw new Error("node worker cancellation outcome is unknown after transport loss"); }; const launch = async (request) => { const originalInput = snapshotLaunchInput(request.input); let input = originalInput; const stableRequest = { ...request, input }; let expected = expectedIdentity(input); let admissionAttempts = 1; const deadline = createDeadline({ now, timeoutMs: request.timeoutMs, ...request.signal ? { signal: request.signal } : {}, label: "node worker launch" }); const availabilityDeadline = createDeadline({ now, timeoutMs: DEFAULT_AVAILABILITY_TIMEOUT_MS, parent: deadline, label: "node worker availability" }); const dispatchDeadline = { ...deadline, signal: availabilityDeadline.signal }; let mayHaveLaunched = false; let dispatchReady = false; let pollStatus = false; let delayMs = pollIntervalMs; const markDispatchReady = () => { mayHaveLaunched = true; if (!dispatchReady) { dispatchReady = true; availabilityDeadline.dispose(); stableRequest.onDispatchReady?.(); } }; try { while (true) { if (deadline.signal.aborted) throw signalError(deadline.signal, "node worker launch aborted"); if (!stableRequest.isDispatchAuthorized()) throw new Error("node worker launch authority closed"); try { const receipt = await invoke({ deviceId: stableRequest.deviceId, command: pollStatus ? NODE_WORKER_SUPERVISOR_STATUS_COMMAND : NODE_WORKER_SUPERVISOR_LAUNCH_COMMAND, payload: pollStatus ? { launchId: input.launchId } : input, isAuthorized: () => { if (!dispatchReady) availabilityDeadline.remainingMs(); return stableRequest.isDispatchAuthorized(); }, deadline: dispatchDeadline, ...!pollStatus ? { onDispatchReady: markDispatchReady } : {} }); if (!receipt) pollStatus = false; else { if (!pollStatus) markDispatchReady(); const validated = validateReceipt(receipt, expected); mayHaveLaunched = true; if (isTerminalReceipt(validated)) { const admissionFailure = validated.state === "completed" ? parseWorkerAdmissionDeadlineResult(JSON.parse(validated.resultJson)) : void 0; const rearmDelayMs = computeBackoff(ADMISSION_REARM_BACKOFF, admissionAttempts); const rearmAdmitsWithinCredential = stableRequest.credentialExpiresAtMs === void 0 || now() + rearmDelayMs + 12e4 <= stableRequest.credentialExpiresAtMs; if (admissionFailure && admissionAttempts < MAX_ADMISSION_ATTEMPTS && rearmAdmitsWithinCredential) { mayHaveLaunched = false; await waitBeforeRetry({ delayMs: rearmDelayMs, deadline }); input = rearmNodeWorkerLaunchInput(originalInput, admissionAttempts++); expected = expectedIdentity(input); pollStatus = false; delayMs = pollIntervalMs; continue; } return validated; } pollStatus = true; delayMs = pollIntervalMs; } } catch (error) { if (deadline.signal.aborted || !dispatchReady && availabilityDeadline.signal.aborted || !stableRequest.isDispatchAuthorized()) throw error; if (!(error instanceof NodeWorkerLaunchTransportError) || !RETRYABLE_TRANSPORT_CODES.has(error.code)) throw error; pollStatus = false; } delayMs = await waitBeforeRetry({ delayMs, deadline: dispatchReady ? deadline : availabilityDeadline }); } } catch (error) { if (!dispatchReady && availabilityDeadline.signal.aborted && !deadline.signal.aborted) throw new WorkerRunnerUnavailableError(); if (error instanceof WorkerRunnerCapacityError) throw error; if (!mayHaveLaunched) throw error; let terminal; try { terminal = await cancelUntilTerminal({ request: stableRequest, expected }); } catch (cancelError) { throw Object.assign(new Error("node worker launch failed and cancellation could not be confirmed", { cause: error instanceof Error ? error : /* @__PURE__ */ new Error("node worker launch failed") }), { cancellationError: cancelError }); } if (deadline.signal.aborted || !stableRequest.isDispatchAuthorized()) return terminal; throw error; } finally { availabilityDeadline.dispose(); deadline.dispose(); } }; return { launch }; } //#endregion export { measureNodeWorkerLaunchBytes as n, raceNodeWorkerOperation as r, createNodeWorkerLaunchAdapter as t };