UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

207 lines (206 loc) 9.59 kB
import { i as resolveGlobalSingleton } from "../global-singleton-Dc_stLtU.js"; import { l as normalizeOptionalString } from "../string-coerce-CIXf7egm.js"; import "../session-key-BnWWjqNc.js"; import { n as normalizeAccountId } from "../account-id-CETVCrTz.js"; import { _ as resolveSessionAgentId } from "../agent-scope-DbtJyKUL.js"; import { c as listCurrentConversationBindingRecordsBySession, i as registerSessionBindingAdapter, l as resolveCurrentConversationBindingRecord, o as unregisterSessionBindingAdapter, s as deleteCurrentConversationBindingRecordsBySession, u as updateCurrentConversationBindingRecord } from "../session-binding-service-DTXs8JDw.js"; import { t as isPluginOwnedBindingMetadata } from "../conversation-binding-metadata-CFOhDjMh.js"; import { t as resolveThreadBindingLifecycle } from "../thread-binding-lifecycle-DRD2ETVq.js"; import { a as resolveThreadBindingIdleTimeoutMsForChannel, s as resolveThreadBindingMaxAgeMsForChannel } from "../thread-bindings-policy-BzrpAQe3.js"; import { n as resolveThreadBindingFarewellText } from "../thread-bindings-messages-BnKw5QTY.js"; import { t as resolveThreadBindingConversationIdFromBindingId } from "../thread-binding-id-BL83Pq2C.js"; //#region src/infra/outbound/account-scoped-conversation-bindings.ts function getState(stateKey) { return resolveGlobalSingleton(stateKey, () => ({ managersByAccountId: /* @__PURE__ */ new Map() })); } function resolveBindingKey(accountId, conversationId) { return `${accountId}:${conversationId}`; } function toSessionBindingRecord(params) { const idleExpiresAt = params.idleTimeoutMs > 0 ? params.record.lastActivityAt + params.idleTimeoutMs : void 0; const maxAgeExpiresAt = params.maxAgeMs > 0 ? params.record.boundAt + params.maxAgeMs : void 0; const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null ? Math.min(idleExpiresAt, maxAgeExpiresAt) : idleExpiresAt ?? maxAgeExpiresAt; return { bindingId: resolveBindingKey(params.record.accountId, params.record.conversationId), targetSessionKey: params.record.targetSessionKey, targetKind: params.toSessionBindingTargetKind(params.record.targetKind), conversation: { channel: params.channel, accountId: params.record.accountId, conversationId: params.record.conversationId }, status: "active", boundAt: params.record.boundAt, expiresAt, metadata: { ...params.metadata, agentId: params.record.agentId, label: params.record.label, boundBy: params.record.boundBy, lastActivityAt: params.record.lastActivityAt, idleTimeoutMs: params.idleTimeoutMs, maxAgeMs: params.maxAgeMs } }; } /** 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 idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({ cfg: params.cfg, channel: params.channel, accountId }); const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({ cfg: params.cfg, channel: params.channel, accountId }); const asSessionBindingRecord = (record, metadata) => toSessionBindingRecord({ channel: params.channel, record, idleTimeoutMs, maxAgeMs, toSessionBindingTargetKind: params.toSessionBindingTargetKind, metadata }); const conversationRef = (conversationId) => ({ channel: params.channel, accountId, conversationId }); 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 bindConversationRecord = (input) => { const normalizedConversationId = input.conversationId.trim(); const normalizedTargetSessionKey = input.targetSessionKey.trim(); if (!normalizedConversationId || !normalizedTargetSessionKey) return null; const now = Date.now(); const { current } = updateCurrentConversationBindingRecord(conversationRef(normalizedConversationId), (existing) => { const previous = existing?.targetSessionKey === normalizedTargetSessionKey && existing.targetKind === input.targetKind ? existing : void 0; const existingLocal = previous ? asAccountBindingRecord(previous) : void 0; const metadata = { ...previous?.metadata, ...input.metadata }; const record = { accountId, conversationId: normalizedConversationId, targetKind: params.toStoredTargetKind(input.targetKind), targetSessionKey: normalizedTargetSessionKey, agentId: normalizeOptionalString(input.metadata?.agentId) ?? existingLocal?.agentId ?? (isPluginOwnedBindingMetadata(metadata) ? void 0 : resolveSessionAgentId({ config: params.cfg, sessionKey: normalizedTargetSessionKey })), label: normalizeOptionalString(input.metadata?.label) ?? existingLocal?.label, boundBy: normalizeOptionalString(input.metadata?.boundBy) ?? existingLocal?.boundBy, boundAt: now, lastActivityAt: now }; return asSessionBindingRecord(record, metadata); }); return current; }; const accountScope = { channel: params.channel, accountId }; 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 record = bindConversationRecord(input); 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({ channel: params.channel, accountId, adapter: sessionBindingAdapter }); } }; const sessionBindingAdapter = { channel: params.channel, accountId, capabilities: { placements: ["current"] }, bind: async (input) => { if (input.conversation.channel !== params.channel || input.placement === "child") return null; return bindConversationRecord({ conversationId: input.conversation.conversationId, targetKind: input.targetKind, targetSessionKey: input.targetSessionKey, metadata: input.metadata }); }, listBySession: (targetSessionKey) => listCurrentConversationBindingRecordsBySession(targetSessionKey, accountScope), resolveByConversation: (ref) => { if (ref.channel !== params.channel) return null; return resolveCurrentConversationBindingRecord(conversationRef(ref.conversationId)); }, touch: (bindingId, at) => { const conversationId = resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId }); if (conversationId) manager.touchConversation(conversationId, at); }, unbind: async (input) => { if (input.targetSessionKey?.trim()) return deleteCurrentConversationBindingRecordsBySession(input.targetSessionKey.trim(), accountScope); const conversationId = resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId: input.bindingId }); if (!conversationId) return []; const { previous } = updateCurrentConversationBindingRecord(conversationRef(conversationId), () => null); return previous ? [previous] : []; } }; 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 };