openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
355 lines (354 loc) • 14.2 kB
JavaScript
import { r as resolveHomeRelativePath } from "./home-dir-BPhrG-aM.js";
import { d as pathExists, o as ensureAbsoluteDirectory, w as root } from "./fs-safe-B6pvPGnf.js";
import { c as resolveAgentConfig, m as resolveAgentWorkspaceDir, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js";
import "./agent-scope-DbtJyKUL.js";
import { g as redactMigrationPlan, l as markMigrationItemConflict, n as MIGRATION_REASON_TARGET_EXISTS, t as MIGRATION_REASON_MISSING_SOURCE_OR_TARGET, u as markMigrationItemError } from "./migration-q6QzXKht.js";
import { n as assertMemoryMigrationSourceRevision, t as MAX_MEMORY_MIGRATION_FILE_BYTES } from "./memory-migration-source-DiHFL-tc.js";
import path from "node:path";
import fs from "node:fs/promises";
import crypto from "node:crypto";
import { writeTextAtomic } from "@openclaw/fs-safe/atomic";
//#region src/plugin-sdk/migration-runtime.ts
/**
* Resolves default agent workspace/state/agent directories. Prefers the runtime resolver,
* then configured agentDir (using effective-home resolution), then canonical state layout.
*/
function resolvePlannedMigrationTargets(ctx) {
const cfg = ctx.config;
const agentId = ctx.targetAgentId ?? resolveDefaultAgentId(cfg);
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const configuredAgentDir = resolveAgentConfig(cfg, agentId)?.agentDir?.trim();
const agentDir = ctx.runtime?.agent?.resolveAgentDir(cfg, agentId) ?? (configuredAgentDir ? resolveHomeRelativePath(configuredAgentDir) : void 0) ?? path.join(ctx.stateDir, "agents", agentId, "agent");
return {
workspaceDir,
stateDir: ctx.stateDir,
agentDir
};
}
/** Wrap migration runtime config access with a cached mutable snapshot during apply. */
function withCachedMigrationConfigRuntime(runtime, fallbackConfig) {
if (!runtime) return;
const configApi = runtime.config;
if (!configApi?.current || !configApi.mutateConfigFile) return runtime;
let cachedConfig;
const current = () => {
cachedConfig ??= structuredClone(configApi.current() ?? fallbackConfig);
return cachedConfig;
};
return {
...runtime,
config: {
...runtime.config,
current,
mutateConfigFile: async (params) => {
const result = await configApi.mutateConfigFile({
...params,
mutate: async (draft, context) => {
const mutationResult = await params.mutate(draft, context);
cachedConfig = structuredClone(draft);
return mutationResult;
}
});
cachedConfig = structuredClone(result.nextConfig);
return result;
},
...configApi.replaceConfigFile ? { replaceConfigFile: async (params) => {
const result = await configApi.replaceConfigFile(params);
cachedConfig = structuredClone(result.nextConfig);
return result;
} } : {}
}
};
}
/** Back up an existing migration target within the report's item backup directory. */
async function backupMigrationItemTarget(target, reportDir, opts = {}) {
if (!await pathExists(target)) return;
const backupRoot = path.join(reportDir, "item-backups");
await fs.mkdir(backupRoot, { recursive: true });
const targetHash = crypto.createHash("sha256").update(path.resolve(target)).digest("hex").slice(0, 12);
const backupDir = await fs.mkdtemp(path.join(backupRoot, `${Date.now()}-${targetHash}-`));
const backupPath = path.join(backupDir, path.basename(target));
await fs.cp(target, backupPath, {
recursive: true,
force: true,
...opts.dereference === void 0 ? {} : { dereference: opts.dereference }
});
return backupPath;
}
async function backupMemoryMigrationTarget(target, contents, reportDir) {
const backupRoot = path.join(reportDir, "item-backups");
await fs.mkdir(backupRoot, { recursive: true });
const targetHash = crypto.createHash("sha256").update(path.resolve(target)).digest("hex").slice(0, 12);
const backupDir = await fs.mkdtemp(path.join(backupRoot, `${Date.now()}-${targetHash}-`));
const backupPath = path.join(backupDir, path.basename(target));
await fs.writeFile(backupPath, contents, {
flag: "wx",
mode: 384
});
return backupPath;
}
async function persistMemoryMigrationRecoveryRecord(recoveryRecordPath, params) {
await writeTextAtomic(recoveryRecordPath, JSON.stringify({
version: 1,
...params
}, null, 2), {
mode: 384,
trailingNewline: true
});
}
async function writeMemoryMigrationRecoveryRecord(params) {
const recoveryRecordPath = path.join(path.dirname(params.backupPath), `recovery-${crypto.randomUUID()}.json`);
await persistMemoryMigrationRecoveryRecord(recoveryRecordPath, {
...params,
status: "prepared"
});
return recoveryRecordPath;
}
function errorCode(error) {
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
}
async function openMemoryMigrationRoot(workspaceDir) {
const options = {
hardlinks: "reject",
maxBytes: MAX_MEMORY_MIGRATION_FILE_BYTES,
mkdir: true,
symlinks: "reject"
};
try {
return await root(workspaceDir, options);
} catch (error) {
if (errorCode(error) !== "not-found" && errorCode(error) !== "ENOENT") throw error;
}
const ensured = await ensureAbsoluteDirectory(workspaceDir, {
scopeLabel: "memory import workspace",
mode: 448
});
if (!ensured.ok) throw ensured.error;
return await root(ensured.path, options);
}
function isFileAlreadyExistsError(err) {
return Boolean(err && typeof err === "object" && "code" in err && (err.code === "ERR_FS_CP_EEXIST" || err.code === "EEXIST"));
}
function readArchiveRelativePath(item) {
const detailPath = item.details?.archiveRelativePath;
const raw = typeof detailPath === "string" && detailPath.trim() ? detailPath : void 0;
const fallback = item.source ? path.basename(item.source) : item.id;
return path.normalize(raw ?? fallback).split(path.sep).filter((part) => part && part !== "." && part !== "..").join(path.sep) || "item";
}
async function resolveUniqueArchivePath(archiveRoot, relativePath) {
const parsed = path.parse(relativePath);
let candidate = path.join(archiveRoot, relativePath);
let index = 2;
while (await pathExists(candidate)) {
const filename = `${parsed.name}-${index}${parsed.ext}`;
candidate = path.join(archiveRoot, parsed.dir, filename);
index += 1;
}
return candidate;
}
/** Archive a migration item source into the report directory and mark the item migrated. */
async function archiveMigrationItem(item, reportDir) {
if (!item.source) return markMigrationItemError(item, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET);
try {
if ((await fs.lstat(item.source)).isSymbolicLink()) return markMigrationItemError(item, "archive source is a symlink");
const archiveRoot = path.join(reportDir, "archive");
const relativePath = readArchiveRelativePath(item);
const archivePath = await resolveUniqueArchivePath(archiveRoot, relativePath);
await fs.mkdir(path.dirname(archivePath), { recursive: true });
await fs.cp(item.source, archivePath, {
recursive: true,
force: false,
errorOnExist: true,
verbatimSymlinks: true
});
return {
...item,
status: "migrated",
target: archivePath,
details: {
...item.details,
archivePath,
archiveRelativePath: relativePath
}
};
} catch (err) {
if (isFileAlreadyExistsError(err)) return markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS);
return markMigrationItemError(item, err instanceof Error ? err.message : String(err));
}
}
/** Copy a migration item source to its target, optionally backing up an overwritten target. */
async function copyMigrationFileItem(item, reportDir, opts = {}) {
if (!item.source || !item.target) return markMigrationItemError(item, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET);
let result = item;
try {
if (await pathExists(item.target) && !opts.overwrite) return markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS);
const backupPath = opts.overwrite ? await backupMigrationItemTarget(item.target, reportDir) : void 0;
result = {
...item,
details: {
...item.details,
...backupPath ? { backupPath } : {}
}
};
await fs.mkdir(path.dirname(item.target), { recursive: true });
await fs.cp(item.source, item.target, {
recursive: true,
force: Boolean(opts.overwrite),
errorOnExist: !opts.overwrite
});
return {
...result,
status: "migrated"
};
} catch (err) {
if (isFileAlreadyExistsError(err)) return markMigrationItemConflict(result, MIGRATION_REASON_TARGET_EXISTS);
return markMigrationItemError(result, err instanceof Error ? err.message : String(err));
}
}
/** Copy one regular memory file through an fs-safe workspace root. */
async function copyMemoryMigrationFileItem(item, reportDir, opts) {
if (!item.source || !item.target) return markMigrationItemError(item, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET);
let backupPath;
let stagedRelative;
let stagingDir;
let recoveryPath;
let recoveryRecordPath;
let journalRecoveryPath;
let relativeTarget;
let targetCreated = false;
let safeRoot;
try {
const workspaceDir = path.resolve(opts.workspaceDir);
relativeTarget = path.relative(workspaceDir, path.resolve(item.target));
safeRoot = await openMemoryMigrationRoot(workspaceDir);
const { buffer: sourceBuffer } = await (await root(path.dirname(item.source), {
hardlinks: "reject",
maxBytes: MAX_MEMORY_MIGRATION_FILE_BYTES,
symlinks: "reject"
})).read(path.basename(item.source), {
hardlinks: "reject",
maxBytes: MAX_MEMORY_MIGRATION_FILE_BYTES,
symlinks: "reject"
});
assertMemoryMigrationSourceRevision(item, sourceBuffer);
if (opts.overwrite === true && await safeRoot.exists(relativeTarget)) {
const existing = await safeRoot.read(relativeTarget, {
hardlinks: "reject",
maxBytes: MAX_MEMORY_MIGRATION_FILE_BYTES,
symlinks: "reject"
});
backupPath = await backupMemoryMigrationTarget(item.target, existing.buffer, reportDir);
stagingDir = path.join(".openclaw-memory-import-staging", crypto.randomUUID());
stagedRelative = path.join(stagingDir, path.basename(relativeTarget));
const plannedRecoveryPath = path.join(safeRoot.rootReal, stagedRelative);
journalRecoveryPath = plannedRecoveryPath;
await safeRoot.mkdir(stagingDir);
recoveryRecordPath = await writeMemoryMigrationRecoveryRecord({
backupPath,
recoveryPath: plannedRecoveryPath,
target: item.target
});
recoveryPath = plannedRecoveryPath;
await safeRoot.move(relativeTarget, stagedRelative, { overwrite: false });
const staged = await safeRoot.read(stagedRelative, {
hardlinks: "reject",
maxBytes: MAX_MEMORY_MIGRATION_FILE_BYTES,
symlinks: "reject"
});
if (!staged.buffer.equals(existing.buffer)) {
backupPath = await backupMemoryMigrationTarget(item.target, staged.buffer, reportDir);
await persistMemoryMigrationRecoveryRecord(recoveryRecordPath, {
backupPath,
recoveryPath: plannedRecoveryPath,
status: "prepared",
target: item.target
});
}
}
await safeRoot.create(relativeTarget, sourceBuffer, {
mkdir: true,
mode: 384
});
targetCreated = true;
if (recoveryRecordPath && backupPath && journalRecoveryPath) await persistMemoryMigrationRecoveryRecord(recoveryRecordPath, {
backupPath,
recoveryPath: journalRecoveryPath,
status: "complete",
target: item.target
});
if (stagedRelative) {
await safeRoot.remove(stagedRelative);
stagedRelative = void 0;
recoveryPath = void 0;
}
if (stagingDir) {
await safeRoot.remove(stagingDir);
await safeRoot.remove(".openclaw-memory-import-staging").catch(() => void 0);
}
if (recoveryRecordPath) try {
await fs.unlink(recoveryRecordPath);
recoveryRecordPath = void 0;
} catch {}
return {
...item,
status: "migrated",
details: {
...item.details,
...backupPath ? { backupPath } : {},
...recoveryRecordPath ? { recoveryRecordPath } : {}
}
};
} catch (error) {
if (safeRoot && stagedRelative && relativeTarget && !targetCreated) try {
if (!await safeRoot.exists(stagedRelative)) recoveryPath = void 0;
else if (!await safeRoot.exists(relativeTarget)) {
await safeRoot.move(stagedRelative, relativeTarget, { overwrite: false });
stagedRelative = void 0;
recoveryPath = void 0;
}
} catch {}
if (recoveryRecordPath && backupPath && journalRecoveryPath) await persistMemoryMigrationRecoveryRecord(recoveryRecordPath, {
backupPath,
recoveryPath: journalRecoveryPath,
status: targetCreated ? "complete" : recoveryPath ? "recovery-required" : "safe",
target: item.target
}).catch(() => void 0);
const details = {
...item.details,
...backupPath ? { backupPath } : {},
...recoveryPath ? { recoveryPath } : {},
...recoveryRecordPath ? { recoveryRecordPath } : {}
};
if (isFileAlreadyExistsError(error) || errorCode(error) === "already-exists") return {
...markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS),
details
};
return {
...markMigrationItemError(item, error instanceof Error ? error.message : String(error)),
details
};
}
}
/** Write redacted JSON and Markdown migration reports into the apply report directory. */
async function writeMigrationReport(result, opts = {}) {
if (!result.reportDir) return;
await fs.mkdir(result.reportDir, { recursive: true });
await fs.writeFile(path.join(result.reportDir, "report.json"), `${JSON.stringify(redactMigrationPlan(result), null, 2)}\n`, "utf8");
const lines = [
`# ${opts.title ?? "Migration Report"}`,
"",
`Source: ${result.source}`,
result.target ? `Target: ${result.target}` : void 0,
result.backupPath ? `Backup: ${result.backupPath}` : void 0,
"",
`Migrated: ${result.summary.migrated}`,
`Skipped: ${result.summary.skipped}`,
`Conflicts: ${result.summary.conflicts}`,
`Errors: ${result.summary.errors}`,
"",
...result.items.map((item) => `- ${item.status}: ${item.id}${item.reason ? ` (${item.reason})` : ""}`)
].filter((line) => typeof line === "string");
await fs.writeFile(path.join(result.reportDir, "summary.md"), `${lines.join("\n")}\n`, "utf8");
}
//#endregion
export { resolvePlannedMigrationTargets as a, copyMigrationFileItem as i, backupMigrationItemTarget as n, withCachedMigrationConfigRuntime as o, copyMemoryMigrationFileItem as r, writeMigrationReport as s, archiveMigrationItem as t };