openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
259 lines (258 loc) • 11.5 kB
JavaScript
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs";
import { y as uniqueStrings } from "./string-normalization-D6DKCpwE.mjs";
import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { l as resolveAgentIdFromSessionKey } from "./session-key-DY3MOX4N.mjs";
import { c as resolveSessionStorePathCore } from "./paths-Bonddxfk.mjs";
import { l as resolveOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs";
import { o as isGatewayExternallySupervised } from "./gateway-supervision-D9M_dluZ.mjs";
import { i as toDatabaseOptions } from "./session-accessor.sqlite-scope-helpers-CfDTAEsL.mjs";
import { o as resolveSqliteReadScope } from "./session-accessor.sqlite-scope-BkgwxatT.mjs";
import { s as loadSessionEntryReadOnlyInScope } from "./session-accessor.sqlite-exact-read-HBi2gDDN.mjs";
import { h as resolveSessionStorePathForScope } from "./session-transcript-worker-runtime-BdmdicNg.mjs";
import { t as getChannelPlugin } from "./registry-Bcrk1FVy.mjs";
import "./plugins-BXkKReMp.mjs";
import { i as inheritSessionCreationPolicy, n as buildInboundSessionCreationStamp } from "./session-entry-provenance-C8CX2hws.mjs";
import { r as mergeSessionEntry } from "./types-BGDhC4_W.mjs";
import { g as updateSessionLastRouteInScope, h as updateSessionLastRoute, s as loadSessionEntryReadOnly, t as applySessionEntryOperation } from "./session-accessor.sqlite-entry-DLa7_9ui.mjs";
import "./session-accessor-C3ljJ974.mjs";
import { n as stripOutboundTargetKindPrefix, r as stripTargetProviderPrefix } from "./channel-target-prefix-CXj70ihd.mjs";
import { o as resolveAgentRoute } from "./resolve-route-DeZAxBYa.mjs";
import { t as buildOutboundBaseSessionKey } from "./base-session-key-ByqjM-Be.mjs";
import "./inbound.runtime-Bhao6nTj.mjs";
//#region src/infra/outbound/outbound-session.ts
const FALLBACK_TARGET_KIND_PREFIXES = [
{
kind: "direct",
pattern: /^(user:|dm:)/i
},
{
kind: "channel",
pattern: /^(channel:|conversation:|thread:)/i
},
{
kind: "group",
pattern: /^(group:|room:)/i
}
];
function inferPeerKind(params) {
const resolvedKind = params.resolvedTarget?.kind;
if (resolvedKind === "user" || resolvedKind === "channel") return resolvedKind === "user" ? "direct" : "channel";
if (resolvedKind === "group") {
const chatTypes = (params.plugin ?? getChannelPlugin(params.channel))?.capabilities?.chatTypes ?? [];
return chatTypes.includes("channel") && !chatTypes.includes("group") ? "channel" : "group";
}
const plugin = params.plugin ?? getChannelPlugin(params.channel);
const strippedTarget = stripTargetProviderPrefix(params.target, params.channel);
const targets = uniqueStrings([params.target, strippedTarget].filter(Boolean));
for (const target of targets) {
const inferred = plugin?.messaging?.inferTargetChatType?.({ to: target });
if (inferred === "direct" || inferred === "group" || inferred === "channel") return inferred;
}
for (const target of targets) {
const fallback = FALLBACK_TARGET_KIND_PREFIXES.find(({ pattern }) => pattern.test(target));
if (fallback) return fallback.kind;
}
const chatTypes = new Set(plugin?.capabilities?.chatTypes?.filter((kind) => kind === "direct" || kind === "group" || kind === "channel"));
return chatTypes.size === 1 ? chatTypes.values().next().value ?? "direct" : "direct";
}
function resolveFallbackSession(params) {
const trimmed = stripTargetProviderPrefix(params.target, params.channel);
if (!trimmed) return null;
const peerKind = inferPeerKind(params);
const peerId = stripOutboundTargetKindPrefix(trimmed);
if (!peerId) return null;
const peer = {
kind: peerKind,
id: peerId
};
const baseSessionKey = buildOutboundBaseSessionKey({
cfg: params.cfg,
agentId: params.agentId,
channel: params.channel,
accountId: params.accountId,
peer
});
return {
sessionKey: baseSessionKey,
baseSessionKey,
recipientSessionExact: false,
peer,
chatType: peerKind,
from: peerKind === "direct" ? `${params.channel}:${peerId}` : `${params.channel}:${peerKind}:${peerId}`,
to: `${peerKind === "direct" ? "user" : "channel"}:${peerId}`
};
}
function resolveOutboundSessionDisplayName(params) {
const resolvedTarget = params.resolvedTarget;
const displayName = normalizeOptionalString(resolvedTarget?.display);
if (!displayName) return;
if (params.channel === "imessage" && resolvedTarget?.resolutionSource === "plugin") return displayName;
if (resolvedTarget?.resolutionSource !== "directory") return;
const target = stripTargetProviderPrefix(resolvedTarget.to, params.channel);
const identifier = stripOutboundTargetKindPrefix(target);
const normalizedDisplay = normalizeLowercaseStringOrEmpty(displayName);
return uniqueStrings([
resolvedTarget.to,
target,
identifier
]).map(normalizeLowercaseStringOrEmpty).filter(Boolean).includes(normalizedDisplay) ? void 0 : displayName;
}
async function resolveOutboundSessionRoute(params) {
const target = params.target.trim();
if (!target) return null;
const nextParams = {
...params,
target
};
const resolver = (params.plugin ?? getChannelPlugin(params.channel))?.messaging?.resolveOutboundSessionRoute;
const route = resolver ? await resolver(nextParams) : resolveFallbackSession(nextParams);
const displayName = resolveOutboundSessionDisplayName(params);
const namedRoute = route && displayName ? {
...route,
displayName
} : route;
if (!namedRoute || namedRoute.recipientSessionExact !== true) return namedRoute;
const bindingRoute = resolveAgentRoute({
cfg: params.cfg,
channel: params.channel,
defaultAgentId: params.agentId,
accountId: params.accountId,
peer: namedRoute.peer
});
const isDirect = namedRoute.peer.kind === "direct";
const globalScope = isDirect ? params.cfg.session?.dmScope ?? "main" : params.cfg.session?.groupScope ?? "per-group";
const bindingScope = isDirect ? bindingRoute.dmScope : bindingRoute.groupScope;
if (normalizeAgentId(bindingRoute.agentId) !== normalizeAgentId(params.agentId)) return {
...namedRoute,
recipientSessionExact: false
};
if (bindingScope === globalScope) return namedRoute;
if (namedRoute.sessionKey !== namedRoute.baseSessionKey && !namedRoute.sessionKey.startsWith(`${namedRoute.baseSessionKey}:`)) return null;
return {
...namedRoute,
sessionKey: `${bindingRoute.sessionKey}${namedRoute.sessionKey.slice(namedRoute.baseSessionKey.length)}`,
baseSessionKey: bindingRoute.sessionKey
};
}
/** Capture logical locators without opening a source store that a completed retry never needs. */
function captureOutboundSessionBinding(params) {
const destination = {
agentId: params.scope.agentId,
databaseAgentId: params.scope.databaseAgentId,
storePath: params.scope.storePath,
env: {
OPENCLAW_STATE_DIR: resolveStateDir(params.scope.env),
...isGatewayExternallySupervised(params.scope.env) ? { OPENCLAW_SUPERVISOR_MODE: "external" } : {}
}
};
if (!params.sourceSessionKey) return { destination };
const source = {
agentId: resolveAgentIdFromSessionKey(params.sourceSessionKey),
env: destination.env,
sessionKey: params.sourceSessionKey
};
return {
destination,
source: {
...source,
storePath: resolveSessionStorePathForScope({
...source,
env: params.scope.env
}, params.cfg)
}
};
}
/** Resolve source ownership only when binding is needed, before asynchronous plugin routing. */
function prepareOutboundSessionBinding(captured) {
const { destination, source } = captured;
if (!source) return { destination };
const target = toDatabaseOptions(resolveSqliteReadScope(source));
return {
destination,
source: {
...source,
databaseAgentId: target.agentId,
storePath: resolveOpenClawAgentSqlitePath(target)
}
};
}
async function persistOutboundSessionEntry(params, prepared) {
const storePath = prepared?.destination.storePath ?? resolveSessionStorePathCore(params.cfg.session?.store, { agentId: resolveAgentIdFromSessionKey(params.route.sessionKey) });
let creation = params.creation;
if (!creation && params.sourceSessionKey) {
const source = prepared?.source ? loadSessionEntryReadOnlyInScope(prepared.source) : loadSessionEntryReadOnly({
sessionKey: params.sourceSessionKey,
storePath: resolveSessionStorePathCore(params.cfg.session?.store, { agentId: resolveAgentIdFromSessionKey(params.sourceSessionKey) })
});
if (source?.sandbox === "required") creation = {
via: source.createdVia ?? "channel",
...inheritSessionCreationPolicy(source)
};
}
const ctx = {
From: params.route.from,
To: params.route.to,
SessionKey: params.route.sessionKey,
AccountId: params.accountId ?? void 0,
ChatType: params.route.chatType,
Provider: params.channel,
Surface: params.channel,
MessageThreadId: params.route.threadId,
OriginatingChannel: params.channel,
OriginatingTo: params.route.to,
NativeDirectUserId: params.route.peer.kind === "direct" ? params.route.peer.id : void 0,
NativeChannelId: params.route.peer.kind === "direct" ? void 0 : params.route.peer.id,
ConversationLabel: params.route.displayName,
GroupSubject: params.route.peer.kind === "direct" ? void 0 : params.route.displayName,
SessionCreation: creation
};
const update = {
storePath,
sessionKey: params.route.sessionKey,
createIfMissing: true,
channel: params.channel,
to: params.route.to,
accountId: params.accountId ?? void 0,
threadId: params.route.threadId,
ctx,
...params.assertCommitAllowed ? { assertCommitAllowed: params.assertCommitAllowed } : {},
...params.workerGuard ? { workerGuard: params.workerGuard } : {}
};
const entry = prepared ? await updateSessionLastRouteInScope({
...prepared.destination,
sessionKey: params.route.sessionKey
}, update) : await updateSessionLastRoute(update);
if (entry && params.mirrorSessionKey && params.mirrorSessionKey !== params.route.sessionKey) {
const fallbackEntry = mergeSessionEntry(void 0, {});
await applySessionEntryOperation({
sessionKey: params.mirrorSessionKey,
storePath: resolveSessionStorePathCore(params.cfg.session?.store, { agentId: resolveAgentIdFromSessionKey(params.mirrorSessionKey) })
}, {
kind: "ensure-identity",
sessionId: fallbackEntry.sessionId,
creation: buildInboundSessionCreationStamp({ SessionCreation: creation })
}, {
fallbackEntry,
preserveActivity: true,
workerGuard: params.workerGuard ?? {},
assertCommitAllowed: params.assertCommitAllowed
});
}
return entry;
}
/** Persists best-effort session metadata for an outbound-only route. */
async function ensureOutboundSessionEntry(params) {
try {
await persistOutboundSessionEntry(params);
} catch (error) {
if (params.creation?.sandbox === "required" || params.sourceSessionKey) createSubsystemLogger("outbound/session").warn(`Failed to preserve outbound session creation policy for ${params.route.sessionKey}: ${String(error)}`);
}
}
/** Persists the route required to bind an exact conversation address to local context. */
async function bindOutboundSessionEntry(params, prepared) {
if (!await persistOutboundSessionEntry(params, prepared)) throw new Error(`Failed to bind outbound session ${params.route.sessionKey}`);
}
//#endregion
export { resolveOutboundSessionRoute as a, prepareOutboundSessionBinding as i, captureOutboundSessionBinding as n, ensureOutboundSessionEntry as r, bindOutboundSessionEntry as t };