UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,632 lines 63.2 kB
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { r as isMissingPathError } from "./errno-CkbDOfLk.js";
import { w as root } from "./fs-safe-B6pvPGnf.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import "./config-Cs0XXL3x.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { n as withOpenClawStateLease } from "./openclaw-state-lease-Ciyr2uVu.js";
import { t as createCrustaceanSlug } from "./session-slug-DUj8bbny.js";
import { t as createCommandError } from "./command-error-D_Bz4MBL.js";
import { a as insideGitCheckout, c as requireGitBuffer, d as worktreePathExists, l as requireGitRaw, o as listGitWorktrees, s as requireGit, t as commandError, u as runGit } from "./git-BuLvhVpD.js";
import { r as createStagedInputPathMatcher, t as STAGED_INPUT_GIT_PATHSPEC } from "./staged-inputs-CJwnNq26.js";
import { a as hasLiveWorktreeRunLease, c as lockWorktreeForProcess, i as finalizeWorktreeRemoval, l as unlockWorktree, r as claimWorktreeRemoval, s as lockState, t as abortWorktreeRemoval } from "./run-lease-DzaY2Oqv.js";
import { C as updateRegistryWorktree, _ as insertRegistryWorktree, a as clearRegistryWorktreeProvisionedChunks, d as getRegistryWorktree, f as getRegistryWorktreeProvisionedChunk, l as findLiveRegistryWorktreeByOwner, m as getRegistryWorktreeProvisionedState, o as deleteRegistryWorktree, p as getRegistryWorktreeProvisionedPaths, t as WorktreeRemovalContentionError, u as findLiveRegistryWorktreeByPath, v as insertRegistryWorktreeProvisionedChunk, y as listRegistryWorktrees } from "./registry-Dvi_fTvk.js";
import { n as formatDiskSpaceBytes, r as tryReadDiskSpace } from "./disk-space-CtKhSv74.js";
import { constants, statSync } 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";
//#region src/agents/worktrees/base-ref.ts
async function resolveWorktreeBase(repoRoot, baseRef, signal) {
	if (baseRef) {
		let gitOperand = baseRef;
		if (baseRef !== "-" && baseRef.startsWith("-")) {
			const symbolic = await runGit(repoRoot, [
				"-c",
				"core.warnAmbiguousRefs=true",
				"rev-parse",
				"--symbolic-full-name",
				"--verify",
				"--end-of-options",
				baseRef
			]);
			const fullRef = symbolic.stdout.trim();
			if (symbolic.code !== 0) throw commandError("git rev-parse --symbolic-full-name --verify", symbolic);
			if (fullRef) {
				if (!fullRef.startsWith("refs/") || fullRef.includes("\n")) throw commandError("git rev-parse --symbolic-full-name --verify", symbolic);
				gitOperand = fullRef;
			} else {
				if (symbolic.stderr.trim()) throw commandError("git rev-parse --symbolic-full-name --verify", symbolic);
				gitOperand = await requireGit(repoRoot, [
					"rev-parse",
					"--verify",
					"--end-of-options",
					`${baseRef}^{commit}`
				]);
			}
		}
		return {
			gitOperand,
			recordRef: baseRef,
			remote: false
		};
	}
	const fetched = await runGit(repoRoot, ["fetch", "origin"], { signal });
	signal?.throwIfAborted();
	if (fetched.code === 0) {
		const remoteHead = await runGit(repoRoot, [
			"symbolic-ref",
			"--quiet",
			"--short",
			"refs/remotes/origin/HEAD"
		]);
		if (remoteHead.code === 0 && remoteHead.stdout.trim()) {
			const remoteRef = remoteHead.stdout.trim();
			return {
				gitOperand: remoteRef,
				recordRef: remoteRef,
				remote: true
			};
		}
	}
	return {
		gitOperand: "HEAD",
		recordRef: "HEAD",
		remote: false
	};
}
//#endregion
//#region src/agents/worktrees/capacity.ts
const GiB = 1024 ** 3;
const WORKTREE_SETUP_HEADROOM_BYTES = 4 * GiB;
/** Admission estimates allocations, not a quota on arbitrary repository scripts or other writers. */
function requireWorktreeDiskSpace(demands, purpose, snapshot = false) {
	const volumes = /* @__PURE__ */ new Map();
	for (const demand of demands) {
		const space = tryReadDiskSpace(demand.path);
		if (!space || space.totalBytes === null) throw new Error(`Cannot determine disk space near ${demand.path}; check the volume and retry ${purpose}.`);
		const device = statSync(space.checkedPath).dev;
		const existing = volumes.get(device);
		if (existing) {
			existing.available = Math.min(existing.available, space.availableBytes);
			existing.bytes += demand.bytes;
		} else volumes.set(device, {
			path: space.checkedPath,
			available: space.availableBytes,
			total: space.totalBytes,
			bytes: demand.bytes
		});
	}
	for (const volume of volumes.values()) {
		const required = (snapshot ? 128 * 1024 ** 2 : Math.max(4 * GiB, Math.min(volume.total / 10, 16 * GiB))) + volume.bytes;
		if (!Number.isSafeInteger(Math.ceil(required)) || volume.available < required) throw new Error(`Insufficient disk space near ${volume.path} for ${purpose}: ${formatDiskSpaceBytes(volume.available)} available; approximately ${formatDiskSpaceBytes(required)} required including safety reserve. Free caches or archive/remove unused worktrees, then retry.`);
	}
}
async function estimateWorktreeGitBytes(repoRoot, ref) {
	const commit = await requireGit(repoRoot, [
		"rev-parse",
		"--verify",
		"--end-of-options",
		`${ref === "-" ? "@{-1}" : ref}^{commit}`
	]);
	const sizes = await requireGit(repoRoot, [
		"ls-tree",
		"-r",
		"--format=%(objectsize)",
		commit,
		"--"
	]);
	let bytes = 0;
	for (const size of sizes.split("\n")) {
		if (!size || size === "-") continue;
		const value = Number(size);
		if (!Number.isSafeInteger(value) || value < 0) throw new Error("Cannot estimate worktree checkout size; inspect the repository objects and retry.");
		bytes += Math.max(4096, Math.ceil(value / 4096) * 4096);
	}
	return bytes;
}
/** Measure without following links; unreadable trees must never be counted as empty. */
async function directorySizeBytes(root, excludeGit = false) {
	let entries;
	try {
		entries = await fs$1.readdir(root, { withFileTypes: true });
	} catch (error) {
		if (isMissingPathError(error)) return 0;
		throw error;
	}
	let total = 0;
	for (const entry of entries) {
		if (excludeGit && entry.name === ".git") continue;
		const child = path.join(root, entry.name);
		if (entry.isDirectory() && !entry.isSymbolicLink()) total += await directorySizeBytes(child, excludeGit);
		else try {
			total += (await fs$1.lstat(child)).size;
		} catch (error) {
			if (!isMissingPathError(error)) throw error;
		}
	}
	return total;
}
//#endregion
//#region src/agents/worktrees/owner.ts
function worktreeOwnerMatches(record, params) {
	return record.ownerKind === (params.ownerKind ?? "manual") && (record.ownerId ?? void 0) === (params.ownerId ?? void 0);
}
//#endregion
//#region src/agents/worktrees/provisioned-files.ts
function normalizeRelativePath(relativePath) {
	if (path.isAbsolute(relativePath)) return;
	const segments = relativePath.split("/");
	if (segments.length === 0 || segments.some((segment) => !segment || segment === "." || segment === "..")) return;
	return segments.join("/");
}
function resolveGitPath(root, relativePath) {
	return path.join(root, ...relativePath.split("/"));
}
async function hasSafeParentDirectories(root, relativePath) {
	const segments = relativePath.split("/");
	let current = root;
	for (const segment of segments.slice(0, -1)) {
		current = path.join(current, segment);
		try {
			const stat = await fs$1.lstat(current);
			if (stat.isSymbolicLink() || !stat.isDirectory()) return false;
		} catch (error) {
			if (error.code !== "ENOENT") throw error;
		}
	}
	return true;
}
async function lstatIfExists(target) {
	try {
		return await fs$1.lstat(target);
	} catch (error) {
		if (error.code === "ENOENT") return;
		throw error;
	}
}
async function copyProvisionedFile(params) {
	const normalized = normalizeRelativePath(params.relativePath);
	if (!normalized || !await hasSafeParentDirectories(params.repoRoot, normalized) || !await hasSafeParentDirectories(params.worktreePath, normalized)) return false;
	const source = resolveGitPath(params.repoRoot, normalized);
	const destination = resolveGitPath(params.worktreePath, normalized);
	const sourceStat = await fs$1.lstat(source).catch(() => void 0);
	if (!sourceStat?.isFile() || sourceStat.isSymbolicLink()) return false;
	await fs$1.mkdir(path.dirname(destination), { recursive: true });
	try {
		await fs$1.copyFile(source, destination, constants.COPYFILE_EXCL);
	} catch (error) {
		if (error.code === "EEXIST") return false;
		throw error;
	}
	await fs$1.chmod(destination, sourceStat.mode);
	return true;
}
async function includedFilePaths(repoRoot) {
	const includePath = path.join(repoRoot, ".worktreeinclude");
	if (!await worktreePathExists(includePath)) return [];
	const candidatesRaw = await requireGitRaw(repoRoot, [
		"ls-files",
		"--others",
		"--ignored",
		"--exclude-standard",
		"-z"
	]);
	const includedRaw = await requireGitRaw(repoRoot, [
		"ls-files",
		"--others",
		"--ignored",
		`--exclude-from=${includePath}`,
		"-z"
	]);
	const included = new Set(includedRaw.split("\0").filter(Boolean));
	return candidatesRaw.split("\0").filter((relativePath) => included.has(relativePath));
}
async function estimateProvisionedFileBytes(repoRoot) {
	let bytes = 0;
	for (const relativePath of await includedFilePaths(repoRoot)) {
		const normalized = normalizeRelativePath(relativePath);
		if (!normalized || !await hasSafeParentDirectories(repoRoot, normalized)) continue;
		const stat = await lstatIfExists(resolveGitPath(repoRoot, normalized));
		if (stat?.isFile()) bytes += Math.max(4096, stat.size);
	}
	return bytes;
}
/** Copies the current manifest matches and returns only paths this call actually created. */
async function provisionIncludedFiles(repoRoot, worktreePath) {
	const provisioned = [];
	for (const relativePath of await includedFilePaths(repoRoot)) {
		const normalized = normalizeRelativePath(relativePath);
		if (normalized && await copyProvisionedFile({
			repoRoot,
			worktreePath,
			relativePath: normalized
		})) provisioned.push(normalized);
	}
	return provisioned.toSorted();
}
const SNAPSHOT_CHUNK_BYTES = 1048576;
async function captureParentDirectoryIdentities(root, relativePath) {
	const segments = relativePath.split("/");
	const directories = [root];
	let current = root;
	for (const segment of segments.slice(0, -1)) {
		current = path.join(current, segment);
		directories.push(current);
	}
	const identities = [];
	for (const directory of directories) {
		const stat = await fs$1.lstat(directory);
		if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`unsafe provisioned parent directory: ${directory}`);
		identities.push({
			path: directory,
			dev: stat.dev,
			ino: stat.ino
		});
	}
	return identities;
}
async function validateDirectoryIdentities(identities) {
	for (const identity of identities) {
		const stat = await fs$1.lstat(identity.path);
		if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== identity.dev || stat.ino !== identity.ino) throw new Error(`provisioned parent directory changed: ${identity.path}`);
	}
}
async function inspectProvisionedFiles(worktreePath, provisionedPaths) {
	if (provisionedPaths === void 0) return;
	const files = [];
	for (const relativePath of provisionedPaths) {
		const normalized = normalizeRelativePath(relativePath);
		if (!normalized || !await hasSafeParentDirectories(worktreePath, normalized)) throw new Error(`unsafe provisioned path: ${relativePath}`);
		const target = resolveGitPath(worktreePath, normalized);
		const stat = await lstatIfExists(target);
		if (!stat) {
			files.push({
				path: normalized,
				target,
				mode: null
			});
			continue;
		}
		if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`provisioned path is no longer a regular file: ${relativePath}`);
		files.push({
			path: normalized,
			target,
			mode: stat.mode & 4095
		});
	}
	return files.toSorted((a, b) => a.path.localeCompare(b.path));
}
async function hasUnsnapshotableProvisionedFiles(worktreePath, provisionedPaths) {
	try {
		return await inspectProvisionedFiles(worktreePath, provisionedPaths) === void 0;
	} catch {
		return true;
	}
}
function sameFileState(left, right) {
	return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
}
/** Stores provisioned bytes outside Git so ignored credentials never enter its object database. */
async function snapshotProvisionedFiles(env, worktreeId, worktreePath, provisionedPaths, commitGuard) {
	const files = await inspectProvisionedFiles(worktreePath, provisionedPaths);
	if (files === void 0) throw new Error("provisioned path ledger is unavailable");
	const ignoredUntracked = new Set((await requireGitRaw(worktreePath, [
		"ls-files",
		"--others",
		"--ignored",
		"--exclude-standard",
		"-z"
	])).split("\0").filter(Boolean));
	const currentTracked = new Set((await requireGitRaw(worktreePath, [
		"ls-files",
		"--cached",
		"-z"
	])).split("\0").filter(Boolean));
	const trackedAtHead = new Set((await requireGitBuffer(worktreePath, [
		"ls-tree",
		"-r",
		"--name-only",
		"-z",
		"HEAD"
	])).toString("utf8").split("\0").filter(Boolean));
	commitGuard?.();
	clearRegistryWorktreeProvisionedChunks(env, worktreeId);
	const states = [];
	try {
		for (const file of files) {
			if (file.mode === null) {
				states.push({
					path: file.path,
					mode: null,
					chunks: 0
				});
				continue;
			}
			if (currentTracked.has(file.path)) throw new Error(`provisioned path is now tracked: ${file.path}`);
			if (trackedAtHead.has(file.path)) throw new Error(`provisioned path is tracked at HEAD: ${file.path}`);
			if (!ignoredUntracked.has(file.path)) throw new Error(`provisioned path is no longer ignored: ${file.path}`);
			const parentIdentities = await captureParentDirectoryIdentities(worktreePath, file.path);
			const handle = await fs$1.open(file.target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
			try {
				await validateDirectoryIdentities(parentIdentities);
				const before = await handle.stat();
				const buffer = Buffer.allocUnsafe(SNAPSHOT_CHUNK_BYTES);
				let chunkIndex = 0;
				let offset = 0;
				while (offset < before.size) {
					const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.byteLength, before.size - offset), offset);
					if (bytesRead === 0) throw new Error(`provisioned file changed while snapshotting: ${file.path}`);
					commitGuard?.();
					insertRegistryWorktreeProvisionedChunk(env, {
						worktreeId,
						path: file.path,
						chunkIndex,
						data: buffer.subarray(0, bytesRead)
					});
					offset += bytesRead;
					chunkIndex += 1;
				}
				const [after, current] = await Promise.all([handle.stat(), fs$1.lstat(file.target)]);
				await validateDirectoryIdentities(parentIdentities);
				if (!sameFileState(before, after) || !sameFileState(before, current)) throw new Error(`provisioned file changed while snapshotting: ${file.path}`);
				states.push({
					path: file.path,
					mode: before.mode & 4095,
					chunks: chunkIndex
				});
			} finally {
				await handle.close();
			}
		}
		return states;
	} catch (error) {
		clearRegistryWorktreeProvisionedChunks(env, worktreeId);
		throw error;
	}
}
async function writeAll(handle, data) {
	let offset = 0;
	while (offset < data.byteLength) {
		const { bytesWritten } = await handle.write(data, offset, data.byteLength - offset);
		if (bytesWritten === 0) throw new Error("provisioned snapshot write made no progress");
		offset += bytesWritten;
	}
}
/** Restores provisioned bytes and modes from SQLite, never from the mutable source checkout. */
async function restoreProvisionedFiles(env, worktreeId, worktreePath, states) {
	for (const state of states) {
		const normalized = normalizeRelativePath(state.path);
		if (!normalized || !await hasSafeParentDirectories(worktreePath, normalized)) throw new Error(`unsafe provisioned path: ${state.path}`);
		const target = resolveGitPath(worktreePath, normalized);
		if (state.mode === null) {
			if (await lstatIfExists(target)) throw new Error(`snapshot expected provisioned path to be absent: ${state.path}`);
			continue;
		}
		await fs$1.mkdir(path.dirname(target), { recursive: true });
		const parentIdentities = await captureParentDirectoryIdentities(worktreePath, normalized);
		const handle = await fs$1.open(target, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), state.mode);
		try {
			await validateDirectoryIdentities(parentIdentities);
			for (let chunkIndex = 0; chunkIndex < state.chunks; chunkIndex += 1) {
				const chunk = getRegistryWorktreeProvisionedChunk(env, {
					worktreeId,
					path: state.path,
					chunkIndex
				});
				if (!chunk) throw new Error(`provisioned snapshot chunk missing: ${state.path}:${chunkIndex}`);
				await writeAll(handle, chunk);
			}
			await handle.chmod(state.mode);
			await validateDirectoryIdentities(parentIdentities);
		} finally {
			await handle.close();
		}
	}
}
//#endregion
//#region src/agents/worktrees/service.ts
const IDLE_GC_MS = 6048e5;
const SNAPSHOT_RETENTION_MS = 2592e6;
const WORKTREE_GC_INTERVAL_MS = 36e5;
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
const WORKTREE_CREATE_LEASE_SCOPE = "core:managed-worktrees:create";
const WORKTREE_CREATE_LEASE_MS = 6e4;
const WORKTREE_CREATE_LEASE_WAIT_MS = 3e5;
const WORKTREE_CHECKOUT_TIMEOUT_MS = 3e5;
/** Removal aborted because snapshot loss was not permitted. */
var WorktreeSnapshotError = class extends Error {
	constructor(snapshotError, options) {
		super(`worktree snapshot failed; removal aborted: ${snapshotError}`, options);
		this.snapshotError = snapshotError;
	}
};
var WorktreeRemovalLockError = class extends Error {
	constructor(kind, message) {
		super(message);
		this.kind = kind;
		this.name = "WorktreeRemovalLockError";
	}
};
function classifyWorktreeRemovalError(error) {
	if (error instanceof WorktreeRemovalContentionError) return "busy";
	if (error instanceof WorktreeRemovalLockError) return error.kind;
	if (error instanceof WorktreeSnapshotError) return "snapshot-failed";
	return "cleanup-failed";
}
var WorktreeRepositoryError = class extends Error {};
const SNAPSHOT_REF_PREFIX = "refs/openclaw/snapshots";
const log = createSubsystemLogger("agents/worktrees");
const WORKTREE_CLEANUP_TARGET = 100;
/** A bounded default; manual and actively used worktrees remain protected. */
function resolveWorktreeCleanupLimits() {
	return { maxCount: WORKTREE_CLEANUP_TARGET };
}
function validateName(name) {
	if (!NAME_PATTERN.test(name)) throw new Error("worktree name must match [a-z0-9][a-z0-9-]{0,63}");
	return name;
}
function findWorktreeByName(env, fingerprint, name) {
	return listRegistryWorktrees(env).find((record) => record.repoFingerprint === fingerprint && record.name === name);
}
async function nameIsUnavailable(env, repoRoot, fingerprint, root, name, owner) {
	const worktreePath = path.join(root, name);
	const registered = findWorktreeByName(env, fingerprint, name);
	if (owner.ownerId && registered && registered.removedAt === void 0 && worktreeOwnerMatches(registered, owner)) return false;
	if (registered || await worktreePathExists(worktreePath)) return true;
	const branch = `openclaw/${name}`;
	const branchExists = await runGit(repoRoot, [
		"show-ref",
		"--quiet",
		"--verify",
		`refs/heads/${branch}`
	]);
	if (branchExists.code === 0) return true;
	if (branchExists.code !== 1) throw commandError("git show-ref --verify", branchExists);
	return (await listGitWorktrees(repoRoot)).some((entry) => path.resolve(entry.path) === path.resolve(worktreePath));
}
function appendNameOrdinal(name, ordinal) {
	const suffix = `-${ordinal}`;
	return `${name.slice(0, 64 - suffix.length).replace(/-+$/g, "")}${suffix}`;
}
async function generateName(env, repoRoot, fingerprint, root, owner, suggestedName) {
	validateName(suggestedName);
	for (let ordinal = 1; ordinal <= 1e3; ordinal += 1) {
		const candidate = ordinal === 1 ? suggestedName : appendNameOrdinal(suggestedName, ordinal);
		if (!await nameIsUnavailable(env, repoRoot, fingerprint, root, candidate, owner)) return candidate;
	}
	throw new Error(`no available worktree name for ${suggestedName}`);
}
async function resolveRepositoryFromRealPath(requested, requestedLabel) {
	const rootResult = await runGit(requested, ["rev-parse", "--show-toplevel"]);
	if (rootResult.code !== 0) {
		if (insideGitCheckout(requested)) throw new Error(`Git metadata is unavailable for ${requested}; checkout preserved. Restore the original repository metadata, then use git worktree repair from that repository. Do not recreate its index or delete the checkout to bypass recovery.`);
		throw new WorktreeRepositoryError(`not a git checkout: ${requestedLabel}`);
	}
	const sourceRoot = await fs$1.realpath(rootResult.stdout.trim());
	if ((await runGit(sourceRoot, [
		"rev-parse",
		"--verify",
		"HEAD^{commit}"
	])).code !== 0) throw new WorktreeRepositoryError(`git checkout has no commits: ${requestedLabel}`);
	const commonRaw = await requireGit(sourceRoot, ["rev-parse", "--git-common-dir"]);
	const commonDir = await fs$1.realpath(path.isAbsolute(commonRaw) ? commonRaw : path.resolve(sourceRoot, commonRaw));
	const primary = (await listGitWorktrees(sourceRoot))[0]?.path ?? sourceRoot;
	const canonicalRoot = await fs$1.realpath(primary);
	const origin = await runGit(canonicalRoot, [
		"config",
		"--get",
		"remote.origin.url"
	]);
	const originUrl = origin.code === 0 ? origin.stdout.trim() : "";
	return {
		repoRoot: canonicalRoot,
		sourceRoot,
		commonDir,
		originUrl,
		fingerprint: createHash("sha256").update(`${commonDir}\n${originUrl}`).digest("hex").slice(0, 16)
	};
}
async function resolveRepository(repoRoot) {
	return await resolveRepositoryFromRealPath(await fs$1.realpath(repoRoot).catch(() => {
		throw new Error(`repository does not exist: ${repoRoot}`);
	}), repoRoot);
}
async function canonicalPathKey(target) {
	const canonical = await fs$1.realpath(target);
	return process.platform === "win32" ? canonical.toLowerCase() : canonical;
}
async function shouldPreserveOrphanCandidate(target, managedPaths, customRoots) {
	const targetKey = await canonicalPathKey(target);
	if (managedPaths.has(targetKey) || [...customRoots].some((root) => isPathInside(root, targetKey) || isPathInside(targetKey, root))) return true;
	return await worktreePathExists(path.join(target, ".git"));
}
async function cleanupFailedCreate(repoRoot, worktreePath, branch) {
	const removed = await runGit(repoRoot, [
		"worktree",
		"remove",
		"--force",
		worktreePath
	]);
	const deletedBranch = await runGit(repoRoot, [
		"branch",
		"-D",
		branch
	]);
	await runGit(repoRoot, ["worktree", "prune"]);
	if (removed.code !== 0 || deletedBranch.code !== 0) {
		const failure = removed.code !== 0 ? commandError("git worktree remove", removed) : commandError("git branch -D", deletedBranch);
		throw new Error(`failed to clean up worktree creation: ${failure.message}`);
	}
}
async function resetFailedWorktreeAdd(repoRoot, worktreePath, branch) {
	if ((await listGitWorktrees(repoRoot)).some((entry) => path.resolve(entry.path) === path.resolve(worktreePath))) {
		const removed = await runGit(repoRoot, [
			"worktree",
			"remove",
			"--force",
			worktreePath
		]);
		if (removed.code !== 0) throw commandError("git worktree remove", removed);
	} else if (await worktreePathExists(worktreePath)) await fs$1.rm(worktreePath, {
		recursive: true,
		force: true
	});
	if ((await runGit(repoRoot, [
		"show-ref",
		"--quiet",
		"--verify",
		`refs/heads/${branch}`
	])).code === 0) await requireGit(repoRoot, [
		"branch",
		"-D",
		branch
	]);
	await requireGit(repoRoot, ["worktree", "prune"]);
}
async function canResetFailedWorktreeAdd(repoRoot, worktreePath, branch, failure) {
	const message = (failure.stderr || failure.stdout).trim().split("\n").slice(-12).join("\n");
	const createdBranch = message.includes(`Preparing worktree (new branch '${branch}')`);
	if (message.includes("unable to checkout working tree") || createdBranch) return true;
	if ((await listGitWorktrees(repoRoot)).some((entry) => path.resolve(entry.path) === path.resolve(worktreePath)) || await worktreePathExists(worktreePath)) return false;
	return (await runGit(repoRoot, [
		"show-ref",
		"--quiet",
		"--verify",
		`refs/heads/${branch}`
	])).code === 1;
}
async function runSetupScript(repoRoot, worktreePath, params) {
	const setupScript = path.join(repoRoot, ".openclaw", "worktree-setup.sh");
	const stat = await fs$1.stat(setupScript).catch(() => void 0);
	if (!stat?.isFile() || (stat.mode & 73) === 0) return;
	const timeoutMs = 12e4;
	params.onProgress?.("setup");
	params.signal?.throwIfAborted();
	params.commitGuard?.();
	const result = await runCommandWithTimeout([setupScript], {
		timeoutMs,
		cwd: worktreePath,
		signal: params.signal,
		killProcessTree: true,
		env: {
			OPENCLAW_SOURCE_TREE_PATH: repoRoot,
			OPENCLAW_WORKTREE_PATH: worktreePath
		}
	});
	params.signal?.throwIfAborted();
	if (result.code !== 0) throw createCommandError("worktree setup", result, { timeoutMs });
}
function splitNullBuffer(input) {
	const fields = [];
	let start = 0;
	for (let index = 0; index < input.length; index += 1) {
		if (input[index] !== 0) continue;
		if (index > start) fields.push(input.subarray(start, index));
		start = index + 1;
	}
	if (start < input.length) fields.push(input.subarray(start));
	return fields;
}
function gitPathKey(gitPath) {
	return gitPath.toString("hex");
}
function checkoutPathFromGitBytes(checkoutRoot, gitPath) {
	if (process.platform === "win32") return path.join(checkoutRoot, ...gitPath.toString("utf8").split("/"));
	return Buffer.concat([
		Buffer.from(checkoutRoot),
		Buffer.from(path.sep),
		gitPath
	]);
}
async function rawPathExists(target) {
	try {
		await fs$1.lstat(target);
		return true;
	} catch (error) {
		if (isMissingPathError(error)) return false;
		throw error;
	}
}
async function containsSnapshotGitMarker(checkoutRoot, snapshotPaths) {
	let visiblePaths = snapshotPaths ? [...snapshotPaths] : [];
	if (!snapshotPaths) {
		if (splitNullBuffer(await requireGitBuffer(checkoutRoot, [
			"ls-files",
			"--stage",
			"-z"
		])).some((entry) => entry.subarray(0, 7).toString() === "160000 ")) return true;
		visiblePaths = splitNullBuffer(await requireGitBuffer(checkoutRoot, [
			"ls-files",
			"-z",
			"--cached",
			"--others",
			"--exclude-standard"
		]));
	}
	const checked = /* @__PURE__ */ new Set();
	const ignoredPaths = splitNullBuffer(await requireGitBuffer(checkoutRoot, [
		"ls-files",
		"-z",
		"--others",
		"--ignored",
		"--exclude-standard"
	]));
	for (const gitPath of [...visiblePaths, ...ignoredPaths]) for (let end = gitPath.indexOf(47); end !== -1; end = gitPath.indexOf(47, end + 1)) {
		const directory = gitPath.subarray(0, end);
		const key = gitPathKey(directory);
		if (checked.has(key)) continue;
		checked.add(key);
		if (await rawPathExists(checkoutPathFromGitBytes(checkoutRoot, Buffer.concat([directory, Buffer.from("/.git")])))) return true;
	}
	return false;
}
async function snapshotWorktree(stateEnv, record, reason, provisionedPaths, commitGuard) {
	commitGuard?.();
	if (!provisionedPaths) throw new Error("provisioned path ledger is unavailable");
	const tempDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "openclaw-worktree-index-"));
	const indexPath = path.join(tempDir, "index");
	const snapshotRef = `${SNAPSHOT_REF_PREFIX}/${record.id}`;
	const filemodeArgs = process.platform === "win32" ? [] : ["-c", "core.filemode=true"];
	const env = {
		GIT_INDEX_FILE: indexPath,
		GIT_AUTHOR_NAME: "OpenClaw",
		GIT_AUTHOR_EMAIL: "openclaw@localhost",
		GIT_COMMITTER_NAME: "OpenClaw",
		GIT_COMMITTER_EMAIL: "openclaw@localhost"
	};
	try {
		const provisioned = new Set(provisionedPaths.map((entry) => gitPathKey(Buffer.from(entry))));
		const snapshotPaths = /* @__PURE__ */ new Map();
		const addSnapshotPath = (entry) => {
			const key = gitPathKey(entry);
			if (!provisioned.has(key)) snapshotPaths.set(key, entry);
		};
		const sparseConfig = await runGit(record.path, [
			"config",
			"--bool",
			"core.sparseCheckout"
		]);
		if (sparseConfig.code !== 0 && sparseConfig.code !== 1) throw commandError("git config --bool core.sparseCheckout", sparseConfig);
		const sparseCheckout = sparseConfig.code === 0 && sparseConfig.stdout.trim() === "true";
		const sparseCandidates = [];
		for (const entry of splitNullBuffer(await requireGitBuffer(record.path, [
			"ls-files",
			"-v",
			"-z"
		]))) {
			if (entry.length < 3) continue;
			const tag = String.fromCharCode(entry[0] ?? 0).toUpperCase();
			const trackedPath = entry.subarray(2);
			if (tag !== "S" || await rawPathExists(checkoutPathFromGitBytes(record.path, trackedPath)) || !sparseCheckout) addSnapshotPath(trackedPath);
			else sparseCandidates.push(trackedPath);
		}
		if (sparseCandidates.length > 0) {
			const included = await requireGitBuffer(record.path, [
				"sparse-checkout",
				"check-rules",
				"-z"
			], { input: Buffer.concat(sparseCandidates.flatMap((entry) => [entry, Buffer.from([0])])) });
			for (const entry of splitNullBuffer(included)) addSnapshotPath(entry);
		}
		for (const args of [[
			"diff-index",
			"--cached",
			"--name-only",
			"-z",
			"HEAD",
			"--"
		], [
			"ls-files",
			"-z",
			"--others",
			"--exclude-standard"
		]]) for (const entry of splitNullBuffer(await requireGitBuffer(record.path, args))) addSnapshotPath(entry);
		const isStagedInput = createStagedInputPathMatcher(await root(record.path));
		for (const entry of splitNullBuffer(await requireGitBuffer(record.path, [
			"ls-files",
			"-z",
			"--others",
			"--ignored",
			"--exclude-standard",
			"--",
			STAGED_INPUT_GIT_PATHSPEC
		]))) if (await isStagedInput(entry.toString("utf8"))) addSnapshotPath(entry);
		if (await containsSnapshotGitMarker(record.path, snapshotPaths.values())) throw new Error("nested git repositories cannot be snapshotted losslessly");
		commitGuard?.();
		await prepareSnapshotIndex(stateEnv, record, snapshotPaths, provisionedPaths, env);
		const provisionedState = await snapshotProvisionedFiles(stateEnv, record.id, record.path, provisionedPaths, commitGuard);
		commitGuard?.();
		await requireGit(record.path, [
			...filemodeArgs,
			"update-index",
			"--add",
			"--remove",
			"-z",
			"--stdin"
		], {
			env,
			input: snapshotPaths.size > 0 ? Buffer.concat([...snapshotPaths.values()].flatMap((entry) => [entry, Buffer.from([0])])) : Buffer.alloc(0)
		});
		commitGuard?.();
		const tree = await requireGit(record.path, [...filemodeArgs, "write-tree"], { env });
		for (const provisionedPath of provisionedPaths) if (await requireGit(record.path, [
			"--literal-pathspecs",
			"ls-tree",
			"-r",
			"--name-only",
			tree,
			"--",
			provisionedPath
		])) throw new Error(`provisioned path entered Git snapshot: ${provisionedPath}`);
		if ((await requireGit(record.path, [
			"ls-tree",
			"-r",
			tree
		])).split("\n").some((entry) => entry.startsWith("160000 "))) throw new Error("nested git repositories cannot be snapshotted losslessly");
		const parent = await requireGit(record.path, ["rev-parse", "HEAD"]);
		commitGuard?.();
		const commit = await requireGit(record.path, [
			...filemodeArgs,
			"commit-tree",
			tree,
			"-p",
			parent,
			"-m",
			`OpenClaw worktree snapshot: ${reason}`
		], { env });
		commitGuard?.();
		await requireGit(record.repoRoot, [
			"update-ref",
			snapshotRef,
			commit
		]);
		return {
			snapshotRef,
			provisionedState
		};
	} finally {
		await fs$1.rm(tempDir, {
			recursive: true,
			force: true
		});
	}
}
async function prepareSnapshotIndex(env, record, snapshotPaths, provisioned, indexEnv) {
	const headPaths = splitNullBuffer(await requireGitBuffer(record.path, [
		"ls-tree",
		"-r",
		"--name-only",
		"-z",
		"HEAD"
	]));
	const metadataBytes = [...headPaths, ...snapshotPaths.values()].reduce((total, entry) => total + 512 + 2 * entry.length, 0);
	requireWorktreeDiskSpace([{
		path: os.tmpdir(),
		bytes: 2 * metadataBytes
	}], "worktree safety snapshot index", true);
	await requireGit(record.path, ["read-tree", "HEAD"], { env: indexEnv });
	const changed = new Set(splitNullBuffer(await requireGitBuffer(record.path, [
		"-c",
		"diff.autoRefreshIndex=true",
		"diff",
		"--no-ext-diff",
		"--no-textconv",
		"--no-renames",
		"--name-only",
		"-z",
		"--"
	], { env: indexEnv })).map(gitPathKey));
	const tracked = new Set(headPaths.map(gitPathKey));
	const unique = new Map([...snapshotPaths].filter(([key]) => changed.has(key) || !tracked.has(key)));
	for (const value of provisioned) unique.set(gitPathKey(Buffer.from(value)), Buffer.from(value));
	const provisionedKeys = new Set(provisioned.map((value) => gitPathKey(Buffer.from(value))));
	let gitBytes = 0, provisionedBytes = 0;
	for (const [key, value] of unique) try {
		const stat = await fs$1.lstat(checkoutPathFromGitBytes(record.path, value));
		if (provisionedKeys.has(key)) provisionedBytes += stat.size;
		else gitBytes += stat.size;
	} catch (error) {
		if (!isMissingPathError(error)) throw error;
	}
	const commonDir = await requireGit(record.repoRoot, ["rev-parse", "--git-common-dir"]);
	requireWorktreeDiskSpace([
		{
			path: path.resolve(record.repoRoot, commonDir),
			bytes: 2 * gitBytes + metadataBytes
		},
		{
			path: resolveStateDir(env),
			bytes: 2 * provisionedBytes
		},
		{
			path: os.tmpdir(),
			bytes: 2 * metadataBytes
		}
	], "worktree safety snapshot", true);
}
var ManagedWorktreeService = class {
	constructor(options = {}) {
		this.env = options.env ?? process.env;
		this.now = options.now ?? Date.now;
		this.getConfig = options.getConfig;
	}
	async worktreesRoot() {
		const root = this.getConfig?.().worktreeRoot ?? path.join(resolveStateDir(this.env), "worktrees");
		await fs$1.mkdir(root, { recursive: true });
		return await fs$1.realpath(root);
	}
	async create(params) {
		params.signal?.throwIfAborted();
		const repository = await resolveRepository(params.repoRoot);
		return await this.withAllocationLease(params, async (guard) => {
			if (params.ownerId) {
				const existing = findLiveRegistryWorktreeByOwner(this.env, params.ownerKind ?? "manual", params.ownerId);
				if (existing && await worktreePathExists(existing.path)) {
					const validated = await this.rebindLiveRepository(existing, guard);
					if (validated.repoRoot !== repository.repoRoot) throw new Error(`worktree owner ${params.ownerKind ?? "manual"} ${params.ownerId} is already bound to another repository`);
					guard.commitGuard?.();
					return validated;
				}
				if (existing) {
					guard.commitGuard?.();
					updateRegistryWorktree(this.env, existing.id, { removedAt: this.now() });
				}
			}
			return await this.createForRepository({
				...params,
				...guard
			}, repository, params.name ?? params.suggestedName ?? createCrustaceanSlug());
		});
	}
	async withAllocationLease(params, run) {
		return await withOpenClawStateLease({
			scope: WORKTREE_CREATE_LEASE_SCOPE,
			key: "capacity",
			database: {
				scope: "shared",
				options: { env: this.env }
			},
			leaseMs: WORKTREE_CREATE_LEASE_MS,
			waitMs: WORKTREE_CREATE_LEASE_WAIT_MS,
			leaseLabel: "managed worktree allocation lease",
			operationLabel: "agents.worktrees.allocation",
			signal: params.signal
		}, async (lease) => await run({
			signal: lease.signal,
			commitGuard: () => {
				lease.assertOwned();
				params.commitGuard?.();
			}
		}));
	}
	requireAllocationSpace(target, repository, bytes = 0) {
		requireWorktreeDiskSpace([
			{
				path: target,
				bytes
			},
			{
				path: repository.commonDir,
				bytes: 0
			},
			{
				path: repository.sourceRoot,
				bytes: 0
			},
			{
				path: resolveStateDir(this.env),
				bytes: 0
			}
		], "worktree allocation");
	}
	async createForRepository(params, repository, inferredName) {
		params.signal?.throwIfAborted();
		params.onProgress?.("checkout");
		const suppliedName = params.name === void 0 ? void 0 : validateName(params.name);
		const existing = suppliedName ? findWorktreeByName(this.env, repository.fingerprint, suppliedName) : void 0;
		if (existing && !existing.removedAt && !worktreeOwnerMatches(existing, params)) throw new Error(`worktree name is already in use by ${existing.ownerKind}${existing.ownerId ? ` ${existing.ownerId}` : ""}: ${suppliedName}`);
		if (existing && existing.removedAt === void 0) {
			if (await worktreePathExists(existing.path)) return await this.rebindLiveRepository(existing, params);
			updateRegistryWorktree(this.env, existing.id, { removedAt: this.now() });
		}
		if (existing && existing.removedAt !== void 0 && existing.snapshotRef) {
			if (!worktreeOwnerMatches(existing, params)) throw new Error(`worktree name is already in use by ${existing.ownerKind}${existing.ownerId ? ` ${existing.ownerId}` : ""}: ${suppliedName}`);
			return await this.restoreWithAllocation({
				id: existing.id,
				signal: params.signal,
				commitGuard: params.commitGuard
			});
		}
		const root = path.join(await this.worktreesRoot(), repository.fingerprint);
		const name = suppliedName ?? await generateName(this.env, repository.repoRoot, repository.fingerprint, root, params, params.suggestedName ?? inferredName);
		const worktreePath = path.join(root, name);
		const branch = `openclaw/${name}`;
		const branchExists = await runGit(repository.repoRoot, [
			"show-ref",
			"--quiet",
			"--verify",
			`refs/heads/${branch}`
		]);
		if (branchExists.code === 0) throw new Error(`branch already exists: ${branch}`);
		if (branchExists.code !== 1) throw commandError("git show-ref --verify", branchExists);
		params.signal?.throwIfAborted();
		params.commitGuard?.();
		this.requireAllocationSpace(worktreePath, repository);
		params.commitGuard?.();
		const base = await resolveWorktreeBase(repository.repoRoot, params.baseRef, params.signal);
		const gitBytes = Math.max(await estimateWorktreeGitBytes(repository.repoRoot, base.gitOperand), base.remote ? await estimateWorktreeGitBytes(repository.repoRoot, "HEAD") : 0);
		const provisionedBytes = await estimateProvisionedFileBytes(repository.sourceRoot);
		const setupStat = params.runSetupScript === false ? void 0 : await fs$1.stat(path.join(repository.sourceRoot, ".openclaw", "worktree-setup.sh")).catch(() => void 0);
		const runRepositorySetup = setupStat?.isFile() === true && (setupStat.mode & 73) !== 0;
		const setupBytes = runRepositorySetup ? Math.max(WORKTREE_SETUP_HEADROOM_BYTES, await directorySizeBytes(repository.sourceRoot, true)) : 0;
		this.requireAllocationSpace(worktreePath, repository, 2 * (gitBytes + provisionedBytes) + setupBytes);
		params.signal?.throwIfAborted();
		params.commitGuard?.();
		await fs$1.mkdir(root, { recursive: true });
		params.signal?.throwIfAborted();
		params.commitGuard?.();
		let gitBase = base.gitOperand;
		let recordBase = base.recordRef;
		const worktreeAddArgs = () => [
			"worktree",
			"add",
			"-b",
			branch,
			"--",
			worktreePath,
			gitBase
		];
		let added = await runGit(repository.repoRoot, worktreeAddArgs(), {
			timeoutMs: WORKTREE_CHECKOUT_TIMEOUT_MS,
			signal: params.signal
		});
		if (added.code !== 0 && base.remote) {
			if (!await canResetFailedWorktreeAdd(repository.repoRoot, worktreePath, branch, added)) throw commandError("git worktree add", added);
			await resetFailedWorktreeAdd(repository.repoRoot, worktreePath, branch);
			params.signal?.throwIfAborted();
			params.commitGuard?.();
			gitBase = "HEAD";
			recordBase = "HEAD";
			added = await runGit(repository.repoRoot, worktreeAddArgs(), {
				timeoutMs: WORKTREE_CHECKOUT_TIMEOUT_MS,
				signal: params.signal
			});
		}
		if (added.code !== 0) throw commandError("git worktree add", added);
		let provisionedPaths;
		try {
			params.signal?.throwIfAborted();
			params.commitGuard?.();
			this.requireAllocationSpace(worktreePath, repository, 2 * provisionedBytes + setupBytes);
			provisionedPaths = await provisionIncludedFiles(repository.sourceRoot, worktreePath);
			if (runRepositorySetup) {
				this.requireAllocationSpace(worktreePath, repository, setupBytes);
				await runSetupScript(repository.sourceRoot, worktreePath, params);
			}
			params.signal?.throwIfAborted();
			params.commitGuard?.();
			this.requireAllocationSpace(worktreePath, repository);
		} catch (error) {
			try {
				await cleanupFailedCreate(repository.repoRoot, worktreePath, branch);
			} catch (cleanupError) {
				throw new Error(`${String(error)}\n${String(cleanupError)}`, { cause: cleanupError });
			}
			throw error;
		}
		const createdAt = this.now();
		const record = {
			id: randomUUID(),
			name,
			repoFingerprint: repository.fingerprint,
			repoRoot: repository.repoRoot,
			path: worktreePath,
			branch,
			baseRef: recordBase,
			ownerKind: params.ownerKind ?? "manual",
			...params.ownerId ? { ownerId: params.ownerId } : {},
			createdAt,
			lastActiveAt: createdAt
		};
		insertRegistryWorktree(this.env, record, { provisionedPaths });
		return record;
	}
	async list() {
		const records = listRegistryWorktrees(this.env);
		for (const record of records) if (record.removedAt === void 0 && !await worktreePathExists(record.path)) {
			const removedAt = this.now();
			updateRegistryWorktree(this.env, record.id, { removedAt });
			record.removedAt = removedAt;
		}
		return records.filter((record) => record.removedAt === void 0 || record.snapshotRef);
	}
	/** Returns persisted worktree facts without probing paths or mutating lifecycle state. */
	listRegistryRecords() {
		return listRegistryWorktrees(this.env);
	}
	findLiveByOwner(ownerKind, ownerId) {
		return findLiveRegistryWorktreeByOwner(this.env, ownerKind, ownerId);
	}
	findLiveById(id) {
		const record = getRegistryWorktree(this.env, id);
		return record?.removedAt === void 0 ? record : void 0;
	}
	/** Resolves the canonical registry root and the caller's own checkout root. */
	async resolveRepositoryPaths(repoRoot) {
		const resolved = await resolveRepository(repoRoot);
		return {
			canonicalRoot: resolved.repoRoot,
			sourceRoot: resolved.sourceRoot
		};
	}
	/** Resolves the repository facts shared by managed worktrees and project discovery. */
	async resolveRepositoryIdentity(repoRoot) {
		const resolved = await resolveRepository(repoRoot);
		return {
			checkoutRoot: resolved.sourceRoot,
			repoRoot: resolved.repoRoot,
			originUrl: resolved.originUrl,
			fingerprint: resolved.fingerprint
		};
	}
	/**
	* Lists selectable base refs for a repository without touching the network.
	* Base-ref pickers must stay snappy; resolveWorktreeBase() still fetches on create
	* when no explicit ref is chosen.
	*/
	async listRepositoryBranches(repoRoot, options = {}) {
		let repository;
		if (options.includeRepositoryStatus) try {
			const requested = await fs$1.realpath(repoRoot);
			if (!(await fs$1.stat(requested)).isDirectory()) return {
				branches: [],
				repositoryStatus: "unavailable"
			};
			if (!insideGitCheckout(requested)) return {
				branches: [],
				repositoryStatus: "not_git"
			};
			repository = await resolveRepositoryFromRealPath(requested, repoRoot);
		} catch {
			return {
				branches: [],
				repositoryStatus: "unavailable"
			};
		}
		else repository = await resolveRepository(repoRoot);
		const branches = /* @__PURE__ */ new Map();
		const remoteRaw = await runGit(repository.repoRoot, [
			"for-each-ref",
			"--format=%(refname)",
			"refs/remotes"
		]);
		if (remoteRaw.code === 0) for (const refname of remoteRaw.stdout.split("\n")) {
			const trimmed = refname.trim();
			if (!trimmed.startsWith("refs/remotes/")) continue;
			const withoutPrefix = trimmed.slice(13);
			const slash = withoutPrefix.indexOf("/");
			if (slash <= 0) continue;
			const shortName = withoutPrefix.slice(slash + 1);
			if (!shortName || shortName === "HEAD") continue;
			branches.set(shortName, {
				name: withoutPrefix,
				kind: "remote"
			});
		}
		const localRaw = await runGit(repository.repoRoot, [
			"for-each-ref",
			"--format=%(refname:short)",
			"refs/heads"
		]);
		if (localRaw.code === 0) for (const line of localRaw.stdout.split("\n")) {
			const name = line.trim();
			if (name) branches.set(name, {
				name,
				kind: "local"
			});
		}
		const remoteHead = await runGit(repository.repoRoot, [
			"symbolic-ref",
			"--quiet",
			"--short",
			"refs/remotes/origin/HEAD"
		]);
		const defaultShort = remoteHead.code === 0 ? remoteHead.stdout.trim().replace(/^origin\//, "") || void 0 : void 0;
		const head = await runGit(repository.repoRoot, [
			"symbolic-ref",
			"--quiet",
			"--short",
			"HEAD"
		]);
		const headBranch = head.code === 0 ? head.stdout.trim() || void 0 : void 0;
		const defaultBranch = defaultShort ? branches.get(defaultShort)?.name ?? defaultShort : void 0;
		const rank = (shortName) => shortName === defaultShort ? 0 : shortName === headBranch ? 1 : 2;
		return {
			branches: [...branches.entries()].toSorted(([aShort, a], [bShort, b]) => rank(aShort) - rank(bShort) || a.name.localeCompare(b.name)).map(([, branch]) => branch),
			...defaultBranch ? { defaultBranch } : {},
			...headBranch ? { headBranch } : {},
			...options.includeRepositoryStatus ? { repositoryStatus: "git" } : {}
		};
	}
	async acquire(id) {
		const record = this.requireLiveRecord(id);
		await lockWorktreeForProcess(record);
		const lastActiveAt = this.now();
		updateRegistryWorktree(this.env, id, { lastActiveAt });
		return {
			...record,
			lastActiveAt
		};
	}
	async release(id) {
		const record = getRegistryWorktree(this.env, id);
		if (!record || record.removedAt !== void 0 || !await worktreePathExists(record.path)) return;
		const state = await lockState(record);
		if (state.kind === "live" && state.pid !== process.pid) return;
		if (state.kind === "foreign") return;
		if (state.kind !== "none") await unlockWorktree(record);
	}
	async remove(params) {
		return await this.withAllocationLease(params, async (guard) => await this.removeWithAllocation({
			...params,
			...guard
		}));
	}
	async removeWithAllocation(params) {
		params.signal?.throwIfAborted();
		params.commitGuard?.();
		let record = this.requireLiveRecord(params.id);
		const claimToken = params.claimToken ?? randomUUID();
		claimWorktreeRemoval(this.env, {
			worktreeId: record.id,
			token: claimToken
		});
		try {
			record = await this.rebindLiveRepository(record, params);
			const state = await lockState(record);
			if (state.kind === "live" || state.kind === "foreign") throw new WorktreeRemovalLockError(state.kind === "live" ? "busy" : "foreign-lock", state.kind === "live" ? `worktree is locked by live OpenClaw pid ${state.pid}` : `worktree has a foreign lock${state.reason ? `: ${state.reason}` : ""}`);
			if (state.kind !== "none") {
				params.commitGuard?.();
				await requireGit(record.repoRoot, [
					"worktree",
					"unlock",
					record.path
				]);
			}
			let snapshotRef = record.snapshotRef;
			let snapshotError;
			try {
				const snapshot = await snapshotWorktree(this.env, record, params.reason, getRegistryWorktreeProvisionedPaths(this.env, record.id), params.commitGuard);
				snapshotRef = snapshot.snapshotRef;
				params.commitGuard?.();
				updateRegistryWorktree(this.env, record.id, {
					snapshotRef,
					provisionedState: snapshot.provisionedState
				});
			} catch (error) {
				snapshotError = error instanceof Error ? error.message : String(error);
				try {
					clearRegistryWorktreeProvisionedChunks(this.env, record.id);
				} catch (cleanupError) {
					throw new WorktreeSnapshotError(`${snapshotError}; provisioned snapshot cleanup failed: ${String(cleanupError)}`, { cause: cleanupError });
				}
				if (!params.allowSnapshotLoss) throw new WorktreeSnapshotError(snapshotError, { cause: error });
			}
			params.signal?.throwIfAborted();
			params.commitGuard?.();
			const removed = await runGit(record.repoRoot, [
				"worktree",
				"remove",
				"--force",
				record.path
			]);
			if (removed.code !== 0) throw commandError("git worktree remove", removed);
			params.commitGuard?.();
			const branchDelete = await runGit(record.repoRoot, [
				"branch",
				"-D",
				record.branch
			]);
			if (branchDelete.code !== 0) throw commandError("git branch -D", branchDelete);
			await requireGit(record.repoRoot, ["worktree", "prune"]);
			await fs$1.rmdir(path.dirname(record.path)).catch(() => void 0);
			params.commitGuard?.();
			const removedAt = this.now();
			updateRegistryWorktree(this.env, record.id, {
				removedAt,
				snapshotRef,
				...params.runEndCleanup ? { runEndCleanup: params.runEndCleanup } : {}
			});
			finalizeWorktreeRemoval(this.env, record.id);
			return {
				removed: true,
				...snapshotRef ? { snapshotRef } : {},
				...snapshotError ? { snapshotError } : {}
			};
		} catch (error) {
			abortWorktreeRemoval(this.env, record.id, claimToken);
			throw error;
		}
	}
	async restore(params) {
		return await this.withAllocationLease(params, async (guard) => await this.restoreWithAllocation({
			...params,
			...guard
		}));
	}
	async restoreWithAllocation(params) {
		params.signal?.throwIfAborted();
		params.commitGuard?.();
		const record = getRegistryWorktree(this.env, params.id);
		if (!record?.snapshotRef || record.removedAt === void 0) throw new Error(`worktree ${params.id} is not restorable`);
		if (!await worktreePathExists(record.repoRoot)) throw new Error(`source repository no longer exists: ${record.repoRoot}`);
		const repository = await resolveRepository(record.repoRoot);
		this.requireAllocationSpace(record.path, repository);
		const provisionedState = getRegistryWorktreeProvisionedState(this.env, record.id);
		if (provisionedState === void 0) throw new Error(`worktree ${record.id} snapshot lacks provisioned file metadata`);
		const provisionedBytes = provisionedState.reduce((sum, entry) => sum + entry.chunks * SNAPSHOT_CHUNK_BYTES, 0);
		const gitBytes = await estimateWorktreeGitBytes(record.repoRoot, record.snapshotRef);
		this.requireAllocationSpace(record.path, repository, 2 * (gitBytes + provisionedBytes));
		const parent = await requireGit(record.repoRoot, ["rev-parse", `${record.snapshotRef}^`]);
		params.commitGuard?.();
		await fs$1.mkdir(path.dirname(record.path), { recursive: true });
		params.commitGuard?.();
		await requireGit(record.repoRoot, [
			"worktree",
			"add",
			"--detach",
			record.path,
			record.snapshotRef
		], {
			timeoutMs: WORKTREE_CHECKOUT_TIMEOUT_MS,
			signal: params.signal
		});
		let branchCreated = false;
		let restoredProvisionedPaths;
		try {
			params.commitGuard?.();
			await requireGit(record.repoRoot, [
				"branch",
				record.branch,
				parent
			]);
			branchCreated = true;
			params.commitGuard?.();
			await requireGit(record.path, [
				"symbolic-ref",
				"HEAD",
				`refs/heads/${record.branch}`
			]);
			params.commitGuard?.();
			await requireGit(record.path, ["reset"]);
			params.commitGuard?.();
			this.requireAllocationSpace(record.path, repository, 2 * provisionedBytes);
			await restoreProvisionedFiles(this.env, record.id, record.path, provisionedState);
			params.commitGuard?.();
			this.requireAllocationSpace(record.path, repository);
			restoredProvisionedPaths = provisionedState.map((state) => state.path);
		} catch (error) {
			const removed = await runGit(record.repoRoot, [
				"worktree",
				"remove",
				"--force",
				record.path
			]);
			const branchDeleted = branchCreated ? await runGit(record.repoRoot, [
				"branch",
				"-D",
				record.branch
			]) : void 0;
			if (removed.code !== 0 || branchDeleted && branchDeleted.code !== 0) {
				const failure = branchDeleted && removed.code === 0 ? commandError("git branch -D", branchDeleted) : commandError("git worktree remove", removed);
				throw new Error(`${String(error)}\nrestore cleanup failed: ${failure.message}`, { cause: error });
			}
			throw error;
		}
		const lastActiveAt = Math.max(this.now(), record.lastActiveAt + 1);
		updateRegistryWorktree(this.env, params.id, {
			removedAt: void 0,
			lastActiveAt,
			provisionedPaths: restoredProvisionedPaths,
			runEndCleanup: void 0
		});
		finalizeWorktreeRemoval(this.env, params.id);
		const restored = {
			...record,
			lastActiveAt
		};
		delete restored.removedAt;
		delete restored.runEndCleanup;
		return restored;
	}
	async removeIfLossless(id) {
		let record = this.requireLiveRecord(id);
		const claimToken = randomUUID();
		const recordOutcome = (outcome, error) => {
			updateRegistryWorktree(this.env, id, { runEndCleanup: {
				outcome,
				at: this.now(),
				...outcome === "failed" ? { reason: truncateUtf16Safe(formatErrorMessage(error), 500) } : {}
			} }, {
				onlyIfLive: true,
				onlyIfActiveAt: record.lastActiveAt
			});
		};
		try {
			claimWorktreeRemoval(this.env, {
				worktreeId: id,
				token: claimToken
			});
		} catch (error) {
			if (error instanceof WorktreeRemovalContentionError) {
				if (error.kind === "finalized") return false;
				recordOutcome("retained-busy");
				return false;
			}
			try {
				recordOutcome("failed", error);
			} catch {}
			throw error;
		}
		try {
			record = await this.rebindLiveRepository(record);
			const status = await requireGit(record.path, ["status", "--porcelain"]);
			const unpushed = await requireGit(record.path, [
				"log",
				"HEAD",
				"--not",
				"--remotes",
				"--oneline"
			]);
			const ignoredDrift = await hasUnsnapshotableProvisionedFiles(record.path, getRegistryWorktreeProvisionedPaths(this.env, record.id));
			const retainedOutcome = status ? "retained-dirty" : unpushed ? "retained-unpushed" : ignoredDrift ? "retained-provisioned-drift" : await containsSnapshotGitMarker(record.path) ? "retained-dirty" : void 0;
			if (retainedOutcome) {
				abortWorktreeRemoval(this.env, id, claimToken);
				recordOutcome(retainedOutcome);
				return false;
			}
		} catch (error) {
			abortWorktreeRemoval(this.env, id, claimToken);
			recordOutcome("failed", error);
			throw error;
		}
		try {
			await this.release(id);
			await this.remove({
				id,
				reason: "run-end",
				claimToken,
				runEndCleanup: {
					outcome: "removed-lossless",
					at: this.now()
				}
			});
		} catch (error) {
			abortWorktreeRemoval(this.env, id, claimToken);
			recordOutcome("failed", error);
			throw error;
		}
		return true;
	}
	async removeIfLosslessByPath(worktreePath, owner) {
		const record = findLiveRegistryWorktreeByPath(this.env, worktreePath);
		if (!record || !worktreeOwnerMatches(record, owner)) return false;
		return await this.removeIfLossless(record.id);
	}
	async releaseByPath(worktreePath) {
		const record = findLiveRegistryWorktreeByPath(this.env, worktreePath);
		if (record) await this.release(record.id);
	}
	async gc(params = {}) {
		const now = this.now();
		let removed = [];
		const records = listRegistryWorktrees(this.env);
		for (const record of records) try {
			if (record.removedAt === void 0 && !await worktreePathExists(record.path)) {
				updateRegistryWorktree(this.env, record.id, { removedAt: now });
				record.removedAt = now;
			}
			const expiresWhenIdle = record.ownerKind === "workboard" || record.ownerKind === "session";
			const retiredOwner = record.ownerId !== void 0 && params.shouldRemoveOwner?.(record.ownerKind, record.ownerId) === true;
			if (record.removedAt === void 0 && expiresWhenIdle && (retiredOwner || now - record.lastActiveAt > 6048e5)) {
				if (await this.isProtectedFromAutoRemoval(record, params.shouldProtectOwner)) continue;
				await this.remove({
					id: record.id,
					reason: retiredOwner ? "owner-gc" : "idle-gc",
					commitGuard: () => this.assertOwnerAllowsCleanup(record, params, retiredOwner)
				});
				removed.push(record.id);
			}
		} catch (error) {
			log.warn(`idle cleanup failed for ${record.id}: ${String(error)}`);
		}
		removed = removed.concat(await this.enforceCleanupLimits(params));
		const orphansDeleted = await this.reconcileOrphans(records);
		let snapshotsPruned = 0;
		for (const record of listRegistryWorktrees(this.env)) {
			if (record.removedAt === void 0 || now - record.removedAt <= 2592e6) continue;
			try {
				if (record.snapshotRef && await worktreePathExists(record.repoRoot)) await requireGit(record.repoRoot, [
					"update-ref",
					"-d",
					record.snapshotRef
				]);
				deleteRegistryWorktree(this.env, record.id);
				snapshotsPruned += 1;
			} catch (error) {
				log.warn(`snapshot retention failed for ${record.id}: ${String(error)}`);
			}
		}
		return {
			removed,
			orphansDeleted,
			snapshotsPruned
		};
	}
	/**
	* Shared auto-removal guard: owners, leases, nested repositories, and live or
	* foreign Git locks veto removal; a dead lock is cleared.
	*/
	async isProtectedFromAutoRemoval(record, shouldProtectOwner) {
		if (record.ownerId !== void 0 && shouldProtectOwner?.(record.ownerKind, record.ownerId) === true) return true;
		if (hasLiveWorktreeRunLease(this.env, record.id)) return true;
		if (await hasUnsnapshotableProvisionedFiles(record.path, getRegistryWorktreeProvisionedPaths(this.env, record.id))) return true;
		const state = await lockState(record);
		if (state.kind === "live" || state.kind === "foreign") return true;
		if (state.kind === "dead") await requireGit(record.repoRoot, [
			"worktree",
			"unlock",
			record.path
		]);
		return await containsSnapshotGitMarker(record.path);
	}
	/**
	* Enforces optional count/size retention across all live managed worktrees.
	* Manual worktrees count toward the totals but are never limit-evicted, so a
	* limit can stay exceeded when only protected worktrees remain.
	*/
	async enforceCleanupLimits(params) {
		const limits = params.limits ?? resolveWorktreeCleanupLimits();
		if (limits.maxCount === void 0 && limits.maxTotalSizeBytes === void 0) return [];
		const live = listRegistryWorktrees(this.env).filter((record) => record.removedAt === void 0);
		const sizes = /* @__PURE__ */ new Map();
		let totalBytes = 0;
		if (limits.maxTotalSizeBytes !== void 0) for (const record of live) try {
			const bytes = await directorySizeBytes(record.path);
			sizes.set(record.id, bytes);
			totalBytes += bytes;
		} catch (error) {
			log.warn(`worktree size measurement failed for ${record.id}: ${String(error)}`);
		}
		let liveCount = live.length;
		const overLimit = () => limits.maxCount !== void 0 && liveCount > limits.maxCount || limits.maxTotalSizeBytes !== void 0 && totalBytes > limits.maxTotalSizeBytes;
		if (!overLimit()) return [];
		const refreshTotals = () => {
			const liveIds = new Set(listRegistryWorktrees(this.env).filter((record) => record.removedAt === void 0).map((record) => record.id));
			liveCount = liveIds.size;
			if (limits.maxTotalSizeBytes !== void 0) {
				totalBytes = 0;
				for (const [id, bytes] of sizes) if (liveIds.has(id)) totalBytes += bytes;
			}
			return liveIds;
		};
		const removed = [];
		const candidates = live.filter((record) => record.ownerKind === "workboard" || record.ownerKind === "session").toSorted((a, b) => a.lastActiveAt - b.lastActiveAt);
		for (const record of candidates) {
			const liveIds = refreshTotals();
			if (!overLimit()) break;
			if (!liveIds.has(record.id)) continue;
			try {
				if (await this.isProtectedFromAutoRemoval(record, params.shouldProtectOwner)) continue;
				await this.remove({
					id: record.id,
					reason: "limit-gc",
					commitGuard: () => this.assertOwnerAllowsCleanup(record, params)
				});
			} catch (error) {
				log.warn(`cleanup limit removal failed for ${record.id}: ${String(error)}`);
				continue;
			}
			removed.push(record.id);
		}
		refreshTotals();
		if (overLimit()) log.warn(`worktree cleanup limits still exceeded after evicting ${removed.length}; remaining worktrees are protected or manual`);
		return removed;
	}
	assertOwnerAllowsCleanup(record, params, retiredOwner = false) {
		if (record.ownerId !== void 0 && (params.shouldProtectOwner?.(record.ownerKind, record.ownerId) === true || retiredOwner && params.shouldRemoveOwner?.(record.ownerKind, record.ownerId) !== true)) throw new WorktreeRemovalLockError("busy", "worktree owner became active during cleanup");
	}
	requireLiveRecord(id) {
		const record = getRegistryWorktree(this.env, id);
		if (!record || record.removedAt !== void 0) throw new Error(`unknown active worktree: ${id}`);
		return record;
	}
	async rebindLiveRepository(record, guard = {}) {
		const worktreePath = await fs$1.realpath(record.path);
		const repository = await resolveRepositoryFromRealPath(worktreePath, record.path);
		if (repository.sourceRoot !== worktreePath) throw new WorktreeRepositoryError(`repository does not own worktree: ${record.path}`);
		if ((await resolveRepository(record.repoRoot)).originUrl !== repository.originUrl) throw new WorktreeRepositoryError(`repository origin does not match: ${record.path}`);
		guard.signal?.throwIfAborted();
		guard.commitGuard?.();
		updateRegistryWorktree(this.env, record.id, { repositoryIdentity: {
			repoRoot: repository.repoRoot,
			repoFingerprint: repository.fingerprint
		} });
		return {
			...record,
			repoRoot: repository.repoRoot,
			repoFingerprint: repository.fingerprint
		};
	}
	async reconcileOrphans(records) {
		const managedPaths = /* @__PURE__ */ new Set();
		for (const record of records) try {
			managedPaths.add(await canonicalPathKey(record.path));
		} catch (error) {
			if (!isMissingPathError(error)) throw error;
		}
		const worktreesRoot = path.join(resolveStateDir(this.env), "worktrees");
		const fingerprints = await fs$1.readdir(worktreesRoot, { withFileTypes: true }).catch(() => []);
		if (fingerprints.length === 0) return 0;
		const defaultRoot = await canonicalPathKey(worktreesRoot);
		const customRoots = /* @__PURE__ */ new Set();
		for (const root of [this.getConfig?.().worktreeRoot, ...records.map((record) => path.dirname(path.dirname(record.path)))]) {
			if (!root) continue;
			try {
				const canonical = await canonicalPathKey(root);
				if (canonical !== defaultRoot) customRoots.add(canonical);
			} catch (error) {
				if (!isMissingPathError(error)) throw error;
			}
		}
		let deleted = 0;
		for (const fingerprint of fingerprints) {
			if (!fingerprint.isDirectory()) continue;
			const fingerprintPath = path.join(worktreesRoot, fingerprint.name);
			if (await shouldPreserveOrphanCandidate(fingerprintPath, managedPaths, customRoots)) continue;
			const names = await fs$1.readdir(fingerprintPath, { withFileTypes: true }).catch(() => []);
			for (const name of names) {
				if (!name.isDirectory()) continue;
				const candidate = path.join(fingerprintPath, name.name);
				if (await shouldPreserveOrphanCandidate(candidate, managedPaths, customRoots)) continue;
				await fs$1.rm(candidate, {
					recursive: true,
					force: true
				});
				deleted += 1;
			}
			await fs$1.rmdir(fingerprintPath).catch(() => void 0);
		}
		return deleted;
	}
};
const managedWorktrees = new ManagedWorktreeService({ getConfig: getRuntimeConfig });
//#endregion
export { WorktreeRemovalLockError as a, classifyWorktreeRemovalError as c, WORKTREE_GC_INTERVAL_MS as i, managedWorktrees as l, ManagedWorktreeService as n, WorktreeRepositoryError as o, SNAPSHOT_RETENTION_MS as r, WorktreeSnapshotError as s, IDLE_GC_MS as t, resolveWorktreeCleanupLimits as u };