UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

247 lines (246 loc) • 12.3 kB
import { l as normalizeOptionalString } from "../string-coerce-1VuiLaaR.mjs"; import { a as resolveGlobalSingleton } from "../global-singleton-0m-JziJW.mjs"; import "../session-key-DY3MOX4N.mjs"; import { n as normalizeAccountId } from "../account-id-ifCOO7_9.mjs"; import { h as resolveSessionAgentId } from "../agent-scope-bJOokm5r.mjs"; import { f as SessionBindingError, n as applyCurrentConversationBindingBind, v as normalizeConversationRef } from "../current-conversation-bindings.kernel-DWqaaBL6.mjs"; import { C as expectedCurrentSessionBinding, E as nativeSessionBindingSelection, S as updateCurrentConversationBindingRecord, T as nativeSessionBindingListBySessions, _ as readCurrentConversationBindingSelectionAsync, b as resolveCurrentConversationBindingRecordAsync, c as registerSessionBindingAdapter, d as bindCurrentConversationRecordAsync, f as deleteCurrentConversationBindingRecordsBySession, g as listCurrentConversationBindingRecordsBySessionsAsync, h as listCurrentConversationBindingRecordsBySession, i as isSessionBindingAdapterCurrent, m as inspectCurrentConversationBindingRecordAsync, p as inspectCurrentConversationBindingRecord, u as unregisterSessionBindingAdapter, v as removeCurrentConversationBindingsAsync, w as nativeSessionBindingInspection, x as touchCurrentConversationBindingRecordAsync, y as resolveCurrentConversationBindingRecord } from "../session-binding-service-wvWqRl2A.mjs"; import { n as resolveThreadBindingLifecycle } from "../thread-binding-lifecycle-Cj9qM4Hm.mjs"; import { a as resolveThreadBindingIdleTimeoutMsForChannel, s as resolveThreadBindingMaxAgeMsForChannel } from "../thread-bindings-policy-Dn2kJAuU.mjs"; import { n as resolveThreadBindingFarewellText } from "../thread-bindings-messages-BiGDDOIC.mjs"; import { t as resolveThreadBindingConversationIdFromBindingId } from "../thread-binding-id-B9JLnrQ9.mjs"; import { n as projectThreadBindingRecord } from "../session-binding-adapter-BkaWpa2M.mjs"; //#region src/infra/outbound/account-scoped-conversation-bindings.ts function getState(stateKey) { return resolveGlobalSingleton(stateKey, () => ({ managersByAccountId: /* @__PURE__ */ new Map() })); } /** Creates a channel/account binding manager and registers it as a session-binding adapter. */ function createAccountScopedConversationBindingManager(params) { const accountId = normalizeAccountId(params.accountId); const state = getState(params.stateKey); const existingManager = state.managersByAccountId.get(accountId); if (existingManager) return existingManager; const accountScope = { channel: params.channel, accountId }; const policyScope = { cfg: params.cfg, ...accountScope }; const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel(policyScope); const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel(policyScope); const asSessionBindingRecord = (record, metadata) => { const idleExpiresAt = idleTimeoutMs > 0 ? record.lastActivityAt + idleTimeoutMs : void 0; const maxAgeExpiresAt = maxAgeMs > 0 ? record.boundAt + maxAgeMs : void 0; const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null ? Math.min(idleExpiresAt, maxAgeExpiresAt) : idleExpiresAt ?? maxAgeExpiresAt; return projectThreadBindingRecord(record, { conversation: { channel: params.channel, conversationId: record.conversationId }, targetKind: params.toSessionBindingTargetKind(record.targetKind), lifecycle: { expiresAt, idleTimeoutMs, maxAgeMs }, metadata: (lifecycleMetadata) => ({ ...metadata, ...lifecycleMetadata }) }); }; const conversationRef = (conversationId) => normalizeConversationRef({ ...accountScope, conversationId }); const conversationIdFromBinding = (bindingId) => resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId }); const asAccountBindingRecord = (record) => { const metadata = record.metadata; return { accountId, conversationId: record.conversation.conversationId, targetKind: params.toStoredTargetKind(record.targetKind), targetSessionKey: record.targetSessionKey, agentId: typeof metadata?.agentId === "string" ? metadata.agentId : void 0, label: typeof metadata?.label === "string" ? metadata.label : void 0, boundBy: typeof metadata?.boundBy === "string" ? metadata.boundBy : void 0, boundAt: record.boundAt, lastActivityAt: typeof metadata?.lastActivityAt === "number" ? metadata.lastActivityAt : record.boundAt }; }; const prepareBind = (input) => { const conversationId = input.conversationId.trim(); const targetSessionKey = input.targetSessionKey.trim(); if (!conversationId || !targetSessionKey) return null; const now = Date.now(); let inferredAgentId; let assertAgentResolved; try { inferredAgentId = resolveSessionAgentId({ config: params.cfg, sessionKey: targetSessionKey }); } catch (error) { assertAgentResolved = () => { throw error; }; } return { assertAgentResolved, record: asSessionBindingRecord({ accountId, conversationId, targetKind: params.toStoredTargetKind(input.targetKind), targetSessionKey, agentId: normalizeOptionalString(input.metadata?.agentId), label: normalizeOptionalString(input.metadata?.label), boundBy: normalizeOptionalString(input.metadata?.boundBy), boundAt: now, lastActivityAt: now }, input.metadata), accountPolicy: { inferredAgentId } }; }; const assertCurrent = () => { if (state.managersByAccountId.get(accountId) !== manager || !isSessionBindingAdapterCurrent(sessionBindingAdapter)) throw new SessionBindingError("BINDING_ADAPTER_UNAVAILABLE", "Account conversation binding manager is no longer active", { channel: params.channel, accountId }); }; const matchesAccount = (ref) => { const normalized = normalizeConversationRef(ref); return normalized.channel === params.channel && normalized.accountId === accountId; }; const readAccountBindingAsync = async (ref, inspect) => { if (!matchesAccount(ref)) return null; if (inspect) assertCurrent(); const record = inspect ? await inspectCurrentConversationBindingRecordAsync(conversationRef(ref.conversationId)) : await resolveCurrentConversationBindingRecordAsync(conversationRef(ref.conversationId), assertCurrent); assertCurrent(); return record; }; const manager = { accountId, getByConversationId: (conversationId) => { const record = resolveCurrentConversationBindingRecord(conversationRef(conversationId)); return record ? asAccountBindingRecord(record) : void 0; }, listBySessionKey: (targetSessionKey) => listCurrentConversationBindingRecordsBySession(targetSessionKey, accountScope).map(asAccountBindingRecord), bindConversation: (input) => { const prepared = prepareBind(input); const record = prepared ? updateCurrentConversationBindingRecord(prepared.record.conversation, (current) => applyCurrentConversationBindingBind(current, prepared, (requiresAgentId) => { if (requiresAgentId) prepared.assertAgentResolved?.(); })).current : null; return record ? asAccountBindingRecord(record) : null; }, touchConversation: (conversationId, at = Date.now()) => { const { current } = updateCurrentConversationBindingRecord(conversationRef(conversationId), (existing) => { if (!existing) return null; const updated = { ...asAccountBindingRecord(existing), lastActivityAt: at }; return asSessionBindingRecord(updated, existing.metadata); }); return current ? asAccountBindingRecord(current) : null; }, unbindConversation: (conversationId) => { const { previous } = updateCurrentConversationBindingRecord(conversationRef(conversationId), () => null); return previous ? asAccountBindingRecord(previous) : null; }, unbindBySessionKey: (targetSessionKey) => deleteCurrentConversationBindingRecordsBySession(targetSessionKey, accountScope).map(asAccountBindingRecord), stop: () => { if (state.managersByAccountId.get(accountId) === manager) state.managersByAccountId.delete(accountId); unregisterSessionBindingAdapter({ ...accountScope, adapter: sessionBindingAdapter }); } }; const sessionBindingAdapter = { channel: params.channel, accountId, capabilities: { placements: ["current"] }, listBySession: (targetSessionKey) => listCurrentConversationBindingRecordsBySession(targetSessionKey, accountScope), resolveByConversation: (ref) => ref.channel === params.channel ? resolveCurrentConversationBindingRecord(conversationRef(ref.conversationId)) : null, touch: (bindingId, at) => { const conversationId = conversationIdFromBinding(bindingId); if (conversationId) manager.touchConversation(conversationId, at); }, bind: async (input) => { if (input.conversation.channel !== params.channel || input.placement === "child") return null; const prepared = prepareBind({ conversationId: input.conversation.conversationId, targetKind: input.targetKind, targetSessionKey: input.targetSessionKey, metadata: input.metadata }); return prepared ? bindCurrentConversationRecordAsync({ ...prepared, expected: input[expectedCurrentSessionBinding] }, () => { assertCurrent(); input.assertCurrent?.(); }, prepared.assertAgentResolved) : null; }, unbind: async (input) => { if (input.targetSessionKey?.trim()) return removeCurrentConversationBindingsAsync({ targetSessionKey: input.targetSessionKey.trim(), scope: accountScope, genericOnly: false }, assertCurrent); const conversationId = conversationIdFromBinding(input.bindingId); return conversationId ? removeCurrentConversationBindingsAsync({ conversation: conversationRef(conversationId), expected: input[expectedCurrentSessionBinding] }, assertCurrent) : []; }, [nativeSessionBindingInspection]: { capture: (ref) => matchesAccount(ref) ? conversationRef(ref.conversationId) : null, assertCurrent }, [nativeSessionBindingSelection]: async (refs) => { const conversations = refs.map((ref) => matchesAccount(ref) ? conversationRef(ref.conversationId) : null); assertCurrent(); const records = await readCurrentConversationBindingSelectionAsync(conversations.filter((ref) => ref !== null), assertCurrent); assertCurrent(); let index = 0; return conversations.map((ref) => ref ? records[index++] ?? null : null); }, [nativeSessionBindingListBySessions]: (targetSessionKeys, context) => listCurrentConversationBindingRecordsBySessionsAsync(targetSessionKeys, accountScope, assertCurrent, context), inspectByConversation: (ref) => ref.channel === params.channel ? inspectCurrentConversationBindingRecord(conversationRef(ref.conversationId)) : null, inspectByConversationAsync: (ref) => readAccountBindingAsync(ref, true), resolveByConversationAsync: (ref) => readAccountBindingAsync(ref, false), touchAsync: async (bindingId, at) => { const conversationId = conversationIdFromBinding(bindingId); if (conversationId) await touchCurrentConversationBindingRecordAsync({ conversation: conversationRef(conversationId), bindingId, at: at === void 0 ? Date.now() : at, accountPolicy: { idleTimeoutMs, maxAgeMs, targetKinds: { subagent: params.toSessionBindingTargetKind(params.toStoredTargetKind("subagent")), session: params.toSessionBindingTargetKind(params.toStoredTargetKind("session")) } } }, assertCurrent); } }; registerSessionBindingAdapter(sessionBindingAdapter); state.managersByAccountId.set(accountId, manager); return manager; } /** Stops registered account-scoped adapters for one test key without clearing durable bindings. */ function resetAccountScopedConversationBindingsForTests(params) { const state = getState(params.stateKey); for (const manager of state.managersByAccountId.values()) manager.stop(); state.managersByAccountId.clear(); } //#endregion export { createAccountScopedConversationBindingManager, registerSessionBindingAdapter, resetAccountScopedConversationBindingsForTests, resolveThreadBindingConversationIdFromBindingId, resolveThreadBindingFarewellText, resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingLifecycle, resolveThreadBindingMaxAgeMsForChannel, unregisterSessionBindingAdapter };