openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
497 lines (496 loc) • 19.9 kB
JavaScript
import { r as isMissingPathError } from "./errno-CkbDOfLk.js";
import { I as sameFileIdentity, t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js";
import "./agent-scope-config-DcbEhP0R.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import "./errors-Db3Ymjlb.js";
import { a as sha256Hex, o as sha256HexPrefixCore } from "./crypto-digest-C4hqTb_e.js";
import "./paths-CXdaYWF_.js";
import { t as KeyedAsyncQueue } from "./keyed-async-queue-CTreGrmR.js";
import { s as withFileLock } from "./file-lock-B0wiaenm.js";
import "./file-lock-DPooFrLa.js";
import { d as syncDirectoryIfSupported } from "./directory-durability-CINgXRM4.js";
import { T as resolveMemoryDreamingWorkspaces } from "./dreaming-DmJZL5-O.js";
import "./memory-state-BanUXlB7.js";
import "./agent-scope-runtime-h57Nypqc.js";
import "./memory-core-host-status-DAqa4uMk.js";
import { t as listStoredMemoryHostEvents } from "./event-store-CpPD6wve.js";
import path from "node:path";
import fs from "node:fs/promises";
function serializeMemoryHostEventExport(storedEvents) {
const lines = [];
let sizeBytes = 0;
for (const entry of storedEvents.toReversed()) {
const line = JSON.stringify(entry.value.event);
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
if (sizeBytes + lineBytes > 1048576) break;
lines.push(line);
sizeBytes += lineBytes;
}
return lines.toReversed().join("\n") + "\n";
}
//#endregion
//#region src/plugin-sdk/memory-host-event-export.ts
function isRejectedWorkspaceArtifactPath(error) {
if (!(error instanceof FsSafeError)) return false;
return error.code === "hardlink" || error.code === "not-file" || error.code === "outside-workspace" || error.code === "path-alias" || error.code === "path-mismatch" || error.code === "symlink";
}
function memoryHostEventExportOwnerContent(owner, content) {
return `${JSON.stringify({
schemaVersion: 3,
kind: "openclaw-memory-host-events-export",
stateHash: owner.stateHash,
workspaceHash: owner.workspaceHash,
...content.identity ? {
fileDev: String(content.identity.dev),
fileIno: String(content.identity.ino)
} : {},
...content.currentSha256 ? { contentSha256: content.currentSha256 } : {},
...content.pendingSha256 ? { pendingContentSha256: content.pendingSha256 } : {}
})}\n`;
}
async function writePinnedMemoryHostEventArtifact(handle, content) {
const bytes = Buffer.from(content, "utf8");
let offset = 0;
while (offset < bytes.length) {
const result = await handle.write(bytes, offset, bytes.length - offset, offset);
if (result.bytesWritten === 0) throw new Error("event export write made no progress");
offset += result.bytesWritten;
}
await handle.truncate(bytes.length);
await handle.chmod(384);
await handle.sync();
}
async function rewriteMemoryHostEventArtifactIfUnchanged(params) {
let observed;
try {
observed = await params.workspaceRoot.open(params.relativePath);
} catch (error) {
if (isMissingPathError(error) || isRejectedWorkspaceArtifactPath(error)) return false;
throw error;
}
try {
if (params.expectedIdentity ? !sameFileIdentity(params.expectedIdentity, observed.stat) : await observed.handle.readFile({ encoding: "utf8" }) !== params.expectedContent) return false;
let writable;
try {
writable = await params.workspaceRoot.openWritable(params.relativePath, {
mode: 384,
writeMode: "update"
});
} catch (error) {
if (isMissingPathError(error) || isRejectedWorkspaceArtifactPath(error)) return false;
throw error;
}
try {
if (!sameFileIdentity(observed.stat, writable.stat)) return false;
await writable.handle.writeFile(params.nextContent, { encoding: "utf8" });
await writable.handle.truncate(Buffer.byteLength(params.nextContent, "utf8"));
await writable.handle.chmod(384);
await writable.handle.sync();
} finally {
await writable.handle.close().catch(() => void 0);
}
let verified;
try {
verified = await params.workspaceRoot.open(params.relativePath);
} catch (error) {
if (isMissingPathError(error) || isRejectedWorkspaceArtifactPath(error)) return false;
throw error;
}
try {
return sameFileIdentity(observed.stat, verified.stat) && await verified.handle.readFile({ encoding: "utf8" }) === params.nextContent;
} finally {
await verified.handle.close().catch(() => void 0);
}
} finally {
await observed.handle.close().catch(() => void 0);
}
}
async function isMemoryHostEventArtifactAtIdentity(params) {
let opened;
try {
opened = await params.workspaceRoot.open(params.relativePath);
} catch (error) {
if (isMissingPathError(error) || isRejectedWorkspaceArtifactPath(error)) return false;
throw error;
}
try {
if (!sameFileIdentity(params.expectedIdentity, opened.stat)) return false;
return params.expectedContent === void 0 || (await opened.handle.readFile()).equals(Buffer.from(params.expectedContent, "utf8"));
} finally {
await opened.handle.close().catch(() => void 0);
}
}
async function publishMemoryHostEventArtifact(params) {
let writable;
try {
writable = await params.workspaceRoot.openWritable(params.owner.relativePath, {
mode: 384,
writeMode: "replace"
});
} catch (error) {
if (isMissingPathError(error) || isRejectedWorkspaceArtifactPath(error)) return;
throw error;
}
try {
if (!writable.createdForWrite) return;
const publishedIdentity = {
dev: writable.stat.dev,
ino: writable.stat.ino
};
await syncDirectoryIfSupported(path.dirname(params.absolutePath));
const identityPendingOwnerContent = memoryHostEventExportOwnerContent(params.owner, {
pendingSha256: params.contentSha256,
identity: publishedIdentity
});
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot: params.workspaceRoot,
relativePath: params.owner.ownerRelativePath,
expectedContent: params.expectedOwnerContent,
nextContent: identityPendingOwnerContent
})) return;
await syncDirectoryIfSupported(path.dirname(params.absolutePath));
await writePinnedMemoryHostEventArtifact(writable.handle, params.content);
if (!await isMemoryHostEventArtifactAtIdentity({
workspaceRoot: params.workspaceRoot,
relativePath: params.owner.relativePath,
expectedIdentity: publishedIdentity,
expectedContent: params.content
})) return;
await syncDirectoryIfSupported(path.dirname(params.absolutePath));
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot: params.workspaceRoot,
relativePath: params.owner.ownerRelativePath,
expectedContent: identityPendingOwnerContent,
nextContent: memoryHostEventExportOwnerContent(params.owner, {
currentSha256: params.contentSha256,
identity: publishedIdentity
})
})) return;
await syncDirectoryIfSupported(path.dirname(params.absolutePath));
if (!await isMemoryHostEventArtifactAtIdentity({
workspaceRoot: params.workspaceRoot,
relativePath: params.owner.relativePath,
expectedIdentity: publishedIdentity,
expectedContent: params.content
})) return;
return publishedIdentity;
} finally {
await writable.handle.close().catch(() => void 0);
}
}
//#endregion
//#region src/plugin-sdk/memory-host-core.ts
/**
* Public SDK facade for memory host runtime core and public artifact discovery.
*/
const MEMORY_HOST_EVENTS_FILENAME = "memory-host-events.jsonl";
const MEMORY_HOST_EVENTS_OWNER_FILENAME = ".openclaw-memory-host-events-owner.json";
const MAX_MEMORY_HOST_PUBLIC_EXPORT_EVENTS = 1e3;
const MEMORY_HOST_EVENT_EXPORT_LOCK_OPTIONS = {
retries: {
retries: 20,
factor: 1.3,
minTimeout: 25,
maxTimeout: 250,
randomize: true
},
stale: 3e4
};
const memoryHostEventExportQueue = new KeyedAsyncQueue();
function isWorkspaceWriteUnavailable(error, seen = /* @__PURE__ */ new Set()) {
if (!error || typeof error !== "object" || seen.has(error)) return false;
seen.add(error);
const code = error.code;
if (code === "EACCES" || code === "EEXIST" || code === "ENOTDIR" || code === "EPERM" || code === "EROFS" || error instanceof FsSafeError && (code === "not-file" || code === "not-removable")) return true;
if (error instanceof FsSafeError && error.category === "policy" && code !== "invalid-path") return false;
return isWorkspaceWriteUnavailable(error.cause, seen);
}
async function resolveMemoryHostEventExportOwner(workspaceDir) {
const requestedStateDir = path.resolve(resolveStateDir());
await fs.mkdir(requestedStateDir, {
recursive: true,
mode: 448
});
const stateDir = await fs.realpath(requestedStateDir);
const stateHash = sha256HexPrefixCore(stateDir, 32);
const workspaceHash = sha256HexPrefixCore(path.resolve(workspaceDir), 32);
const exportDirectory = path.posix.join("memory", "events", stateHash);
return {
queueKey: `${stateHash}\0${workspaceHash}`,
lockTarget: path.join(stateDir, `.memory-host-events-export-${workspaceHash}`),
relativePath: path.posix.join(exportDirectory, MEMORY_HOST_EVENTS_FILENAME),
ownerRelativePath: path.posix.join(exportDirectory, MEMORY_HOST_EVENTS_OWNER_FILENAME),
stateHash,
workspaceHash
};
}
async function readMemoryHostEventExportOwnership(workspaceRoot, owner) {
const content = await workspaceRoot.readText(owner.ownerRelativePath).catch((error) => {
if (isMissingPathError(error)) return;
if (isRejectedWorkspaceArtifactPath(error)) return null;
throw error;
});
if (content === null) return { kind: "foreign" };
if (content === void 0) return { kind: "missing" };
let parsed;
try {
parsed = JSON.parse(content);
} catch {
return { kind: "foreign" };
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.schemaVersion !== 3 || parsed.kind !== "openclaw-memory-host-events-export" || parsed.stateHash !== owner.stateHash || parsed.workspaceHash !== owner.workspaceHash || parsed.contentSha256 !== void 0 && typeof parsed.contentSha256 !== "string" || parsed.pendingContentSha256 !== void 0 && typeof parsed.pendingContentSha256 !== "string" || parsed.contentSha256 === void 0 && parsed.pendingContentSha256 === void 0 || parsed.fileDev === void 0 !== (parsed.fileIno === void 0) || parsed.fileDev !== void 0 && (typeof parsed.fileDev !== "string" || !/^\d+$/u.test(parsed.fileDev) || typeof parsed.fileIno !== "string" || !/^\d+$/u.test(parsed.fileIno))) return { kind: "foreign" };
const storedIdentity = typeof parsed.fileDev === "string" && typeof parsed.fileIno === "string" ? {
dev: BigInt(parsed.fileDev),
ino: BigInt(parsed.fileIno)
} : void 0;
let openedExport;
try {
openedExport = await workspaceRoot.open(owner.relativePath);
} catch (error) {
if (isMissingPathError(error)) openedExport = void 0;
else if (isRejectedWorkspaceArtifactPath(error)) return { kind: "foreign" };
else throw error;
}
if (!openedExport) return typeof parsed.pendingContentSha256 === "string" ? {
kind: "pending-missing",
ownerContent: content
} : {
kind: "orphan",
ownerContent: content
};
let exportContent;
const exportIdentity = {
dev: openedExport.stat.dev,
ino: openedExport.stat.ino
};
const identityOwned = storedIdentity !== void 0 && sameFileIdentity(storedIdentity, exportIdentity);
try {
if (openedExport.stat.size > 1048576) return identityOwned ? {
kind: "owned",
content: void 0,
identity: exportIdentity,
ownerContent: content,
needsFinalize: true
} : { kind: "foreign" };
exportContent = await openedExport.handle.readFile({ encoding: "utf8" });
} finally {
await openedExport.handle.close().catch(() => void 0);
}
const exportSha256 = sha256Hex(exportContent);
const currentSha256 = parsed.contentSha256;
const pendingSha256 = parsed.pendingContentSha256;
return identityOwned ? {
kind: "owned",
content: exportContent,
identity: exportIdentity,
ownerContent: content,
needsFinalize: exportSha256 !== currentSha256 || pendingSha256 !== void 0
} : { kind: "foreign" };
}
async function listMarkdownFilesRecursive(rootDir) {
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
const files = [];
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
files.push(...await listMarkdownFilesRecursive(fullPath));
continue;
}
if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
}
return files.toSorted((left, right) => left.localeCompare(right));
}
async function materializeMemoryHostEventExport(params) {
const requestedWorkspace = path.resolve(params.workspaceDir);
if (!(await fs.stat(requestedWorkspace).catch((error) => {
if (isMissingPathError(error)) return;
throw error;
}))?.isDirectory()) return;
const workspaceRoot = await root(requestedWorkspace, {
hardlinks: "reject",
mkdir: true,
mode: 384,
symlinks: "reject"
});
const workspaceKey = workspaceRoot.rootReal;
const owner = await resolveMemoryHostEventExportOwner(workspaceKey);
return memoryHostEventExportQueue.enqueue(owner.queueKey, async () => {
const absolutePath = path.join(workspaceKey, ...owner.relativePath.split("/"));
return await withFileLock(owner.lockTarget, MEMORY_HOST_EVENT_EXPORT_LOCK_OPTIONS, async () => {
const storedEvents = listStoredMemoryHostEvents({
workspaceDir: workspaceKey,
limit: MAX_MEMORY_HOST_PUBLIC_EXPORT_EVENTS
});
const ownership = await readMemoryHostEventExportOwnership(workspaceRoot, owner);
if (ownership.kind === "foreign") return;
if (storedEvents.length === 0 && ownership.kind !== "owned") return;
const content = storedEvents.length > 0 ? serializeMemoryHostEventExport(storedEvents) : "";
const contentSha256 = sha256Hex(content);
let publishedIdentity;
if (ownership.kind === "missing") {
if (await workspaceRoot.readText(owner.relativePath).catch((error) => {
if (isMissingPathError(error)) return;
if (isRejectedWorkspaceArtifactPath(error)) return null;
throw error;
}) !== void 0) return;
try {
const pendingOwnerContent = memoryHostEventExportOwnerContent(owner, { pendingSha256: contentSha256 });
await workspaceRoot.create(owner.ownerRelativePath, pendingOwnerContent, {
mkdir: true,
mode: 384
});
await syncDirectoryIfSupported(path.dirname(absolutePath));
publishedIdentity = await publishMemoryHostEventArtifact({
workspaceRoot,
owner,
absolutePath,
expectedOwnerContent: pendingOwnerContent,
content,
contentSha256
});
if (!publishedIdentity) return;
} catch (error) {
if (isWorkspaceWriteUnavailable(error)) return;
throw error;
}
} else if (ownership.kind === "pending-missing" || ownership.kind === "orphan") try {
const pendingOwnerContent = memoryHostEventExportOwnerContent(owner, { pendingSha256: contentSha256 });
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot,
relativePath: owner.ownerRelativePath,
expectedContent: ownership.ownerContent,
nextContent: pendingOwnerContent
})) return;
await syncDirectoryIfSupported(path.dirname(absolutePath));
publishedIdentity = await publishMemoryHostEventArtifact({
workspaceRoot,
owner,
absolutePath,
expectedOwnerContent: pendingOwnerContent,
content,
contentSha256
});
if (!publishedIdentity) return;
} catch (error) {
if (isWorkspaceWriteUnavailable(error)) return;
throw error;
}
else if (ownership.content !== content) {
publishedIdentity = ownership.identity;
try {
const updateOwnerContent = memoryHostEventExportOwnerContent(owner, {
pendingSha256: contentSha256,
identity: ownership.identity,
...ownership.content === void 0 ? {} : { currentSha256: sha256Hex(ownership.content) }
});
const currentOwnerContent = memoryHostEventExportOwnerContent(owner, {
currentSha256: contentSha256,
identity: ownership.identity
});
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot,
relativePath: owner.ownerRelativePath,
expectedContent: ownership.ownerContent,
nextContent: updateOwnerContent
})) return;
await syncDirectoryIfSupported(path.dirname(absolutePath));
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot,
relativePath: owner.relativePath,
expectedIdentity: ownership.identity,
nextContent: content
})) return;
await syncDirectoryIfSupported(path.dirname(absolutePath));
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot,
relativePath: owner.ownerRelativePath,
expectedContent: updateOwnerContent,
nextContent: currentOwnerContent
})) return;
await syncDirectoryIfSupported(path.dirname(absolutePath));
} catch (error) {
if (isWorkspaceWriteUnavailable(error)) return;
throw error;
}
} else if (ownership.needsFinalize) {
publishedIdentity = ownership.identity;
try {
if (!await rewriteMemoryHostEventArtifactIfUnchanged({
workspaceRoot,
relativePath: owner.ownerRelativePath,
expectedContent: ownership.ownerContent,
nextContent: memoryHostEventExportOwnerContent(owner, {
currentSha256: contentSha256,
identity: ownership.identity
})
})) return;
await syncDirectoryIfSupported(path.dirname(absolutePath));
} catch (error) {
if (isWorkspaceWriteUnavailable(error)) return;
throw error;
}
} else publishedIdentity = ownership.identity;
if (storedEvents.length === 0 || !publishedIdentity) return;
return await isMemoryHostEventArtifactAtIdentity({
workspaceRoot,
relativePath: owner.relativePath,
expectedIdentity: publishedIdentity,
expectedContent: content
}) ? {
absolutePath,
relativePath: owner.relativePath
} : void 0;
});
});
}
/** Lists public memory artifacts for one workspace, including notes and event logs. */
async function listMemoryWorkspacePublicArtifacts(params) {
const artifacts = [];
if (new Set((await fs.readdir(params.workspaceDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => entry.name)).has("MEMORY.md")) {
const absolutePath = path.join(params.workspaceDir, "MEMORY.md");
artifacts.push({
kind: "memory-root",
workspaceDir: params.workspaceDir,
relativePath: "MEMORY.md",
absolutePath,
agentIds: [...params.agentIds],
contentType: "markdown"
});
}
const memoryDir = path.join(params.workspaceDir, "memory");
for (const absolutePath of await listMarkdownFilesRecursive(memoryDir)) {
const relativePath = path.relative(params.workspaceDir, absolutePath).replace(/\\/g, "/");
artifacts.push({
kind: relativePath.startsWith("memory/dreaming/") ? "dream-report" : "daily-note",
workspaceDir: params.workspaceDir,
relativePath,
absolutePath,
agentIds: [...params.agentIds],
contentType: "markdown"
});
}
const eventExport = await materializeMemoryHostEventExport({ workspaceDir: params.workspaceDir });
if (eventExport) artifacts.push({
kind: "event-log",
workspaceDir: params.workspaceDir,
relativePath: eventExport.relativePath,
absolutePath: eventExport.absolutePath,
agentIds: [...params.agentIds],
contentType: "json"
});
const deduped = /* @__PURE__ */ new Map();
for (const artifact of artifacts) deduped.set(`${artifact.workspaceDir}\0${artifact.relativePath}\0${artifact.kind}`, artifact);
return [...deduped.values()];
}
/** Lists public memory artifacts across all configured memory workspaces. */
async function listMemoryHostPublicArtifacts(params) {
const workspaces = resolveMemoryDreamingWorkspaces(params.cfg);
const artifacts = [];
for (const workspace of workspaces) artifacts.push(...await listMemoryWorkspacePublicArtifacts({
workspaceDir: workspace.workspaceDir,
agentIds: workspace.agentIds
}));
return artifacts;
}
//#endregion
export { listMemoryHostPublicArtifacts as t };