UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

166 lines (165 loc) 8.9 kB
import { c as normalizeOptionalLowercaseString } from "./string-coerce-CIXf7egm.js"; import { r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { o as resolveSessionStorePathCore } from "./paths-CXdaYWF_.js"; import { c as getOpenClawAgentDatabaseIfOpen, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js"; import { W as upsertConversationIdentity, Y as parseStoredConversationRouteContext } from "./session-accessor.sqlite-entry-store-BxYl0nro.js"; import { i as getSessionKysely, m as toDatabaseOptions, s as resolveSqliteReadScope } from "./session-accessor.sqlite-scope-2KfMzb44.js"; import { S as parseSessionEntryJson } from "./session-accessor.sqlite-transcript-state-DwF2owZS.js"; //#region src/config/sessions/conversation-registry.ts const CONVERSATION_REF_PATTERN = /^conv_[a-f0-9]{32}$/u; function resolveConversationRegistryScope(params) { const configuredStore = params.config.session?.store; return { agentId: params.agentId, ...configuredStore ? { storePath: resolveSessionStorePathCore(configuredStore, { agentId: params.agentId }) } : {} }; } function normalizeConversationRef(value) { const normalized = value.trim().toLowerCase(); if (!CONVERSATION_REF_PATTERN.test(normalized)) throw new Error(`Invalid conversationRef: ${value}`); return normalized; } function mapConversationRow(row) { if (row.kind !== "direct" && row.kind !== "group" && row.kind !== "channel") return null; const role = row.role === "primary" || row.role === "participant" || row.role === "related" ? row.role : void 0; const hasCurrentBinding = (row.current_entry_json ? parseSessionEntryJson({ entry_json: row.current_entry_json }) : null)?.sessionId === row.current_session_id; const associationIsCurrent = hasCurrentBinding && row.associated_session_id === row.current_session_id; const routeContext = parseStoredConversationRouteContext(row.route_context_json, row.last_seen_at); return { associationIsCurrent, record: { conversationRef: row.conversation_id, channel: row.channel, accountId: row.account_id, kind: row.kind, peerId: row.peer_id, target: row.delivery_target, ...row.parent_conversation_id ? { parentConversationRef: row.parent_conversation_id } : {}, ...row.thread_id ? { threadId: row.thread_id } : {}, ...row.native_channel_id ? { nativeChannelId: row.native_channel_id } : {}, ...row.native_direct_user_id ? { nativeDirectUserId: row.native_direct_user_id } : {}, ...row.label ? { label: row.label } : {}, ...role && hasCurrentBinding && row.current_session_id && row.current_session_key ? { sessionId: row.current_session_id, sessionKey: row.current_session_key, role } : {}, ...role ? { observedFromSession: true } : {}, ...routeContext ? { routeContextObserved: true } : {}, ...routeContext?.context ? { routeContext: routeContext.context } : {}, firstSeenAt: row.first_seen_at ?? row.conversation_created_at, lastSeenAt: row.last_seen_at ?? row.conversation_updated_at } }; } function selectConversationRows(scope, options = {}) { const resolved = resolveSqliteReadScope({ agentId: scope.agentId, ...scope.env ? { env: scope.env } : {}, ...scope.storePath ? { storePath: scope.storePath } : {} }); const databaseOptions = toDatabaseOptions(resolved); const readRows = (database) => { let query = getSessionKysely(database.db).selectFrom("conversations as c").leftJoin("session_conversations as sc", "sc.conversation_id", "c.conversation_id").leftJoin("session_windows as s", "s.session_id", "sc.session_id").leftJoin("session_nodes as sn", "sn.session_key", "s.session_key").select([ "c.conversation_id", "c.channel", "c.account_id", "c.kind", "c.peer_id", "c.delivery_target", "c.parent_conversation_id", "c.thread_id", "c.native_channel_id", "c.native_direct_user_id", "c.label", "c.created_at as conversation_created_at", "c.updated_at as conversation_updated_at", "sc.role", "sc.route_context_json", "sc.first_seen_at", "sc.last_seen_at", "s.session_id as associated_session_id", "sn.current_session_id as current_session_id", "sn.entry_json as current_entry_json", "sn.session_key as current_session_key" ]); const channel = normalizeOptionalLowercaseString(options.channel); if (channel) query = query.where("c.channel", "=", channel); if (options.conversationRef) query = query.where("c.conversation_id", "=", normalizeConversationRef(options.conversationRef)); if (options.primarySession) query = query.where("s.session_id", "=", options.primarySession.sessionId).where("s.session_key", "=", options.primarySession.sessionKey).where("sn.current_session_id", "=", options.primarySession.sessionId).where("sn.entry_valid", "=", 1).whereRef("s.primary_conversation_id", "=", "c.conversation_id").where("sc.role", "=", "primary"); if (options.currentBindingOnly) query = query.whereRef("s.session_id", "=", "sn.current_session_id").where("sn.entry_valid", "=", 1).where((eb) => eb.or([eb("sc.role", "=", "participant"), eb.and([eb("sc.role", "=", "primary"), eb("s.primary_conversation_id", "=", eb.ref("c.conversation_id"))])])); const rows = executeSqliteQuerySync(database.db, query.orderBy((eb) => eb.fn.coalesce("sc.last_seen_at", "c.updated_at"), "desc").orderBy("sn.updated_at", "desc")).rows; const unique = /* @__PURE__ */ new Map(); for (const row of rows) { const mapped = mapConversationRow(row); if (!mapped) continue; const existing = unique.get(mapped.record.conversationRef); if (!existing) { unique.set(mapped.record.conversationRef, mapped); continue; } if (!existing.associationIsCurrent && mapped.associationIsCurrent && mapped.record.sessionId && mapped.record.sessionKey && mapped.record.role) { const { routeContext: _staleRouteContext, routeContextObserved: _staleRouteContextObserved, ...existingRecord } = existing.record; unique.set(mapped.record.conversationRef, { associationIsCurrent: true, record: { ...existingRecord, sessionId: mapped.record.sessionId, sessionKey: mapped.record.sessionKey, role: mapped.record.role, ...mapped.record.routeContextObserved ? { routeContextObserved: true } : {}, ...mapped.record.routeContext ? { routeContext: mapped.record.routeContext } : {} } }); } } const values = [...unique.values()].map(({ record }) => record); return options.limit === void 0 ? values : values.slice(0, options.limit); }; const held = getOpenClawAgentDatabaseIfOpen(databaseOptions); if (held?.db.isTransaction) return readRows(held); const read = withOpenClawAgentDatabaseReadOnly(readRows, databaseOptions); return read.found ? read.value : []; } /** Catalogs routable addresses without creating model-context sessions. */ function registerConversationAddresses(scope, identities, discoveredAt = Date.now()) { if (identities.length === 0) return; const resolved = resolveSqliteReadScope({ agentId: scope.agentId, ...scope.env ? { env: scope.env } : {}, ...scope.storePath ? { storePath: scope.storePath } : {} }); const database = openOpenClawAgentDatabase(toDatabaseOptions(resolved)); for (const identity of identities) upsertConversationIdentity(database, identity, discoveredAt); } /** Lists stable external addresses for one agent, newest activity first. */ function listConversations(scope, options = {}) { return selectConversationRows(scope, options); } /** Resolves an opaque address to one exact channel target and its context binding, when present. */ function resolveConversation(scope, conversationRef) { return selectConversationRows(scope, { conversationRef: normalizeConversationRef(conversationRef), limit: 1 })[0]; } /** Reads only an authoritative association on an address's current session window. */ function resolveCurrentConversationSession(scope, conversationRef) { const [conversation] = selectConversationRows(scope, { conversationRef: normalizeConversationRef(conversationRef), currentBindingOnly: true, limit: 1 }); return conversation?.sessionKey && conversation.sessionId ? { sessionKey: conversation.sessionKey, sessionId: conversation.sessionId } : void 0; } /** Reads only the primary address bound to this exact current session window. */ function resolveCurrentSessionPrimaryConversation(scope) { const [conversation] = selectConversationRows(scope, { primarySession: scope }); return conversation?.sessionId === scope.sessionId && conversation.sessionKey === scope.sessionKey ? conversation : void 0; } //#endregion export { resolveCurrentConversationSession as a, resolveConversationRegistryScope as i, registerConversationAddresses as n, resolveCurrentSessionPrimaryConversation as o, resolveConversation as r, listConversations as t };