openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
428 lines (427 loc) • 18 kB
JavaScript
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { i as resolveGlobalSingleton } from "./global-singleton-Dc_stLtU.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { m as readNonBlankString } from "./string-coerce-CIXf7egm.js";
import { $ as normalizeAgentRunTerminalReplySnapshot, Q as mergeAgentRunTerminalReplySnapshot, rt as normalizeAgentRunTerminalReceipt } from "./openclaw-state-db-BRTnL-D8.js";
import { t as setSafeTimeout } from "./timer-delay-x5n129Nx.js";
import { d as onAgentEvent } from "./agent-events-CoxiItUi.js";
import { r as getAsyncWorkSignal } from "./async-work-scope-CMQS2uTf.js";
import { i as buildAgentRunTerminalOutcomeFromLifecycleEvent, l as isStickyAgentRunTerminalOutcome, n as buildAgentRunTerminalOutcome, s as hasExecutionSettlement, t as AGENT_RUN_TERMINAL_RETRY_GRACE_MS } from "./agent-run-terminal-outcome-CigeY75d.js";
import { t as mergeAgentRunTerminalOutcome } from "./agent-run-terminal-outcome-merge-DXNYLPhQ.js";
import { r as formatErrorMessageForDisplay } from "./error-diagnostics-BM-F2vsk.js";
import { n as normalizeAgentRunTerminalDeliverySnapshot, t as isNonTerminalAgentRunStatus } from "./agent-run-status-Ie-4cjf6.js";
//#region src/gateway/agent-turn/agent-job.ts
const AGENT_RUN_CACHE_TTL_MS = 6e5;
const AGENT_RUN_CACHE_MAX_ENTRIES = 5e3;
const agentJobState = resolveGlobalSingleton(Symbol.for("openclaw.agentJobState"), () => ({
jobs: /* @__PURE__ */ new Map(),
runStarts: /* @__PURE__ */ new Map(),
pendingErrors: /* @__PURE__ */ new Map(),
pendingTimeouts: /* @__PURE__ */ new Map(),
waiters: /* @__PURE__ */ new Map(),
version: 0
}), (state) => {
for (const pending of state.pendingErrors.values()) clearTimeout(pending.timer);
for (const pending of state.pendingTimeouts.values()) clearTimeout(pending.timer);
state.jobs.clear();
state.runStarts.clear();
state.pendingErrors.clear();
state.pendingTimeouts.clear();
const waiters = Array.from(state.waiters.values()).flatMap((entries) => Array.from(entries));
state.waiters.clear();
for (const waiter of waiters) waiter(true);
});
const agentJobs = agentJobState.jobs;
const agentRunStarts = agentJobState.runStarts;
const pendingAgentRunErrors = agentJobState.pendingErrors;
const pendingAgentRunTimeouts = agentJobState.pendingTimeouts;
const agentRunWaiters = agentJobState.waiters;
let agentRunListenerStarted = false;
function nextAgentRunVersion() {
agentJobState.version += 1;
return agentJobState.version;
}
function pruneAgentRunCache(now = Date.now()) {
for (const [runId, job] of agentJobs) {
if (now - job.cachedAt <= AGENT_RUN_CACHE_TTL_MS) continue;
agentJobs.delete(runId);
}
}
function enforceAgentRunCacheMaxEntries() {
if (agentJobs.size <= AGENT_RUN_CACHE_MAX_ENTRIES) return;
const toRemove = agentJobs.size - AGENT_RUN_CACHE_MAX_ENTRIES;
let removed = 0;
for (const runId of agentJobs.keys()) {
if (removed >= toRemove) break;
if ((agentRunWaiters.get(runId)?.size ?? 0) > 0) continue;
agentJobs.delete(runId);
removed += 1;
}
}
function terminalOutcomeFromSnapshot(snapshot) {
if (snapshot.pendingError) return;
return buildAgentRunTerminalOutcome(snapshot);
}
function shouldPreserveTerminalSnapshot(existing, incoming) {
const existingOutcome = terminalOutcomeFromSnapshot(existing);
const incomingOutcome = terminalOutcomeFromSnapshot(incoming);
if (!existingOutcome || !incomingOutcome) return false;
return mergeAgentRunTerminalOutcome(existingOutcome, incomingOutcome) === existingOutcome;
}
function mergeSnapshot(existing, incoming) {
if (!existing) return incoming;
const terminalReply = mergeAgentRunTerminalReplySnapshot(existing.terminalReply, incoming.terminalReply);
const terminalDelivery = incoming.terminalDelivery ?? existing.terminalDelivery;
const terminalReceipt = incoming.terminalReceipt ?? existing.terminalReceipt;
return {
...shouldPreserveTerminalSnapshot(existing, incoming) ? existing : incoming,
...terminalDelivery ? { terminalDelivery } : {},
...terminalReceipt ? { terminalReceipt } : {},
...terminalReply ? { terminalReply } : {},
cachedAt: incoming.cachedAt,
recordedAt: incoming.recordedAt,
version: incoming.version
};
}
function recordAgentRunSnapshot(snapshot, version = nextAgentRunVersion()) {
const entry = {
...snapshot,
cachedAt: Date.now(),
version
};
pruneAgentRunCache(entry.cachedAt);
const snapshotsBySource = agentJobs.get(entry.runId)?.snapshotsBySource ?? /* @__PURE__ */ new Map();
const sourceSnapshot = mergeSnapshot(snapshotsBySource.get(entry.source), entry);
snapshotsBySource.set(entry.source, sourceSnapshot);
agentJobs.set(entry.runId, {
cachedAt: entry.cachedAt,
snapshotsBySource
});
enforceAgentRunCacheMaxEntries();
for (const waiter of agentRunWaiters.get(entry.runId) ?? []) waiter();
}
function clearPendingAgentRunTerminals(runId) {
for (const pendingRuns of [pendingAgentRunErrors, pendingAgentRunTimeouts]) {
const pending = pendingRuns.get(runId);
if (pending) {
clearTimeout(pending.timer);
pendingRuns.delete(runId);
}
}
}
function beginAgentJob(runId, startedAt) {
nextAgentRunVersion();
clearPendingAgentRunTerminals(runId);
agentJobs.delete(runId);
if (startedAt !== void 0) agentRunStarts.set(runId, startedAt);
}
function mergePendingAgentRunTerminal(snapshot) {
return [pendingAgentRunErrors, pendingAgentRunTimeouts].reduce((current, pendingRuns) => {
const pending = pendingRuns.get(snapshot.runId)?.snapshot;
return pending && shouldPreserveTerminalSnapshot(pending, current) ? pending : current;
}, snapshot);
}
function schedulePendingAgentRunTerminal(pendingRuns, snapshot) {
const terminalSnapshot = mergePendingAgentRunTerminal(snapshot);
if (terminalSnapshot !== snapshot) {
terminalSnapshot.version = snapshot.version;
return;
}
const replacesPendingTimeout = pendingAgentRunTimeouts.has(snapshot.runId);
clearPendingAgentRunTerminals(snapshot.runId);
const timer = setSafeTimeout(() => {
const pending = pendingRuns.get(snapshot.runId);
if (!pending || pending.timer !== timer) return;
if (pendingRuns === pendingAgentRunErrors && !replacesPendingTimeout && terminalOutcomeFromSnapshot(pending.snapshot)?.reason === "failed" && agentRunWaiters.has(snapshot.runId)) {
pending.timer = void 0;
return;
}
pendingRuns.delete(snapshot.runId);
recordAgentRunSnapshot(pending.snapshot, pending.snapshot.version);
}, AGENT_RUN_TERMINAL_RETRY_GRACE_MS);
timer.unref?.();
pendingRuns.set(snapshot.runId, {
snapshot,
timer
});
}
function createPendingErrorTimeoutSnapshot(snapshot) {
return {
status: "timeout",
startedAt: snapshot.startedAt,
error: snapshot.error,
pendingError: true,
...snapshot.providerStarted !== void 0 ? { providerStarted: snapshot.providerStarted } : {},
...snapshot.terminalDelivery ? { terminalDelivery: snapshot.terminalDelivery } : {}
};
}
function createSnapshotFromLifecycleEvent(params) {
const { runId, phase, data } = params;
const startedAt = typeof data?.startedAt === "number" ? data.startedAt : agentRunStarts.get(runId);
const endedAt = typeof data?.endedAt === "number" ? data.endedAt : void 0;
const terminalOutcome = buildAgentRunTerminalOutcomeFromLifecycleEvent({
phase,
data,
startedAt,
endedAt
});
const legacyBareAbort = !hasExecutionSettlement(data) && terminalOutcome.reason === "aborted" && data?.stopReason == null && data?.status == null;
const terminalDelivery = normalizeAgentRunTerminalDeliverySnapshot(data?.terminalDelivery);
const terminalReply = normalizeAgentRunTerminalReplySnapshot(data?.terminalReply);
const normalizedTerminalReceipt = normalizeAgentRunTerminalReceipt(data?.terminalReceipt);
const terminalReceipt = normalizedTerminalReceipt?.runId === runId ? normalizedTerminalReceipt : void 0;
return {
runId,
source: "lifecycle",
recordedAt: Date.now(),
status: legacyBareAbort ? "timeout" : terminalOutcome.status,
startedAt,
endedAt,
error: legacyBareAbort ? void 0 : terminalOutcome.error,
stopReason: legacyBareAbort ? void 0 : terminalOutcome.stopReason,
livenessState: terminalOutcome.livenessState,
...data?.yielded === true ? { yielded: true } : {},
...terminalOutcome.timeoutPhase ? { timeoutPhase: terminalOutcome.timeoutPhase } : {},
...terminalOutcome.providerStarted !== void 0 ? { providerStarted: terminalOutcome.providerStarted } : {},
...terminalDelivery ? { terminalDelivery } : {},
...terminalReply ? { terminalReply } : {},
...terminalReceipt ? { terminalReceipt } : {},
version: nextAgentRunVersion()
};
}
function ensureAgentRunListener() {
if (agentRunListenerStarted) return;
agentRunListenerStarted = true;
onAgentEvent((evt) => {
if (!evt || evt.stream !== "lifecycle") return;
const phase = evt.data?.phase;
if (phase === "start") {
const startedAt = typeof evt.data?.startedAt === "number" ? evt.data.startedAt : Date.now();
beginAgentJob(evt.runId, startedAt);
return;
}
if (phase !== "end" && phase !== "error") return;
const snapshot = createSnapshotFromLifecycleEvent({
runId: evt.runId,
phase,
data: evt.data
});
agentRunStarts.delete(evt.runId);
const executionSettled = hasExecutionSettlement(evt.data);
if (!executionSettled && phase === "error" && evt.data?.fallbackExhaustedFailure !== true) {
schedulePendingAgentRunTerminal(pendingAgentRunErrors, snapshot);
return;
}
if (!executionSettled && phase === "end" && snapshot.status === "timeout") {
schedulePendingAgentRunTerminal(pendingAgentRunTimeouts, snapshot);
return;
}
const terminalSnapshot = mergePendingAgentRunTerminal(snapshot);
clearPendingAgentRunTerminals(evt.runId);
recordAgentRunSnapshot(terminalSnapshot, snapshot.version);
});
}
function parseDedupeObservation(entry) {
const payload = entry.payload;
const status = typeof payload?.status === "string" ? payload.status : void 0;
if (isNonTerminalAgentRunStatus(status)) return { state: "active" };
const terminalStatus = status === "ok" || status === "timeout" || status === "error" ? status : entry.ok ? void 0 : "error";
if (!terminalStatus) return { state: "untracked" };
const resultMeta = asOptionalRecord(asOptionalRecord(payload?.result)?.meta);
const terminalReply = normalizeAgentRunTerminalReplySnapshot(payload?.terminalReply ?? resultMeta?.terminalReply);
const startedAt = asFiniteNumber(payload?.startedAt);
const endedAt = asFiniteNumber(payload?.endedAt) ?? entry.ts;
const stopReason = readNonBlankString(payload?.stopReason) ?? readNonBlankString(resultMeta?.stopReason);
const livenessState = readNonBlankString(payload?.livenessState) ?? readNonBlankString(resultMeta?.livenessState);
const errorMessage = typeof payload?.error === "string" ? payload.error : typeof payload?.summary === "string" ? payload.summary : entry.error?.message;
const terminalOutcome = buildAgentRunTerminalOutcome({
status: terminalStatus,
startedAt,
endedAt,
error: errorMessage === void 0 ? void 0 : formatErrorMessageForDisplay(entry.error, errorMessage),
stopReason,
livenessState,
timeoutPhase: payload?.timeoutPhase ?? resultMeta?.timeoutPhase,
providerStarted: payload?.providerStarted ?? resultMeta?.providerStarted
});
return {
state: "terminal",
snapshot: {
status: terminalOutcome.status,
startedAt,
endedAt,
error: terminalOutcome.status === "ok" ? void 0 : terminalOutcome.error,
stopReason,
livenessState,
...payload?.yielded === true || resultMeta?.yielded === true ? { yielded: true } : {},
...terminalOutcome.timeoutPhase ? { timeoutPhase: terminalOutcome.timeoutPhase } : {},
...terminalOutcome.providerStarted !== void 0 ? { providerStarted: terminalOutcome.providerStarted } : {},
...terminalReply ? { terminalReply } : {}
}
};
}
function parseDedupeKey(key) {
const separator = key.indexOf(":");
if (separator === -1) return;
const source = key.slice(0, separator);
const runId = key.slice(separator + 1);
if (source !== "agent" && source !== "chat" || !runId) return;
return {
runId,
source
};
}
function setGatewayDedupeEntry(params) {
const existing = params.dedupe.get(params.key);
const existingObservation = existing ? parseDedupeObservation(existing) : void 0;
const incomingObservation = parseDedupeObservation(params.entry);
const existingOutcome = existingObservation?.state === "terminal" ? terminalOutcomeFromSnapshot(existingObservation.snapshot) : void 0;
const incomingOutcome = incomingObservation.state === "terminal" ? terminalOutcomeFromSnapshot(incomingObservation.snapshot) : void 0;
if (existingOutcome && isStickyAgentRunTerminalOutcome(existingOutcome) && (!incomingOutcome || mergeAgentRunTerminalOutcome(existingOutcome, incomingOutcome) === existingOutcome)) return;
const entry = existing?.requestIdentity ? {
...params.entry,
requestIdentity: existing.requestIdentity
} : params.entry;
params.dedupe.set(params.key, entry);
const key = parseDedupeKey(params.key);
if (!key) return;
if (incomingObservation.state === "active") {
beginAgentJob(key.runId);
return;
}
if (incomingObservation.state === "terminal") recordAgentRunSnapshot({
...incomingObservation.snapshot,
runId: key.runId,
source: key.source,
recordedAt: params.entry.ts
});
}
function getFreshestDedupeSnapshot(snapshotsBySource) {
const agent = snapshotsBySource.get("agent");
const chat = snapshotsBySource.get("chat");
if (agent && chat) return chat.recordedAt > agent.recordedAt ? mergeSnapshot(agent, chat) : mergeSnapshot(chat, agent);
return agent ?? chat;
}
function getCanonicalAgentRunSnapshot(snapshotsBySource) {
const dedupe = getFreshestDedupeSnapshot(snapshotsBySource);
const lifecycle = snapshotsBySource.get("lifecycle");
if (!dedupe || !lifecycle) return dedupe ?? lifecycle;
return dedupe.version > lifecycle.version ? mergeSnapshot(lifecycle, dedupe) : mergeSnapshot(dedupe, lifecycle);
}
function getAgentRunSnapshot(params) {
pruneAgentRunCache();
const job = agentJobs.get(params.runId);
const snapshot = params.source ? job?.snapshotsBySource.get(params.source) : job ? getCanonicalAgentRunSnapshot(job.snapshotsBySource) : void 0;
return snapshot && snapshot.version > params.afterVersion ? snapshot : void 0;
}
function addAgentRunWaiter(runId, waiter) {
const waiters = agentRunWaiters.get(runId) ?? /* @__PURE__ */ new Set();
waiters.add(waiter);
agentRunWaiters.set(runId, waiters);
return () => {
waiters.delete(waiter);
if (waiters.size === 0) {
agentRunWaiters.delete(runId);
const pendingError = pendingAgentRunErrors.get(runId);
if (pendingError && !pendingError.timer) {
pendingAgentRunErrors.delete(runId);
recordAgentRunSnapshot(pendingError.snapshot, pendingError.snapshot.version);
}
}
};
}
function publicSnapshot(snapshot) {
return {
status: snapshot.status,
startedAt: snapshot.startedAt,
endedAt: snapshot.endedAt,
error: snapshot.error,
stopReason: snapshot.stopReason,
livenessState: snapshot.livenessState,
yielded: snapshot.yielded,
pendingError: snapshot.pendingError,
timeoutPhase: snapshot.timeoutPhase,
providerStarted: snapshot.providerStarted,
...snapshot.terminalDelivery ? { terminalDelivery: snapshot.terminalDelivery } : {},
terminalReceipt: snapshot.terminalReceipt,
terminalReply: snapshot.terminalReply
};
}
async function waitForAgentJob(params) {
ensureAgentRunListener();
const afterVersion = params.ignoreCachedSnapshot ? agentJobState.version : -1;
const cached = getAgentRunSnapshot({
runId: params.runId,
source: params.source,
afterVersion
});
if (cached) return publicSnapshot(cached);
const signal = getAsyncWorkSignal();
if (params.timeoutMs <= 0 || signal?.aborted) return null;
return await new Promise((resolve) => {
let settled = false;
let removeWaiter = () => {};
const finish = (snapshot) => {
if (settled) return;
settled = true;
clearTimeout(timeoutHandle);
signal?.removeEventListener("abort", onClose);
removeWaiter();
resolve(snapshot);
};
const onClose = () => finish(null);
const onWake = (lifecycleReset = false) => {
if (lifecycleReset) {
finish({
status: "timeout",
timeoutPhase: "gateway_draining"
});
return;
}
const snapshot = getAgentRunSnapshot({
runId: params.runId,
source: params.source,
afterVersion
});
if (snapshot) finish(publicSnapshot(snapshot));
};
removeWaiter = addAgentRunWaiter(params.runId, onWake);
const timeoutHandle = setSafeTimeout(() => {
if (!params.source) {
const pending = pendingAgentRunErrors.get(params.runId);
const pendingError = pending?.snapshot;
if (pendingError && pendingError.version > afterVersion) {
finish(!pending.timer || isStickyAgentRunTerminalOutcome(terminalOutcomeFromSnapshot(pendingError)) ? publicSnapshot(pendingError) : createPendingErrorTimeoutSnapshot(pendingError));
return;
}
const pendingTimeout = pendingAgentRunTimeouts.get(params.runId)?.snapshot;
if (pendingTimeout && pendingTimeout.version > afterVersion && terminalOutcomeFromSnapshot(pendingTimeout)?.reason === "hard_timeout") {
finish(publicSnapshot(pendingTimeout));
return;
}
}
finish(null);
}, params.timeoutMs);
timeoutHandle.unref?.();
signal?.addEventListener("abort", onClose, { once: true });
if (signal?.aborted) onClose();
else onWake();
});
}
ensureAgentRunListener();
//#endregion
//#region src/gateway/chat-abort-ops.ts
function createChatAbortOps(context) {
return {
chatAbortControllers: context.chatAbortControllers,
chatRunState: context.chatRunState,
removeChatRun: context.removeChatRun,
agentRunSeq: context.agentRunSeq,
getRuntimeConfig: context.getRuntimeConfig,
broadcast: context.broadcast,
nodeSendToSession: context.nodeSendToSession,
onRunAborted: context.cancelRunBoundApprovals
};
}
//#endregion
export { setGatewayDedupeEntry as n, waitForAgentJob as r, createChatAbortOps as t };