UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,284 lines • 58.8 kB
import { E as resolveDateTimestampMs } from "./number-coercion-CuVhdCBs.mjs";
import { n as collectErrorGraphCandidates } from "./error-coercion-CvJK4lmH.mjs";
import { o as resolveUserPath } from "./home-dir-Dc6nN6HB.mjs";
import { r as createLazyRuntimeModule } from "./lazy-runtime-CJrzFzqU.mjs";
import { n as sliceUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { w as root } from "./fs-safe-BUXBSN-4.mjs";
import { n as isErrno, t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import { d as resolveHomeDir, h as sleep } from "./utils-DgZn2dLu.mjs";
import { v as resolveGatewayLockDir } from "./paths-DGIwWbpu.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { l as resolveRuntimeServiceVersion } from "./version-BJGPJZu6.mjs";
import { a as resolveOpenClawRegisteredAgentDatabasePath, c as resolveOpenClawStateSqlitePath, l as resolveQuarantineStorePath } from "./openclaw-state-db.paths-DfifnSGp.mjs";
import { a as resolveSqliteDatabaseFilePaths, r as isAppleDoubleMetadataFile, t as SQLITE_SIDECAR_SUFFIXES } from "./sqlite-files-BEC3zbaw.mjs";
import { x as assertOpenClawStateDatabaseOwner } from "./openclaw-state-db-fast-path-CPAamWzU.mjs";
import { a as publishFileExclusive, f as syncDirectoryIfSupported, n as getPublishFileExclusiveFailureDetails, r as isHardlinkFallbackError, s as requireDirectorySync } from "./directory-durability-CqSvP1tc.mjs";
import { i as sameFileMutationFingerprint } from "./file-descriptor-B4yBUiAP.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { f as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-CC57AD_q.mjs";
import "./session-key-C09ow4h0.mjs";
import { n as CONFIG_AUDIT_SCOPE } from "./io.audit-BXzMurur.mjs";
import { n as readOpenClawAgentDatabaseRegistryRows } from "./openclaw-agent-db-registry.read-DUQ6FrvR.mjs";
import { n as assertOpenClawAgentDatabaseOwner } from "./openclaw-agent-db-maintenance-CYe8pVkd.mjs";
import { t as listDefaultAgentDatabasePaths } from "./agent-database-path-discovery-B_nrhsNg.mjs";
import { i as stageBackupConfigCapture } from "./backup-config-capture-BAWhp0cV.mjs";
import { a as isLegacyAuditMigrationBackupPath, i as hasLegacyAuditBackupSources, n as isTransientSqliteBackupPath } from "./backup-volatile-filter-I3J38C_Z.mjs";
import { n as isUpdateCapturePath, t as assertNotUpdateCapturePath } from "./update-capture-paths-BuDS3zdN.mjs";
import { t as createVerifiedSqliteSnapshot } from "./sqlite-snapshot-c6hBDowh.mjs";
import { n as SYSTEM_AGENT_AUDIT_SCOPE } from "./audit-FqvDk6WR.mjs";
import { a as canonicalizePathForContainment, d as describeCapturedBackupSqliteSnapshots, f as sealBackupResourceInventory, g as recordArchiveSymbolicLink, i as buildBackupArchiveRoot, l as resolveBackupPlanFromDisk, n as buildBackupArchiveBasename, r as buildBackupArchivePath } from "./backup-shared-D8dux81d.mjs";
import { t as backupManifestSizeError } from "./backup-verify-manifest-B58e7VAA.mjs";
import { n as finishBackupScratch, r as maintainBackupScratch, t as createBackupScratchDirectory } from "./backup-scratch-CTd0Pvbc.mjs";
import { t as embedSessionColdArchivesInSnapshot } from "./session-cold-storage-backup-BfxM5Vhp.mjs";
import { n as sanitizeOpenClawStateLeaseRows, t as sanitizeOpenClawGlobalStateSnapshot } from "./openclaw-state-snapshot-sanitizer-B8rMlqrV.mjs";
import { a as readLegacyAuditRecoverySourceForBackup, d as prepareLegacyAuditRecords, f as serializePreparedAuditRecords, h as legacyAuditSourceGenerationKey, i as findPreviousLegacyAuditRawCheckpoint, o as readLegacyAuditSourcePrefixSnapshotForBackup, p as detectLegacyAuditLogs, t as withLegacyAuditMigrationLease } from "./state-migrations.audit-coordination-mREmABLG.mjs";
import fs, { constants, createWriteStream } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { createHash, randomUUID } from "node:crypto";
import { sameFileIdentity } from "@openclaw/fs-safe/advanced";
import { isPathInside } from "@openclaw/fs-safe/path";
import { walkDirectory } from "@openclaw/fs-safe/walk";
import { Transform, compose } from "node:stream";
import { createGzip } from "node:zlib";
import { pipeline as pipeline$1 } from "node:stream/promises";
//#region src/infra/backup-create-stream.ts
const BACKUP_ARCHIVE_IDLE_TIMEOUT_MS = 3e5;
/** Seal the manifest from observed entries after the single payload traversal. */
function appendBackupManifest(payload, createManifest) {
	return compose(payload, async function* (source) {
		let tail = Buffer.alloc(0);
		for await (const chunk of source) {
			const length = Math.max(0, tail.length - Math.max(0, 1024 - chunk.length));
			if (length) yield tail.subarray(0, length);
			tail = length < tail.length ? Buffer.concat([tail.subarray(length), chunk]) : chunk;
		}
		if (tail.length > 1024) yield tail.subarray(0, -1024);
		yield createManifest();
	}, createGzip());
}
function removePreparedBackupArchive(prepared) {
	let currentIdentity;
	try {
		currentIdentity = fs.lstatSync(prepared.archivePath);
	} catch {
		return false;
	}
	if (!currentIdentity.isFile() || !sameFileIdentity(prepared.identity, currentIdentity)) return false;
	try {
		fs.unlinkSync(prepared.archivePath);
		return true;
	} catch {
		return false;
	}
}
async function writeArchiveStreamToFile(params) {
	const controller = new AbortController();
	let archiveStream;
	let openedIdentity;
	let idleTimer;
	let idleTimeoutError;
	let lastEntryPath;
	let lastProgress;
	let outputBytes = 0;
	let producerBytes = 0;
	let settled = false;
	const reportProgress = (progress) => {
		if (settled) return;
		if (progress) {
			lastProgress = progress;
			if (progress.entryPath) lastEntryPath = progress.entryPath;
			if (progress.bytes) {
				if (progress.phase === "output") outputBytes += progress.bytes;
				else if (progress.phase === "raw") producerBytes += progress.bytes;
			}
		}
		idleTimer = idleTimer?.refresh() ?? setTimeout(() => {
			const entrySuffix = lastEntryPath ? `, entry=${JSON.stringify(sliceUtf16Safe(lastEntryPath, -512))}` : "";
			idleTimeoutError = /* @__PURE__ */ new Error(`Backup archive write stalled: no progress observed for ${BACKUP_ARCHIVE_IDLE_TIMEOUT_MS}ms (phase=${lastProgress?.phase ?? "starting"}${entrySuffix}, rawBytes=${producerBytes}, outputBytes=${outputBytes})`);
			archiveStream?.destroy(idleTimeoutError);
			controller.abort(idleTimeoutError);
		}, BACKUP_ARCHIVE_IDLE_TIMEOUT_MS);
	};
	const progress = new Transform({ transform(chunk, _encoding, callback) {
		reportProgress({
			phase: "output",
			bytes: chunk.length
		});
		callback(null, chunk);
	} });
	const archiveWriteStream = createWriteStream(params.archivePath, {
		flags: "wx",
		flush: true,
		mode: 384
	});
	archiveWriteStream.once("open", (fileDescriptor) => {
		try {
			openedIdentity = fs.fstatSync(fileDescriptor);
		} catch (error) {
			archiveWriteStream.destroy(error);
		}
	});
	try {
		archiveStream = params.createArchiveStream(reportProgress);
		const pipelinePromise = pipeline$1(archiveStream, progress, archiveWriteStream, { signal: controller.signal });
		reportProgress();
		await pipelinePromise;
		const currentIdentity = await fs$1.lstat(params.archivePath);
		if (!openedIdentity?.isFile() || !currentIdentity.isFile() || !sameFileIdentity(openedIdentity, currentIdentity)) throw new Error(`Backup archive path changed while writing: ${params.archivePath}`);
		return {
			archivePath: params.archivePath,
			identity: currentIdentity
		};
	} catch (err) {
		archiveWriteStream.destroy();
		let cleanupReceipt = openedIdentity ? {
			archivePath: params.archivePath,
			identity: openedIdentity
		} : void 0;
		if (!cleanupReceipt) try {
			const currentIdentity = fs.lstatSync(params.archivePath);
			cleanupReceipt = currentIdentity.isFile() ? {
				archivePath: params.archivePath,
				identity: currentIdentity
			} : { archivePath: params.archivePath };
		} catch (cleanupError) {
			if (cleanupError.code !== "ENOENT") cleanupReceipt = { archivePath: params.archivePath };
		}
		if (cleanupReceipt && (!cleanupReceipt.identity || !removePreparedBackupArchive(cleanupReceipt))) params.onPartialArchive(cleanupReceipt);
		throw idleTimeoutError ?? err;
	} finally {
		settled = true;
		if (idleTimer) clearTimeout(idleTimer);
	}
}
//#endregion
//#region src/infra/backup-archive-publication.ts
function pathsEqual(left, right) {
	const resolvedLeft = path.resolve(left);
	const resolvedRight = path.resolve(right);
	return process.platform === "win32" ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase() : resolvedLeft === resolvedRight;
}
async function assertTargetAbsent(targetPath) {
	try {
		await fs$1.lstat(targetPath);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
	throw new Error(`Refusing to overwrite existing backup archive: ${targetPath}`);
}
async function removeDirectoryIfOwned(directoryPath, expectedIdentity) {
	const currentIdentity = await fs$1.lstat(directoryPath).catch(() => void 0);
	if (!currentIdentity || !currentIdentity.isDirectory() || !sameFileIdentity(expectedIdentity, currentIdentity)) return false;
	try {
		await fs$1.rmdir(directoryPath);
		return true;
	} catch {
		return false;
	}
}
async function createBackupArchivePublication(outputPath) {
	const requestedOutputPath = path.resolve(outputPath);
	const requestedParentPath = path.dirname(requestedOutputPath);
	const canonicalParentPath = await fs$1.realpath(requestedParentPath);
	const parentIdentity = await fs$1.lstat(canonicalParentPath, { bigint: true });
	if (!parentIdentity.isDirectory()) throw new Error(`Backup output parent is not a directory: ${requestedParentPath}`);
	const canonicalOutputPath = path.join(canonicalParentPath, path.basename(requestedOutputPath));
	await assertTargetAbsent(canonicalOutputPath);
	const stagingDir = await fs$1.mkdtemp(path.join(canonicalParentPath, `.openclaw-backup-publish-${randomUUID()}-`));
	let stagingIdentity;
	try {
		stagingIdentity = await fs$1.lstat(stagingDir);
		await fs$1.chmod(stagingDir, 448);
		return {
			canonicalOutputPath,
			canonicalParentPath,
			parentReceipt: {
				path: canonicalParentPath,
				realPath: canonicalParentPath,
				identity: parentIdentity
			},
			pendingCleanupArchives: [],
			requestedOutputPath,
			requestedParentPath,
			stagingDir,
			stagingIdentity,
			tempArchivePath: path.join(stagingDir, "archive.tar.gz.tmp")
		};
	} catch (error) {
		if (stagingIdentity) await removeDirectoryIfOwned(stagingDir, stagingIdentity);
		throw error;
	}
}
function retainArchiveForCleanup(plan, receipt) {
	for (const [index, candidate] of plan.pendingCleanupArchives.entries()) {
		if (!pathsEqual(candidate.archivePath, receipt.archivePath)) continue;
		if (!candidate.identity || !receipt.identity) {
			if (!candidate.identity && receipt.identity) plan.pendingCleanupArchives[index] = receipt;
			return;
		}
		if (sameFileIdentity(candidate.identity, receipt.identity)) return;
	}
	plan.pendingCleanupArchives.push(receipt);
}
async function removePendingBackupArchive(plan, receipt) {
	if (!pathsEqual(path.dirname(receipt.archivePath), plan.stagingDir)) return false;
	if (receipt.identity) return removePreparedBackupArchive(receipt);
	let currentIdentity;
	try {
		currentIdentity = await fs$1.lstat(receipt.archivePath);
	} catch (error) {
		return error.code === "ENOENT";
	}
	if (!currentIdentity.isFile()) return false;
	return removePreparedBackupArchive({
		archivePath: receipt.archivePath,
		identity: currentIdentity
	});
}
async function cleanupBackupArchivePublication(plan, log) {
	const retainedArchives = plan.pendingCleanupArchives.splice(0);
	for (const receipt of retainedArchives) if (!await removePendingBackupArchive(plan, receipt)) retainArchiveForCleanup(plan, receipt);
	if (await removeDirectoryIfOwned(plan.stagingDir, plan.stagingIdentity)) {
		await syncDirectoryIfSupported(plan.canonicalParentPath).catch(() => void 0);
		return;
	}
	if (await fs$1.lstat(plan.stagingDir).catch(() => void 0)) log?.(`Backup archiver preserved changed or non-empty staging directory ${plan.stagingDir}.`);
}
async function publishPreparedBackupArchive(params) {
	const { plan, prepared } = params;
	let publicationPreserved = false;
	let committed = false;
	try {
		try {
			const publication = await publishFileExclusive({
				sourcePath: prepared.archivePath,
				targetPath: plan.canonicalOutputPath,
				expectedSourceIdentity: prepared.identity,
				parentReceipt: plan.parentReceipt,
				strategy: "link-required",
				onSyncFailure: "preserve"
			});
			publicationPreserved = true;
			requireDirectorySync(publication.directorySync, "Backup publication directory");
			committed = true;
		} catch (error) {
			const details = getPublishFileExclusiveFailureDetails(error);
			publicationPreserved ||= details?.cleanup === "preserved";
			if (error.code === "EEXIST") throw new Error(`Refusing to overwrite existing backup archive: ${plan.requestedOutputPath}`, { cause: error });
			if (isHardlinkFallbackError(error)) throw new Error(`Atomic backup publication requires hard-link support in ${plan.requestedParentPath}.`, { cause: error });
			if (error.code === "path-mismatch") throw new Error(`Backup archive changed during publication: ${plan.requestedOutputPath}`, { cause: error });
			throw error;
		}
		if (!removePreparedBackupArchive(prepared)) {
			retainArchiveForCleanup(plan, prepared);
			params.log?.(`Backup archiver preserved changed staging file ${prepared.archivePath}.`);
		}
		if (!await removeDirectoryIfOwned(plan.stagingDir, plan.stagingIdentity)) params.log?.(`Backup archiver preserved changed or non-empty staging directory ${plan.stagingDir}.`);
		await syncDirectoryIfSupported(plan.canonicalParentPath).catch((error) => {
			params.log?.(`Backup archiver could not sync cleanup in ${plan.canonicalParentPath}: ${error.code ?? String(error)}.`);
		});
	} catch (error) {
		if (!committed) {
			if (publicationPreserved) params.log?.(`Backup archiver preserved the final archive after publication failed: ${plan.requestedOutputPath}.`);
			if (!removePreparedBackupArchive(prepared)) retainArchiveForCleanup(plan, prepared);
			await removeDirectoryIfOwned(plan.stagingDir, plan.stagingIdentity);
		}
		throw error;
	}
}
//#endregion
//#region src/infra/backup-sqlite-source-groups.ts
function hasKnownIdentity(identity) {
	return process.platform !== "win32" || identity.dev !== 0 && identity.ino !== 0;
}
async function readRegularSidecar(pathname) {
	let identity;
	try {
		identity = await fs$1.lstat(pathname);
	} catch (error) {
		if (hasErrnoCode(error, "ENOENT")) return null;
		throw error;
	}
	if (!identity.isFile()) throw new Error(`SQLite hardlink sidecar must be a regular file: ${pathname}`);
	return identity;
}
async function assertGroupPaths(group) {
	for (const source of group.sources) {
		const current = await fs$1.lstat(source.path);
		if (!current.isFile() || !sameFileIdentity(source.identity, current) || group.sources.length > 1 && !hasKnownIdentity(current) || await fs$1.realpath(source.path) !== source.path) throw new Error(`SQLite backup source identity changed: ${source.path}`);
		if (current.nlink !== group.sources.length) throw new Error(`SQLite hardlink journal owner may be outside the backup inventory: ${source.path} has ${current.nlink} links, but ${group.sources.length} paths were admitted. Include every database hardlink before retrying.`);
	}
}
async function readGroupWalOwners(group) {
	const owners = [];
	for (const source of group.sources) {
		const wal = await readRegularSidecar(`${source.path}-wal`);
		const journal = await readRegularSidecar(`${source.path}-journal`);
		await readRegularSidecar(`${source.path}-shm`);
		if (journal && journal.size > 0) throw new Error(`SQLite hardlink journal ownership cannot be established safely while a rollback journal is present: ${source.path}. Close the database cleanly before retrying.`);
		if (wal && wal.size > 0) {
			if (!hasKnownIdentity(wal)) throw new Error(`SQLite hardlink WAL identity cannot be verified: ${source.path}-wal`);
			owners.push({
				path: source.path,
				identity: wal
			});
		}
	}
	if (owners.length > 1) throw new Error(`Ambiguous SQLite hardlink journal ownership: multiple non-empty WAL files for ${group.sources.map((source) => source.path).join(", ")}. Close the database writers before retrying.`);
	return owners;
}
async function assertBackupSqliteSourceGroup(group) {
	await assertGroupPaths(group);
	if (group.sources.length === 1) return;
	const [owner] = await readGroupWalOwners(group);
	if (group.walIdentity ? !owner || owner.path !== group.sourcePath || !sameFileIdentity(group.walIdentity, owner.identity) : owner !== void 0) throw new Error(`SQLite hardlink journal ownership changed during backup: ${group.sourcePath}`);
	if (!sameFileMutationFingerprint(group.databaseFingerprint, await fs$1.stat(group.sourcePath, { bigint: true }))) throw new Error(`SQLite hardlink database changed during backup: ${group.sourcePath}. Close database writers cleanly before retrying.`);
}
/** Resolve journal ownership after canonical role validation and physical path resolution. */
async function planBackupSqliteSourceGroups(sources) {
	const groups = /* @__PURE__ */ new Map();
	for (const source of sources) {
		const knownIdentity = hasKnownIdentity(source.identity);
		if (!knownIdentity && source.identity.nlink > 1) throw new Error(`SQLite hardlink identity cannot be verified: ${source.path}`);
		const key = knownIdentity ? `${source.identity.dev}:${source.identity.ino}` : source.path;
		const group = groups.get(key);
		if (group) {
			if (!group.sources.some((entry) => entry.path === source.path)) group.sources.push(source);
		} else groups.set(key, {
			sources: [source],
			sourcePath: source.path,
			walIdentity: null,
			databaseFingerprint: await fs$1.stat(source.path, { bigint: true })
		});
	}
	const byPath = /* @__PURE__ */ new Map();
	for (const group of groups.values()) {
		await assertGroupPaths(group);
		if (group.sources.length > 1) {
			const [owner] = await readGroupWalOwners(group);
			if (owner) {
				group.sourcePath = owner.path;
				group.walIdentity = owner.identity;
			}
		}
		for (const source of group.sources) byPath.set(source.path, group);
	}
	return byPath;
}
async function captureBackupSqliteSourceGroup(group, capture) {
	await assertBackupSqliteSourceGroup(group);
	await capture();
	await assertBackupSqliteSourceGroup(group);
}
//#endregion
//#region src/infra/state-migrations.audit-backup.ts
var LegacyAuditBackupStateChangedError = class extends Error {
	constructor(message = "Legacy audit state changed while backup was capturing it") {
		super(message);
		this.name = "LegacyAuditBackupStateChangedError";
	}
};
const LEGACY_AUDIT_RAW_CHECKPOINT_SCOPE = "migration.legacy-audit-raw";
function createLegacyAuditDatabaseWitness(database) {
	const rows = database.prepare(`SELECT scope, event_key, payload_json, created_at, sequence
       FROM diagnostic_events
       WHERE (scope IN (?, ?) AND event_key GLOB 'legacy:*') OR scope = ?
       ORDER BY scope, event_key, sequence`).all(CONFIG_AUDIT_SCOPE, SYSTEM_AGENT_AUDIT_SCOPE, LEGACY_AUDIT_RAW_CHECKPOINT_SCOPE);
	return createHash("sha256").update(JSON.stringify(rows)).digest("hex");
}
function legacyAuditBackupCapturesMatch(left, right) {
	return left.filesystemWitness === right.filesystemWitness && left.databaseWitness === right.databaseWitness;
}
/** Replaces live raw checkpoints with metadata for the transformed backup files. */
function rewriteLegacyAuditBackupCheckpoints(database, snapshots) {
	if (database.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get("diagnostic_events")?.ok !== 1) return;
	const scope = LEGACY_AUDIT_RAW_CHECKPOINT_SCOPE;
	database.prepare("DELETE FROM diagnostic_events WHERE scope = ?").run(scope);
	const insert = database.prepare(`INSERT INTO diagnostic_events (
        scope, event_key, payload_json, created_at, sequence
      ) VALUES (?, ?, ?, ?, ?)`);
	let sequence = 1;
	for (const snapshot of snapshots) {
		if (!snapshot.checkpoint) continue;
		insert.run(scope, snapshot.checkpoint.key, JSON.stringify(snapshot.checkpoint.value), 0, sequence);
		sequence += 1;
	}
}
async function createLegacyAuditBackupSnapshotsOnce(params) {
	const detected = detectLegacyAuditLogs({
		stateDir: params.stateDir,
		doctorOnlyStateMigrations: true
	});
	if (detected.sources.length === 0) return {
		snapshots: [],
		filesystemWitness: createHash("sha256").digest("hex")
	};
	const root$1 = await root(params.stateDir, {
		hardlinks: "reject",
		maxBytes: Number.MAX_SAFE_INTEGER,
		mkdir: false,
		mode: 384,
		symlinks: "reject"
	});
	const snapshots = [];
	const filesystemWitness = createHash("sha256");
	for (const [index, source] of detected.sources.entries()) {
		const sourceRelativePath = path.relative(path.resolve(params.stateDir), source.sourcePath);
		const snapshot = source.storage === "raw-archive" ? await readLegacyAuditRecoverySourceForBackup(root$1, sourceRelativePath) : await readLegacyAuditSourcePrefixSnapshotForBackup(root$1, sourceRelativePath);
		const sourceGeneration = legacyAuditSourceGenerationKey(sourceRelativePath);
		const previousCheckpoint = source.storage === "raw-archive" ? findPreviousLegacyAuditRawCheckpoint(params.stateDir, sourceRelativePath) : void 0;
		const prepared = prepareLegacyAuditRecords(source, snapshot.raw, sourceGeneration, previousCheckpoint?.recordOrdinalBase ?? 0);
		if (!prepared.ok) throw new Error(`Legacy ${source.label} append archive cannot be sanitized for backup: ${prepared.warnings.join("; ")}`);
		const sourcePath = path.join(params.tempDir, `legacy-audit-raw-${index}.jsonl`);
		await fs$1.writeFile(sourcePath, prepared.sanitizedJsonl, { mode: 384 });
		let checkpoint;
		if (previousCheckpoint) {
			if (previousCheckpoint.recordCount > prepared.records.length) throw new Error(`Legacy ${source.label} append archive is shorter than its durable checkpoint`);
			const transformedPrefix = Buffer.from(serializePreparedAuditRecords(prepared.records.slice(0, previousCheckpoint.recordCount)), "utf8");
			const value = {
				...previousCheckpoint,
				dev: 0,
				ino: 0,
				mtimeMs: 0,
				size: transformedPrefix.length,
				contentHash: createHash("sha256").update(transformedPrefix).digest("hex")
			};
			checkpoint = {
				key: value.generationKey,
				value
			};
		}
		const backupSnapshot = {
			sourcePath,
			archiveSourcePath: source.sourcePath,
			...checkpoint ? { checkpoint } : {},
			skippedSourcePaths: /* @__PURE__ */ new Set([
				path.resolve(source.sourcePath),
				path.resolve(`${source.sourcePath}.doctor-scrub-progress`),
				path.resolve(`${source.sourcePath}.doctor-scrub-restore`),
				path.resolve(`${source.sourcePath}.doctor-scrub-staging`)
			])
		};
		const witnessMetadata = JSON.stringify([
			backupSnapshot.archiveSourcePath,
			snapshot.dev,
			snapshot.ino,
			snapshot.mtimeMs,
			snapshot.size,
			backupSnapshot.checkpoint
		]);
		for (const value of [witnessMetadata, prepared.sanitizedJsonl]) filesystemWitness.update(String(Buffer.byteLength(value))).update(":").update(value);
		snapshots.push(backupSnapshot);
	}
	return {
		snapshots,
		filesystemWitness: filesystemWitness.digest("hex")
	};
}
async function createLegacyAuditBackupCapture(params) {
	let capture;
	for (let attempt = 0;; attempt += 1) try {
		capture = await createLegacyAuditBackupSnapshotsOnce(params);
		break;
	} catch (error) {
		if (attempt === 2) throw error;
		await new Promise((resolve) => {
			setTimeout(resolve, 25);
		});
	}
	const databaseWitness = withExistingOpenClawStateDatabaseReadOnly(({ db }) => createLegacyAuditDatabaseWitness(db), { env: {
		...process.env,
		OPENCLAW_STATE_DIR: params.stateDir
	} }) ?? createHash("sha256").digest("hex");
	return {
		...capture,
		databaseWitness
	};
}
//#endregion
//#region src/infra/backup-sqlite-snapshot.ts
function findLegacyAuditBackupStateChange(error) {
	return collectErrorGraphCandidates(error, (candidate) => candidate instanceof Error ? [candidate.cause] : []).find((candidate) => candidate instanceof LegacyAuditBackupStateChangedError);
}
function resolveSqliteBackupDatabasePath(sourcePath) {
	for (const suffix of SQLITE_SIDECAR_SUFFIXES) if (sourcePath.endsWith(suffix)) {
		const databasePath = sourcePath.slice(0, -suffix.length);
		return databasePath.endsWith(".sqlite") ? databasePath : void 0;
	}
	return sourcePath.endsWith(".sqlite") ? sourcePath : void 0;
}
function classifyBackupSqliteSource(sourcePath, inventory) {
	const resolvedSourcePath = path.resolve(sourcePath);
	const transient = isTransientSqliteBackupPath(resolvedSourcePath);
	const databasePath = resolveSqliteBackupDatabasePath(resolvedSourcePath);
	if (!transient && !databasePath) return;
	if (!(isPathInside(inventory.stateDir, resolvedSourcePath) || inventory.agentRoots.some(({ sourcePath: agentRoot }) => isPathInside(agentRoot, resolvedSourcePath))) || inventory.isPackageContent(resolvedSourcePath)) return;
	if (transient || !inventory.isIncluded(resolvedSourcePath)) return "excluded";
	if (isAppleDoubleMetadataFile(resolvedSourcePath)) return "excluded";
	const source = databasePath && inventory.resolveSqliteSource(databasePath);
	if (source && source.role === "unresolvable-link") return resolvedSourcePath === databasePath ? "opaque-skip" : "opaque";
	return source ? "sqlite" : "opaque";
}
async function discoverBackupSqliteSources(params) {
	const snapshotPaths = /* @__PURE__ */ new Set();
	const discoveredSourcePaths = /* @__PURE__ */ new Set();
	const visitedDirectories = /* @__PURE__ */ new Set();
	const gatewayLockDir = resolveGatewayLockDir(params.inventory.stateDir);
	const isRetainedPath = (pathname) => !isPathInside(gatewayLockDir, pathname) && !params.inventory.isVolatile(pathname);
	for (const directory of [params.inventory.stateDir, ...params.inventory.agentRoots.map(({ sourcePath }) => sourcePath)]) {
		const root = path.resolve(directory);
		if (visitedDirectories.has(root)) continue;
		visitedDirectories.add(root);
		const { entries, failedDirs } = await walkDirectory(root, {
			symlinks: "include",
			include: (entry) => (entry.kind === "file" || entry.kind === "symlink") && isRetainedPath(entry.path) && classifyBackupSqliteSource(entry.path, params.inventory) === "sqlite",
			descend: (entry) => {
				if (visitedDirectories.has(entry.path) || !isRetainedPath(entry.path) || !params.inventory.isTraversable(entry.path) || params.inventory.isPackageContent(entry.path)) return false;
				visitedDirectories.add(entry.path);
				return true;
			}
		});
		const failure = failedDirs.find(({ error }) => !hasErrnoCode(error, "ENOENT"));
		if (failure) throw failure.error;
		for (const entry of entries) {
			discoveredSourcePaths.add(entry.path);
			if (entry.name.endsWith(".sqlite")) snapshotPaths.add(entry.path);
		}
	}
	for (const sourcePath of params.inventory.coreDatabaseSourcePaths) if (params.inventory.isIncluded(sourcePath)) {
		snapshotPaths.add(sourcePath);
		discoveredSourcePaths.add(sourcePath);
	}
	return {
		snapshotPaths: [...snapshotPaths].toSorted((left, right) => left.localeCompare(right)),
		discoveredSourcePaths
	};
}
async function planBackupSqliteSources(inventory, snapshotPaths) {
	const sources = [];
	for (const archiveSourcePath of snapshotPaths) {
		const identity = await fs$1.stat(archiveSourcePath);
		const owner = inventory.resolveSqliteSource(archiveSourcePath, identity);
		if (!owner || owner.role === "unresolvable-link") throw new Error(`SQLite ownership changed after discovery: ${archiveSourcePath}`);
		let canonicalSource;
		if (owner.role !== "plugin") {
			if (!owner.identity || !sameFileIdentity(owner.identity, identity)) throw new Error(`Canonical SQLite path changed after discovery: ${archiveSourcePath}`);
			canonicalSource = {
				...owner,
				archiveSourcePath: owner.sourcePath,
				sourcePath: await fs$1.realpath(owner.sourcePath),
				identity: owner.identity
			};
			assertNotUpdateCapturePath(canonicalSource.sourcePath, inventory.stateDir);
		}
		sources.push({
			archiveSourcePath,
			path: canonicalSource ? await fs$1.realpath(archiveSourcePath) : archiveSourcePath,
			identity,
			canonicalSource
		});
	}
	return {
		sources,
		groups: await planBackupSqliteSourceGroups(sources)
	};
}
async function createBackupSqliteSnapshotPlan(params) {
	const capturedSnapshots = /* @__PURE__ */ new Map();
	const snapshots = [];
	let capturedAgents = [];
	async function captureSource(archiveSourcePath, sourceGroup, canonicalSource) {
		if (!sourceGroup) throw new Error(`SQLite ownership changed after discovery: ${archiveSourcePath}`);
		if (canonicalSource && !sameFileIdentity(canonicalSource.identity, await fs$1.stat(archiveSourcePath))) throw new Error(`Canonical SQLite path changed after discovery: ${archiveSourcePath}`);
		const sourceDatabasePath = sourceGroup.sourcePath;
		assertNotUpdateCapturePath(archiveSourcePath, params.resources.stateDir);
		assertNotUpdateCapturePath(sourceDatabasePath, params.resources.stateDir);
		const sourcePath = path.join(params.tempDir, `openclaw-state-db-${snapshots.length}.sqlite`);
		try {
			const capture = () => createVerifiedSqliteSnapshot({
				sourcePath: sourceDatabasePath,
				targetPath: sourcePath,
				requireNonEmptySource: Boolean(canonicalSource),
				validate: canonicalSource?.role === "global" ? (database, pathname) => assertOpenClawStateDatabaseOwner(database, { pathname }) : canonicalSource?.role === "agent" ? (database, pathname) => assertOpenClawAgentDatabaseOwner(database, {
					agentId: canonicalSource.agentId,
					pathname
				}) : void 0,
				transform: async (database) => {
					if (canonicalSource?.role === "global") {
						capturedAgents = readOpenClawAgentDatabaseRegistryRows(database, canonicalSource.archiveSourcePath).map((row) => ({
							role: "agent",
							agentId: normalizeAgentId(row.agent_id),
							sourcePath: resolveOpenClawRegisteredAgentDatabasePath(canonicalSource.archiveSourcePath, row.path)
						}));
						if (params.legacyAuditDatabaseWitness !== void 0 && createLegacyAuditDatabaseWitness(database) !== params.legacyAuditDatabaseWitness) throw new LegacyAuditBackupStateChangedError("Legacy audit database rows changed during SQLite backup");
						sanitizeOpenClawGlobalStateSnapshot(database);
						rewriteLegacyAuditBackupCheckpoints(database, params.legacyAuditSnapshots);
					} else if (canonicalSource?.role === "agent") sanitizeOpenClawStateLeaseRows(database);
					await embedSessionColdArchivesInSnapshot({
						database,
						sourceStorePath: canonicalSource?.archiveSourcePath ?? sourceDatabasePath
					});
				}
			});
			const capturedPath = capturedSnapshots.get(sourceGroup);
			await captureBackupSqliteSourceGroup(sourceGroup, () => capturedPath ? fs$1.copyFile(capturedPath, sourcePath, fs$1.constants.COPYFILE_EXCL) : capture());
			capturedSnapshots.set(sourceGroup, sourcePath);
		} catch (error) {
			const stateChange = findLegacyAuditBackupStateChange(error);
			if (stateChange) throw stateChange;
			throw new Error(`SQLite database cannot be compacted safely for backup: ${archiveSourcePath}. ${formatErrorMessage(error)}. The source must pass full integrity checks, online SQLite backup, and offline compaction with its required SQLite capabilities; a direct file copy was refused because it can retain deleted data.`, { cause: error });
		}
		snapshots.push({
			sourcePath,
			archiveSourcePath,
			skippedSourcePaths: new Set([archiveSourcePath, sourceDatabasePath].flatMap((databasePath) => resolveSqliteDatabaseFilePaths(databasePath).map((pathname) => path.resolve(pathname))))
		});
	}
	const globalPath = resolveOpenClawStateSqlitePath({
		...process.env,
		OPENCLAW_STATE_DIR: params.resources.stateDir
	});
	const globalEntry = await fs$1.lstat(globalPath).catch((error) => {
		if (hasErrnoCode(error, "ENOENT")) return;
		throw error;
	});
	let globalIdentity;
	let globalGroup;
	if (globalEntry) {
		globalIdentity = globalEntry.isSymbolicLink() ? await fs$1.stat(globalPath) : globalEntry;
		if (!globalIdentity.isFile()) throw new Error(`Canonical global SQLite path must be a regular file or symlink to one: ${globalPath}`);
		const globalSource = {
			role: "global",
			archiveSourcePath: globalPath,
			sourcePath: await fs$1.realpath(globalPath),
			identity: globalIdentity
		};
		const globalInventory = sealBackupResourceInventory(params.resources, [{
			role: "global",
			sourcePath: globalPath,
			identity: globalIdentity
		}]);
		const { snapshotPaths } = await discoverBackupSqliteSources({ inventory: globalInventory });
		const { groups } = await planBackupSqliteSources(globalInventory, snapshotPaths.filter((sourcePath) => globalInventory.resolveSqliteSource(sourcePath)?.role === "global"));
		globalGroup = groups.get(globalSource.sourcePath);
		await captureSource(globalPath, globalGroup, globalSource);
	} else for (const sidecar of resolveSqliteDatabaseFilePaths(globalPath).slice(1)) if (await fs$1.lstat(sidecar).catch((error) => {
		if (hasErrnoCode(error, "ENOENT")) return;
		throw error;
	})) throw new Error(`Canonical global SQLite database is missing but a sidecar remains: ${sidecar}`);
	const coreDatabases = [{
		role: "global",
		sourcePath: globalPath,
		identity: globalIdentity
	}];
	const defaultDatabases = await listDefaultAgentDatabasePaths(params.resources.stateDir);
	const discoveredDatabasePaths = [];
	const candidates = [
		{
			role: "quarantine",
			sourcePath: resolveQuarantineStorePath({
				...process.env,
				OPENCLAW_STATE_DIR: params.resources.stateDir
			})
		},
		...capturedAgents,
		...params.resources.agentRoots.map(({ agentId, databasePath }) => ({
			role: "agent",
			agentId,
			sourcePath: databasePath
		})),
		...defaultDatabases.toSorted((a, b) => a.path.localeCompare(b.path)).map(({ agentId, path: sourcePath }) => ({
			role: "agent",
			agentId: normalizeAgentId(agentId),
			sourcePath,
			discoveredPath: sourcePath
		}))
	];
	for (const { discoveredPath, ...database } of candidates) {
		const identity = await fs$1.stat(database.sourcePath).catch((error) => {
			if (hasErrnoCode(error, "ENOENT")) return;
			throw error;
		});
		if (identity && !identity.isFile()) throw new Error(`Core SQLite path must resolve to a regular file: ${database.sourcePath}`);
		if (discoveredPath && !identity) continue;
		if (discoveredPath) {
			discoveredDatabasePaths.push(discoveredPath);
			database.sourcePath = await fs$1.realpath(database.sourcePath);
		}
		coreDatabases.push(Object.freeze({
			...database,
			identity
		}));
	}
	const inventory = sealBackupResourceInventory({
		...params.resources,
		protectedPaths: [...params.resources.protectedPaths, ...discoveredDatabasePaths]
	}, coreDatabases);
	const discovery = await discoverBackupSqliteSources({ inventory });
	const { sources, groups } = await planBackupSqliteSources(inventory, discovery.snapshotPaths);
	for (const source of sources) if (source.archiveSourcePath !== globalPath) await captureSource(source.archiveSourcePath, source.canonicalSource?.role === "global" ? globalGroup : groups.get(source.path), source.canonicalSource);
	if (globalGroup) await assertBackupSqliteSourceGroup(globalGroup);
	for (const source of coreDatabases) if (source.identity && !sameFileIdentity(source.identity, await fs$1.stat(source.sourcePath))) throw new Error(`Canonical SQLite path changed after discovery: ${source.sourcePath}`);
	return {
		inventory,
		snapshots,
		discoveredSourcePaths: discovery.discoveredSourcePaths
	};
}
//#endregion
//#region src/infra/backup-tar-retry.ts
const BACKUP_TAR_MAX_ATTEMPTS = 3;
const BACKUP_TAR_BACKOFF_MS = [1e4, 2e4];
async function writeTarArchiveWithRetry(params) {
	const sleepFn = params.sleepMs ?? sleep;
	let lastErr;
	let attempts = 0;
	for (let attempt = 1; attempt <= BACKUP_TAR_MAX_ATTEMPTS; attempt += 1) {
		attempts = attempt;
		const attemptTempArchivePath = attempt === 1 ? params.tempArchivePath : `${params.tempArchivePath}.retry-${attempt}`;
		try {
			return await params.runTar(attemptTempArchivePath);
		} catch (err) {
			lastErr = err;
			if (!hasErrnoCode(err, "EOF") || attempt === BACKUP_TAR_MAX_ATTEMPTS) break;
			const backoff = BACKUP_TAR_BACKOFF_MS[attempt - 1] ?? 0;
			const offendingPath = err.path;
			params.log?.(`Backup archiver hit a live-write race${offendingPath ? ` on ${offendingPath}` : ""} (attempt ${attempt}/${BACKUP_TAR_MAX_ATTEMPTS}); retrying in ${Math.round(backoff / 1e3)}s.`);
			await sleepFn(backoff);
		}
	}
	const final = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
	const offendingPath = lastErr?.path;
	const attemptSuffix = `after ${attempts} attempt${attempts === 1 ? "" : "s"}`;
	const suffix = offendingPath ? ` (last offending path: ${offendingPath}, ${attemptSuffix})` : ` (${attemptSuffix})`;
	throw new Error(`Backup archive write failed: ${final.message}${suffix}`, { cause: final });
}
//#endregion
//#region src/infra/backup-tar-walk.ts
/** Open sources before emitting headers so a vanished name cannot leave a partial entry. */
async function* walkBackupTar(params) {
	const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
	const assertDirectory = async ({ sourcePath, stat }) => {
		const current = await fs$1.lstat(sourcePath);
		if (!current.isDirectory() || !sameFileIdentity(stat, current)) throw new Error(`Backup directory changed during traversal: ${sourcePath}`);
	};
	async function* visit(sourcePath, parent) {
		params.onProgress(sourcePath);
		if (params.skip(sourcePath)) return;
		let handle;
		let stat;
		let names;
		let linkpath;
		try {
			try {
				stat = await fs$1.lstat(sourcePath);
				if (parent) await assertDirectory(parent);
				if (!params.filter(sourcePath, stat)) return;
				if (stat.isDirectory()) {
					names = await fs$1.readdir(sourcePath);
					await assertDirectory({
						sourcePath,
						stat
					});
				} else if (stat.isSymbolicLink()) {
					linkpath = await fs$1.readlink(sourcePath);
					if (process.platform === "win32") linkpath = linkpath.replaceAll("\\", "/");
				} else if (stat.isFile()) {
					handle = await fs$1.open(sourcePath, constants.O_RDONLY | noFollow);
					const opened = await handle.stat();
					if (!opened.isFile()) throw new Error(`Backup source changed while opening: ${sourcePath}`);
					if (!noFollow) {
						const current = await fs$1.lstat(sourcePath).catch((error) => {
							if (hasErrnoCode(error, "ENOENT")) return;
							throw error;
						});
						if (current?.isSymbolicLink()) throw new Error(`Backup source became a symbolic link while opening: ${sourcePath}`);
						if (!sameFileIdentity(stat, opened) && (!current?.isFile() || !sameFileIdentity(current, opened))) throw new Error(`Backup source identity changed while opening: ${sourcePath}`);
					}
					stat = opened;
				} else return;
				if (parent) await assertDirectory(parent);
			} catch (error) {
				if (!hasErrnoCode(error, "ENOENT")) throw error;
				params.onVanished(sourcePath);
				return;
			}
			const directory = stat.isDirectory();
			let mode = (stat.mode & 4095 | 384) & -19;
			if (directory) mode |= (mode & 292) >> 2;
			const header = {
				path: sourcePath,
				type: directory ? "Directory" : stat.isSymbolicLink() ? "SymbolicLink" : "File",
				mode,
				size: handle ? stat.size : 0,
				mtime: directory ? void 0 : stat.mtime,
				linkpath
			};
			params.onEntry(sourcePath, header);
			if (directory) header.path += "/";
			const block = Buffer.alloc(512);
			if (new params.tar.Header(header).encode(block)) yield new params.tar.Pax(header).encode();
			yield block;
			if (handle) {
				let position = 0;
				while (position < stat.size) {
					const buffer = Buffer.allocUnsafe(Math.min(1048576, stat.size - position));
					const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
					if (!bytesRead) throw Object.assign(/* @__PURE__ */ new Error("encountered unexpected EOF"), {
						code: "EOF",
						path: sourcePath
					});
					position += bytesRead;
					params.onProgress(sourcePath, bytesRead);
					yield buffer.subarray(0, bytesRead);
				}
				const padding = (512 - stat.size % 512) % 512;
				if (padding) yield Buffer.alloc(padding);
			}
		} finally {
			await handle?.close();
		}
		for (const name of names ?? []) yield* visit(path.join(sourcePath, name), {
			sourcePath,
			stat
		});
		if (names && !parent) await assertDirectory({
			sourcePath,
			stat
		});
	}
	for (const sourcePath of params.paths) yield* visit(sourcePath);
	yield Buffer.alloc(1024);
}
//#endregion
//#region src/infra/backup-create.ts
const loadTarRuntime = createLazyRuntimeModule(() => import("tar"));
async function resolveOutputPath(params) {
	const basename = buildBackupArchiveBasename(params.nowMs);
	const rawOutput = params.output?.trim();
	if (!rawOutput) {
		const cwd = path.resolve(process.cwd());
		const canonicalCwd = await fs$1.realpath(cwd).catch(() => cwd);
		const defaultDir = params.includedAssets.some((asset) => isPathInside(asset.sourcePath, canonicalCwd)) ? resolveHomeDir() ?? path.dirname(params.stateDir) : cwd;
		return path.resolve(defaultDir, basename);
	}
	const resolved = resolveUserPath(rawOutput);
	if (rawOutput.endsWith("/") || rawOutput.endsWith("\\")) return path.join(resolved, basename);
	try {
		if ((await fs$1.stat(resolved)).isDirectory()) return path.join(resolved, basename);
	} catch {}
	return resolved;
}
function formatBackupOutputFailure(error, outputPath, phase, ownedRoot) {
	const cause = phase === "write" && error instanceof Error ? error.cause : void 0;
	const filesystemError = isErrno(error) ? error : isErrno(cause) ? cause : null;
	if (!filesystemError) return error;
	if (ownedRoot) {
		const failedPath = filesystemError.path;
		if (typeof failedPath !== "string" || !isPathInside(ownedRoot, path.resolve(failedPath))) return error;
	}
	const outputParent = path.dirname(outputPath);
	const retry = "run `openclaw backup create --output <archive>` again.";
	let detail;
	switch (filesystemError.code) {
		case "ENOENT":
			detail = `Backup output directory could not be created: ${outputParent}. Check the path and ${retry}`;
			break;
		case "EACCES":
		case "EPERM":
		case "EROFS":
			detail = `Backup output directory is not writable: ${outputParent}. Check the path and directory permissions, then ${retry}`;
			break;
		case "EEXIST":
		case "ENOTDIR":
			if (phase !== "parent") return error;
			detail = `Backup output parent is not a directory: ${outputParent}. Choose a directory path and ${retry}`;
			break;
		case "ENOSPC":
			detail = `The destination does not have enough free space: ${outputParent}. Free up disk space and ${retry}`;
			break;
		case "EDQUOT":
			detail = `The destination storage quota is exhausted: ${outputParent}. Free up space or choose another path, then ${retry}`;
			break;
		default: detail = `The output path could not be prepared: ${outputParent}. Check the path and filesystem, then ${retry}`;
	}
	return new Error(`Backup archive creation failed: ${outputPath}. ${detail}`, { cause: error });
}
async function assertOutputPathReady(outputPath) {
	try {
		await fs$1.access(outputPath);
		throw new Error(`Refusing to overwrite existing backup archive: ${outputPath}`);
	} catch (error) {
		const code = error?.code;
		if (code === "ENOENT" || code === "ENOTDIR") return;
		throw formatBackupOutputFailure(error, outputPath, "parent");
	}
}
async function prepareBackupOutputParent(outputPath) {
	try {
		await fs$1.mkdir(path.dirname(outputPath), { recursive: true });
	} catch (error) {
		throw formatBackupOutputFailure(error, outputPath, "parent");
	}
}
async function chooseBackupTempRoot(params) {
	const systemTmp = os.tmpdir();
	const canonicalSystemTmp = await canonicalizePathForContainment(systemTmp);
	if (!params.assets.some((asset) => isPathInside(asset.sourcePath, canonicalSystemTmp))) return systemTmp;
	const fallback = path.dirname(params.outputPath);
	const canonicalFallback = await canonicalizePathForContainment(fallback);
	const fallbackInsideAsset = params.assets.find((asset) => isPathInside(asset.sourcePath, canonicalFallback));
	if (fallbackInsideAsset) throw new Error(`Backup temp root cannot be placed outside every source path: ${systemTmp} and ${fallback} both overlap ${fallbackInsideAsset.sourcePath}.`);
	return fallback;
}
function buildManifest(result, plan) {
	return {
		schemaVersion: 1,
		createdAt: result.createdAt,
		archiveRoot: result.archiveRoot,
		runtimeVersion: resolveRuntimeServiceVersion(),
		platform: process.platform,
		nodeVersion: process.version,
		options: {
			includeWorkspace: result.includeWorkspace,
			onlyConfig: result.onlyConfig
		},
		paths: {
			stateDir: plan.resources.stateDir,
			configPath: plan.configPath,
			oauthDir: plan.oauthDir,
			workspaceDirs: plan.workspaceDirs,
			...result.agentRoots ? { agentRoots: [...result.agentRoots] } : {}
		},
		assets: result.assets.map((asset) => ({
			kind: asset.kind,
			sourcePath: asset.sourcePath,
			archivePath: asset.archivePath
		})),
		skipped: result.skipped.map((entry) => ({
			kind: entry.kind,
			sourcePath: entry.sourcePath,
			reason: entry.reason,
			coveredBy: entry.coveredBy
		}))
	};
}
const MAX_LEGACY_AUDIT_CAPTURE_ATTEMPTS = 3;
async function createConsistentStateSnapshotPlan(params) {
	if (params.onlyConfig) return {
		legacyAuditSnapshots: [],
		stateSqliteBackup: {
			inventory: sealBackupResourceInventory(params.resources, []),
			snapshots: [],
			discoveredSourcePaths: /* @__PURE__ */ new Set()
		}
	};
	if (!params.stateDir) return {
		legacyAuditSnapshots: [],
		stateSqliteBackup: await createBackupSqliteSnapshotPlan({
			resources: params.resources,
			tempDir: params.tempDir,
			legacyAuditSnapshots: []
		})
	};
	const stateDir = params.stateDir;
	if (!await hasLegacyAuditBackupSources(stateDir)) {
		const fastAttemptDir = path.join(params.tempDir, "state-snapshot-no-legacy");
		await fs$1.mkdir(fastAttemptDir, { recursive: true });
		const stateSqliteBackup = await createBackupSqliteSnapshotPlan({
			resources: params.resources,
			tempDir: fastAttemptDir,
			legacyAuditSnapshots: []
		});
		if (!await hasLegacyAuditBackupSources(stateDir)) return {
			legacyAuditSnapshots: [],
			stateSqliteBackup
		};
		await fs$1.rm(fastAttemptDir, {
			recursive: true,
			force: true
		});
	}
	let lastStateChangeMessage;
	for (let attempt = 0; attempt < MAX_LEGACY_AUDIT_CAPTURE_ATTEMPTS; attempt += 1) {
		const attemptDir = path.join(params.tempDir, `state-snapshot-attempt-${attempt + 1}`);
		const verificationDir = path.join(attemptDir, "legacy-verification");
		await fs$1.mkdir(attemptDir, { recursive: true });
		try {
			const firstCapture = await withLegacyAuditMigrationLease(stateDir, () => createLegacyAuditBackupCapture({
				stateDir,
				tempDir: attemptDir
			}));
			const stateSqliteBackup = await createBackupSqliteSnapshotPlan({
				resources: params.resources,
				tempDir: attemptDir,
				legacyAuditSnapshots: firstCapture.snapshots,
				legacyAuditDatabaseWitness: firstCapture.databaseWitness
			});
			await fs$1.mkdir(verificationDir, { recursive: true });
			if (!legacyAuditBackupCapturesMatch(firstCapture, await withLegacyAuditMigrationLease(stateDir, () => createLegacyAuditBackupCapture({
				stateDir,
				tempDir: verificationDir
			})))) throw new LegacyAuditBackupStateChangedError();
			await fs$1.rm(verificationDir, {
				recursive: true,
				force: true
			});
			return {
				legacyAuditSnapshots: firstCapture.snapshots,
				stateSqliteBackup
			};
		} catch (error) {
			await fs$1.rm(attemptDir, {
				recursive: true,
				force: true
			});
			if (!(error instanceof LegacyAuditBackupStateChangedError)) throw error;
			lastStateChangeMessage = error.message;
		}
	}
	throw new LegacyAuditBackupStateChangedError(`${lastStateChangeMessage ?? "Legacy audit state changed while backup was capturing it"}; retry backup after legacy audit migration settles`);
}
async function createBackupArchive(opts = {}) {
	const nowMs = resolveDateTimestampMs(opts.nowMs);
	const archiveRoot = buildBackupArchiveRoot(nowMs);
	const onlyConfig = Boolean(opts.onlyConfig);
	const includeWorkspace = onlyConfig ? false : opts.includeWorkspace ?? true;
	const plan = await resolveBackupPlanFromDisk({
		includeWorkspace,
		onlyConfig,
		nowMs
	});
	const outputPath = await resolveOutputPath({
		output: opts.output,
		nowMs,
		includedAssets: plan.included,
		stateDir: plan.stateDir
	});
	if (plan.included.length === 0) throw new Error(onlyConfig ? "No OpenClaw config file was found to back up." : "No local OpenClaw state was found to back up.");
	const canonicalOutputPath = await canonicalizePathForContainment(outputPath);
	const overlappingAsset = plan.included.find((asset) => isPathInside(asset.sourcePath, canonicalOutputPath));
	if (overlappingAsset) throw new Error(`Backup output must not be written inside a source path: ${outputPath} is inside ${overlappingAsset.sourcePath}`);
	if (!opts.dryRun) await assertOutputPathReady(outputPath);
	const createdAt = new Date(nowMs).toISOString();
	const stateAsset = plan.included.find((asset) => asset.kind === "state");
	const stateDir = plan.resources.stateDir;
	const result = {
		createdAt,
		archiveRoot,
		archivePath: outputPath,
		dryRun: Boolean(opts.dryRun),
		includeWorkspace,
		onlyConfig,
		verified: false,
		assets: plan.included,
		...onlyConfig ? {} : { agentRoots: plan.resources.agentRoots.map(({ agentId, sourcePath }) => ({
			agentId,
			sourcePath
		})) },
		skipped: plan.skipped,
		skippedVolatileCount: 0
	};
	if (opts.dryRun) return result;
	await prepareBackupOutputParent(outputPath);
	const tempRoot = await chooseBackupTempRoot({
		assets: result.assets,
		outputPath
	});
	await fs$1.mkdir(tempRoot, { recursive: true });
	const maintenance = await maintainBackupScratch({
		roots: [tempRoot],
		repair: true,
		log: opts.log
	});
	if (maintenance.warnings.length) result.warnings = maintenance.warnings;
	for (const directory of maintenance.reclaimed) opts.log?.(`Removed abandoned backup scratch: ${directory}`);
	const scratch = await createBackupScratchDirectory(tempRoot);
	const tempDir = scratch.directory;
	let publication;
	try {
		publication = await createBackupArchivePublication(outputPath);
	} catch (error) {
		await finishBackupScratch(scratch, opts.log);
		throw formatBackupOutputFailure(error, outputPath, "publication");
	}
	const tempArchivePath = publication.tempArchivePath;
	let snapshotFacts = [];
	try {
		const configRemaps = await stageBackupConfigCapture(plan.configCapture, tempDir);
		const { legacyAuditSnapshots, stateSqliteBackup } = await createConsistentStateSnapshotPlan({
			resources: plan.resources,
			stateDir: stateAsset?.sourcePath,
			tempDir,
			onlyConfig
		});
		const inventory = stateSqliteBackup.inventory;
		snapshotFacts = describeCapturedBackupSqliteSnapshots(inventory, stateSqliteBackup.snapshots.map((snapshot) => snapshot.archiveSourcePath));
		const sourcePathRemaps = new Map(configRemaps);
		const skippedStateSourcePaths = new Set(configRemaps.values());
		if (plan.configCapture?.files.length === 0) {
			skippedStateSourcePaths.add(path.resolve(plan.configPath));
			skippedStateSourcePaths.add(await canonicalizePathForContainment(plan.configPath));
		}
		for (const snapshot of [...stateSqliteBackup.snapshots, ...legacyAuditSnapshots]) {
			sourcePathRemaps.set(path.resolve(snapshot.sourcePath), snapshot.archiveSourcePath);
			for (const skippedSourcePath of snapshot.skippedSourcePaths) skippedStateSourcePaths.add(skippedSourcePath);
		}
		const requiredSourcePaths = /* @__PURE__ */ new Set([...sourcePathRemaps.keys(), ...result.assets.map((asset) => asset.sourcePath)]);
		const externalSymbolicLinks = [];
		const tar = await loadTarRuntime();
		const gatewayLockDir = resolveGatewayLockDir(plan.stateDir);
		const skippedEntries = /* @__PURE__ */ new Map();
		const opaqueSqliteSourcePaths = /* @__PURE__ */ new Map();
		const tarFilter = (entryPath, entryStat) => {
			const resolvedEntryPath = path.resolve(entryPath);
			if (isUpdateCapturePath(sourcePathRemaps.get(resolvedEntryPath) ?? resolvedEntryPath, plan.stateDir)) return false;
			const isDirectory = entryStat.isDirectory();
			const inventoryPath = sourcePathRemaps.get(resolvedEntryPath) ?? resolvedEntryPath;
			if (!onlyConfig && !(isDirectory || entryStat.isSymbolicLink() ? inventory.isTraversable(inventoryPath) : inventory.isIncluded(inventoryPath))) return false;
			if (isPathInside(gatewayLockDir, resolvedEntryPath)) return false;
			if (stateAsset && isLegacyAuditMigrationBackupPath(resolvedEntryPath, stateAsset.sourcePath)) return false;
			const sqliteSourceKind = onlyConfig ? void 0 : classifyBackupSqliteSource(resolvedEntryPath, inventory);
			if (sqliteSourceKind === "opaque-skip") {
				opaqueSqliteSourcePaths.set(resolvedEntryPath, "skipped");
				return false;
			}
			if (sqliteSourceKind === "excluded") return false;
			if (sqliteSourceKind === "sqlite" && (entryStat.isFile() || entryStat.isSymbolicLink())) throw new Error(`SQLite state appeared after snapshot discovery: ${entryPath}. Retry backup so it can be snapshotted.`);
			if (sqliteSourceKind === "opaque" && entryStat.isFile()) opaqueSqliteSourcePaths.set(resolvedEntryPath, "archived");
			return true;
		};
		const completedArchive = await writeTarArchiveWithRetry({
			tempArchivePath,
			log: opts.log,
			runTar: async (attemptTempArchivePath) => {
				for (const [sourcePath, reason] of skippedEntries) if (reason === "volatile") skippedEntries.delete(sourcePath);
				externalSymbolicLinks.length = 0;
				opaqueSqliteSourcePaths.clear();
				const prepared = await writeArchiveStreamToFile({
					archivePath: attemptTempArchivePath,
					createArchiveStream: (reportProgress) => appendBackupManifest(walkBackupTar({
						tar,
						paths: [...requiredSourcePaths],
						skip: (sourcePath) => {
							if (!requiredSourcePaths.has(sourcePath) && inventory.isVolatile(sourcePath)) {
								skippedEntries.set(sourcePath, "volatile");
								return true;
							}
							return skippedStateSourcePaths.has(sourcePath) || stateSqliteBackup.discoveredSourcePaths.has(sourcePath);
						},
						filter: tarFilter,
						onVanished: (sourcePath) => {
							if (requiredSourcePaths.has(sourcePath)) throw new Error(`Required backup source disappeared: ${sourcePath}`);
							opaqueSqliteSourcePaths.delete(sourcePath);
							skippedEntries.set(sourcePath, "vanished");
						},
						onProgress: (entryPath, bytes) => reportProgress({
							phase: bytes === void 0 ? "traversal" : "raw",
							entryPath,
							bytes
						}),
						onEntry: (sourcePath, header) => {
							skippedEntries.delete(sourcePath);
							const archiveEntryPath = buildBackupArchivePath(archiveRoot, sourcePathRemaps.get(sourcePath) ?? sourcePath);
							if (header.type === "SymbolicLink") {
								const { external, ...link } = recordArchiveSymbolicLink({
									archiveRoot,
									entryPath: archiveEntryPath,
									linkpath: header.linkpath,
									platform: process.platform,
									state: {
										sourcePath: stateDir,
										archivePath: buildBackupArchivePath(archiveRoot, stateDir)
									},
									hasExternalLinkReport: true,
									assets: result.assets
								});
								if (external) externalSymbolicLinks.push(link);
							}
							header.path = archiveEntryPath;
						}
					}), () => {
						result.skipped = [...plan.skipped, ...[...skippedEntries].map(([sourcePath, reason]) => ({
							kind: "entry",
							sourcePath,
							displayPath: sourcePath,
							reason
						}))];
						const manifest = buildManifest({
							...result,
							skipped: plan.skipped
						}, plan);
						manifest.externalSymbolicLinks = externalSymbolicLinks;
						manifest.sqliteSnapshots = snapshotFacts.map((snapshot) => snapshot.role === "agent" ? {
							sourcePath: snapshot.sourcePath,
							role: "agent",
							agentId: snapshot.agentId
						} : {
							sourcePath: snapshot.sourcePath,
							role: "global"
						});
						const contents = Buffer.from(JSON.stringify(manifest, null, 2) + "\n");
						const sizeError = backupManifestSizeError(contents.length);
						if (sizeError) throw sizeError;
						const header = Buffer.alloc(512);
						new tar.Header({
							path: archiveRoot + "/manifest.json",
							type: "File",
							mode: 420,
							size: contents.length
						}).encode(header);
						return Buffer.concat([
							header,
							contents,
							Buffer.alloc((512 - contents.length % 512) % 512 + 1024)
						]);
					}),
					onPartialArchive: (partialArchive) => {
						publication.pendingCleanupArchives.push(partialArchive);
					}
				});
				try {
					await plan.configCapture?.assertRootAlias?.();
				} catch (error) {
					if (!removePreparedBackupArchive(prepared)) publication.pendingCleanupArchives.push(prepared);
					throw error;
				}
				return prepared;
			}
		}).catch((error) => {
			throw formatBackupOutputFailure(error, outputPath, "write", publication.stagingDir);
		});
		const skippedVolatileCount = [...skippedEntries.values()].filter((reason) => reason === "volatile").length;
		result.skippedVolatileCount = skippedVolatileCount;
		const vanishedWarnings = [...skippedEntries].filter(([, reason]) => reason === "vanished").map(([sourcePath]) => `Skipped vanished entry (ENOENT): ${sourcePath}`);
		if (opaqueSqliteSourcePaths.size) result.warnings = [...result.warnings ?? [], ...[...opaqueSqliteSourcePaths].toSorted(([left], [right]) => left.localeCompare(right)).map(([sourcePath, action]) => action === "skipped" ? `Skipped unresolvable opaque SQLite link: ${sourcePath}` : `SQLite file archived as opaque bytes without a live snapshot or integrity checks: ${sourcePath}`)];
		if (vanishedWarnings.length) result.warnings = [...result.warnings ?? [], ...vanishedWarnings];
		if (externalSymbolicLinks.length) result.externalSymbolicLinks = externalSymbolicLinks;
		if (skippedVolatileCount > 0) opts.log?.(`Backup skipped ${skippedVolatileCount} volatile file${skippedVolatileCount === 1 ? "" : "s"} (live sessions, cron logs, queues, managed runtime paths, sockets, pid/tmp).`);
		try {
			await publishPreparedBackupArchive({
				plan: publication,
				prepared: completedArchive,
				log: opts.log
			});
		} catch (error) {
			throw formatBackupOutputFailure(error, outputPath, "publication");
		}
	} finally {
		try {
			await cleanupBackupArchivePublication(publication, opts.log);
		} finally {
			const warning = await finishBackupScratch(scratch, opts.log);
			if (warning) result.warnings = [...result.warnings ?? [], warning];
		}
	}
	opts.onSqliteSnapshots?.(snapshotFacts);
	return result;
}
//#endregion
export { createBackupArchive as t };