UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

812 lines (811 loc) 38.2 kB
import { C as parseStrictNonNegativeInteger, F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js"; import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { a as writeRuntimeJson } from "./runtime-CF2WjnNZ.js"; import { r as createLazyPromiseLoader } from "./lazy-promise-DGqyc4Y4.js"; import { c as readFileDescriptorBounded } from "./boundary-file-read-uaJcf6X6.js"; import { t as createAbortError } from "./abort-signal-D2k14JsD.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { t as formatCliCommand } from "./command-format-C7YfyMTd.js"; import { D as tryResolveLegacyCompatibilityAgentId, O as tryResolveSoleAgentId, o as listAgentIds, t as AgentSelectionRequiredError } from "./agent-scope-config-DcbEhP0R.js"; import { _ as scopeLegacySessionKeyToAgent, a as buildAgentMainSessionKey, c as classifySessionKeyShape, f as resolveAgentIdFromSessionKey, l as isUnscopedSessionKeySentinel } from "./session-key-BnWWjqNc.js"; import { i as tryGetLegacyDefaultAgentId, t as inheritLegacyDefaultAgentId } from "./legacy.default-agent-owner-BGwEdQRe.js"; import { b as waitForDiagnosticEventsDrained } from "./diagnostic-events-Cwe92uV3.js"; import { a as routeLogsToStderr } from "./console-BSaonU6s.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { t as migratePersistedImplicitMainRoster } from "./legacy.roster-CQ9vDbV2.js"; import { n as resolvePersistedSessionStoreOwnerForKey } from "./session-store-owner-CR2Ag3xK.js"; import { a as READ_SCOPE, t as ADMIN_SCOPE } from "./operator-scopes-Dw7Gu2cA.js"; import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-B1bPgeKr.js"; import { n as isGatewaySecretRefUnavailableError } from "./credentials-CXbS4PS0.js"; import { n as readGatewayDispatchConfigWithShellEnvFallback, t as readGatewayDispatchConfig } from "./gateway-dispatch-config-CWzUgXmC.js"; import { t as GatewayProtocolRequestError } from "./protocol-request-BMUN1re6.js"; import { g as randomIdempotencyKey, m as isGatewayExplicitAuthRequiredError, o as callGateway, p as isGatewayCredentialsRequiredError } from "./call-BWR5pPgw.js"; import { r as isGatewayTransportError } from "./transport-error-D_LRKgla.js"; import { n as normalizeMessageChannel } from "./message-channel-core-AaEWic-A.js"; import "./message-channel-normalize-B81s0XMO.js"; import { s as recordCliGatewayRunFailure } from "./failure-output-Dc5ce9_F.js"; import { n as buildAgentRunTerminalOutcome, o as classifyAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-CigeY75d.js"; import { n as isExecutionIdentityCollectionEnabled } from "./audit-config-BKFiXlHH.js"; import { n as readAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-BwslKylZ.js"; import { r as withProgress } from "./progress-DSeqxbrQ.js"; import { t as measureAgentStartup } from "./startup-timing-BSpqN9FO.js"; import { n as createEmbeddedStateSignalBridge } from "./embedded-state-lock-DAKfCLca.js"; import { TextDecoder } from "node:util"; import fs from "node:fs/promises"; //#region src/plugins/one-shot-diagnostics.ts const log = createSubsystemLogger("plugins"); const ONE_SHOT_DIAGNOSTICS_SERVICE_IDS = /* @__PURE__ */ new Set(["diagnostics-otel"]); const ONE_SHOT_DIAGNOSTICS_DRAIN_TIMEOUT_MS = 5e3; const ONE_SHOT_DIAGNOSTICS_FLUSH_TIMEOUT_MS = 1e4; function suppressOtelStdoutLogSink(config) { const diagnostics = config.diagnostics; const otel = diagnostics?.otel; if (otel?.logs !== true || otel.logsExporter !== "stdout" && otel.logsExporter !== "both") return config; return { ...config, diagnostics: { ...diagnostics, otel: { ...otel, logs: otel.logsExporter === "both", logsExporter: "otlp" } } }; } function isOtelExportConfigured(config) { const diagnostics = config.diagnostics; return Boolean(diagnostics && diagnostics.enabled !== false && diagnostics.otel?.enabled); } /** * Bounds how long the CLI waits for one exit step. Timing out stops the wait but * cannot cancel the work: a collector that accepts a connection and never answers * keeps its socket open until the exporter's own request timeout * (`OTEL_EXPORTER_OTLP_TIMEOUT`). Accepted — that bound is the exporter's to own, * and cancelling an in-flight export would mean reaching into the OTel SDK. */ async function runBoundedExitStep(label, timeoutMs, run) { let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms`)), timeoutMs); timer.unref?.(); }); try { await Promise.race([run(), timeout]); } catch (err) { log.warn(`one-shot diagnostics ${label} failed: ${String(err)}`); } finally { clearTimeout(timer); } } /** * Start the diagnostics OTel exporter for a one-shot embedded agent run. * * Gateway processes start diagnostics exporters via startPluginServices at * startup; one-shot `openclaw agent --local` runs execute the agent in the CLI * process where no plugin service ever starts, so diagnostic events had no OTel * subscriber and spans were dropped. * Returns null when OTel export is not configured or the plugin is not * enabled/installed; the returned handle's stop() drains the diagnostic event * queue and shuts the SDK down (force-flush) before the process exits. */ async function startOneShotDiagnosticsExporters(params) { const config = params.suppressStdoutDiagnosticLogs === true ? suppressOtelStdoutLogSink(params.config) : params.config; if (!isOtelExportConfigured(config)) return null; const [{ loadOpenClawPlugins }, { startPluginServices }] = await Promise.all([import("./plugins/loader.js"), import("./services-wg5DeUW5.js")]); const registry = loadOpenClawPlugins({ config, onlyPluginIds: [...ONE_SHOT_DIAGNOSTICS_SERVICE_IDS], activate: false, preferBuiltPluginArtifacts: true }); const services = registry.services.filter((entry) => ONE_SHOT_DIAGNOSTICS_SERVICE_IDS.has(entry.service.id)); if (services.length === 0) { log.warn("diagnostics.otel is enabled but the diagnostics-otel plugin is not installed or not enabled; this run exports no telemetry."); return null; } const handle = await startPluginServices({ registry: { ...registry, services }, config }); return { stop: async () => { await runBoundedExitStep("event drain", ONE_SHOT_DIAGNOSTICS_DRAIN_TIMEOUT_MS, waitForDiagnosticEventsDrained); await runBoundedExitStep("exporter flush", ONE_SHOT_DIAGNOSTICS_FLUSH_TIMEOUT_MS, () => handle.stop()); } }; } //#endregion //#region src/commands/agent-via-gateway.ts const NO_GATEWAY_TIMEOUT_MS = 2147e6; const GATEWAY_TRANSIENT_CONNECT_RETRY_DELAYS_MS = [ 1e3, 2e3, 5e3, 1e4, 15e3 ]; function usesImplicitRemoteCompatibilityDefault(roster) { return !roster.selectionRequired && (roster.ownership === "legacy" || !roster.ownership && roster.agentIds.length > 1); } function resolveImplicitCliAgentId(cfg, remote) { const migratedConfig = remote ? cfg : migratePersistedImplicitMainRoster(cfg).config; const selectionCfg = remote ? cfg : inheritLegacyDefaultAgentId(tryGetLegacyDefaultAgentId(cfg) ? cfg : migratedConfig, migratedConfig); const selected = remote ? remote.selectionRequired ? void 0 : remote.defaultId : tryResolveLegacyCompatibilityAgentId(selectionCfg); if (selected) return selected; const agentIds = remote?.agentIds ?? listAgentIds(selectionCfg); throw new AgentSelectionRequiredError(agentIds, { surface: "agent turn", hint: `Pass --agent <id> to select one of: ${agentIds.join(", ")}.` }); } const GATEWAY_ABORT_RETRY_DELAYS_MS = [ 50, 150, 300, 600 ]; const GATEWAY_ABORT_REQUEST_TIMEOUT_MS = 2e3; const AGENT_CLI_SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143 }; const MESSAGE_FILE_DECODER = new TextDecoder("utf-8", { fatal: true }); const defaultAgentSessionModuleLoader = () => import("./session.runtime.js"); let agentSessionModuleLoader = defaultAgentSessionModuleLoader; const embeddedAgentCommandLoader = createLazyPromiseLoader(() => import("./agent-BD0xoiB7.js").then((module) => module.agentCommand), { cacheRejections: true }); const localAuditModuleLoader = createLazyPromiseLoader(() => import("./agent-local-audit-CdkbqJHA.js"), { cacheRejections: true }); const agentSessionModuleCache = createLazyPromiseLoader(() => agentSessionModuleLoader(), { cacheRejections: true }); const runtimeConfigModuleLoader = createLazyPromiseLoader(() => import("./io-ssMwu02B.js"), { cacheRejections: true }); const embeddedStateLockModuleLoader = createLazyPromiseLoader(() => import("./embedded-state-lock-6tAlep1Z.js"), { cacheRejections: true }); const replyPayloadModuleLoader = createLazyPromiseLoader(() => import("./plugin-sdk/reply-payload.js"), { cacheRejections: true }); let gatewayAbortRetryDelaysMsForTests; function resolveGatewayAbortRetryDelaysMs() { return gatewayAbortRetryDelaysMsForTests ?? GATEWAY_ABORT_RETRY_DELAYS_MS; } const loadAgentSessionModule = agentSessionModuleCache.load; async function startEmbeddedRunDiagnosticsExporters(runtime, options, config) { try { return await startOneShotDiagnosticsExporters({ config, suppressStdoutDiagnosticLogs: options.suppressStdoutDiagnosticLogs }); } catch (err) { runtime.error?.(`diagnostics exporter startup failed for embedded run: ${String(err)}`); return null; } } /** * Run the embedded agent command with OTel diagnostics export for this * one-shot process: the Gateway only starts diagnostics exporters in its own * process, so embedded runs start one here and flush it before the CLI exits * (including signal exits, which happen after this returns). */ async function runEmbeddedAgentCommand(opts, runtime, deps, diagnosticsOptions) { const agentCommand = await measureAgentStartup("command-import", () => embeddedAgentCommandLoader.load()); const config = await loadRuntimeConfig(); const diagnostics = await startEmbeddedRunDiagnosticsExporters(runtime, diagnosticsOptions, config); let stopLocalAuditWriter; if (isExecutionIdentityCollectionEnabled(config)) try { stopLocalAuditWriter = (await localAuditModuleLoader.load()).startAgentLocalAuditWriter(); } catch {} try { return await agentCommand(opts, runtime, deps); } finally { await Promise.all([diagnostics?.stop(), stopLocalAuditWriter?.().catch(() => void 0)]); } } async function loadRuntimeConfig() { const { getRuntimeConfig } = await runtimeConfigModuleLoader.load(); return getRuntimeConfig(); } function usesRemoteGateway(cfg) { return Boolean(cfg.gateway?.mode === "remote" || normalizeOptionalString(process.env.OPENCLAW_GATEWAY_URL)); } async function loadRemoteGatewayRoster(cfg) { const result = await callGateway({ method: "agents.list", params: {}, config: cfg, clientName: GATEWAY_CLIENT_NAMES.CLI, mode: GATEWAY_CLIENT_MODES.CLI, scopes: [READ_SCOPE] }); return { agentIds: result.agents.filter((entry) => entry.kind !== "system").map((entry) => normalizeAgentId(entry.id)), defaultId: normalizeAgentId(result.defaultId), ownership: result.ownership, selectionRequired: result.selectionRequired ?? result.ownership === "explicit", mainKey: result.mainKey, scope: result.scope }; } async function loadRemoteGatewayRosterWithShellEnvFallback(cfg) { try { return { config: cfg, roster: await loadRemoteGatewayRoster(cfg) }; } catch (error) { if (!shouldRetryGatewayDispatchWithShellEnvFallback(error)) throw error; const fallbackConfig = await readGatewayDispatchConfigWithShellEnvFallback(); return { config: fallbackConfig, roster: await loadRemoteGatewayRoster(fallbackConfig) }; } } function formatActiveGatewayLocalRefusal(identity) { return `A Gateway is running for this state directory (pid ${identity.pid}, port ${identity.port}). Run without --local to use it, or stop the Gateway first (${formatCliCommand("openclaw gateway stop")}).`; } async function acquireEmbeddedAgentStateLock(options, signal) { const { acquireEmbeddedStateLock } = await embeddedStateLockModuleLoader.load(); return await acquireEmbeddedStateLock({ options, signal, formatActiveGatewayRefusal: formatActiveGatewayLocalRefusal }); } const loadReplyPayloadModule = replyPayloadModuleLoader.load; /** Test-only hooks for resetting lazy imports and shortening retry timing. */ const agentViaGatewayTesting = { resetLazyImportsForTests() { embeddedAgentCommandLoader.clear(); localAuditModuleLoader.clear(); agentSessionModuleCache.clear(); runtimeConfigModuleLoader.clear(); embeddedStateLockModuleLoader.clear(); replyPayloadModuleLoader.clear(); agentSessionModuleLoader = defaultAgentSessionModuleLoader; }, setAgentSessionModuleLoaderForTests(loader) { agentSessionModuleCache.clear(); agentSessionModuleLoader = loader; }, setGatewayAbortRetryDelaysMsForTests(delays) { gatewayAbortRetryDelaysMsForTests = delays; } }; function protectJsonStdout(opts) { if (opts.json === true) routeLogsToStderr(); } function missingAgentMessageError() { return /* @__PURE__ */ new Error(`Missing message. Use ${formatCliCommand("openclaw agent --message \"...\" --agent <id>")} or ${formatCliCommand("openclaw agent --message-file <path> --agent <id>")}.`); } function formatMessageFileReadFailure(messageFile, err) { const code = typeof err?.code === "string" ? err.code : ""; if (code === "ENOENT") return `Message file not found: ${messageFile}`; if (code === "EISDIR") return `Message file is a directory: ${messageFile}`; return `Unable to read message file ${messageFile}: ${err instanceof Error ? err.message : String(err)}`; } const AGENT_MESSAGE_FILE_MAX_BYTES = 4194304; async function readAgentMessageFile(messageFile) { let handle; try { handle = await fs.open(messageFile, "r"); } catch (err) { throw new Error(formatMessageFileReadFailure(messageFile, err), { cause: err }); } let buffer; try { const stat = await handle.stat(); if (stat.isDirectory()) throw Object.assign(/* @__PURE__ */ new Error("Message file is a directory"), { code: "EISDIR" }); if (stat.isFile() && stat.size > AGENT_MESSAGE_FILE_MAX_BYTES) throw new Error(`File exceeds ${AGENT_MESSAGE_FILE_MAX_BYTES} bytes: ${messageFile}`); buffer = await readFileDescriptorBounded(handle.fd, AGENT_MESSAGE_FILE_MAX_BYTES); } catch (err) { throw new Error(formatMessageFileReadFailure(messageFile, err), { cause: err }); } finally { await handle.close().catch(() => void 0); } try { return MESSAGE_FILE_DECODER.decode(buffer).replace(/^\uFEFF/, ""); } catch { throw new Error(`Message file must be valid UTF-8: ${messageFile}`); } } async function resolveAgentMessageOpts(opts) { const { messageFile: rawMessageFile, ...rest } = opts; const messageFile = rawMessageFile?.trim(); if (opts.message !== void 0 && messageFile) throw new Error("Use either --message or --message-file, not both."); if (rawMessageFile !== void 0 && !messageFile) throw new Error("--message-file must not be empty."); if (messageFile) { const message = await readAgentMessageFile(messageFile); if (!message.trim()) throw new Error(`Message file is empty: ${messageFile}`); return { ...rest, message }; } const message = opts.message ?? ""; if (!message.trim()) throw missingAgentMessageError(); return { ...rest, message }; } function parseTimeoutSeconds(opts) { const raw = opts.timeout !== void 0 ? parseStrictNonNegativeInteger(opts.timeout) : opts.cfg.agents?.defaults?.timeoutSeconds ?? 600; if (raw === void 0) throw new Error(`Invalid --timeout. Use seconds as a non-negative integer, for example --timeout 600. Use --timeout 0 to disable the timeout.`); return raw; } function resolveGatewayAgentTimeoutMs(timeoutSeconds) { if (timeoutSeconds === 0) return NO_GATEWAY_TIMEOUT_MS; return resolveTimerTimeoutMs((timeoutSeconds + 30) * 1e3, 1e4, 1e4); } async function formatPayloadForLog(payload) { const { resolveSendableOutboundReplyParts } = await loadReplyPayloadModule(); const parts = resolveSendableOutboundReplyParts({ text: payload.text, mediaUrls: payload.mediaUrls, mediaUrl: typeof payload.mediaUrl === "string" ? payload.mediaUrl : void 0 }); const lines = []; if (parts.text) lines.push(parts.text.trimEnd()); for (const url of parts.mediaUrls) lines.push(`Attachment: ${url}`); return lines.join("\n").trimEnd(); } function isCompactControlCommand(message) { return /^\/compact(?:\s|:|$)/iu.test(message.trim()); } function isSessionResetCommand(message) { return /^\/(?:new|reset)(?:\s|$)/i.test(message.trim()); } function shouldRetryGatewayDispatchWithShellEnvFallback(err) { return isGatewayCredentialsRequiredError(err) || isGatewayExplicitAuthRequiredError(err) || isGatewaySecretRefUnavailableError(err); } function resolveGatewayAgentFailureHint(err) { if (!isGatewayTransportError(err)) return; return err.kind === "timeout" ? "timed out" : "connection closed"; } function isTransientGatewayAgentConnectClose(err) { if (!isGatewayTransportError(err) || err.kind !== "closed") return false; const code = typeof err.code === "number" ? err.code : void 0; const reason = normalizeOptionalString(err.reason); return code === 1e3 && (!reason || reason === "no close reason"); } function validateExplicitSessionKeyForDispatch(opts) { const sessionKey = opts.sessionKey?.trim(); if (!sessionKey) return; if (classifySessionKeyShape(sessionKey) === "malformed_agent") throw new Error(`Invalid --session-key "${sessionKey}". Agent-prefixed session keys must use agent:<agent-id>:<session-key>.`); const agentIdRaw = opts.agent?.trim() || void 0; if (!agentIdRaw || classifySessionKeyShape(sessionKey) !== "agent") return; const agentId = normalizeAgentId(agentIdRaw); const sessionAgentId = resolveAgentIdFromSessionKey(sessionKey); if (sessionAgentId !== agentId) throw new Error(`Agent id "${agentIdRaw}" does not match session key agent "${sessionAgentId}".`); } async function normalizeSessionKeyOptsForDispatch(opts) { let normalizedOpts = opts; const rawSessionKey = opts.sessionKey?.trim(); const rawTo = opts.to?.trim(); if (!rawSessionKey && !opts.sessionId?.trim() && classifySessionKeyShape(rawTo) === "agent") return normalizeSessionKeyOptsForDispatch({ ...opts, to: void 0, sessionKey: rawTo }); const isLegacySessionKey = rawSessionKey && classifySessionKeyShape(rawSessionKey) === "legacy_or_alias"; const explicitAgentIdRaw = opts.agent?.trim(); let agentIdRaw = explicitAgentIdRaw; const hasExplicitSessionTarget = Boolean(opts.sessionId?.trim()) || [rawSessionKey, rawTo].some((value) => classifySessionKeyShape(value) === "agent"); let selectionCfg; let remoteGatewayRoster; if (opts.local !== true) { const cfg = readGatewayDispatchConfig(); normalizedOpts = { ...normalizedOpts, gatewayDispatchConfig: cfg }; selectionCfg = cfg; if (rawSessionKey && usesRemoteGateway(cfg) && classifySessionKeyShape(rawSessionKey) !== "agent") return normalizedOpts; } if (!agentIdRaw && !hasExplicitSessionTarget && !(opts.local === true && rawTo)) { let cfg = opts.local === true ? await loadRuntimeConfig() : selectionCfg ?? readGatewayDispatchConfig(); if (opts.local !== true && usesRemoteGateway(cfg)) { const loaded = await loadRemoteGatewayRosterWithShellEnvFallback(cfg); cfg = loaded.config; remoteGatewayRoster = loaded.roster; normalizedOpts = { ...normalizedOpts, gatewayDispatchConfig: cfg, remoteGatewayRoster }; } selectionCfg = cfg; const effectiveOwnerSessionKey = rawSessionKey ?? (cfg.session?.scope === "global" ? "global" : void 0); const persistedKeyOwner = remoteGatewayRoster ? { kind: "none" } : resolvePersistedSessionStoreOwnerForKey(cfg, effectiveOwnerSessionKey); if (persistedKeyOwner.kind === "retired") throw new AgentSelectionRequiredError(listAgentIds(cfg), { surface: `session key "${rawSessionKey}"`, hint: `The shared fixed-store row belongs to retired agent "${persistedKeyOwner.agentId}".` }); if (persistedKeyOwner.kind === "configured" && rawSessionKey === void 0 && effectiveOwnerSessionKey === "global") normalizedOpts = { ...normalizedOpts, sessionKey: "global" }; const selectedAgentId = persistedKeyOwner.kind === "configured" ? persistedKeyOwner.agentId : resolveImplicitCliAgentId(cfg, remoteGatewayRoster); const implicitSoleAgent = remoteGatewayRoster ? remoteGatewayRoster.ownership === "sole" || !remoteGatewayRoster.ownership && remoteGatewayRoster.agentIds.length === 1 : tryResolveSoleAgentId(cfg) === selectedAgentId; const implicitCompatibilityDefault = remoteGatewayRoster ? usesImplicitRemoteCompatibilityDefault(remoteGatewayRoster) : !implicitSoleAgent; const implicitGlobalSession = !explicitAgentIdRaw && rawSessionKey === void 0 && (remoteGatewayRoster ? remoteGatewayRoster.scope === "global" : (opts.local === true || !usesRemoteGateway(cfg)) && cfg.session?.scope === "global"); const unscopedSession = isUnscopedSessionKeySentinel(rawSessionKey) || implicitGlobalSession; agentIdRaw = (implicitSoleAgent || implicitCompatibilityDefault) && unscopedSession ? void 0 : selectedAgentId; if (!remoteGatewayRoster && implicitCompatibilityDefault) normalizedOpts = { ...normalizedOpts, localGatewayCompatibilityAgentId: selectedAgentId }; if (agentIdRaw && implicitCompatibilityDefault && !rawSessionKey && !rawTo) normalizedOpts = { ...normalizedOpts, sessionKey: buildAgentMainSessionKey({ agentId: selectedAgentId, mainKey: remoteGatewayRoster?.mainKey ?? cfg.session?.mainKey }) }; else if (agentIdRaw && !implicitCompatibilityDefault) normalizedOpts = { ...normalizedOpts, agent: selectedAgentId }; } const shouldScopeDefaultAgentKey = isLegacySessionKey && !agentIdRaw && !isUnscopedSessionKeySentinel(rawSessionKey); const cfg = isLegacySessionKey && (agentIdRaw || shouldScopeDefaultAgentKey) ? normalizedOpts.local === true ? await loadRuntimeConfig() : selectionCfg ?? readGatewayDispatchConfig() : void 0; if ((cfg && rawSessionKey && isLegacySessionKey && !isUnscopedSessionKeySentinel(rawSessionKey) ? resolvePersistedSessionStoreOwnerForKey(cfg, rawSessionKey) : void 0)?.kind === "configured" || isUnscopedSessionKeySentinel(rawSessionKey)) return normalizedOpts; const sessionKey = scopeLegacySessionKeyToAgent({ agentId: agentIdRaw, sessionKey: normalizedOpts.sessionKey, mainKey: remoteGatewayRoster?.mainKey ?? cfg?.session?.mainKey }); if (sessionKey === normalizedOpts.sessionKey) return normalizedOpts; return { ...normalizedOpts, sessionKey }; } function isAbortError(err) { return err instanceof Error && err.name === "AbortError"; } function readAcceptedRunContext(payload) { const accepted = asOptionalRecord(payload); if (accepted?.status !== "accepted") return; return { runId: normalizeOptionalString(accepted.runId), sessionKey: normalizeOptionalString(accepted.sessionKey), agentId: normalizeOptionalString(accepted.agentId) }; } function createAgentCliSignalBridge(processLike = process) { return { ...createEmbeddedStateSignalBridge(processLike), setExitCode: (code) => { processLike.exitCode = code; } }; } function isAgentCliProcessLike(value) { return Boolean(value) && typeof value === "object" && typeof value.on === "function" && typeof value.off === "function"; } function resolveAgentCliProcessLike(deps) { if (!deps || !Object.hasOwn(deps, "process")) return process; const processLike = deps.process; return isAgentCliProcessLike(processLike) ? processLike : process; } function createAbortDelayError() { return createAbortError("gateway agent retry aborted"); } function delayMs(ms, signal) { if (signal?.aborted) return Promise.reject(createAbortDelayError()); return new Promise((resolve, reject) => { const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); const onAbort = () => { clearTimeout(timer); signal?.removeEventListener("abort", onAbort); reject(createAbortDelayError()); }; signal?.addEventListener("abort", onAbort, { once: true }); }); } function isConfirmedChatAbortResponseForRun(value, runId) { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const response = value; if (response.aborted !== true) return false; if (response.runIds === void 0) return true; return Array.isArray(response.runIds) && response.runIds.includes(runId); } async function abortAcceptedGatewayAgentRunWithRequest(params) { if (!params.signal || !params.runId || !params.sessionKey) return false; try { if (isConfirmedChatAbortResponseForRun(await params.request("chat.abort", { sessionKey: params.sessionKey, runId: params.runId, ...params.agentId ? { agentId: params.agentId } : {} }, { timeoutMs: GATEWAY_ABORT_REQUEST_TIMEOUT_MS }), params.runId)) return true; if (params.logFailure !== false) params.runtime.error?.(`Interrupted by ${params.signal}; Gateway run ${params.runId} was not confirmed aborted.`); return false; } catch (err) { if (params.logFailure !== false) params.runtime.error?.(`Interrupted by ${params.signal}; failed to abort Gateway run ${params.runId}: ${String(err)}`); return false; } } async function abortAcceptedGatewayAgentRunWithGatewayCall(params) { const request = async (method, requestParams, opts) => await callGateway({ method, params: requestParams, timeoutMs: opts?.timeoutMs ?? void 0, expectFinal: opts?.expectFinal, config: params.config, ...params.gatewayIdentity }); const retryDelaysMs = resolveGatewayAbortRetryDelaysMs(); for (const [attempt, retryDelayMs] of [...retryDelaysMs, 0].entries()) { const isFinalAttempt = attempt === retryDelaysMs.length; if (await abortAcceptedGatewayAgentRunWithRequest({ runId: params.runId, sessionKey: params.sessionKey, agentId: params.agentId, signal: params.signal, runtime: params.runtime, request, logFailure: isFinalAttempt }) || isFinalAttempt) return; await delayMs(retryDelayMs); } } async function abortAcceptedGatewayAgentRunOnActiveConnection(params) { const retryDelaysMs = resolveGatewayAbortRetryDelaysMs(); for (const [attempt, retryDelayMs] of [...retryDelaysMs, 0].entries()) { const isFinalAttempt = attempt === retryDelaysMs.length; const aborted = await abortAcceptedGatewayAgentRunWithRequest({ runId: params.runId, sessionKey: params.sessionKey, agentId: params.agentId, signal: params.signal, runtime: params.runtime, request: params.request, logFailure: false }); if (aborted || isFinalAttempt) return aborted; await delayMs(retryDelayMs); } return false; } function exitForReceivedSignal(signal, runtime) { if (!signal) return false; runtime.exit(AGENT_CLI_SIGNAL_EXIT_CODES[signal]); return true; } function returnAfterSignalExit(value, signal, runtime) { return exitForReceivedSignal(signal, runtime) ? void 0 : value; } function buildGatewayJsonResponse(response) { const deliveryStatus = response.result?.deliveryStatus; if (deliveryStatus === void 0) return response; return { ...response, deliveryStatus }; } function isInFlightGatewayAgentResponse(response) { return response.status === "in_flight"; } function markAgentRunExitCode(status, signalBridge) { const waitStatus = status === "ok" || status === "completed" ? "ok" : status === "timeout" ? "timeout" : status === void 0 || status === null || status === "" ? void 0 : "error"; if (!waitStatus) return; const outcome = buildAgentRunTerminalOutcome({ status: waitStatus }); signalBridge.setExitCode(classifyAgentRunTerminalOutcome(outcome) === "success" ? 0 : 1); } function formatInFlightGatewayAgentMessage(response) { return response.runId ? `Agent run ${response.runId} is already in flight; not starting a duplicate run.` : "Agent run is already in flight; not starting a duplicate run."; } async function agentViaGatewayCommand(opts, runtime, signalBridge, runContext) { const body = opts.message; const explicitSessionKey = opts.sessionKey?.trim(); let cfg = opts.gatewayDispatchConfig ?? readGatewayDispatchConfig(); const remoteGateway = usesRemoteGateway(cfg); const remoteRosterIsSole = opts.remoteGatewayRoster?.ownership === "sole" || !opts.remoteGatewayRoster?.ownership && opts.remoteGatewayRoster?.agentIds.length === 1; const remoteRosterUsesCompatibilityDefault = Boolean(opts.remoteGatewayRoster && usesImplicitRemoteCompatibilityDefault(opts.remoteGatewayRoster)); const hasImplicitGlobalTarget = (opts.remoteGatewayRoster?.scope ?? cfg.session?.scope) === "global" && (opts.remoteGatewayRoster ? !opts.remoteGatewayRoster.selectionRequired && (remoteRosterIsSole || remoteRosterUsesCompatibilityDefault) : !remoteGateway && (tryResolveSoleAgentId(cfg) !== void 0 || opts.localGatewayCompatibilityAgentId !== void 0)); if (!opts.to && !opts.sessionId && !opts.agent && !explicitSessionKey && !hasImplicitGlobalTarget) throw new Error(`No target session selected. Use --agent <id>, --session-key <key>, --session-id <id>, or --to <E.164>. Run ${formatCliCommand("openclaw agents list")} to see agents.`); const agentIdRaw = opts.agent?.trim(); const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : void 0; if (agentId) { const knownAgents = opts.remoteGatewayRoster?.agentIds ?? (remoteGateway ? void 0 : listAgentIds(cfg)); if (knownAgents && !knownAgents.includes(agentId)) throw new Error(`Unknown agent id "${agentIdRaw}". Use "${formatCliCommand("openclaw agents list")}" to see configured agents.`); } const timeoutSeconds = parseTimeoutSeconds({ cfg, timeout: opts.timeout }); const gatewayTimeoutMs = resolveGatewayAgentTimeoutMs(timeoutSeconds); const channel = normalizeMessageChannel(opts.channel); const deferExplicitRecipientSession = Boolean(!explicitSessionKey && !opts.sessionId?.trim() && agentId && channel && channel !== "last" && opts.to?.trim() && classifySessionKeyShape(opts.to) !== "agent"); const deferRemoteSessionId = Boolean(remoteGateway && opts.sessionId?.trim() && !explicitSessionKey); const deferRemoteBareSessionKey = Boolean(remoteGateway && explicitSessionKey && classifySessionKeyShape(explicitSessionKey) !== "agent"); const deferAgentDefaultSession = Boolean(agentId && !explicitSessionKey && !opts.sessionId?.trim() && !opts.to?.trim()); const sessionKey = (remoteRosterIsSole || remoteRosterUsesCompatibilityDefault) && !agentId && (isUnscopedSessionKeySentinel(explicitSessionKey) || hasImplicitGlobalTarget) || deferRemoteBareSessionKey ? explicitSessionKey : deferAgentDefaultSession || deferExplicitRecipientSession || deferRemoteSessionId ? void 0 : classifySessionKeyShape(explicitSessionKey) === "agent" ? explicitSessionKey : (await loadAgentSessionModule()).resolveSessionKeyForRequest({ cfg, agentId, to: opts.to, sessionId: opts.sessionId, sessionKey: explicitSessionKey }).sessionKey; const abortSessionKey = deferRemoteSessionId ? void 0 : deferExplicitRecipientSession ? (await loadAgentSessionModule()).resolveSessionKeyForRequest({ cfg, agentId }).sessionKey : sessionKey; const idempotencyKey = normalizeOptionalString(opts.runId) || randomIdempotencyKey(); const modelOverride = normalizeOptionalString(opts.model); const gatewayIdentity = Boolean(modelOverride) || isSessionResetCommand(body) ? { clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, mode: GATEWAY_CLIENT_MODES.BACKEND, scopes: [ADMIN_SCOPE] } : { clientName: GATEWAY_CLIENT_NAMES.CLI, mode: GATEWAY_CLIENT_MODES.CLI, ...remoteGateway ? {} : { scopes: [ADMIN_SCOPE] } }; let activeConnectionAbortAttempted = false; let activeConnectionAbortSucceeded = false; let response; const dispatchGatewayAgentCall = async (activeCfg) => await withProgress({ label: "Waiting for agent reply…", indeterminate: true, enabled: opts.json !== true }, async () => await callGateway({ method: "agent", params: { message: body, agentId, model: modelOverride, to: opts.to, replyTo: opts.replyTo, sessionId: opts.sessionId, sessionKey, thinking: opts.thinking, deliver: Boolean(opts.deliver), channel, replyChannel: opts.replyChannel, replyAccountId: opts.replyAccount, bestEffortDeliver: opts.bestEffortDeliver, timeout: timeoutSeconds, lane: opts.lane, extraSystemPrompt: opts.extraSystemPrompt, cleanupBundleMcpOnRunEnd: true, idempotencyKey }, expectFinal: true, timeoutMs: gatewayTimeoutMs, config: activeCfg, signal: signalBridge.signal, onAccepted: (payload) => { runContext.accepted = readAcceptedRunContext(payload); }, onSignalAbort: async (request) => { activeConnectionAbortAttempted = true; activeConnectionAbortSucceeded = await abortAcceptedGatewayAgentRunOnActiveConnection({ runId: runContext.accepted?.runId ?? idempotencyKey, sessionKey: runContext.accepted?.sessionKey ?? abortSessionKey, agentId: runContext.accepted?.agentId, signal: signalBridge.getReceivedSignal(), runtime, request }); }, ...gatewayIdentity })); let shellEnvFallbackRetriesRemaining = 1; const consumeShellEnvFallbackRetry = () => shellEnvFallbackRetriesRemaining-- > 0; for (;;) try { response = await dispatchGatewayAgentCall(cfg); break; } catch (err) { if (!runContext.accepted && shouldRetryGatewayDispatchWithShellEnvFallback(err) && consumeShellEnvFallbackRetry()) { cfg = await readGatewayDispatchConfigWithShellEnvFallback(); continue; } if (isAbortError(err) && !activeConnectionAbortSucceeded && (runContext.accepted || activeConnectionAbortAttempted)) await abortAcceptedGatewayAgentRunWithGatewayCall({ runId: runContext.accepted?.runId ?? idempotencyKey, sessionKey: runContext.accepted?.sessionKey ?? abortSessionKey, agentId: runContext.accepted?.agentId, signal: signalBridge.getReceivedSignal(), runtime, gatewayIdentity, config: cfg }); const payload = err instanceof GatewayProtocolRequestError ? asOptionalRecord(err.responsePayload) : void 0; recordCliGatewayRunFailure(err, normalizeOptionalString(payload?.runId) ?? runContext.accepted?.runId); throw err; } if (!response) throw new Error("gateway agent call did not return a response"); markAgentRunExitCode(response.status, signalBridge); if (opts.json) { writeRuntimeJson(runtime, buildGatewayJsonResponse(response)); return response; } const payloads = (response?.result)?.payloads ?? []; if (isInFlightGatewayAgentResponse(response)) { runtime.error?.(formatInFlightGatewayAgentMessage(response)); return response; } if (payloads.length === 0) { if (response?.status !== "ok") runtime.log(response?.summary ? response.summary : "No reply from agent."); return response; } for (const payload of payloads) { const out = await formatPayloadForLog(payload); if (out) runtime.log(out); } return response; } async function agentViaGatewayCommandWithTransientRetries(opts, runtime, signalBridge) { const runContext = {}; for (const [attempt, retryDelayMs] of [...GATEWAY_TRANSIENT_CONNECT_RETRY_DELAYS_MS, 0].entries()) try { return await agentViaGatewayCommand(opts, runtime, signalBridge, runContext); } catch (err) { if (isAbortError(err)) throw err; if (attempt === GATEWAY_TRANSIENT_CONNECT_RETRY_DELAYS_MS.length || !isTransientGatewayAgentConnectClose(err)) throw err; runtime.error?.(`Gateway agent connection closed during handshake; retrying in ${retryDelayMs}ms before failing.`); await delayMs(retryDelayMs, signalBridge.signal); } throw new Error("Gateway agent retry loop exhausted unexpectedly."); } async function agentCliCommand(opts, runtime, deps) { for (const [flag, value] of [ ["--agent", opts.agent], ["--session-id", opts.sessionId], ["--session-key", opts.sessionKey], ["--to", opts.to] ]) if (value !== void 0 && !value.trim()) throw new Error(`${flag} must not be blank`); protectJsonStdout(opts); const messageOpts = await resolveAgentMessageOpts(opts); if (isCompactControlCommand(messageOpts.message)) { runtime.error?.("Slash commands cannot be executed via --message from the CLI. Use: openclaw sessions compact <key>"); runtime.exit(1); return; } const dispatchOpts = await normalizeSessionKeyOptsForDispatch(messageOpts); validateExplicitSessionKeyForDispatch(dispatchOpts); const gatewayDispatchOpts = dispatchOpts.runId ? dispatchOpts : { ...dispatchOpts, runId: randomIdempotencyKey() }; const signalBridge = createAgentCliSignalBridge(resolveAgentCliProcessLike(deps)); try { if (dispatchOpts.local === true) { const stateLock = await acquireEmbeddedAgentStateLock(deps?.localGatewayLockOptions, signalBridge.signal); let result; try { result = await runEmbeddedAgentCommand({ ...gatewayDispatchOpts, agentId: gatewayDispatchOpts.agent ?? gatewayDispatchOpts.localGatewayCompatibilityAgentId, replyAccountId: gatewayDispatchOpts.replyAccount, cleanupBundleMcpOnRunEnd: true, cleanupCliLiveSessionOnRunEnd: true, oneShotCliRun: true, abortSignal: signalBridge.signal }, runtime, deps, { suppressStdoutDiagnosticLogs: dispatchOpts.json === true }); } finally { await stateLock?.release(); } markAgentRunExitCode(readAgentRunTerminalOutcome(result), signalBridge); return returnAfterSignalExit(result, signalBridge.getReceivedSignal(), runtime); } try { return returnAfterSignalExit(await agentViaGatewayCommandWithTransientRetries(gatewayDispatchOpts, runtime, signalBridge), signalBridge.getReceivedSignal(), runtime); } catch (err) { if (isAbortError(err)) { if (exitForReceivedSignal(signalBridge.getReceivedSignal(), runtime)) return; throw err; } const failureHint = resolveGatewayAgentFailureHint(err); if (failureHint) runtime.error?.(`Gateway agent call ${failureHint}; the Gateway may still be running this turn. Check \`openclaw gateway status\` and the session transcript before retrying or rerunning with --local, so the turn does not execute twice.`); throw err; } } catch (err) { if (isAbortError(err) && exitForReceivedSignal(signalBridge.getReceivedSignal(), runtime)) return; throw err; } finally { signalBridge.dispose(); } } //#endregion export { agentCliCommand, agentViaGatewayTesting };