openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
237 lines (236 loc) • 10 kB
JavaScript
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs";
import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs";
import { s as resolveIncognitoOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-DVbbFaej.mjs";
import { u as withSessionEntryReadOnlyInWorker } from "./session-entry-read-runtime-0qc4qizi.mjs";
import { i as prepareSessionGenerationFacts, t as isSessionDeliveryGenerationRevokedError } from "./session-delivery-generation-D3PSr-Ms.mjs";
//#region src/plugins/memory-audience.ts
const MAX_LINEAGE_HOPS = 64;
const SESSION_ID_PATTERN = /^[a-f0-9-]{36}$/;
const hostAudiences = /* @__PURE__ */ new WeakSet();
const audienceRecords = /* @__PURE__ */ new WeakMap();
const audienceSessionKeys = /* @__PURE__ */ new WeakMap();
function releaseRecordLeases(record) {
for (const lease of record.leases.splice(0)) lease.release();
}
const collectedAudienceRecords = new FinalizationRegistry((record) => {
for (let current = record; current; current = current.parent) {
current.holders -= 1;
if (current.holders > 0) return;
releaseRecordLeases(current);
}
});
function isAgentSessionKey(sessionKey, agentId) {
return parseAgentSessionKey(sessionKey)?.agentId === agentId;
}
function isSessionId(sessionId) {
return typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId);
}
function resolveGenerationStorePath(agentId, sessionKey, storePath) {
return isIncognitoSessionKey(sessionKey) ? resolveIncognitoOpenClawAgentSqlitePath({ agentId }) : storePath;
}
async function readEntryInWorker(params) {
const read = await withSessionEntryReadOnlyInWorker({
agentId: params.agentId,
sessionKey: params.sessionKey,
storePath: params.storePath
}, params.assertCallerCurrent, async (result) => result);
if (!read.ok) throw read.error;
return read.value;
}
async function leaseSessionGeneration(params) {
try {
return await prepareSessionGenerationFacts({
agentId: params.agentId,
sessionKey: params.sessionKey,
storePath: resolveGenerationStorePath(params.agentId, params.sessionKey, params.storePath),
sessionId: params.entry?.sessionId ?? null,
lifecycleRevision: params.entry?.lifecycleRevision ?? null
});
} catch (error) {
if (isSessionDeliveryGenerationRevokedError(error)) return;
throw error;
}
}
function assertRecordCurrent(record) {
for (let current = record; current; current = current.parent) {
if (current.state === "released") throw new Error("memory audience was released by its owner");
if (current.state === "stale") throw new Error("memory audience is no longer current");
for (const lease of current.leases) try {
lease.assertCurrent();
} catch (error) {
if (isSessionDeliveryGenerationRevokedError(error)) {
current.state = "stale";
releaseRecordLeases(current);
throw new Error("memory audience is no longer current", { cause: error });
}
throw new Error("memory audience currency is unavailable; retry the operation", { cause: error });
}
}
}
function mintMemoryAudience(audience, sessionKey, record) {
const frozen = Object.freeze(audience);
hostAudiences.add(frozen);
audienceRecords.set(frozen, record);
audienceSessionKeys.set(frozen, sessionKey);
for (let current = record; current; current = current.parent) current.holders += 1;
collectedAudienceRecords.register(frozen, record);
return {
audience: frozen,
release: () => {
if (record.state !== "current") return;
record.state = "released";
releaseRecordLeases(record);
}
};
}
/**
* Resolve from the current entry already admitted by the host turn owner.
* Parent rows are read on the read-only worker; every hop holds a session
* generation lease that is rechecked after the final await, before minting.
*/
async function resolveMemoryAudienceFromEntry(params, admittedEntry) {
let { sessionKey, sessionId, senderIsOwner } = params;
const { agentId, storePath } = params;
const assertCallerCurrent = params.assertCallerCurrent ?? (() => {});
const seen = /* @__PURE__ */ new Set();
const record = {
leases: [],
state: "current",
holders: 0
};
let requiredLifecycleRevision;
const deny = (reason, legacyLineage) => {
releaseRecordLeases(record);
return {
status: "denied",
reason,
...legacyLineage ? { legacyLineage } : {}
};
};
try {
for (let depth = 0; depth < MAX_LINEAGE_HOPS; depth += 1) {
if (!isAgentSessionKey(sessionKey, agentId) || !isSessionId(sessionId) || seen.has(sessionKey)) return deny(`session lineage at ${sessionKey} is malformed, cyclic, or cross-agent`);
seen.add(sessionKey);
const entry = depth === 0 ? admittedEntry : await readEntryInWorker({
agentId,
sessionKey,
storePath,
assertCallerCurrent
});
if (!entry || entry.sessionId !== sessionId || depth > 0 && entry.lifecycleRevision !== requiredLifecycleRevision) return deny(`session ${sessionKey} is missing or no longer matches its recorded lineage`);
const lease = await leaseSessionGeneration({
agentId,
sessionKey,
storePath,
entry
});
if (!lease) return deny(`session ${sessionKey} changed while its memory audience was resolved`);
record.leases.push(lease);
if (entry.spawnedBy !== void 0) {
if (typeof entry.spawnedBySenderIsOwner !== "boolean") return deny(`spawned session ${sessionKey} predates memory lineage receipts (spawnedBySenderIsOwner), so it has no inherited memory audience. Respawn it from its parent session to restore memory access.`, true);
if (!entry.spawnedBy || entry.parentSessionKey !== void 0 && entry.parentSessionKey !== entry.spawnedBy || !isSessionId(entry.spawnedBySessionId) || entry.parentSessionLifecycleRevision === "") return deny(`spawned session ${sessionKey} has malformed lineage receipts`);
sessionKey = entry.spawnedBy;
sessionId = entry.spawnedBySessionId;
requiredLifecycleRevision = entry.parentSessionLifecycleRevision;
senderIsOwner = entry.spawnedBySenderIsOwner;
continue;
}
assertCallerCurrent();
assertRecordCurrent(record);
if (entry.chatType === "direct" && senderIsOwner === true) return {
status: "granted",
...mintMemoryAudience({
kind: "owner-private",
agentId
}, params.sessionKey, record)
};
if (entry.chatType === "direct" || entry.chatType === "group" || entry.chatType === "channel") return {
status: "granted",
...mintMemoryAudience({
kind: "conversation",
agentId,
sessionKey,
sessionId
}, params.sessionKey, record)
};
return deny(`root session ${sessionKey} has no durable chat type`);
}
return deny(`session lineage exceeds ${MAX_LINEAGE_HOPS} hops`);
} catch (error) {
releaseRecordLeases(record);
assertCallerCurrent();
return deny(`session storage could not verify the lineage: ${formatErrorMessage(error)}`);
}
}
/** Return whether a memory audience was minted by this host process. */
function isHostMemoryAudience(value) {
return typeof value === "object" && value !== null && hostAudiences.has(value);
}
/** Reject an audience presented outside its host-bound invocation session. */
function assertMemoryAudienceSession(audience, sessionKey) {
if (!isHostMemoryAudience(audience)) throw new Error("memory audience was not minted by this host");
if (audienceSessionKeys.get(audience) !== sessionKey) throw new Error("memory audience is bound to a different session");
}
/**
* Bind a child audience to the child session's current incarnation and to the
* parent's lineage, so a child reset or reassignment rejects it before provider I/O.
* A detached child has no row; its grant binds that absence instead.
*/
async function delegateMemoryAudience(audience, child) {
assertMemoryAudienceCurrent(audience);
const parent = audienceRecords.get(audience);
const { agentId } = audience;
const assertCallerCurrent = child.assertCallerCurrent ?? (() => {});
if (!isAgentSessionKey(child.sessionKey, agentId)) throw new Error("memory audience delegation requires a session for the same agent");
const entry = await readEntryInWorker({
agentId,
sessionKey: child.sessionKey,
storePath: child.storePath,
assertCallerCurrent
});
if (entry ? typeof entry.sessionId !== "string" || entry.sessionId.length === 0 : child.detached !== true) throw new Error(`memory audience delegation requires an existing child session: ${child.sessionKey}`);
const lease = await leaseSessionGeneration({
agentId,
sessionKey: child.sessionKey,
storePath: child.storePath,
entry
});
if (!lease) throw new Error(`child session changed during memory audience delegation: ${child.sessionKey}`);
const record = {
parent,
leases: [lease],
state: "current",
holders: 0
};
try {
assertCallerCurrent();
assertRecordCurrent(record);
} catch (error) {
releaseRecordLeases(record);
throw error;
}
return mintMemoryAudience({ ...audience }, child.sessionKey, record);
}
/** Reject a retained audience after any captured session incarnation changes. */
function assertMemoryAudienceCurrent(audience) {
const record = audienceRecords.get(audience);
if (!record) throw new Error("memory audience was not minted by this host");
assertRecordCurrent(record);
}
/**
* Check a memory caller's whole authority: its own currency, its audience's
* session binding and currency, and its abort signal. Final guards before
* provider I/O or prompt publication use this, never the caller's check alone.
*/
function assertMemoryCallerCurrent(context) {
context.assertCurrent();
const { authority } = context;
if (authority.kind === "session" && authority.audience) {
assertMemoryAudienceSession(authority.audience, authority.sessionKey);
assertMemoryAudienceCurrent(authority.audience);
}
context.signal?.throwIfAborted();
}
//#endregion
export { isHostMemoryAudience as a, delegateMemoryAudience as i, assertMemoryAudienceSession as n, resolveMemoryAudienceFromEntry as o, assertMemoryCallerCurrent as r, assertMemoryAudienceCurrent as t };