openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
519 lines (518 loc) • 21.8 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { j as resolveTimerTimeoutMs, y as parseStrictNonNegativeInteger } from "./number-coercion-CJQ8TR--.js";
import "./parse-finite-number-Z7n6tXLk.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { r as writeRuntimeJson } from "./runtime-B4lgFmsS.js";
import { a as routeLogsToStderr } from "./console-BOsG6sD1.js";
import { c as isUnscopedSessionKeySentinel, f as resolveAgentIdFromSessionKey, g as scopeLegacySessionKeyToAgent, s as classifySessionKeyShape, u as normalizeAgentId } from "./session-key-B_NoIfpX.js";
import { c as resolveDefaultAgentId, n as listAgentIds } from "./agent-scope-config-CgCYpZfK.js";
import { n as isGatewaySecretRefUnavailableError } from "./credentials-irvgw8Le.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-CcqJJIan.js";
import { f as isGatewayCredentialsRequiredError, h as randomIdempotencyKey, m as isGatewayTransportError, o as callGateway, p as isGatewayExplicitAuthRequiredError, v as readGatewayDispatchConfig, y as readGatewayDispatchConfigWithShellEnvFallback } from "./call-B5-GYOlf.js";
import { t as ADMIN_SCOPE } from "./operator-scopes-CS3xdS-V.js";
import { i as normalizeMessageChannel } from "./message-channel-normalize-BLLf0ubu.js";
import { r as withProgress } from "./progress-CN3xUiCO.js";
import { randomUUID } from "node:crypto";
//#region src/commands/agent-via-gateway.ts
const NO_GATEWAY_TIMEOUT_MS = 2147e6;
const EMBEDDED_FALLBACK_META = {
transport: "embedded",
fallbackFrom: "gateway"
};
const GATEWAY_TIMEOUT_FALLBACK_SESSION_PREFIX = "gateway-fallback-";
const GATEWAY_TRANSIENT_CONNECT_RETRY_DELAYS_MS = [
1e3,
2e3,
5e3,
1e4,
15e3
];
const AGENT_CLI_SIGNALS = ["SIGINT", "SIGTERM"];
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
};
let embeddedAgentCommandPromise;
let agentSessionModulePromise;
let runtimeConfigModulePromise;
let replyPayloadModulePromise;
const defaultAgentSessionModuleLoader = () => import("./session-BYG-Oz_w.js");
let agentSessionModuleLoader = defaultAgentSessionModuleLoader;
function resolveGatewayAbortRetryDelaysMs() {
return GATEWAY_ABORT_RETRY_DELAYS_MS;
}
function loadEmbeddedAgentCommand() {
embeddedAgentCommandPromise ??= import("./agent-DscNH0lZ.js").then((module) => module.agentCommand);
return embeddedAgentCommandPromise;
}
function loadAgentSessionModule() {
agentSessionModulePromise ??= agentSessionModuleLoader();
return agentSessionModulePromise;
}
async function loadRuntimeConfig() {
runtimeConfigModulePromise ??= import("./io-BYWoyJ4I.js");
const { getRuntimeConfig } = await runtimeConfigModulePromise;
return getRuntimeConfig();
}
function loadReplyPayloadModule() {
replyPayloadModulePromise ??= import("./plugin-sdk/reply-payload.js");
return replyPayloadModulePromise;
}
function protectJsonStdout(opts) {
if (opts.json === true) routeLogsToStderr();
}
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 getGatewayDispatchConfig(options) {
if (options?.skipShellEnvFallback === false) return await readGatewayDispatchConfigWithShellEnvFallback();
return readGatewayDispatchConfig();
}
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 isGatewayAgentTimeoutError(err) {
if (isGatewayTransportError(err)) return err.kind === "timeout";
return err instanceof Error && err.message.includes("gateway request timeout for agent");
}
function isControlCommandThatMustNotFallback(opts) {
const normalized = opts.message.trim().toLowerCase();
return normalized === "/compact" || normalized.startsWith("/compact ");
}
function isSessionResetCommand(message) {
return /^\/(?:new|reset)(?:\s|$)/i.test(message.trim());
}
function shouldRetryGatewayDispatchWithShellEnvFallback(err) {
return isGatewayCredentialsRequiredError(err) || isGatewayExplicitAuthRequiredError(err) || isGatewaySecretRefUnavailableError(err);
}
function isGatewayAgentEmbeddedFallbackError(err) {
return isGatewayTransportError(err);
}
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) {
const rawSessionKey = opts.sessionKey?.trim();
const isLegacySessionKey = rawSessionKey && classifySessionKeyShape(rawSessionKey) === "legacy_or_alias";
const agentIdRaw = opts.agent?.trim();
const shouldScopeDefaultAgentKey = isLegacySessionKey && !agentIdRaw && !isUnscopedSessionKeySentinel(rawSessionKey);
const cfg = isLegacySessionKey && (agentIdRaw || shouldScopeDefaultAgentKey) ? opts.local === true ? await loadRuntimeConfig() : await getGatewayDispatchConfig() : void 0;
const sessionKey = scopeLegacySessionKeyToAgent({
agentId: agentIdRaw ?? (shouldScopeDefaultAgentKey ? resolveDefaultAgentId(cfg) : void 0),
sessionKey: opts.sessionKey,
mainKey: cfg?.session?.mainKey
});
if (sessionKey === opts.sessionKey) return opts;
return {
...opts,
sessionKey
};
}
function isAbortError(err) {
return err instanceof Error && err.name === "AbortError";
}
function readAcceptedRunContext(payload) {
if (!payload || typeof payload !== "object") return {};
const runId = payload.runId;
const sessionKey = payload.sessionKey;
if (payload.status !== "accepted") return {};
return {
runId: typeof runId === "string" && runId.trim() ? runId.trim() : void 0,
sessionKey: typeof sessionKey === "string" && sessionKey.trim() ? sessionKey.trim() : void 0
};
}
function createAgentCliSignalBridge(processLike = process) {
const controller = new AbortController();
let receivedSignal;
const handlers = /* @__PURE__ */ new Map();
const detachHandlers = () => {
for (const [signal, handler] of handlers) processLike.off(signal, handler);
handlers.clear();
};
for (const signal of AGENT_CLI_SIGNALS) {
const handler = () => {
receivedSignal = signal;
if (!controller.signal.aborted) {
controller.abort();
detachHandlers();
}
};
handlers.set(signal, handler);
processLike.on(signal, handler);
}
return {
signal: controller.signal,
getReceivedSignal: () => receivedSignal,
dispose: detachHandlers
};
}
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() {
const err = /* @__PURE__ */ new Error("gateway agent retry aborted");
err.name = "AbortError";
return err;
}
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
}, { 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,
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,
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 createGatewayTimeoutFallbackSessionId() {
return `${GATEWAY_TIMEOUT_FALLBACK_SESSION_PREFIX}${randomUUID()}`;
}
function createGatewayTimeoutFallbackSession(agentId) {
const sessionId = createGatewayTimeoutFallbackSessionId();
return {
sessionId,
sessionKey: `agent:${normalizeAgentId(agentId)}:explicit:${sessionId.trim()}`
};
}
async function resolveAgentIdForGatewayTimeoutFallback(opts) {
const explicitSessionKey = opts.sessionKey?.trim();
if (classifySessionKeyShape(explicitSessionKey) === "agent") return resolveAgentIdFromSessionKey(explicitSessionKey);
if (isUnscopedSessionKeySentinel(explicitSessionKey)) return resolveDefaultAgentId(await getGatewayDispatchConfig());
const agentIdRaw = opts.agent?.trim();
if (agentIdRaw) return normalizeAgentId(agentIdRaw);
if (!opts.to && !opts.sessionId) return;
const cfg = await getGatewayDispatchConfig();
const { resolveSessionKeyForRequest } = await loadAgentSessionModule();
const resolvedSessionKey = resolveSessionKeyForRequest({
cfg,
to: opts.to,
sessionId: opts.sessionId
}).sessionKey;
return classifySessionKeyShape(resolvedSessionKey) === "agent" ? resolveAgentIdFromSessionKey(resolvedSessionKey) : void 0;
}
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 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) {
protectJsonStdout(opts);
const body = (opts.message ?? "").trim();
const explicitSessionKey = opts.sessionKey?.trim();
if (!body) throw new Error(`Missing message. Use ${formatCliCommand("openclaw agent --message \"...\" --agent <id>")} or pass --to/--session-key/--session-id for an existing conversation.`);
if (!opts.to && !opts.sessionId && !opts.agent && !explicitSessionKey) 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.`);
let cfg = await getGatewayDispatchConfig();
const agentIdRaw = opts.agent?.trim();
const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : void 0;
if (agentId) {
if (!listAgentIds(cfg).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 sessionKey = classifySessionKeyShape(explicitSessionKey) === "agent" ? explicitSessionKey : (await loadAgentSessionModule()).resolveSessionKeyForRequest({
cfg,
agentId,
to: opts.to,
sessionId: opts.sessionId,
sessionKey: explicitSessionKey
}).sessionKey;
const channel = normalizeMessageChannel(opts.channel);
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
};
let acceptedRunId = idempotencyKey;
let acceptedSessionKey = sessionKey;
let acceptedGatewayRun = false;
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,
idempotencyKey
},
expectFinal: true,
timeoutMs: gatewayTimeoutMs,
config: activeCfg,
signal: signalBridge.signal,
onAccepted: (payload) => {
acceptedGatewayRun = true;
const accepted = readAcceptedRunContext(payload);
acceptedRunId = accepted.runId ?? acceptedRunId;
acceptedSessionKey = accepted.sessionKey ?? acceptedSessionKey;
},
onSignalAbort: async (request) => {
activeConnectionAbortAttempted = true;
activeConnectionAbortSucceeded = await abortAcceptedGatewayAgentRunOnActiveConnection({
runId: acceptedRunId,
sessionKey: acceptedSessionKey,
signal: signalBridge.getReceivedSignal(),
runtime,
request
});
},
...gatewayIdentity
}));
let shellEnvFallbackRetriesRemaining = 1;
const consumeShellEnvFallbackRetry = () => shellEnvFallbackRetriesRemaining-- > 0;
for (;;) try {
response = await dispatchGatewayAgentCall(cfg);
break;
} catch (err) {
if (!acceptedGatewayRun && shouldRetryGatewayDispatchWithShellEnvFallback(err) && consumeShellEnvFallbackRetry()) {
cfg = await getGatewayDispatchConfig({ skipShellEnvFallback: false });
continue;
}
if (isAbortError(err) && !activeConnectionAbortSucceeded && (acceptedGatewayRun || activeConnectionAbortAttempted)) await abortAcceptedGatewayAgentRunWithGatewayCall({
runId: acceptedRunId,
sessionKey: acceptedSessionKey,
signal: signalBridge.getReceivedSignal(),
runtime,
gatewayIdentity,
config: cfg
});
throw err;
}
if (!response) throw new Error("gateway agent call did not return a response");
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) {
for (const [attempt, retryDelayMs] of [...GATEWAY_TRANSIENT_CONNECT_RETRY_DELAYS_MS, 0].entries()) try {
return await agentViaGatewayCommand(opts, runtime, signalBridge);
} 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 embedded fallback.`);
await delayMs(retryDelayMs, signalBridge.signal);
}
throw new Error("Gateway agent retry loop exhausted unexpectedly.");
}
async function agentCliCommand(opts, runtime, deps) {
protectJsonStdout(opts);
const dispatchOpts = await normalizeSessionKeyOptsForDispatch(opts);
validateExplicitSessionKeyForDispatch(dispatchOpts);
const gatewayDispatchOpts = dispatchOpts.runId ? dispatchOpts : {
...dispatchOpts,
runId: randomIdempotencyKey()
};
const signalBridge = createAgentCliSignalBridge(resolveAgentCliProcessLike(deps));
const localOpts = {
...gatewayDispatchOpts,
agentId: gatewayDispatchOpts.agent,
replyAccountId: gatewayDispatchOpts.replyAccount,
cleanupBundleMcpOnRunEnd: true,
cleanupCliLiveSessionOnRunEnd: true,
abortSignal: signalBridge.signal
};
try {
if (dispatchOpts.local === true) return returnAfterSignalExit(await (await loadEmbeddedAgentCommand())(localOpts, runtime, deps), 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;
}
if (isGatewayAgentTimeoutError(err)) {
if (isControlCommandThatMustNotFallback(dispatchOpts)) throw err;
const fallbackSession = createGatewayTimeoutFallbackSession(await resolveAgentIdForGatewayTimeoutFallback(dispatchOpts));
runtime.error?.(`EMBEDDED FALLBACK: Gateway agent timed out; running embedded agent with fresh session ${fallbackSession.sessionId}: ${String(err)}`);
return returnAfterSignalExit(await (await loadEmbeddedAgentCommand())({
...localOpts,
sessionId: fallbackSession.sessionId,
sessionKey: fallbackSession.sessionKey,
runId: fallbackSession.sessionId,
resultMetaOverrides: {
...EMBEDDED_FALLBACK_META,
fallbackReason: "gateway_timeout",
fallbackSessionId: fallbackSession.sessionId,
fallbackSessionKey: fallbackSession.sessionKey
}
}, runtime, deps), signalBridge.getReceivedSignal(), runtime);
}
if (!isGatewayAgentEmbeddedFallbackError(err)) throw err;
runtime.error?.(`EMBEDDED FALLBACK: Gateway agent failed; running embedded agent: ${String(err)}`);
return returnAfterSignalExit(await (await loadEmbeddedAgentCommand())({
...localOpts,
resultMetaOverrides: EMBEDDED_FALLBACK_META
}, runtime, deps), signalBridge.getReceivedSignal(), runtime);
}
} catch (err) {
if (isAbortError(err) && exitForReceivedSignal(signalBridge.getReceivedSignal(), runtime)) return;
throw err;
} finally {
signalBridge.dispose();
}
}
//#endregion
export { agentCliCommand };