UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

316 lines (315 loc) 13.9 kB
import { n as replaceFileAtomic } from "./replace-file-BAJ-TWzD.js"; import { u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js"; import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js"; import { E as tableExists } from "./openclaw-state-db-cache-C7ljO0xP.js"; import { p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js"; import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js"; import "./sqlite-runtime-BpLlraG0.js"; import "./security-runtime-Ckf0kc0h.js"; import { a as readDreamsFile, t as DREAMS_FILENAMES } from "./dreaming-dreams-file-DQfkc_tk.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; //#region extensions/memory-core/src/short-term-promotion-memory-write.ts function buildPromotionMarker(candidateKey) { return `<!-- openclaw-memory-promotion:${candidateKey} -->`; } function extractPromotionKeys(content) { return [...content.matchAll(/<!--\s*openclaw-memory-promotion:([^\n]*?)\s*-->/giu)].map((match) => match[1]?.trim()).filter((key) => Boolean(key)); } var MemoryWriteConflictError = class extends Error { constructor(message = "MEMORY.md changed before the dreaming write could commit") { super(message); this.name = "MemoryWriteConflictError"; } }; async function resolveMemoryWritePath(filePath) { try { return await fs.realpath(filePath); } catch (err) { const hasTrailingSeparator = filePath.endsWith(path.sep) || process.platform === "win32" && filePath.endsWith(path.posix.sep); if (err?.code !== "ENOENT" || hasTrailingSeparator) throw err; } const parentPath = await fs.realpath(path.dirname(filePath)); const canonicalPath = path.join(parentPath, path.basename(filePath)); let linkTarget; try { linkTarget = await fs.readlink(canonicalPath); } catch (err) { const code = err?.code; if (code === "ENOENT" || code === "EINVAL") return canonicalPath; throw err; } return await resolveMemoryWritePath(process.platform === "win32" && /^[\\/](?![\\/])/.test(linkTarget) ? `${path.parse(parentPath).root.replace(/[\\/]$/, "")}${linkTarget}` : path.isAbsolute(linkTarget) ? linkTarget : `${parentPath}${parentPath.endsWith(path.sep) ? "" : path.sep}${linkTarget}`); } async function readMemoryContent(filePath) { return await fs.readFile(filePath, "utf-8").catch((error) => { if (error.code === "ENOENT") return ""; throw error; }); } function isAtomicReplacePermissionError(error) { const code = error.code; return code === "EACCES" || code === "EPERM" || code === "EEXIST" || code === "EROFS"; } async function writeExistingMemoryInPlace(params) { if (await readMemoryContent(params.filePath) !== params.expectedContent) throw new MemoryWriteConflictError(params.conflictMessage); let handle; try { handle = await fs.open(params.filePath, "r+"); } catch { return false; } try { await handle.writeFile(params.content, { encoding: "utf-8" }); await handle.truncate(Buffer.byteLength(params.content)); await handle.sync(); return true; } catch (error) { const original = Buffer.from(params.expectedContent, "utf-8"); try { let restored = 0; while (restored < original.length) { const { bytesWritten } = await handle.write(original, restored, original.length - restored, restored); if (bytesWritten <= 0) throw new Error(`${path.basename(params.filePath)} restore write made no progress`, { cause: error }); restored += bytesWritten; } await handle.truncate(original.length); await handle.sync(); } catch (restoreError) { throw new Error(`${path.basename(params.filePath)} in-place write failed and restoring the original content also failed`, { cause: restoreError }); } throw error; } finally { await handle.close(); } } function hashMemoryContent(content) { return createHash("sha256").update(content).digest("hex"); } async function commitMemoryContent(params) { if (params.content === null) { if (await readMemoryContent(params.filePath) !== params.expectedContent) throw new MemoryWriteConflictError(params.conflictMessage); await fs.unlink(params.filePath); return; } const memoryDirMode = (await fs.stat(path.dirname(params.filePath))).mode & 4095; try { await replaceFileAtomic({ filePath: params.filePath, content: params.content, dirMode: memoryDirMode, mode: 384, preserveExistingMode: true, tempPrefix: params.tempPrefix, syncTempFile: true, syncParentDir: true, throwOnCleanupError: true, beforeRename: async () => { if (params.expectedHash && hashMemoryContent(await readMemoryContent(params.filePath)) !== params.expectedHash) throw new MemoryWriteConflictError(params.conflictMessage); }, fileSystem: { promises: { mkdir: fs.mkdir, chmod: fs.chmod, writeFile: fs.writeFile, rename: fs.rename, copyFile: fs.copyFile, unlink: fs.unlink, rm: fs.rm, open: fs.open, stat: fs.stat, lstat: fs.lstat } } }); } catch (error) { if (!params.allowInPlaceFallback || params.expectedContent === void 0 || !isAtomicReplacePermissionError(error) || !await writeExistingMemoryInPlace({ filePath: params.filePath, expectedContent: params.expectedContent, content: params.content, conflictMessage: params.conflictMessage })) throw error; } } //#endregion //#region extensions/memory-core/src/memory-entry-origins.ts const ensuredDatabases = /* @__PURE__ */ new WeakSet(); const ensuredTombstoneDatabases = /* @__PURE__ */ new WeakSet(); function openMemoryOriginDatabase(agentId) { const db = openOpenClawAgentDatabase({ agentId }).db; if (!ensuredDatabases.has(db)) { db.exec(`CREATE TABLE IF NOT EXISTS memory_entry_origins ( entry_key TEXT NOT NULL, agent_id TEXT NOT NULL, session_id TEXT NOT NULL, session_key TEXT, origin_class TEXT NOT NULL CHECK (origin_class IN ('owner', 'agent', 'untrusted', 'system')), observed_at INTEGER NOT NULL, PRIMARY KEY (entry_key, agent_id, session_id) ) STRICT`); ensuredDatabases.add(db); } return db; } function readOrigin(row) { return { entryKey: row.entry_key, agentId: row.agent_id, sessionId: row.session_id, sessionKey: row.session_key, originClass: row.origin_class, observedAt: row.observed_at }; } function listMemoryEntryOrigins(params) { if (params.sessionIds?.length === 0 || params.entryKeys?.length === 0) return []; const result = withOpenClawAgentDatabaseReadOnly(({ db }) => { if (!ensuredDatabases.has(db) && !tableExists(db, "memory_entry_origins")) return []; let query = getNodeSqliteKysely(db).selectFrom("memory_entry_origins").selectAll().where("agent_id", "=", params.agentId); if (params.sessionIds) query = query.where("session_id", "in", params.sessionIds); if (params.entryKeys) query = query.where("entry_key", "in", params.entryKeys); return executeSqliteQuerySync(db, query.orderBy("entry_key", "asc").orderBy("session_id", "asc")).rows.map(readOrigin); }, params); return result.found ? result.value : []; } function listMemorySessionTombstones(params) { if (params.sessionIds?.length === 0) return []; const result = withOpenClawAgentDatabaseReadOnly(({ db }) => { if (!ensuredTombstoneDatabases.has(db) && !tableExists(db, "memory_session_tombstones")) return []; let query = getNodeSqliteKysely(db).selectFrom("memory_session_tombstones").selectAll().where("agent_id", "=", params.agentId); if (params.sessionIds) query = query.where("session_id", "in", params.sessionIds); return executeSqliteQuerySync(db, query.orderBy("session_id", "asc")).rows.map((row) => ({ sessionId: row.session_id, agentId: row.agent_id, reason: row.reason, createdAt: row.created_at })); }, params); return result.found ? result.value : []; } function recordMemorySessionTombstones(params) { const sessionIds = [...new Set(params.sessionIds)]; if (sessionIds.length === 0) return 0; const db = openOpenClawAgentDatabase({ agentId: params.agentId }).db; if (!ensuredTombstoneDatabases.has(db)) { db.exec(`CREATE TABLE IF NOT EXISTS memory_session_tombstones ( session_id TEXT NOT NULL PRIMARY KEY, agent_id TEXT NOT NULL, reason TEXT NOT NULL, created_at INTEGER NOT NULL ) STRICT`); ensuredTombstoneDatabases.add(db); } const reason = params.reason ?? "forgotten"; const createdAt = params.createdAt ?? Date.now(); return runSqliteImmediateTransactionSync(db, () => { const kysely = getNodeSqliteKysely(db); let recorded = 0; for (const sessionId of sessionIds) { const result = executeSqliteQuerySync(db, kysely.insertInto("memory_session_tombstones").values({ session_id: sessionId, agent_id: params.agentId, reason, created_at: createdAt }).onConflict((conflict) => conflict.column("session_id").doNothing())); recorded += Number(result.numAffectedRows ?? 0n); } if (recorded > 0) executeSqliteQuerySync(db, kysely.updateTable("memory_index_state").set((expression) => ({ revision: expression("revision", "+", 1) })).where("id", "=", 1)); return recorded; }); } function hasMemorySessionTombstone(db, agentId, sessionId) { if (!ensuredTombstoneDatabases.has(db) && !tableExists(db, "memory_session_tombstones")) return false; const kysely = getNodeSqliteKysely(db); return executeSqliteQuerySync(db, kysely.selectFrom("memory_session_tombstones").select("session_id").where("agent_id", "=", agentId).where("session_id", "=", sessionId)).rows.length > 0; } function recordMemoryEntryOrigins(params) { if (params.origins.length === 0) return []; const db = openMemoryOriginDatabase(params.agentId); return runSqliteImmediateTransactionSync(db, () => { const kysely = getNodeSqliteKysely(db); return params.origins.flatMap((origin) => { if (origin.agentId !== params.agentId) throw new Error("memory entry origin belongs to another agent"); return executeSqliteQuerySync(db, kysely.insertInto("memory_entry_origins").values({ entry_key: params.entryKey ?? origin.entryKey, agent_id: origin.agentId, session_id: origin.sessionId, session_key: origin.sessionKey, origin_class: origin.originClass, observed_at: origin.observedAt }).onConflict((conflict) => conflict.columns([ "entry_key", "agent_id", "session_id" ]).doNothing()).returningAll()).rows.map(readOrigin); }); }); } function deleteMemoryEntryOrigins(params) { if (listMemoryEntryOrigins(params).length === 0) return 0; const db = openMemoryOriginDatabase(params.agentId); return runSqliteImmediateTransactionSync(db, () => { let query = getNodeSqliteKysely(db).deleteFrom("memory_entry_origins").where("agent_id", "=", params.agentId).where("entry_key", "in", params.entryKeys); if (params.sessionIds) query = query.where("session_id", "in", params.sessionIds); return Number(executeSqliteQuerySync(db, query).numAffectedRows ?? 0n); }); } function reserveMemoryEntryOrigins(params) { const previousLines = params.previousMemory.replace(/\r\n/gu, "\n").split("\n"); const operationParents = params.operations.map((operation) => { const parentKeys = /* @__PURE__ */ new Set([operation.candidateKey]); for (const entry of operation.priorEntries) { const entryIndex = previousLines.findIndex((line) => line.trim() === entry); const marker = previousLines[entryIndex - 1]?.trim(); const parentKey = /^<!--\s*openclaw-memory-promotion:([^\n]*?)\s*-->$/u.exec(marker ?? "")?.[1]?.trim(); if (parentKey) parentKeys.add(parentKey); } return { operation, parentKeys }; }); const affectedKeys = [...new Set(operationParents.flatMap(({ parentKeys }) => [...parentKeys]))]; const reservations = []; const rollback = () => { for (const reservation of reservations.toReversed()) deleteMemoryEntryOrigins(reservation); }; try { for (const agentId of [...new Set(params.agentIds)].toSorted()) { const origins = listMemoryEntryOrigins({ agentId, entryKeys: affectedKeys }); for (const { operation, parentKeys } of operationParents) { const added = recordMemoryEntryOrigins({ agentId, origins: origins.filter((origin) => parentKeys.has(origin.entryKey)), entryKey: operation.candidateKey }); if (added.length > 0) reservations.push({ agentId, entryKeys: [operation.candidateKey], sessionIds: added.map((origin) => origin.sessionId) }); } } } catch (error) { rollback(); throw error; } return rollback; } async function pruneMemoryEntryOrigins(params) { const entryKeys = [...new Set(params.entryKeys)].filter((key) => !params.retainedEntryKeys.has(key)); if (entryKeys.length === 0) return; const diaries = await Promise.all(DREAMS_FILENAMES.map((name) => readDreamsFile(path.join(params.workspaceDir, name)))); const diaryKeys = new Set(diaries.flatMap(extractPromotionKeys)); for (const agentId of new Set(params.agentIds)) { const indexed = withOpenClawAgentDatabaseReadOnly(({ db }) => new Set(executeSqliteQuerySync(db, getNodeSqliteKysely(db).selectFrom("memory_index_chunks").select("text").where("source", "=", "memory").where("text", "like", "%openclaw-memory-promotion:%")).rows.flatMap(({ text }) => extractPromotionKeys(text))), { agentId }); deleteMemoryEntryOrigins({ agentId, entryKeys: entryKeys.filter((key) => !diaryKeys.has(key) && !(indexed.found && indexed.value.has(key))) }); } } //#endregion export { pruneMemoryEntryOrigins as a, reserveMemoryEntryOrigins as c, commitMemoryContent as d, extractPromotionKeys as f, resolveMemoryWritePath as g, readMemoryContent as h, listMemorySessionTombstones as i, MemoryWriteConflictError as l, isAtomicReplacePermissionError as m, hasMemorySessionTombstone as n, recordMemoryEntryOrigins as o, hashMemoryContent as p, listMemoryEntryOrigins as r, recordMemorySessionTombstones as s, deleteMemoryEntryOrigins as t, buildPromotionMarker as u };