UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

172 lines (171 loc) • 6.58 kB
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { t as AsyncWorkScope } from "./async-work-scope-DDiH0P7W.mjs"; import { v as registerOpenClawStateDatabaseAsyncResource } from "./openclaw-state-db-cache-CmYLFRSt.mjs"; import { o as runWithSqliteWorkerStateContext, t as captureSqliteWorkerStateContext } from "./sqlite-worker-state-context-G3Xbcj1W.mjs"; import { r as captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context-BvvgY6-t.mjs"; import { t as runBestEffortCleanup } from "./non-fatal-cleanup-BoCq8AND.mjs"; import { l as releaseSessionSourceAuthorities } from "./session-source-authority-h1UO3u0I.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; //#region src/talk/client-voice-session-lifecycle.ts const current = new AsyncLocalStorage(); const lifetimes = /* @__PURE__ */ new Map(); function createLifetime({ admission }) { let work = new AsyncWorkScope(); const scopes = /* @__PURE__ */ new Set([work]); let gateways = 0; let closing = false; const drain = (scope) => AsyncWorkScope.runWhenAllIdle(() => [scope], async () => { await scope.drain(); scopes.delete(scope); }); const owner = { matchesDatabase(context) { const identity = context.admission.identity; return admission.identity.key === identity.key && admission.identity.birthtime === identity.birthtime; }, retainGateway() { if (closing) { work = new AsyncWorkScope(); scopes.add(work); closing = false; } gateways += 1; let released = false; let pending; const beginClose = () => { if (!released) { released = true; if (--gateways === 0) { closing = true; pending = drain(work); } } }; return { beginClose, drain: () => { beginClose(); return pending ?? Promise.resolve(); } }; }, assertOpen(inherited) { if (closing && !inherited) throw new Error("Voice session persistence admission is closed"); }, scope: () => work }; const unregister = registerOpenClawStateDatabaseAsyncResource({ async close(identity) { if (!identity || identity.key === admission.identity.key || identity.canonicalPath === admission.identity.canonicalPath) { closing = true; await Promise.all([...scopes].map(drain)); if (gateways > 0) { work = new AsyncWorkScope(); scopes.add(work); closing = false; } else { for (const [key, candidate] of lifetimes) if (candidate === owner) lifetimes.delete(key); unregister(); } } } }); return owner; } function lifetime(context) { let owner = lifetimes.get(context.admission.coordinationKey); if (!owner) { owner = [...lifetimes.values()].find((candidate) => candidate.matchesDatabase(context)); owner ??= createLifetime(context); lifetimes.set(context.admission.coordinationKey, owner); } return owner; } /** Retain the original owner for later admissions without retaining an accepted permit. */ function captureClientVoiceSessionSettlementContext(env) { const inherited = current.getStore()?.context; const selected = inherited && inherited.environment.OPENCLAW_STATE_DIR === env.OPENCLAW_STATE_DIR ? inherited : captureOpenClawStateWorkerContext({ env }); return { ...captureSqliteWorkerStateContext(selected), admission: selected.admission }; } /** Retain accepted work before a bounded queue or provider can yield. */ function captureClientVoiceSessionSettlement(source) { const inherited = current.getStore(); const selected = source ?? inherited?.context ?? captureOpenClawStateWorkerContext(); const inheritedSource = inherited && matchesSettlementContext(inherited.context, selected) ? inherited : void 0; if (inheritedSource && !inheritedSource.active) throw new Error("Voice session persistence lost its accepted owner"); selected.admission.assertCurrent(); const context = inheritedSource?.context ?? { ...captureSqliteWorkerStateContext(selected), admission: selected.admission }; const owner = lifetime(context); owner.assertOpen(Boolean(inheritedSource)); const scope = inheritedSource?.scope ?? owner.scope(); const settled = createDeferredCore(); scope.track(() => settled.promise); const operation = { context, scope, active: true }; return { run(run) { if (!operation.active) throw new Error("Voice session persistence lost its accepted owner"); context.admission.assertCurrent(); return scope.run(() => current.run(operation, () => runWithSqliteWorkerStateContext(context, run))); }, release() { operation.active = false; settled.resolve(); } }; } async function withClientVoiceSessionSettlement(run, onAdmissionFailure, source) { let accepted; let entered = false; try { accepted = captureClientVoiceSessionSettlement(source); return await accepted.run(() => { entered = true; return run(); }); } catch (error) { if (!entered && onAdmissionFailure) return await onAdmissionFailure(error); throw error; } finally { accepted?.release(); } } function matchesSettlementContext(left, right) { return left.admission.coordinationKey === right.admission.coordinationKey && left.admission.identity.key === right.admission.identity.key && left.admission.identity.birthtime === right.admission.identity.birthtime; } function assertClientVoiceSessionSettlementCurrent(source) { const accepted = current.getStore(); if (accepted && (!source || matchesSettlementContext(accepted.context, source))) { if (!accepted.active) throw new Error("Voice session persistence lost its accepted owner"); accepted.context.admission.assertCurrent(); } } function prepareClientVoiceSessionClose() { return lifetime(captureOpenClawStateWorkerContext()).retainGateway(); } /** Await all cleanup without making an acknowledged operation appear replayable. */ async function withClientVoiceSessionResources(resources, operation) { const errors = []; const release = () => releaseSessionSourceAuthorities(typeof resources === "function" ? resources() : resources, errors); try { return await operation(); } catch (error) { errors.push(error); throw error; } finally { if (errors.length > 0) await release(); else await runBestEffortCleanup({ cleanup: release, onError: (error) => console.warn("[talk] voice session operation completed before cleanup failed", error) }); } } //#endregion export { withClientVoiceSessionResources as a, prepareClientVoiceSessionClose as i, captureClientVoiceSessionSettlement as n, withClientVoiceSessionSettlement as o, captureClientVoiceSessionSettlementContext as r, assertClientVoiceSessionSettlementCurrent as t };