openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
247 lines (246 loc) • 12.3 kB
JavaScript
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 };