UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

388 lines (387 loc) • 16.8 kB
import { n as MAX_TIMER_TIMEOUT_MS } from "./number-coercion-CkhMiRw8.mjs"; import { d as sleepWithAbort, n as computeBackoff } from "./src-D1BHGNTj.mjs"; import { l as getGatewayRestartDrainSignal } from "./gateway-work-admission-DAV5kkgV.mjs"; import "./backoff-BrzXG7iD.mjs"; import { u as isAgentRunRestartAbortReason } from "./run-termination-5c-0T0hN.mjs"; import { D as NODE_WORKER_SUPERVISOR_STATUS_COMMAND, E as NODE_WORKER_SUPERVISOR_LAUNCH_COMMAND, T as NODE_WORKER_SUPERVISOR_CANCEL_COMMAND } from "./node-commands-Bxo-2Y4j.mjs"; import "./worker-admission-BFR8zOaU.mjs"; import { t as boundedWorkerError } from "./worker-error-Db8qkNbb.mjs"; import { a as createNodeRunnerInventoryIssueError, c as resolveNodeWorkerExecutionIssue, n as NODE_RUNNER_UPDATE_REQUIRED_ISSUE } from "./node-runner-inventory-7Le5X7EN.mjs"; import { c as parseNodeWorkerLaunchInput, i as nodeWorkerTurnMatchesIdentity, n as NODE_WORKER_STATUS_WAIT_MAX_MS, r as nodeWorkerPlanHash, u as parseNodeWorkerSupervisorReceipt } from "./node-supervisor-protocol-DypTICcX.mjs"; import { n as measureWorkerProcessTurnBytes } from "./worker-process-protocol-DIQTOqtb.mjs"; import { u as parseWorkerAdmissionDeadlineResult } from "./worker-connection-contract-BauuFVnE.mjs"; import { n as serializeNodeEvent, t as buildNodeInvokeRequest } from "./node-invoke-request-NMUgqchA.mjs"; import { t as raceNodeWorkerOperation } from "./node-worker-abort-CMtJOat1.mjs"; import { n as WorkerRunnerUnavailableError, t as WorkerRunnerCapacityError } from "./tunnel-contract-DJT0nCJA.mjs"; import { createHash, randomUUID } from "node:crypto"; //#region src/gateway/worker-environments/node-launch-adapter.ts function nodeWorkerSpawnResultFromReceipt(receipt) { return { stdout: receipt.state === "completed" ? receipt.resultJson : "", stderr: receipt.state === "completed" ? "" : receipt.errorText, code: receipt.state === "completed" ? 0 : 1, signal: null, killed: receipt.state === "cancelled" || receipt.state === "interrupted", termination: "exit" }; } 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_NODE_WORKER_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 !== "pending" && receipt.state !== "running"; } 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) { const measured = input.descriptor.admission.handshake.protocolFeatures.includes("node-worker-idle-retention-v1") ? { ...input, idleRetention: true } : input; return Math.max(...[measured, rearmNodeWorkerLaunchInput(measured, 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, attempt.idleRetention)])); } function expectedIdentity(input) { 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 validateReceipt(receipt, expected) { if (!nodeWorkerTurnMatchesIdentity(receipt, expected)) throw new Error("node worker supervisor receipt identity mismatch"); return receipt; } 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 node; try { node = await raceNodeWorkerOperation(params.transport.getCurrentNode(params.deviceId), params.signal); } catch (error) { if (params.signal.aborted) throw error; throw new NodeWorkerLaunchTransportError("UNAVAILABLE", "device worker node discovery is unavailable"); } 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 || resolveNodeWorkerExecutionIssue(node.workerHost))) throw createNodeRunnerInventoryIssueError(node.nodeId, NODE_RUNNER_UPDATE_REQUIRED_ISSUE); params.prepareLaunch?.(node); if (!params.prepareLaunch && params.idleRetention && node.workerHost.idleRetention !== true) throw new NodeWorkerLaunchTransportError("PRIVATE_DIALECT_UNAVAILABLE", "node worker idle retention is unavailable"); const statusWait = node.workerHost.statusWait === 1; const operation = transport.invoke({ node, command: params.command, params: params.command === "worker.status.v1" && statusWait ? { ...params.payload, waitMs: Math.min(NODE_WORKER_STATUS_WAIT_MAX_MS, Math.max(1, Math.floor(rpcBudgetMs * .75))) } : params.payload, timeoutMs: rpcBudgetMs, signal, idempotencyKey: params.command === "worker.launch.v1" ? params.payload.launchId : void 0, isDispatchAuthorized: params.isAuthorized, ...params.onDispatchReady ? { onDispatchReady: params.onDispatchReady } : {} }); const result = await raceNodeWorkerOperation(operation, signal); if (!result.ok) { const code = result.error?.code ?? "UNAVAILABLE"; if (code === "WORKER_CAPACITY_EXHAUSTED") throw new WorkerRunnerCapacityError(); const detail = result.error?.message?.trim(); throw new NodeWorkerLaunchTransportError(code, boundedWorkerError(`node worker supervisor ${params.command} failed (${code})${detail ? `: ${detail}` : ""}`)); } return { receipt: parseInvokeReceipt(result.payloadJSON), statusWait }; } 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 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_NODE_WORKER_TRANSPORT_CODES.has(error.code)) throw error; } delayMs = await waitBeforeRetry({ delayMs, deadline }); } } finally { deadline.dispose(); } throw new Error("node worker cancellation did not produce a terminal receipt before its deadline"); }; const launch = async (request) => { const restartSignal = getGatewayRestartDrainSignal(); 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"); const launchMayAlreadyExist = mayHaveLaunched; try { const { receipt, statusWait } = await invoke({ deviceId: stableRequest.deviceId, command: pollStatus ? NODE_WORKER_SUPERVISOR_STATUS_COMMAND : NODE_WORKER_SUPERVISOR_LAUNCH_COMMAND, payload: pollStatus ? { launchId: input.launchId } : input, ...!pollStatus && input.idleRetention ? { idleRetention: true } : {}, ...!pollStatus && !mayHaveLaunched ? { prepareLaunch: (node) => { if (input.descriptor.assignment.inference === "runtime-local" && node.workerHost.nativeInference !== 1) throw createNodeRunnerInventoryIssueError(node.nodeId, NODE_RUNNER_UPDATE_REQUIRED_ISSUE); if (node.workerHost.idleRetention === true && input.descriptor.admission.handshake.protocolFeatures.includes("node-worker-idle-retention-v1")) input.idleRetention = true; else delete input.idleRetention; expected = expectedIdentity(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; if (statusWait) continue; } } catch (error) { if (!pollStatus && !launchMayAlreadyExist && error instanceof NodeWorkerLaunchTransportError && error.code === "INVALID_REQUEST") { mayHaveLaunched = false; throw error; } if (deadline.signal.aborted || !dispatchReady && availabilityDeadline.signal.aborted || !stableRequest.isDispatchAuthorized()) throw error; if (!(error instanceof NodeWorkerLaunchTransportError) || !RETRYABLE_NODE_WORKER_TRANSPORT_CODES.has(error.code)) throw error; pollStatus = false; } delayMs = await waitBeforeRetry({ delayMs, deadline: dispatchReady ? deadline : availabilityDeadline }); } } catch (error) { if (restartSignal.aborted && isAgentRunRestartAbortReason(deadline.signal.reason)) throw deadline.signal.reason; 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 new AggregateError([error, cancelError], "node worker launch failed and cancellation could not be confirmed", { cause: cancelError }); } if (deadline.signal.aborted || !stableRequest.isDispatchAuthorized()) return terminal; throw error; } finally { availabilityDeadline.dispose(); deadline.dispose(); } }; return { launch }; } //#endregion export { nodeWorkerSpawnResultFromReceipt as i, createNodeWorkerLaunchAdapter as n, measureNodeWorkerLaunchBytes as r, RETRYABLE_NODE_WORKER_TRANSPORT_CODES as t };