openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
242 lines (241 loc) • 10.3 kB
JavaScript
import { C as resolveExpiresAtMsFromDurationMs, S as resolveDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { c as resolveDefaultAgentId } from "./agent-scope-config-CgCYpZfK.js";
import { i as emitAgentEvent } from "./agent-events-C1B8VhOg.js";
import { n as isAbortRequestText } from "./abort-primitives-CNNnD8oN.js";
import { r as jsonUtf8Bytes } from "./json-utf8-bytes-C14lActR.js";
import { n as projectLiveAssistantBufferedText } from "./live-chat-projector-CnA2pXR3.js";
//#region src/gateway/chat-abort.ts
const DEFAULT_CHAT_RUN_ABORT_GRACE_MS = 6e4;
function isChatStopCommandText(text) {
return isAbortRequestText(text);
}
function createChatAbortSignalReason(stopReason) {
if (stopReason !== "timeout") return;
const reason = /* @__PURE__ */ new Error("chat run timed out");
reason.name = "TimeoutError";
return reason;
}
function resolveChatRunExpiresAtMs(params) {
const { now, timeoutMs, graceMs = DEFAULT_CHAT_RUN_ABORT_GRACE_MS, minMs = 2 * 6e4, maxMs = 1440 * 6e4 } = params;
const safeNow = asDateTimestampMs(now);
if (safeNow === void 0) return 0;
const target = resolveExpiresAtMsFromDurationMs(Math.max(0, timeoutMs) + graceMs, { nowMs: safeNow });
const min = resolveExpiresAtMsFromDurationMs(minMs, { nowMs: safeNow });
const max = resolveExpiresAtMsFromDurationMs(maxMs, { nowMs: safeNow });
if (target === void 0 || min === void 0 || max === void 0) return 0;
return Math.min(max, Math.max(min, target));
}
function resolveAgentRunExpiresAtMs(params) {
const graceMs = Math.max(0, params.graceMs ?? DEFAULT_CHAT_RUN_ABORT_GRACE_MS);
return resolveChatRunExpiresAtMs({
now: params.now,
timeoutMs: params.timeoutMs,
graceMs,
minMs: graceMs,
maxMs: Math.max(0, params.timeoutMs) + graceMs
});
}
function registerChatAbortController(params) {
const controller = new AbortController();
const cleanup = () => {
if (params.chatAbortControllers.get(params.runId)?.controller === controller) params.chatAbortControllers.delete(params.runId);
};
if (!params.sessionKey || params.chatAbortControllers.has(params.runId)) return {
controller,
registered: false,
cleanup
};
const rawNow = params.now ?? Date.now();
const now = resolveDateTimestampMs(rawNow, 0);
const explicitExpiresAtMs = params.expiresAtMs === void 0 ? void 0 : asDateTimestampMs(params.expiresAtMs) ?? 0;
const entry = {
controller,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
agentId: normalizeActiveAgentId(params.agentId),
startedAtMs: now,
expiresAtMs: explicitExpiresAtMs ?? resolveChatRunExpiresAtMs({
now: rawNow,
timeoutMs: params.timeoutMs
}),
ownerConnId: params.ownerConnId,
ownerDeviceId: params.ownerDeviceId,
providerId: normalizeProviderIdForActiveRun(params.providerId),
authProviderId: normalizeProviderIdForActiveRun(params.authProviderId),
controlUiVisible: params.controlUiVisible,
projectSessionActive: true,
kind: params.kind
};
params.chatAbortControllers.set(params.runId, entry);
return {
controller,
registered: true,
entry,
cleanup
};
}
function normalizeProviderIdForActiveRun(providerId) {
return providerId?.trim().toLowerCase() || void 0;
}
function normalizeActiveAgentId(agentId) {
return agentId?.trim().toLowerCase() || void 0;
}
/**
* Snapshot the live assistant text of any in-flight run for a session+agent. Used
* by chat.history so a run that kept streaming while the client was switched away
* — whose deltas the gateway delivered to a delivery key this client is no longer
* subscribed to — is restored on switch-back.
*
* Matches a run the same way sessions.list's active-run projection does: an abort
* entry can hold the requested key while chat run state holds the canonical store
* key, so accept a match on EITHER `requestedSessionKey` or `canonicalSessionKey`,
* scoping the shared "global" session by agent. Only runs still projected active
* (`projectSessionActive !== false`, matching sessions.list; the terminal lifecycle
* flips it to false), not aborted, and visible chat-send runs are returned, so a
* finalized run — already in persisted history — is not duplicated and hidden
* agent runs cannot be adopted by chat clients that will not receive their final
* events.
*/
function resolveInFlightRunSnapshot(params) {
const matchesKey = (entry, key) => {
if (entry.sessionKey !== key) return false;
if (key !== "global") return true;
const requestedAgentId = normalizeActiveAgentId(params.agentId) ?? normalizeActiveAgentId(params.defaultAgentId);
if (!requestedAgentId) return false;
return (normalizeActiveAgentId(entry.agentId) ?? normalizeActiveAgentId(params.defaultAgentId)) === requestedAgentId;
};
if (!(params.chatAbortControllers instanceof Map)) return;
let best;
for (const [runId, entry] of params.chatAbortControllers) {
if (entry.projectSessionActive === false || entry.controlUiVisible === false || entry.controller.signal.aborted || entry.kind === "agent") continue;
if (!matchesKey(entry, params.requestedSessionKey) && !matchesKey(entry, params.canonicalSessionKey)) continue;
const newer = best === void 0 || entry.startedAtMs > best.startedAtMs;
const tie = best !== void 0 && entry.startedAtMs === best.startedAtMs && runId > best.runId;
if (newer || tie) best = {
runId,
startedAtMs: entry.startedAtMs
};
}
if (best === void 0) return;
const projected = projectLiveAssistantBufferedText(params.chatRunBuffers?.get(best.runId) ?? "", { suppressLeadFragments: true });
return {
runId: best.runId,
text: projected.suppress ? "" : projected.text
};
}
function boundInFlightRunSnapshotForChatHistory(params) {
if (!params.snapshot?.text) return params.snapshot;
if (jsonUtf8Bytes(params.messages) + jsonUtf8Bytes(params.snapshot) <= params.maxBytes) return params.snapshot;
return {
runId: params.snapshot.runId,
text: ""
};
}
function abortTrackedChatRunById(ops, params) {
return abortChatRunById({
chatAbortControllers: ops.chatAbortControllers,
chatRunBuffers: ops.chatRunBuffers,
chatAbortedRuns: ops.chatRunState.abortedRuns,
clearChatRunState: ops.chatRunState.clearRun,
removeChatRun: ops.removeChatRun,
agentRunSeq: ops.agentRunSeq,
broadcast: ops.broadcast,
nodeSendToSession: ops.nodeSendToSession
}, params);
}
function resolveChatAbortDeliverySessionKeys(ops, sessionKey, agentId) {
if (sessionKey !== "global") return [sessionKey];
const scopedAgentId = normalizeActiveAgentId(agentId);
if (!scopedAgentId) return [sessionKey];
const keys = [`agent:${scopedAgentId}:global`];
const cfg = ops.getRuntimeConfig?.();
const defaultAgentId = cfg ? resolveDefaultAgentId(cfg) : void 0;
if (defaultAgentId && scopedAgentId === defaultAgentId) keys.push(sessionKey);
return keys;
}
function broadcastChatAborted(ops, params) {
const { runId, sessionKey, stopReason, partialText } = params;
const defaultGlobalAgentId = sessionKey === "global" ? normalizeActiveAgentId(resolveDefaultGlobalAgentId(ops)) : void 0;
const payloadAgentId = sessionKey === "global" ? normalizeActiveAgentId(params.agentId) ?? defaultGlobalAgentId : normalizeActiveAgentId(params.agentId);
const payload = {
runId,
sessionKey,
...payloadAgentId ? { agentId: payloadAgentId } : {},
seq: (ops.agentRunSeq.get(runId) ?? 0) + 1,
state: "aborted",
stopReason,
message: partialText ? {
role: "assistant",
content: [{
type: "text",
text: partialText
}],
timestamp: Date.now()
} : void 0
};
ops.broadcast("chat", payload);
for (const deliverySessionKey of resolveChatAbortDeliverySessionKeys(ops, sessionKey, payloadAgentId)) ops.nodeSendToSession(deliverySessionKey, "chat", payload);
}
function resolveDefaultGlobalAgentId(ops) {
const cfg = ops.getRuntimeConfig?.();
return cfg ? resolveDefaultAgentId(cfg) : void 0;
}
function abortChatRunById(ops, params) {
const { runId, sessionKey, stopReason } = params;
const active = ops.chatAbortControllers.get(runId);
if (!active) return { aborted: false };
if (active.sessionKey !== sessionKey) return { aborted: false };
const bufferedText = ops.chatRunBuffers.get(runId);
const partialText = bufferedText && bufferedText.trim() ? bufferedText : void 0;
ops.chatAbortedRuns.set(runId, Date.now());
if (stopReason) active.abortStopReason = stopReason;
active.controller.abort(createChatAbortSignalReason(stopReason));
ops.chatAbortControllers.delete(runId);
ops.clearChatRunState(runId);
const removed = ops.removeChatRun(runId, runId, sessionKey);
if (active.controlUiVisible !== false) broadcastChatAborted(ops, {
runId,
sessionKey,
agentId: active.agentId,
stopReason,
partialText
});
emitAgentEvent({
runId,
sessionKey,
agentId: active.agentId,
stream: "lifecycle",
data: {
phase: "end",
status: "cancelled",
aborted: true,
stopReason,
startedAt: active.startedAtMs,
endedAt: Date.now()
}
});
ops.agentRunSeq.delete(runId);
if (removed?.clientRunId) ops.agentRunSeq.delete(removed.clientRunId);
return { aborted: true };
}
function updateChatRunProvider(chatAbortControllers, params) {
const entry = chatAbortControllers.get(params.runId);
if (!entry) return false;
entry.providerId = normalizeProviderIdForActiveRun(params.providerId);
entry.authProviderId = normalizeProviderIdForActiveRun(params.authProviderId);
return true;
}
function abortChatRunsForProvider(ops, params) {
const providerId = normalizeProviderIdForActiveRun(params.providerId);
if (!providerId) return { runIds: [] };
const matches = [...ops.chatAbortControllers.entries()].filter(([, entry]) => normalizeProviderIdForActiveRun(entry.authProviderId) === providerId || normalizeProviderIdForActiveRun(entry.providerId) === providerId);
const runIds = [];
for (const [runId, entry] of matches) if (abortChatRunById(ops, {
runId,
sessionKey: entry.sessionKey,
stopReason: params.stopReason
}).aborted) runIds.push(runId);
return { runIds };
}
//#endregion
export { isChatStopCommandText as a, resolveInFlightRunSnapshot as c, boundInFlightRunSnapshotForChatHistory as i, updateChatRunProvider as l, abortChatRunsForProvider as n, registerChatAbortController as o, abortTrackedChatRunById as r, resolveAgentRunExpiresAtMs as s, abortChatRunById as t };