UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

532 lines (531 loc) 21.3 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { a as normalizeCloudRepo } from "./zod-schema.cloud-workers-By8wz7-f.js"; import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Ai as validateSessionsReclaimParams } from "./src-BiL5aQto.js"; import { n as validateSessionsMoveParams, t as validateSessionsDispatchParams } from "./session-placement-validators-TzOfpMSr.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js"; import { t as resolveWorkerPlacementCapabilities } from "./placement-capabilities-1KvP5S1_.js"; import { r as resolveWorkerPlacementSessionRuntime } from "./placement-session-runtime-Cts2P0Rr.js"; import { t as isFailedWorkerPlacementEnvironmentGone } from "./session-placement-lifecycle-7DQHpaT9.js"; import { o as isForceAbandonedWorkerPlacement } from "./placement-record-BOfTzlFO.js"; import { H as SessionMutationAuthorizationChangedError } from "./session-sharing-B7MI8hNo.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import "./device-provider-identity-v6nXqNq_.js"; import { i as readWorkerPlacementIdentity, r as projectWorkerSessionPlacement } from "./placement-projector-C4zEtZS1.js"; import { n as emitSessionsChanged } from "./session-change-event-DzmH4zlz.js"; import { l as managedWorktrees } from "./service-B1SvAvxa.js"; import { i as loadAccessorSessionEntryForGatewayTarget, r as isWorkerDispatchInputError, s as requireSessionKey } from "./sessions-shared-D_lV07q2.js"; import { i as deviceUnavailableText } from "./device-provider-BjFBZG2n.js"; import { n as listGatewayEnvironments } from "./environments-djcZnGtm.js"; import { n as resolveDevicePlacementEligibility, t as resolveWorkerPlacementDestination } from "./placement-destination-D7sGR7yg.js"; //#region src/gateway/worker-environments/device-placement-selector.ts async function selectDevicePlacementCandidates(params) { const { requirement } = params; if (!requirement) return { ok: false, error: `runtime ${params.runtimeId} does not support paired-device placement; select a compatible runtime or cloud worker provider` }; const nodes = params.environments.filter((environment) => environment.type === "node").toSorted((left, right) => left.id.localeCompare(right.id)); const outdated = nodes.find((node) => node.issues?.some((issue) => issue.code === "update-required")); const outdatedError = outdated && deviceUnavailableText(outdated.id.slice(5), { available: false, issue: outdated.issues?.[0] }); const hosts = nodes.filter((node) => node.sessionHost === true); if (hosts.length === 0) return { ok: false, error: outdatedError ?? "no paired session-host nodes are available; pair a node, enable session hosting, then retry" }; const connected = hosts.filter((node) => node.status === "available"); if (connected.length === 0) { const deviceId = hosts[0].id.slice(5); return { ok: false, error: `all paired session-host nodes are disconnected; ${deviceUnavailableText(deviceId, { available: false, unavailableReason: "disconnected" })}` }; } const attempts = await Promise.all(connected.filter((node) => !node.issues?.some((issue) => issue.code === "update-required")).map(async (node) => { const deviceId = node.id.slice(5); const eligibility = await resolveDevicePlacementEligibility({ environmentService: params.environmentService, deviceId, runtimeId: params.runtimeId, requirement, config: params.config, currentNode: params.nodeRegistry.get(deviceId) }); return { deviceId, availableSlots: eligibility.ok ? eligibility.availableSlots : node.workerSlots?.available ?? 0, eligibility }; })); const candidates = attempts.filter((attempt) => attempt.eligibility.ok).map(({ deviceId, availableSlots }) => ({ deviceId, availableSlots })).toSorted((left, right) => (requirement.consumesWorkerSlot ? right.availableSlots - left.availableSlots : 0) || left.deviceId.localeCompare(right.deviceId)); if (candidates.length > 0) return { ok: true, candidates }; if (attempts.length === 0 && outdatedError) return { ok: false, error: outdatedError }; if (requirement.consumesWorkerSlot && attempts.every(({ availableSlots }) => availableSlots === 0)) return { ok: false, error: `all paired session-host nodes are at capacity; ${deviceUnavailableText(attempts[0].deviceId, { available: false, unavailableReason: "at-capacity" })}` }; const failed = attempts.find(({ eligibility }) => !eligibility.ok); return { ok: false, error: failed && !failed.eligibility.ok ? failed.eligibility.error : "no paired session-host node supports this runtime; check node commands and reconnect an eligible host" }; } //#endregion //#region src/gateway/server-methods/sessions-dispatch.ts function respondInvalidWorkerSession(respond, message) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, message)); } const PROJECT_ORIGIN_TIMEOUT_MS = 4e3; const MAX_AUTO_DEVICE_PLACEMENT_ATTEMPTS = 3; var CloudWorkerProjectProfileError = class extends Error { constructor(..._args) { super(..._args); this.code = "invalid_profile"; } }; function resolveWorkerSessionTarget(params) { const cfg = params.context.getRuntimeConfig(); const requestedAgent = resolveRequestedSessionAgentId(cfg, params.key, params.agentId); if (!requestedAgent.ok) { params.respond(false, void 0, requestedAgent.error); return; } const destination = resolveWorkerPlacementDestination({ cfg, profileId: params.profileId, deviceId: params.deviceId, machineClass: params.machineClass }); if (!destination.ok) { respondInvalidWorkerSession(params.respond, destination.error); return; } const target = loadAccessorSessionEntryForGatewayTarget({ key: params.key, cfg, agentId: requestedAgent.agentId }); const entry = target.entry; const sessionId = normalizeOptionalString(entry?.sessionId); if (!entry || !sessionId) { respondInvalidWorkerSession(params.respond, `session not found: ${params.key}`); return; } return { cfg, target, entry, sessionId, dispatchTarget: destination.value }; } function resolveManagedSessionWorktree(params) { const worktree = managedWorktrees.findLiveByOwner("session", params.sessionKey); if (params.entry.worktree?.id && worktree && worktree.id === params.entry.worktree.id && worktree.ownerId === params.sessionKey) return worktree; const article = params.method === "sessions.dispatch" ? "a" : "the"; respondInvalidWorkerSession(params.respond, `${params.method} requires ${article} session-owned managed worktree`); } async function resolveProjectProfileDestination(params) { let originUrl; try { const result = await runCommandWithTimeout([ "git", "-C", params.worktree.path, "config", "--get", "remote.origin.url" ], { timeoutMs: PROJECT_ORIGIN_TIMEOUT_MS }); if (result.code !== 0) return; originUrl = result.stdout.trim(); } catch { return; } const projectKey = normalizeCloudRepo(originUrl); if (!projectKey) return; const profileId = params.cfg.cloudWorkers?.projectProfiles?.[projectKey]; if (!profileId) return; if (!Object.hasOwn(params.cfg.cloudWorkers?.profiles ?? {}, profileId)) throw new CloudWorkerProjectProfileError(`cloudWorkers.projectProfiles mapping ${projectKey} references unconfigured profile ${profileId}`); return { profileId }; } async function validateDispatchExecutionMode(params) { if (params.target.deviceId !== void 0) { const eligibility = await resolveDevicePlacementEligibility({ environmentService: params.context.workerEnvironmentService, deviceId: params.target.deviceId, runtimeId: params.sessionRuntime, requirement: params.devicePlacement, config: params.context.getRuntimeConfig(), currentNode: params.context.nodeRegistry?.get?.(params.target.deviceId) }); if (eligibility.ok) return true; respondInvalidWorkerSession(params.respond, eligibility.error); return false; } if (params.context.workerEnvironmentService?.supportsExecutionMode(params.target.profileId, params.executionMode)) return true; respondInvalidWorkerSession(params.respond, `runtime ${params.sessionRuntime} requires a cloud worker provider that supports ${params.executionMode}; choose a compatible provider, or select an agent/model route with agentRuntime.id "openclaw"`); return false; } function respondWorkerPlacement(params) { params.respond(true, { ok: true, key: params.key, sessionId: params.sessionId, placement: projectWorkerSessionPlacement(params.placement, params.context.workerPlacementDiskSpaceReader?.read(params.placement), params.context.workerPlacementRunnerAvailabilityReader?.read(params.placement), readWorkerPlacementIdentity(params.placement, params.context.workerEnvironmentService)) }, void 0); } function respondWorkerMove(params) { params.respond(true, { ok: true, key: params.key, sessionId: params.sessionId, placement: { state: params.placement.state, generation: params.placement.generation } }, void 0); } function respondWorkerDispatchError(error, respond) { if (error instanceof SessionMutationAuthorizationChangedError) throw error; respond(false, void 0, errorShape(isWorkerDispatchInputError(error) ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error))); } const sessionDispatchHandlers = { "sessions.dispatch": async ({ params, respond, context, sessionMutationAuthorization }) => { if (params.autoDevice === true && (params.profileId !== void 0 || params.deviceId !== void 0)) { respondInvalidWorkerSession(respond, "choose exactly one dispatch target: autoDevice, deviceId, or profileId"); return; } if (!assertValidParams(params, validateSessionsDispatchParams, "sessions.dispatch", respond)) return; const key = requireSessionKey(params.key, respond); if (!key) return; const dispatchService = context.workerPlacementDispatchService; const placementReader = context.workerSessionPlacementService; if (!dispatchService || !placementReader) { respondInvalidWorkerSession(respond, "cloud worker dispatch is not configured"); return; } const resolved = resolveWorkerSessionTarget({ key, agentId: params.agentId, profileId: params.profileId, deviceId: params.deviceId, machineClass: params.machineClass, context, respond }); if (!resolved) return; const { cfg, target, entry, sessionId } = resolved; let { dispatchTarget } = resolved; const autoDevice = params.autoDevice === true; const canUseProjectProfile = !autoDevice && params.profileId === void 0 && params.deviceId === void 0; if (!dispatchTarget && !canUseProjectProfile && !autoDevice) { respondInvalidWorkerSession(respond, "worker dispatch target is missing"); return; } if (entry.archivedAt !== void 0) { respondInvalidWorkerSession(respond, "cannot dispatch an archived session"); return; } const sessionRuntime = resolveWorkerPlacementSessionRuntime({ cfg, entry, agentId: target.target.agentId, sessionKey: target.canonicalKey }); const { executionMode, devicePlacement } = resolveWorkerPlacementCapabilities(sessionRuntime); if (!executionMode) { respondInvalidWorkerSession(respond, `runtime ${sessionRuntime} lacks cloud placement support`); return; } let automaticDeviceIds = []; if (autoDevice) { const selection = await selectDevicePlacementCandidates({ environments: await listGatewayEnvironments(context), nodeRegistry: context.nodeRegistry, environmentService: context.workerEnvironmentService, requirement: devicePlacement, runtimeId: sessionRuntime, config: cfg }); if (!selection.ok) { respondInvalidWorkerSession(respond, selection.error); return; } automaticDeviceIds = selection.candidates.slice(0, MAX_AUTO_DEVICE_PLACEMENT_ATTEMPTS).map(({ deviceId }) => deviceId); const destination = resolveWorkerPlacementDestination({ cfg, deviceId: automaticDeviceIds[0] }); if (!destination.ok || !destination.value) { respondInvalidWorkerSession(respond, destination.ok ? "automatic device placement did not select a node" : destination.error); return; } dispatchTarget = destination.value; } if (!autoDevice && dispatchTarget && !await validateDispatchExecutionMode({ context, executionMode, sessionRuntime, devicePlacement, target: dispatchTarget, respond })) return; const existingPlacement = placementReader.getMany([sessionId]).get(sessionId); if (existingPlacement?.state === "failed" && !isFailedWorkerPlacementEnvironmentGone({ environmentService: context.workerEnvironmentService, placement: existingPlacement })) { let providerId; try { providerId = readWorkerPlacementIdentity(existingPlacement, context.workerEnvironmentService)?.providerId; } catch {} respondInvalidWorkerSession(respond, providerId === "device" ? "device worker placement must be abandoned before redispatch; use Continue on Gateway" : "cloud worker environment must be stopped before redispatch; use Stop cloud worker"); return; } if (existingPlacement && (existingPlacement.state === "active" || existingPlacement.state === "draining" || existingPlacement.state === "reconciling")) { respondInvalidWorkerSession(respond, `session cannot dispatch from placement ${existingPlacement.state}`); return; } const worktree = resolveManagedSessionWorktree({ entry, sessionKey: target.canonicalKey, method: "sessions.dispatch", respond }); if (!worktree) return; if (!dispatchTarget && canUseProjectProfile) try { dispatchTarget = await resolveProjectProfileDestination({ cfg, worktree }); } catch (error) { respondWorkerDispatchError(error, respond); return; } if (!dispatchTarget) { respondInvalidWorkerSession(respond, "worker dispatch target is missing"); return; } if (canUseProjectProfile && !await validateDispatchExecutionMode({ context, executionMode, sessionRuntime, devicePlacement, target: dispatchTarget, respond })) return; let lastEligibilityError; const candidates = autoDevice ? automaticDeviceIds : [dispatchTarget.deviceId]; for (let attempt = 0; attempt < candidates.length; attempt += 1) { if (attempt > 0) { const destination = resolveWorkerPlacementDestination({ cfg, deviceId: candidates[attempt] }); if (!destination.ok || !destination.value) { respondInvalidWorkerSession(respond, destination.ok ? "automatic device placement did not select a node" : destination.error); return; } dispatchTarget = destination.value; } if (autoDevice) { const eligibility = await resolveDevicePlacementEligibility({ environmentService: context.workerEnvironmentService, deviceId: candidates[attempt], runtimeId: sessionRuntime, requirement: devicePlacement, config: cfg, currentNode: context.nodeRegistry.get(candidates[attempt]) }); if (!eligibility.ok) { lastEligibilityError = eligibility.error; continue; } } try { const placement = await dispatchService.dispatch({ sessionId, sessionKey: target.canonicalKey, agentId: target.target.agentId, executionMode, ...dispatchTarget, ...devicePlacement ? { devicePlacement } : {} }, () => emitSessionsChanged(context, { reason: "dispatch", sessionKey: target.canonicalKey }), sessionMutationAuthorization?.assertCurrent); respondWorkerPlacement({ respond, key: target.canonicalKey, sessionId, context, placement }); return; } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError) throw error; if (!autoDevice || !dispatchTarget.deviceId) { respondWorkerDispatchError(error, respond); return; } const eligibility = await resolveDevicePlacementEligibility({ environmentService: context.workerEnvironmentService, deviceId: dispatchTarget.deviceId, runtimeId: sessionRuntime, requirement: devicePlacement, config: cfg, currentNode: context.nodeRegistry.get(dispatchTarget.deviceId) }); const failedPlacement = placementReader.getMany([sessionId]).get(sessionId); if (eligibility.ok || formatErrorMessage(error) !== eligibility.error || failedPlacement && (failedPlacement.state !== "failed" || failedPlacement.environmentId !== null)) { respondWorkerDispatchError(error, respond); return; } lastEligibilityError = eligibility.error; } } respondWorkerDispatchError(/* @__PURE__ */ new Error(`automatic device placement failed after ${candidates.length} attempts; ${lastEligibilityError ?? "no eligible host remains; reconnect a paired session-host node and retry"}`), respond); }, "sessions.move": async ({ params, respond, context, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsMoveParams, "sessions.move", respond)) return; const key = requireSessionKey(params.key, respond); if (!key) return; const placementService = context.workerPlacementDispatchService; const placementReader = context.workerSessionPlacementService; if (!placementService?.move || !placementReader) { respondInvalidWorkerSession(respond, "session placement move is not configured"); return; } const resolved = resolveWorkerSessionTarget({ key, agentId: params.agentId, context, respond }); if (!resolved) return; const { target, entry, sessionId } = resolved; if (entry.archivedAt !== void 0) { respondInvalidWorkerSession(respond, "cannot move an archived session"); return; } const existingPlacement = placementReader.getMany([sessionId]).get(sessionId); const retryAbandonment = "abandonSource" in params && isForceAbandonedWorkerPlacement(existingPlacement); if (existingPlacement?.state !== "active" && existingPlacement?.state !== "draining" && !retryAbandonment) { respondInvalidWorkerSession(respond, `session cannot move from placement ${existingPlacement?.state ?? "local"}`); return; } if (!resolveManagedSessionWorktree({ entry, sessionKey: target.canonicalKey, method: "sessions.move", respond })) return; try { const placement = await placementService.move({ sessionId, sessionKey: target.canonicalKey, agentId: target.target.agentId, source: params.expected, target: params.target, ..."abandonSource" in params ? { abandonSource: true } : {} }, () => emitSessionsChanged(context, { reason: "move", sessionKey: target.canonicalKey }), sessionMutationAuthorization?.assertCurrent); respondWorkerMove({ respond, key: target.canonicalKey, sessionId, placement }); } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError) throw error; try { emitSessionsChanged(context, { reason: "move", sessionKey: target.canonicalKey }); } catch {} respondWorkerDispatchError(error, respond); } }, "sessions.reclaim": async ({ params, respond, context, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsReclaimParams, "sessions.reclaim", respond)) return; const key = requireSessionKey(params.key, respond); if (!key) return; const placementService = context.workerPlacementDispatchService; const placementReader = context.workerSessionPlacementService; if (!placementService?.reclaim || !placementReader) { respondInvalidWorkerSession(respond, "cloud worker stop is not configured"); return; } const resolved = resolveWorkerSessionTarget({ key, agentId: params.agentId, context, respond }); if (!resolved) return; const { target, entry, sessionId } = resolved; const existingPlacement = placementReader.getMany([sessionId]).get(sessionId); const reportPlacementChange = (placement) => { if (!placement || existingPlacement && placement.state === existingPlacement.state && placement.generation === existingPlacement.generation && placement.updatedAtMs === existingPlacement.updatedAtMs) return; try { emitSessionsChanged(context, { reason: "reclaim", sessionKey: target.canonicalKey }); } catch {} }; if (existingPlacement?.state !== "failed" && !resolveManagedSessionWorktree({ entry, sessionKey: target.canonicalKey, method: "sessions.reclaim", respond })) return; let placement; try { placement = await placementService.reclaim({ sessionId, sessionKey: target.canonicalKey, agentId: target.target.agentId }, sessionMutationAuthorization?.assertCurrent); } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError) throw error; reportPlacementChange(placementReader.getMany([sessionId]).get(sessionId)); respondWorkerDispatchError(error, respond); return; } reportPlacementChange(placement); respondWorkerPlacement({ respond, key: target.canonicalKey, sessionId, context, placement }); } }; //#endregion export { sessionDispatchHandlers as t };