openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
158 lines (157 loc) • 7.19 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { p as normalizeTrimmedStringList } from "./string-normalization-CECOqf64.mjs";
import { o as createAgentRunRestartAbortError, p as resolveAgentRunAbortLifecycleFields } from "./run-termination-DGv2EKii.mjs";
import { t as chatRunBelongsToAgent } from "./chat-run-owner-Ci8Mz1ow.mjs";
//#region src/gateway/chat-abort-diagnostics.ts
/** Diagnostics retain owner facts without copying arbitrary abort error text. */
function resolveChatAbortDiagnosticReason(signal, entry) {
if (entry?.abortDiagnosticReason) return entry.abortDiagnosticReason;
switch (entry?.abortStopReason) {
case "rpc":
case "stop":
case "timeout":
case "restart":
case "archive":
case "delete": return entry.abortStopReason;
default: return resolveAgentRunAbortLifecycleFields(signal).stopReason ?? "aborted";
}
}
//#endregion
//#region src/gateway/chat-queued-turns.ts
/**
* Gateway-owned cancel identity for turns that have been admitted to the
* followup/collect queue but are not yet (or no longer) active chat-send runs.
*
* Active runs stay in chatAbortControllers. Queued waits must NOT look like
* active runs (projection, timeout ownership, terminal dedupe), but they must
* remain abortable by authorized requesters after chat.send terminalizes.
*/
function isQueuedChatTurnForSession(turns, runId, scope) {
const queued = turns?.get(runId);
return Boolean(queued && queued.sessionId === scope.sessionId && queued.sessionKey === scope.sessionKey && queued.agentId === scope.agentId && queued.abortable !== false && !queued.controller.signal.aborted);
}
function createQueuedChatAbortSignalReason(stopReason) {
if (stopReason === "restart") return createAgentRunRestartAbortError();
return stopReason ? /* @__PURE__ */ new Error(`queued turn aborted: ${stopReason}`) : void 0;
}
function detachQueuedChatTurnAbortListener(entry) {
if (entry.abortListener) {
entry.controller.signal.removeEventListener("abort", entry.abortListener);
entry.abortListener = void 0;
}
}
function deleteQueuedChatTurnEntry(chatQueuedTurns, runId, entry) {
if (chatQueuedTurns.get(runId) !== entry) return false;
detachQueuedChatTurnAbortListener(entry);
return chatQueuedTurns.delete(runId);
}
function registerQueuedChatTurn(params) {
const runId = params.runId;
const sessionKey = normalizeOptionalString(params.sessionKey);
if (!runId || !sessionKey) return false;
if (params.controller.signal.aborted) return false;
const existing = params.chatQueuedTurns.get(runId);
if (existing && existing.controller === params.controller) return true;
if (existing) return false;
const entry = {
controller: params.controller,
sessionId: params.sessionId,
sessionKey,
agentId: normalizeOptionalString(params.agentId)?.toLowerCase(),
ownerConnId: normalizeOptionalString(params.ownerConnId),
ownerDeviceId: normalizeOptionalString(params.ownerDeviceId),
...params.holdPendingInputWithdrawal ? { holdPendingInputWithdrawal: params.holdPendingInputWithdrawal } : {}
};
params.chatQueuedTurns.set(runId, entry);
entry.abortListener = () => {
if (entry.abortable !== false && params.chatQueuedTurns.get(runId) === entry) try {
params.onAborted?.(resolveChatAbortDiagnosticReason(params.controller.signal, entry));
} finally {
deleteQueuedChatTurnEntry(params.chatQueuedTurns, runId, entry);
}
};
params.controller.signal.addEventListener("abort", entry.abortListener, { once: true });
return true;
}
function completeQueuedChatTurn(chatQueuedTurns, runId, controller) {
if (!runId) return false;
const entry = chatQueuedTurns.get(runId);
return entry?.controller === controller ? deleteQueuedChatTurnEntry(chatQueuedTurns, runId, entry) : false;
}
/**
* Retain the live run identity for idempotency while transferring cancellation
* to a collect aggregate. Completion still removes the entry.
*/
function retireQueuedChatTurnCancellation(chatQueuedTurns, runId, controller) {
const entry = runId ? chatQueuedTurns.get(runId) : void 0;
if (!entry || entry.controller !== controller) return false;
entry.abortable = false;
detachQueuedChatTurnAbortListener(entry);
return true;
}
/**
* Abort a single queued turn by runId. Does not authorize; caller must check.
* Returns false when missing or already aborted/removed.
*/
function abortQueuedChatTurnById(chatQueuedTurns, params) {
const runId = params.runId;
const sessionKey = normalizeOptionalString(params.sessionKey);
if (!runId || !sessionKey) return { aborted: false };
const entry = chatQueuedTurns.get(runId);
if (!entry || entry.abortable === false) return { aborted: false };
if (!params.allowSessionMismatch && entry.sessionKey !== sessionKey) return { aborted: false };
if (!entry.controller.signal.aborted) {
entry.abortDiagnosticReason = resolveChatAbortDiagnosticReason(entry.controller.signal, {
abortDiagnosticReason: params.diagnosticReason,
abortStopReason: params.stopReason
});
entry.controller.abort(createQueuedChatAbortSignalReason(params.stopReason));
}
deleteQueuedChatTurnEntry(chatQueuedTurns, runId, entry);
return { aborted: true };
}
/**
* List queued turns matching session keys / session ids / optional agent scope.
* Authorization is left to the caller.
*/
function listQueuedChatTurnsForSession(params) {
const sessionKeys = new Set(normalizeTrimmedStringList([...params.sessionKeys]));
const sessionIds = new Set(normalizeTrimmedStringList([...params.sessionIds ?? []]));
const agentId = normalizeOptionalString(params.agentId)?.toLowerCase();
const defaultAgentId = normalizeOptionalString(params.defaultAgentId)?.toLowerCase();
const matches = [];
for (const [runId, entry] of params.chatQueuedTurns) {
if (entry.abortable === false) continue;
if (!sessionKeys.has(entry.sessionKey) && !sessionIds.has(entry.sessionId)) continue;
if (params.requiredSessionId !== void 0 && (!sessionKeys.has(entry.sessionKey) || entry.sessionId !== params.requiredSessionId)) continue;
if (agentId && !chatRunBelongsToAgent({
agentId: entry.agentId,
sessionKey: entry.sessionKey,
defaultAgentId
}, agentId)) continue;
matches.push({
runId,
entry
});
}
return matches;
}
/**
* Abort all provided queued turns (already authorized by caller).
* Order: abort signals first, then remove from map, so drain cannot promote mid-loop.
*/
function abortQueuedChatTurns(chatQueuedTurns, matches, stopReason) {
const runIds = [];
for (const { runId, entry } of matches) {
if (chatQueuedTurns.get(runId) !== entry) continue;
if (!entry.controller.signal.aborted) {
entry.abortDiagnosticReason = resolveChatAbortDiagnosticReason(entry.controller.signal, { abortStopReason: stopReason });
entry.controller.abort(createQueuedChatAbortSignalReason(stopReason));
}
deleteQueuedChatTurnEntry(chatQueuedTurns, runId, entry);
runIds.push(runId);
}
return runIds;
}
//#endregion
export { listQueuedChatTurnsForSession as a, resolveChatAbortDiagnosticReason as c, isQueuedChatTurnForSession as i, abortQueuedChatTurns as n, registerQueuedChatTurn as o, completeQueuedChatTurn as r, retireQueuedChatTurnCancellation as s, abortQueuedChatTurnById as t };