openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,149 lines (1,148 loc) • 44.7 kB
JavaScript
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { w as root } from "./fs-safe-BBzelG8F.mjs";
import { r as isMissingPathError } from "./errno-FR4CRyya.mjs";
import "./errors-BvOkmQM5.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { c as readGitWorktreeAdministrations, o as readGitObjectStorageDependencies, r as readGitMetadataDirectories, s as readGitRefs } from "./git-root-ChTibfp1.mjs";
import { f as requireGitCommandOutput, r as createGitCommandError, u as normalizeGitPathForFilesystem } from "./git-exec-BOEpfMeZ.mjs";
import { t as lstatIfExists } from "./path-existence-B679BE7y.mjs";
import { a as requestGitWorkerEffect } from "./git-worker-context-DDPUBONa.mjs";
import { a as createStagedInputPathMatcher, f as stagedInputPathDirectory } from "./staged-inputs-CCmC9PQx.mjs";
import { c as requireGitBuffer, d as runGit, f as runGitBuffered, l as resolveGitMetadataPath, m as worktreePathExists, o as listGitWorktrees, s as requireGit, t as WORKTREE_CHECKOUT_TIMEOUT_MS } from "./git-D3obqZyl.mjs";
import { a as parseGitIndexPaths, c as rawPathStat, i as gitPathspecBatches, l as splitNullBuffer, n as containsGitMarker, o as parseGitTreePaths, r as gitPathKey, s as rawPathExists, t as checkoutPathFromGitBytes } from "./git-path-inventory-u5kor_DQ.mjs";
import { c as normalizeProvisionedRelativePath, d as captureExactState, f as exactSnapshotPrefix, h as writeExactStateCommit, i as indexWorktreePaths, l as resolveGitPath, n as snapshotProvisionedFiles, o as hasSafeParentDirectories, s as hasUnsnapshotableProvisionedFiles } from "./provisioned-snapshot-DRAqwkB7.mjs";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { createHash } from "node:crypto";
import { safeRealpathSync } from "@openclaw/fs-safe/path";
import { setImmediate } from "node:timers/promises";
//#region src/agents/worktrees/capacity.runtime.ts
const log = createSubsystemLogger("agents/worktrees");
async function missingCommitObjects(repoRoot, commit) {
return (await requireGitBuffer(repoRoot, [
"rev-list",
"--objects",
"--missing=print",
"--no-object-names",
"--max-count=1",
commit
], { env: { GIT_NO_LAZY_FETCH: "1" } })).toString("utf8").split("\n").filter((line) => line.startsWith("?")).map((line) => line.slice(1));
}
async function readOptionalGitConfig(repoRoot, args) {
const result = await runGit(repoRoot, ["config", ...args]);
return result.termination === "exit" && result.code === 1 ? "" : requireGitCommandOutput(`git config ${args.join(" ")}`, result).trim();
}
function missingObjectsError(commit, count) {
return /* @__PURE__ */ new Error(`Repository is missing ${count} objects for ${commit}; fetch or repair the clone.`);
}
async function resolveCommit(repoRoot, ref) {
return await requireGit(repoRoot, [
"rev-parse",
"--verify",
"--end-of-options",
`${ref === "-" ? "@{-1}" : ref}^{commit}`
]);
}
async function hydrateCommitObjects(repoRoot, commit) {
const missing = await missingCommitObjects(repoRoot, commit);
if (missing.length > 0) {
const remote = await readOptionalGitConfig(repoRoot, ["--get", "extensions.partialclone"]) || /^remote\.(.+)\.promisor true$/m.exec(await readOptionalGitConfig(repoRoot, [
"--bool",
"--get-regexp",
"^remote\\..*\\.promisor$"
]))?.[1];
if (!remote) throw missingObjectsError(commit, missing.length);
await requireGit(repoRoot, [
"fetch",
"--refetch",
"--no-auto-maintenance",
remote,
"--no-tags",
"--no-write-fetch-head",
"--recurse-submodules=no",
"--stdin"
], {
input: Buffer.from(`${missing.join("\n")}\n`),
timeoutMs: WORKTREE_CHECKOUT_TIMEOUT_MS
}).catch((error) => {
log.warn(`worktree prefetch failed: ${missing.length} missing objects for ${commit}; check the promisor remote.`);
throw error;
});
}
}
function allocatedBlobBytes(size) {
const value = Number(size);
if (!size || !Number.isSafeInteger(value) || value < 0) throw new Error("Cannot estimate worktree checkout size; inspect the repository objects and retry.");
return Math.max(4096, Math.ceil(value / 4096) * 4096);
}
const checkoutSizeFacts = /* @__PURE__ */ new Map();
const MAX_CHECKOUT_SIZE_FACTS = 32;
async function commitObjectBytes(repoRoot, commit, replacementRefBase, preparationKey) {
const replacements = replacementRefBase === void 0 ? void 0 : await requireGit(repoRoot, [
"for-each-ref",
"--format=%(refname)",
"--",
replacementRefBase
]);
let cacheKey;
let cached;
if (replacements === "") {
const canonicalRoot = await fs$1.realpath(repoRoot);
const identity = await fs$1.stat(canonicalRoot);
cacheKey = JSON.stringify([
canonicalRoot,
identity.dev,
identity.ino,
commit
]);
cached = checkoutSizeFacts.get(cacheKey);
if (cached && preparationKey && cached.preparationKey === preparationKey) return cached.bytes;
}
await hydrateCommitObjects(repoRoot, commit);
if (cached && cacheKey) {
checkoutSizeFacts.set(cacheKey, {
bytes: cached.bytes,
preparationKey
});
return cached.bytes;
}
try {
const sizes = (await requireGitBuffer(repoRoot, [
"ls-tree",
"-r",
"--format=%(objectsize)",
commit,
"--"
], { env: { GIT_NO_LAZY_FETCH: "1" } })).toString("utf8");
let bytes = 0;
for (const size of sizes.split("\n")) {
if (!size || size === "-") continue;
bytes += allocatedBlobBytes(size);
}
if (cacheKey) {
checkoutSizeFacts.set(cacheKey, {
bytes,
preparationKey
});
pruneMapToMaxSize(checkoutSizeFacts, MAX_CHECKOUT_SIZE_FACTS);
}
return bytes;
} catch (error) {
const remaining = await missingCommitObjects(repoRoot, commit);
if (remaining.length > 0) throw missingObjectsError(commit, remaining.length);
throw error;
}
}
async function estimateCheckoutObjectBytes(repoRoot, ref, replacementRefBase, preparationKey) {
return await commitObjectBytes(repoRoot, await resolveCommit(repoRoot, ref), replacementRefBase, preparationKey);
}
async function estimateCheckoutTransitionBytes(repoRoot, baseRef, targetRef, replacementRefBase) {
const base = await resolveCommit(repoRoot, baseRef);
const target = await resolveCommit(repoRoot, targetRef);
if (target !== base) await hydrateCommitObjects(repoRoot, base);
const targetBytes = await commitObjectBytes(repoRoot, target, replacementRefBase);
if (target === base) return {
targetBytes,
changedBytes: 0,
requiresFullCheckout: false
};
const diff = await runGitBuffered(repoRoot, [
"diff-tree",
"--no-commit-id",
"--raw",
"-z",
"--no-renames",
"--no-abbrev",
"-r",
base,
target,
"--"
], { env: { GIT_NO_LAZY_FETCH: "1" } });
if (diff.termination === "output-limit") return {
targetBytes,
changedBytes: targetBytes,
requiresFullCheckout: true
};
if (diff.termination !== "exit" || diff.code !== 0) throw createGitCommandError("git diff-tree", diff);
const changes = diff.stdout.toString("utf8").split("\0");
if (changes.at(-1) !== "" || changes.length % 2 !== 1) throw new Error("Cannot estimate worktree overlay size; inspect the repository diff and retry.");
const blobs = [];
let checkoutAttributesChanged = false;
for (let offset = 0; offset < changes.length - 1; offset += 2) {
const metadata = /^:[0-7]{6} ([0-7]{6}) [a-f0-9]+ ([a-f0-9]+) [AMDT]$/u.exec(changes[offset]);
if (!metadata || changes[offset + 1] === void 0) throw new Error("Cannot estimate worktree overlay size; inspect the repository diff and retry.");
const changedPath = changes[offset + 1];
checkoutAttributesChanged ||= changedPath === ".gitattributes" || changedPath.endsWith("/.gitattributes");
if (metadata[1] !== "000000" && metadata[1] !== "160000") blobs.push(metadata[2]);
}
if (checkoutAttributesChanged) return {
targetBytes,
changedBytes: targetBytes,
requiresFullCheckout: true
};
if (blobs.length === 0) return {
targetBytes,
changedBytes: 0,
requiresFullCheckout: false
};
const sizes = (await requireGitBuffer(repoRoot, ["cat-file", "--batch-check=%(objecttype) %(objectsize)"], {
input: Buffer.from(`${blobs.join("\n")}\n`),
env: { GIT_NO_LAZY_FETCH: "1" }
})).toString("utf8").trimEnd().split("\n");
if (sizes.length !== blobs.length || sizes.some((size) => !size.startsWith("blob "))) throw new Error("Cannot estimate worktree overlay size; inspect the repository objects and retry.");
return {
targetBytes,
changedBytes: sizes.reduce((bytes, size) => bytes + allocatedBlobBytes(size.slice(5)), 0),
requiresFullCheckout: false
};
}
/** Measure without following links; unreadable trees must never be counted as empty. */
async function measureDirectoryTreeBytes(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()) total += await measureDirectoryTreeBytes(child, excludeGit);
else total += (await rawPathStat(child))?.size ?? 0;
}
return total;
}
//#endregion
//#region src/agents/worktrees/eviction.runtime.ts
const classifications = /* @__PURE__ */ new Map();
const defaultPatches = /* @__PURE__ */ new Map();
function gitOptions(root, storage = readGitMetadataDirectories(root)) {
if (!storage) throw new Error(`Git metadata is unavailable for ${root}`);
return {
env: {
GIT_DIR: storage.gitDir,
GIT_COMMON_DIR: storage.commonDir,
GIT_WORK_TREE: void 0,
GIT_OBJECT_DIRECTORY: path.join(storage.commonDir, "objects"),
GIT_ALTERNATE_OBJECT_DIRECTORIES: void 0,
GIT_NO_REPLACE_OBJECTS: "1",
GIT_NO_LAZY_FETCH: "1"
},
maxOutputBytes: 67108864,
terminateOnOutputLimit: true
};
}
function dependencyInspectionBudget() {
const started = performance.now();
return () => {
if (performance.now() - started >= 5e3) throw new Error("Worktree dependency inspection exceeded its 5-second budget; retry cleanup");
};
}
function checkoutKey(checkout) {
const resolved = safeRealpathSync(checkout) ?? path.resolve(checkout);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
function administrationInventory(commonDir, assertBudget) {
const inventory = readGitWorktreeAdministrations(commonDir, assertBudget);
const byCheckout = /* @__PURE__ */ new Map();
for (const entry of inventory.entries) {
assertBudget();
const key = checkoutKey(entry.checkoutPath);
const entries = byCheckout.get(key) ?? [];
entries.push(entry);
byCheckout.set(key, entries);
}
return {
...inventory,
byCheckout
};
}
function readWorktreeSourceDependencies(input) {
const assertBudget = dependencyInspectionBudget();
const sourceStorage = readGitMetadataDirectories(input.sourceRoot, assertBudget);
let complete = sourceStorage !== void 0;
const requiredPaths = /* @__PURE__ */ new Set([
...input.requiredPaths,
input.sourceRoot,
checkoutKey(input.sourceRoot),
input.commonDir,
...sourceStorage?.paths ?? []
]);
for (const commonDir of /* @__PURE__ */ new Set([input.commonDir, sourceStorage?.commonDir])) {
if (!commonDir) continue;
const objects = readGitObjectStorageDependencies(commonDir, assertBudget);
complete &&= objects.complete;
for (const dependency of objects.paths) requiredPaths.add(dependency);
}
const repositories = /* @__PURE__ */ new Map();
const inventories = /* @__PURE__ */ new Map();
const ownedPaths = [];
for (const record of input.records) {
assertBudget();
ownedPaths.push(record, {
id: record.id,
path: checkoutKey(record.path)
});
if (!repositories.has(record.repoRoot)) repositories.set(record.repoRoot, readGitMetadataDirectories(record.repoRoot, assertBudget));
const storage = repositories.get(record.repoRoot);
if (storage && !inventories.has(storage.commonDir)) inventories.set(storage.commonDir, administrationInventory(storage.commonDir, assertBudget));
const administrations = storage ? inventories.get(storage.commonDir) : void 0;
for (const entry of administrations?.byCheckout.get(checkoutKey(record.path)) ?? []) ownedPaths.push({
id: record.id,
path: entry.adminPath
}, {
id: record.id,
path: entry.physicalAdminPath
});
}
const ancestors = indexWorktreePaths(ownedPaths);
const worktreeIds = /* @__PURE__ */ new Set();
for (const required of requiredPaths) {
assertBudget();
for (const id of ancestors(required)) worktreeIds.add(id);
}
return {
worktreeIds: [...worktreeIds],
complete
};
}
async function prepareWorktreeEvictionRepositories(repoRoots) {
const repositories = [];
for (const repoRoot of repoRoots) {
const repository = {
repoRoot,
commonDir: readGitMetadataDirectories(repoRoot)?.commonDir,
heads: {}
};
try {
repository.defaultHead = await defaultBranchHead(repoRoot);
if (repository.defaultHead) {
for (const checkout of await listGitWorktrees(repoRoot, gitOptions(repoRoot))) if (checkout.head && !/^0+$/u.test(checkout.head)) repository.heads[path.resolve(checkout.path)] = checkout.head;
}
} catch {
await requestGitWorkerEffect({
type: "worktree.assert-current",
input: {}
});
}
repositories.push(repository);
}
return repositories;
}
async function defaultBranchHead(repoRoot) {
const remoteHead = await runGit(repoRoot, [
"rev-parse",
"--verify",
"refs/remotes/origin/HEAD^{commit}"
], gitOptions(repoRoot));
if (remoteHead.code === 0) return remoteHead.stdout.trim();
return (await runGit(repoRoot, [
"config",
"--get",
"remote.origin.url"
], gitOptions(repoRoot))).code === 1 ? await requireGit(repoRoot, [
"rev-parse",
"--verify",
"HEAD^{commit}"
], gitOptions(repoRoot)) : void 0;
}
async function patchIds(repoRoot, patch) {
const ids = await requireGit(repoRoot, ["patch-id", "--stable"], {
...gitOptions(repoRoot),
input: patch
});
return new Set(ids.split("\n").map((line) => line.split(" ")[0]).filter(Boolean));
}
async function classifyBranch(repoRoot, head, upstream) {
const cacheKey = `${repoRoot}:${head}:${upstream}`;
const cached = classifications.get(cacheKey);
if (cached) return cached;
const ancestor = await runGit(repoRoot, [
"merge-base",
"--is-ancestor",
head,
upstream
], gitOptions(repoRoot));
let reason = "idle-age";
if (ancestor.code === 0) reason = "merged";
else if (ancestor.code === 1) {
const base = await requireGit(repoRoot, [
"merge-base",
head,
upstream
], gitOptions(repoRoot));
const cherry = await requireGit(repoRoot, [
"cherry",
upstream,
head
], gitOptions(repoRoot));
if (cherry.trim() && !cherry.split("\n").some((line) => line.startsWith("+"))) reason = "squashed";
else {
const branchIds = await patchIds(repoRoot, await requireGitBuffer(repoRoot, [
"diff",
"--binary",
"--no-ext-diff",
"--no-textconv",
base,
head
], gitOptions(repoRoot)));
const historyKey = `${repoRoot}:${base}:${upstream}`;
let landedIds = defaultPatches.get(historyKey);
if (!landedIds) {
landedIds = await patchIds(repoRoot, await requireGitBuffer(repoRoot, [
"log",
"--no-merges",
"--format=medium",
"--binary",
"--no-ext-diff",
"--no-textconv",
"-p",
`${base}..${upstream}`
], gitOptions(repoRoot)));
defaultPatches.set(historyKey, landedIds);
pruneMapToMaxSize(defaultPatches, 64);
}
if (branchIds.size > 0 && [...branchIds].every((id) => landedIds.has(id))) reason = "squashed";
}
} else throw createGitCommandError("git merge-base --is-ancestor", ancestor);
classifications.set(cacheKey, reason);
pruneMapToMaxSize(classifications, 8192);
return reason;
}
async function classifyWorktreeEvictions(records) {
const candidates = [];
for (const record of records) {
let reason = "idle-age";
try {
const { head, defaultHead: upstream } = record;
if (head && upstream) {
reason = await classifyBranch(record.repoRoot, head, upstream);
if (reason === "idle-age" && record.mergedHeads?.some((landed) => landed.sha === head)) {
if ((await runGit(record.repoRoot, [
"show-ref",
"--verify",
"--quiet",
`refs/remotes/origin/${record.branch}`
], gitOptions(record.repoRoot))).code === 1) for (const landed of record.mergedHeads) {
if (landed.sha !== head || !landed.mergeCommitSha) continue;
if ((await runGit(record.repoRoot, [
"merge-base",
"--is-ancestor",
landed.mergeCommitSha,
upstream
], gitOptions(record.repoRoot))).code === 0) {
reason = "squashed";
break;
}
}
}
}
} catch {
await requestGitWorkerEffect({
type: "worktree.assert-current",
input: {}
});
}
candidates.push({
id: record.id,
reason
});
}
return candidates;
}
/** The host holds the registry removal claim and joins this write without cancellation. */
async function purgeWorktreeCheckout(record, live) {
const checkout = path.resolve(record.path);
const source = path.resolve(record.repoRoot);
const stat = fs.lstatSync(checkout, {
bigint: true,
throwIfNoEntry: false
});
if (stat && !stat.isDirectory()) throw new Error("Managed worktree checkout is not a directory; repair its path before eviction");
const physicalCheckout = stat ? fs.realpathSync.native(checkout) : checkout;
const sourceStorage = readGitMetadataDirectories(source);
const primary = (sourceStorage ? await listGitWorktrees(source, gitOptions(source, sourceStorage)).catch(() => []) : [])[0]?.path;
const assertBudget = dependencyInspectionBudget();
let deletionStorage = readGitMetadataDirectories(source, assertBudget);
const readAdministration = (storage) => {
if (!storage) return;
const inventory = administrationInventory(storage.commonDir, assertBudget);
const matches = inventory.byCheckout.get(checkoutKey(checkout)) ?? [];
if (!inventory.complete || matches.length > 1) throw new Error("Managed worktree administration is unavailable or ambiguous; retry cleanup");
return matches[0];
};
const administration = readAdministration(deletionStorage);
const registered = administration !== void 0;
const targets = [{
path: checkout,
physicalPath: physicalCheckout,
stat,
physicalStat: stat
}];
if (administration) {
const adminStat = fs.lstatSync(administration.adminPath, { bigint: true });
targets.push({
path: administration.adminPath,
physicalPath: administration.physicalAdminPath,
stat: adminStat,
physicalStat: fs.statSync(administration.adminPath, { bigint: true })
});
}
const assertTargetsCurrent = () => {
for (const target of targets) {
const current = fs.lstatSync(target.path, {
bigint: true,
throwIfNoEntry: false
});
const physical = current ? fs.statSync(target.path, { bigint: true }) : void 0;
if (target.stat ? !current || current.dev !== target.stat.dev || current.ino !== target.stat.ino || physical?.dev !== target.physicalStat?.dev || physical?.ino !== target.physicalStat?.ino || fs.realpathSync.native(target.path) !== target.physicalPath : current !== void 0) throw new Error("Managed worktree purge target changed; retry cleanup");
}
};
const others = live.filter((candidate) => candidate.id !== record.id);
const roots = [.../* @__PURE__ */ new Set([source, ...others.map((candidate) => candidate.repoRoot)])];
const contains = (target) => {
return targets.some(({ physicalPath }) => {
const relative = path.relative(physicalPath, target);
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
});
};
const fenced = /* @__PURE__ */ new Set();
let admitted = false;
for (;;) {
await setImmediate();
assertTargetsCurrent();
const objects = /* @__PURE__ */ new Map();
const objectDependencies = (storage) => {
if (!storage) return;
let dependencies = objects.get(storage.commonDir);
if (!dependencies) {
dependencies = readGitObjectStorageDependencies(storage.commonDir, assertBudget);
objects.set(storage.commonDir, dependencies);
}
return dependencies;
};
const repositories = /* @__PURE__ */ new Map();
for (const root of roots) {
const storage = readGitMetadataDirectories(root, assertBudget);
repositories.set(root, {
physicalRoot: safeRealpathSync(root) ?? root,
storage,
objects: objectDependencies(storage)
});
assertBudget();
}
const currentSource = repositories.get(source);
const currentAdministration = readAdministration(currentSource.storage);
if (currentAdministration?.adminPath !== administration?.adminPath || currentAdministration?.physicalAdminPath !== administration?.physicalAdminPath) throw new Error("Managed worktree purge administration changed; retry cleanup");
deletionStorage = currentSource.storage;
if (contains(currentSource.physicalRoot)) throw new Error("Managed worktree eviction cannot remove the source repository");
if (primary && (path.resolve(primary) === checkout || path.resolve(primary).startsWith(`${checkout}${path.sep}`))) throw new Error("Managed worktree eviction cannot remove the primary repository");
const checkoutStorage = readGitMetadataDirectories(checkout, assertBudget);
const checkoutCommonDir = checkoutStorage && path.relative(checkoutStorage.gitDir, checkoutStorage.commonDir) !== "" ? checkoutStorage.commonDir : void 0;
const checkoutObjects = checkoutCommonDir ? objectDependencies(checkoutStorage) : void 0;
if ([
...currentSource.storage?.paths ?? [],
checkoutCommonDir,
...currentSource.objects?.paths ?? [],
...checkoutObjects?.paths ?? []
].some((directory) => directory !== void 0 && contains(directory))) throw new Error("Managed worktree eviction cannot remove its source Git metadata; relocate the shared Git directory outside the checkout first");
if (currentSource.objects?.complete === false || checkoutObjects?.complete === false) throw new Error("Managed worktree source Git object dependencies are incomplete; repair its object storage before eviction");
const unresolved = [];
for (const candidate of others) {
const repository = repositories.get(candidate.repoRoot);
const physicalPath = safeRealpathSync(candidate.path);
const storage = readGitMetadataDirectories(candidate.path, assertBudget);
const checkoutDependencies = objectDependencies(storage);
if ([
candidate.path,
physicalPath,
candidate.repoRoot,
repository.physicalRoot,
...repository.storage?.paths ?? [],
...storage?.paths ?? [],
...repository.objects?.paths ?? [],
...checkoutDependencies?.paths ?? []
].some((target) => target != null && contains(target))) throw new Error(`Worktree acquired a live checkout or repository dependency: ${candidate.id}; retry cleanup after that checkout retires`);
if ((!repository.storage || !storage || !repository.objects?.complete || !checkoutDependencies?.complete) && !fenced.has(candidate.id)) unresolved.push(candidate.id);
assertBudget();
}
assertBudget();
assertTargetsCurrent();
if (unresolved.length === 0) {
if (admitted) break;
await requestGitWorkerEffect({
type: "worktree.eviction-admit",
input: {}
});
admitted = true;
continue;
}
await requestGitWorkerEffect({
type: "worktree.eviction-fence",
input: { worktreeIds: unresolved }
});
for (const id of unresolved) fenced.add(id);
}
if (registered) await requireGit(source, [
"worktree",
"remove",
"--force",
"--force",
"--",
checkout
], {
...gitOptions(source, deletionStorage),
killProcessTree: true,
waitForExit: true
});
else if (stat) await fs$1.rm(checkout, {
recursive: true,
force: true
});
}
//#endregion
//#region src/agents/worktrees/gc-fingerprint.runtime.ts
/** A deferral is a scheduling hint, never permission to delete a checkout. */
async function readWorktreeCleanupFingerprint(checkoutPath) {
try {
const directories = readGitMetadataDirectories(checkoutPath);
if (!directories) return null;
const { gitDir: gitdir, commonDir: common } = directories;
const head = await fs$1.readFile(path.join(gitdir, "HEAD"), "utf8");
const reference = head.startsWith("ref: ") ? head.slice(5).trim() : void 0;
const tip = reference ? readGitRefs(common, [reference]).get(reference) : head;
const identities = await Promise.all([
checkoutPath,
path.join(checkoutPath, ".git"),
path.join(gitdir, "HEAD"),
path.join(gitdir, "index")
].map(async (entry) => {
const stat = await fs$1.lstat(entry, { bigint: true });
return [
stat.dev,
stat.ino,
stat.size,
stat.mtimeNs,
stat.ctimeNs
].map(String);
}));
return createHash("sha256").update(JSON.stringify([
directories,
head,
tip,
identities
])).digest("hex");
} catch {
return null;
}
}
//#endregion
//#region src/agents/worktrees/snapshot-inventory.ts
const assertCurrent = () => requestGitWorkerEffect({
type: "worktree.assert-current",
input: {}
});
function openRawDirectory(directoryPath) {
const openDirectory = fs$1.opendir;
return openDirectory(directoryPath, { encoding: "buffer" });
}
async function listCollapsedOtherPaths(checkoutPath, ignored) {
return splitNullBuffer(await requireGitBuffer(checkoutPath, [
"ls-files",
"-z",
"--others",
...ignored ? ["--ignored"] : [],
"--exclude-standard",
"--directory"
]));
}
async function walkCollapsedPaths(checkoutPath, entries, options) {
const visitDirectory = async (relative) => {
if (await rawPathExists(checkoutPathFromGitBytes(checkoutPath, Buffer.concat([relative, Buffer.from("/.git")])))) return true;
const directory = await openRawDirectory(checkoutPathFromGitBytes(checkoutPath, relative));
for await (const entry of directory) {
if (!Buffer.isBuffer(entry.name)) throw new Error("Expected raw directory-entry bytes");
const child = Buffer.concat([
relative,
Buffer.from("/"),
entry.name
]);
if (options.skip?.has(gitPathKey(child))) continue;
if (entry.isDirectory()) {
if (await visitDirectory(child)) return true;
} else await options.visitFile?.(child);
}
return false;
};
for (const entry of entries) if (entry.at(-1) === 47) {
if (await visitDirectory(entry.subarray(0, -1))) return true;
} else await options.visitFile?.(entry);
return false;
}
/** Index entries whose flags make Git skip its worktree comparison for that path. */
function unstattedIndexPaths(index) {
return index.filter((entry) => entry.assumeUnchanged || entry.skipWorktree).map((entry) => entry.path);
}
/**
* Inspect untracked and ignored paths without buffering every filename through Git.
* Resolves true when a nested repository marker is found; ignored paths nested in
* untracked trees stay out of the untracked visit so ignore semantics are preserved.
* `unstattedIndexPaths` carries the index entries Git will not compare against the
* filesystem, whose replacements only a direct stat can discover.
*/
async function inspectOtherPaths(checkoutPath, options = {}) {
const untracked = await listCollapsedOtherPaths(checkoutPath, false);
const candidates = /* @__PURE__ */ new Map();
for (const entry of [...splitNullBuffer(await requireGitBuffer(checkoutPath, [
"diff-files",
"-z",
"--name-only",
"--diff-filter=DU"
])), ...options.unstattedIndexPaths ?? []]) candidates.set(gitPathKey(entry), entry);
const replaced = [...candidates.values()];
for (let offset = 0; offset < replaced.length; offset += 64) {
const batch = replaced.slice(offset, offset + 64);
const stats = await Promise.allSettled(batch.map((entry) => rawPathStat(checkoutPathFromGitBytes(checkoutPath, entry))));
for (const [index, result] of stats.entries()) if (result.status === "rejected") throw result.reason;
else if (result.value?.isDirectory()) untracked.push(Buffer.concat([batch[index], Buffer.from("/")]));
}
const ignored = await listCollapsedOtherPaths(checkoutPath, true);
if (await containsGitMarker(checkoutPath, [...untracked, ...ignored])) return true;
const ignoredKeys = new Set(ignored.map((entry) => gitPathKey(entry.at(-1) === 47 ? entry.subarray(0, -1) : entry)));
return await walkCollapsedPaths(checkoutPath, untracked, {
visitFile: options.untracked,
skip: ignoredKeys
}) || await walkCollapsedPaths(checkoutPath, ignored, { visitFile: options.ignored });
}
async function collectSnapshotInventory(input) {
const head = await requireGit(input.checkoutPath, [
"rev-parse",
"--verify",
"HEAD^{commit}"
]);
const headPaths = parseGitTreePaths(await requireGitBuffer(input.checkoutPath, [
"ls-tree",
"-r",
"-z",
head
]));
const index = parseGitIndexPaths(await requireGitBuffer(input.checkoutPath, [
"ls-files",
"--stage",
"-v",
"-z"
]));
const provisioned = new Set(input.provisionedPaths.map((entry) => gitPathKey(Buffer.from(entry))));
const paths = /* @__PURE__ */ new Map();
const add = (entry) => {
const key = gitPathKey(entry);
if (!provisioned.has(key)) paths.set(key, entry);
};
const sparseConfig = await runGit(input.checkoutPath, [
"config",
"--bool",
"core.sparseCheckout"
]);
if (sparseConfig.code !== 0 && sparseConfig.code !== 1) throw createGitCommandError("git config --bool core.sparseCheckout", sparseConfig);
const sparse = sparseConfig.code === 0 && sparseConfig.stdout.trim() === "true";
const sourcePaths = /* @__PURE__ */ new Set();
const sparseCandidates = [];
const headKeys = new Set(headPaths.map((entry) => gitPathKey(entry.path)));
for (const entry of index) {
sourcePaths.add(gitPathKey(entry.path));
if (!headKeys.has(gitPathKey(entry.path)) && (await rawPathStat(checkoutPathFromGitBytes(input.checkoutPath, entry.path)))?.isDirectory()) continue;
if (!entry.skipWorktree || await rawPathExists(checkoutPathFromGitBytes(input.checkoutPath, entry.path)) || !sparse) add(entry.path);
else sparseCandidates.push(entry.path);
}
if (sparseCandidates.length > 0) {
const included = await requireGitBuffer(input.checkoutPath, [
"sparse-checkout",
"check-rules",
"-z"
], { input: Buffer.concat(sparseCandidates.flatMap((entry) => [entry, Buffer.from([0])])) });
for (const entry of splitNullBuffer(included)) add(entry);
}
for (const entry of headPaths) if (!sourcePaths.has(gitPathKey(entry.path))) add(entry.path);
const isStagedInput = createStagedInputPathMatcher(await root(input.checkoutPath));
let untracked = 0;
if (await inspectOtherPaths(input.checkoutPath, {
unstattedIndexPaths: unstattedIndexPaths(index),
untracked: async (entry) => {
untracked++;
add(entry);
},
ignored: async (entry) => {
const relativePath = entry.toString("utf8");
if (stagedInputPathDirectory(relativePath) && await isStagedInput(relativePath)) add(entry);
}
}) || await containsGitMarker(input.checkoutPath, paths.values())) throw new Error("nested git repositories cannot be snapshotted losslessly");
await requestGitWorkerEffect({
type: "worktree.snapshot-inventory",
input: {
tracked: sourcePaths.size,
untracked
}
});
return {
head,
headPaths,
paths
};
}
const snapshotIndexArgs = [
"-c",
"core.splitIndex=false",
"-c",
"core.sparseCheckout=false",
"-c",
"index.sparse=false",
"-c",
"core.ignoreStat=false",
"-c",
"core.trustctime=true",
"-c",
"core.checkStat=default",
...process.platform === "win32" ? [] : ["-c", "core.filemode=true"]
];
async function seedSnapshotIndex(input, inventory, indexEnv) {
const source = await resolveGitMetadataPath(input.checkoutPath, "index");
const destination = indexEnv.GIT_INDEX_FILE;
try {
const stat = await fs$1.stat(source);
const sourceIndex = await fs$1.realpath(source);
const [sourceRoot, destinationRoot] = await Promise.all([root(path.dirname(sourceIndex)), root(path.dirname(destination))]);
await destinationRoot.copyIn(path.basename(destination), {
root: sourceRoot,
relativePath: `./${path.basename(sourceIndex)}`
}, {
clone: "auto",
durable: false,
mkdir: false,
overwrite: true,
preserveSourceMode: true,
sourceHardlinks: "allow"
});
const timestamp = Math.floor(stat.mtimeMs / 1e3);
await fs$1.utimes(destination, timestamp, timestamp);
} catch (error) {
if (!isMissingPathError(error)) throw error;
}
await requireGit(input.checkoutPath, [
...snapshotIndexArgs,
"read-tree",
"--reset",
inventory.head
], { env: indexEnv });
const paths = Buffer.concat(inventory.headPaths.flatMap((entry) => [entry.path, Buffer.from([0])]));
for (const flag of ["--no-assume-unchanged", "--no-skip-worktree"]) await requireGit(input.checkoutPath, [
...snapshotIndexArgs,
"update-index",
flag,
"-z",
"--stdin"
], {
env: indexEnv,
input: paths
});
}
async function prepareSnapshotIndex(input, inventory, indexEnv, temporaryDirectory) {
const metadataBytes = [...inventory.headPaths.map((entry) => entry.path), ...inventory.paths.values()].reduce((total, entry) => total + 512 + 2 * entry.length, 0);
await requestGitWorkerEffect({
type: "worktree.snapshot-capacity",
input: {
demands: [{
path: temporaryDirectory,
bytes: 2 * metadataBytes
}],
purpose: "worktree safety snapshot index"
}
});
await seedSnapshotIndex(input, inventory, indexEnv);
const tracked = new Set(inventory.headPaths.map((entry) => gitPathKey(entry.path)));
const changed = new Set(splitNullBuffer(await requireGitBuffer(input.checkoutPath, [
...snapshotIndexArgs,
"-c",
"diff.autoRefreshIndex=true",
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-renames",
"--name-only",
"-z",
"--"
], { env: indexEnv })).map(gitPathKey));
const unique = new Map([...inventory.paths].filter(([key]) => changed.has(key) || !tracked.has(key)));
for (const value of input.provisionedPaths) unique.set(gitPathKey(Buffer.from(value)), Buffer.from(value));
const provisioned = new Set(input.provisionedPaths.map((value) => gitPathKey(Buffer.from(value))));
const missing = /* @__PURE__ */ new Set();
let gitBytes = 0;
let provisionedBytes = 0;
const candidates = [...unique];
for (let offset = 0; offset < candidates.length; offset += 64) {
const batch = candidates.slice(offset, offset + 64);
const stats = await Promise.allSettled(batch.map(([, value]) => rawPathStat(checkoutPathFromGitBytes(input.checkoutPath, value))));
for (const [index, result] of stats.entries()) {
const key = batch[index][0];
if (result.status === "rejected") throw result.reason;
if (result.value) {
if (provisioned.has(key)) provisionedBytes += result.value.size;
else gitBytes += result.value.size;
} else if (tracked.has(key)) missing.add(key);
}
}
const common = normalizeGitPathForFilesystem(await requireGit(input.repoRoot, ["rev-parse", "--git-common-dir"]));
await requestGitWorkerEffect({
type: "worktree.snapshot-capacity",
input: {
demands: [{
path: path.resolve(input.repoRoot, common),
bytes: 2 * gitBytes + metadataBytes
}, {
path: temporaryDirectory,
bytes: 2 * metadataBytes
}],
stateBytes: 2 * provisionedBytes,
purpose: "worktree safety snapshot"
}
});
return {
missing,
tracked
};
}
function assertNoProvisionedTreePaths(tree, provisionedPaths) {
const provisioned = new Set(provisionedPaths.map((entry) => gitPathKey(Buffer.from(entry))));
for (const entry of tree) {
if (entry.mode === "160000") throw new Error("nested git repositories cannot be snapshotted losslessly");
for (let end = entry.path.length; end >= 0; end = entry.path.lastIndexOf(47, end - 1)) {
const prefix = entry.path.subarray(0, end);
if (provisioned.has(gitPathKey(prefix))) throw new Error(`provisioned path entered Git snapshot: ${prefix.toString("utf8")}`);
if (end === 0) break;
}
}
}
async function snapshotWorktree(input) {
await assertCurrent();
const temporaryDirectory = await requestGitWorkerEffect({
type: "git.temporary-directory",
input: {}
});
const snapshotRef = input.exactState ? `${exactSnapshotPrefix}${input.worktreeId}` : `refs/openclaw/snapshots/${input.worktreeId}`;
const filemodeArgs = process.platform === "win32" ? [] : ["-c", "core.filemode=true"];
const env = {
GIT_INDEX_FILE: path.join(temporaryDirectory, "index"),
GIT_AUTHOR_NAME: "OpenClaw",
GIT_AUTHOR_EMAIL: "openclaw@localhost",
GIT_COMMITTER_NAME: "OpenClaw",
GIT_COMMITTER_EMAIL: "openclaw@localhost"
};
const inventory = await collectSnapshotInventory(input);
await assertCurrent();
const prepared = input.exactState ? void 0 : await prepareSnapshotIndex(input, inventory, env, temporaryDirectory);
const missing = prepared?.missing ?? /* @__PURE__ */ new Set();
const tracked = prepared?.tracked ?? /* @__PURE__ */ new Set();
const exact = input.exactState ? await captureExactState({
checkoutPath: input.checkoutPath,
...input.exactState,
paths: inventory.paths.values(),
provisionedPaths: input.provisionedPaths,
write: true,
temporaryDirectory
}) : void 0;
const provisionedState = await snapshotProvisionedFiles(input.checkoutPath, input.provisionedPaths, exact ? {
algorithm: exact.metadata.head.length === 64 ? "sha256" : "sha1",
files: exact.metadata.files.filter((entry) => entry.provisioned).map((entry) => ({
path: Buffer.from(entry.path, "hex").toString("utf8"),
mode: entry.kind === "missing" ? null : entry.mode,
size: entry.size,
blob: entry.blob
}))
} : void 0);
const missingPaths = [];
const trackedPaths = [];
const addedPaths = [];
for (const [key, entry] of inventory.paths) if (missing.has(key)) missingPaths.push(entry);
else if (tracked.has(key)) trackedPaths.push(entry);
else addedPaths.push(entry);
missingPaths.sort((left, right) => Buffer.compare(right, left));
await assertCurrent();
if (!exact) {
await requireGit(input.checkoutPath, [
...snapshotIndexArgs,
"update-index",
"--add",
"--remove",
"-z",
"--stdin"
], {
env,
input: Buffer.concat([
...missingPaths,
...trackedPaths,
...addedPaths
].flatMap((entry) => [entry, Buffer.from([0])]))
});
await assertCurrent();
}
if (exact) {
await requireGit(input.checkoutPath, [
...snapshotIndexArgs,
"read-tree",
"--empty"
], { env });
await requireGit(input.checkoutPath, [
...snapshotIndexArgs,
"update-index",
"-z",
"--index-info"
], {
env,
input: Buffer.concat(exact.treeEntries)
});
}
const tree = await requireGit(input.checkoutPath, [...snapshotIndexArgs, "write-tree"], { env });
assertNoProvisionedTreePaths(parseGitTreePaths(await requireGitBuffer(input.checkoutPath, [
"ls-tree",
"-r",
"-z",
tree
])), input.provisionedPaths);
const assertHeadCurrent = async () => {
if (await requireGit(input.checkoutPath, [
"rev-parse",
"--verify",
"HEAD^{commit}"
]) !== inventory.head) throw new Error("Worktree HEAD changed while preparing its snapshot; retry cleanup.");
await assertCurrent();
};
await assertHeadCurrent();
const exactCommit = exact ? await writeExactStateCommit(input.checkoutPath, exact) : void 0;
if (exact) await verifyExactStateSnapshot({
...input,
expectedDigest: exact.digest
});
const commit = await requireGit(input.checkoutPath, [
...filemodeArgs,
"commit-tree",
tree,
"-p",
inventory.head,
...exactCommit ? ["-p", exactCommit] : [],
"-m",
`OpenClaw worktree snapshot: ${input.reason}`
], { env });
await assertHeadCurrent();
await requireGit(input.checkoutPath, [
"update-ref",
"--stdin",
"-z"
], { input: Buffer.from(`start\0verify HEAD\0${inventory.head}\0${input.exactState ? `verify refs/heads/${input.exactState.branch}\0${input.exactState.expected.branchHead}\0` : ""}update ${snapshotRef}\0${commit}\0\0prepare\0commit\0`) });
return {
snapshotRef,
provisionedState,
...exact ? { exactStateDigest: exact.digest } : {}
};
}
async function verifyExactStateSnapshot(input) {
if (!input.exactState) throw new Error("Exact-state verification requires captured authority");
await assertCurrent();
const inventory = await collectSnapshotInventory(input);
const captured = await captureExactState({
checkoutPath: input.checkoutPath,
...input.exactState,
paths: inventory.paths.values(),
provisionedPaths: input.provisionedPaths,
write: false
});
await assertCurrent();
if (captured.digest !== input.expectedDigest) throw new Error("Worktree contents or metadata changed after exact-state capture; checkout preserved");
return true;
}
async function inspectNestedRepository(checkoutPath) {
const index = parseGitIndexPaths(await requireGitBuffer(checkoutPath, [
"ls-files",
"--stage",
"-v",
"-z"
]));
if (index.some((entry) => entry.mode === "160000")) return true;
return await containsGitMarker(checkoutPath, index.map((entry) => entry.path)) || await inspectOtherPaths(checkoutPath, { unstattedIndexPaths: unstattedIndexPaths(index) });
}
//#endregion
//#region src/agents/worktrees/git-worktree-operations.runtime.ts
async function inspectProvisioning(sourceRoot) {
const includePath = path.join(sourceRoot, ".worktreeinclude");
if (!await worktreePathExists(includePath)) return {
paths: [],
estimatedBytes: 0
};
if (!(await fs$1.stat(includePath)).isFile()) throw new Error(".worktreeinclude must resolve to a regular file");
const included = splitNullBuffer(await requireGitBuffer(sourceRoot, [
"ls-files",
"--others",
"--ignored",
`--exclude-from=${includePath}`,
"-z"
])).map((entry) => entry.toString("utf8"));
const paths = [];
for (const batch of gitPathspecBatches(included)) {
const candidates = await requireGitBuffer(sourceRoot, [
"--literal-pathspecs",
"ls-files",
"--others",
"--ignored",
"--exclude-standard",
"-z",
"--",
...batch
]);
paths.push(...splitNullBuffer(candidates).map((entry) => entry.toString("utf8")));
}
let estimatedBytes = 0;
for (const relativePath of paths) {
const normalized = normalizeProvisionedRelativePath(relativePath);
if (!normalized || !await hasSafeParentDirectories(sourceRoot, normalized)) continue;
const stat = await lstatIfExists(resolveGitPath(sourceRoot, normalized), "ENOENT");
if (stat?.isFile()) estimatedBytes += Math.max(4096, stat.size);
}
return {
paths,
estimatedBytes
};
}
async function inspectCleanup(input) {
if (input.kind === "nested-repository") return { retainedReason: await inspectNestedRepository(input.checkoutPath) ? "nested-repository" : void 0 };
if (input.kind === "provisioned") return { retainedReason: await hasUnsnapshotableProvisionedFiles(input.checkoutPath, input.provisionedPaths) ? "provisioned-drift" : void 0 };
const status = await requireGitBuffer(input.checkoutPath, ["status", "--porcelain"]);
const unpushed = await requireGitBuffer(input.checkoutPath, [
"log",
"HEAD",
"--not",
"--remotes",
"--oneline"
]);
const drift = await hasUnsnapshotableProvisionedFiles(input.checkoutPath, input.provisionedPaths);
return { retainedReason: status.toString("utf8").trim() ? "dirty" : unpushed.toString("utf8").trim() ? "unpushed" : drift ? "provisioned-drift" : await inspectNestedRepository(input.checkoutPath) ? "nested-repository" : void 0 };
}
async function executeGitWorktreeOperation(operation) {
switch (operation.type) {
case "worktree.eviction-source": return readWorktreeSourceDependencies(operation.input);
case "worktree.eviction-repositories": return await prepareWorktreeEvictionRepositories(operation.input.repoRoots);
case "worktree.eviction-classify": return await classifyWorktreeEvictions(operation.input.records);
case "worktree.eviction-purge": return await purgeWorktreeCheckout(operation.input.record, operation.input.live);
case "worktree.cleanup-fingerprint": return await readWorktreeCleanupFingerprint(operation.input.checkoutPath);
case "worktree.snapshot-verify-exact": return await verifyExactStateSnapshot(operation.input);
case "worktree.snapshot": return await snapshotWorktree(operation.input);
case "worktree.provisioning-inspection": return await inspectProvisioning(operation.input.sourceRoot);
case "worktree.cleanup-inspection": return await inspectCleanup(operation.input);
case "worktree.git-size": return await estimateCheckoutObjectBytes(operation.input.repoRoot, operation.input.ref, operation.input.replacementRefBase, operation.input.preparationKey);
case "worktree.checkout-transition-size": return await estimateCheckoutTransitionBytes(operation.input.repoRoot, operation.input.baseRef, operation.input.targetRef, operation.input.replacementRefBase);
case "worktree.directory-size": return await measureDirectoryTreeBytes(operation.input.root, operation.input.excludeGit);
default: throw new Error("Unknown managed-worktree Git operation");
}
}
//#endregion
export { executeGitWorktreeOperation };