UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

301 lines (300 loc) 9.92 kB
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { i as isPidDefinitelyDead, t as getFileLockProcessStartTime } from "./pid-alive-XuW58Ofz.js"; import { o as listGitWorktrees, t as commandError, u as runGit } from "./git-BuLvhVpD.js"; import { c as finalizeWorktreeRemovalRows, d as getRegistryWorktree, g as hasLiveWorktreeRunLeaseRow, i as claimWorktreeRemovalRow, n as abortWorktreeRemovalRow, r as admitWorktreeRunLeaseRow, x as releaseWorktreeRunLeaseRow, y as listRegistryWorktrees } from "./registry-Dvi_fTvk.js"; import path from "node:path"; import fs from "node:fs/promises"; import { randomUUID } from "node:crypto"; //#region src/agents/worktrees/git-lock.ts const OPENCLAW_LOCK_PATTERN = /^openclaw pid=(\d+)$/; async function lockState(record) { const entry = (await listGitWorktrees(record.repoRoot)).find((candidate) => path.resolve(candidate.path) === path.resolve(record.path)); if (!entry || entry.lockedReason === void 0) return { kind: "none" }; const match = OPENCLAW_LOCK_PATTERN.exec(entry.lockedReason); if (!match) return { kind: "foreign", reason: entry.lockedReason }; const pid = Number(match[1]); return isPidDefinitelyDead(pid) ? { kind: "dead", pid } : { kind: "live", pid }; } function heldByThisProcess(state) { return state.kind === "live" && state.pid === process.pid; } async function runLock(record) { return await runGit(record.repoRoot, [ "worktree", "lock", "--reason", `openclaw pid=${process.pid}`, record.path ]); } async function lockWorktreeForProcess(record) { const result = await runLock(record); if (result.code === 0) return; const state = await lockState(record); if (heldByThisProcess(state)) return; if (state.kind !== "dead") throw commandError("git worktree lock", result); await unlockWorktree(record); const retry = await runLock(record); if (retry.code !== 0 && !heldByThisProcess(await lockState(record))) throw commandError("git worktree lock", retry); } async function unlockWorktree(record) { const result = await runGit(record.repoRoot, [ "worktree", "unlock", record.path ]); if (result.code !== 0) throw commandError("git worktree unlock", result); } //#endregion //#region src/agents/worktrees/run-lease.ts const log = createSubsystemLogger("agents/worktrees"); const RELEASE_MAX_ATTEMPTS = 3; const heldGitLocks = /* @__PURE__ */ new Map(); const gitLockTransitionTails = /* @__PURE__ */ new Map(); let ownerChecks = {}; let resolveSelfStartTime = getFileLockProcessStartTime; let releaseRunLeaseRow = releaseWorktreeRunLeaseRow; let unlockWorktreeImpl = unlockWorktree; const pendingLeaseCleanups = /* @__PURE__ */ new Set(); let exitCleanupRegistered = false; async function withGitLockTransition(id, operation) { const previous = gitLockTransitionTails.get(id) ?? Promise.resolve(); let finish; const current = new Promise((resolve) => { finish = resolve; }); const tail = previous.then(() => current); gitLockTransitionTails.set(id, tail); await previous; try { return await operation(); } finally { finish(); if (gitLockTransitionTails.get(id) === tail) gitLockTransitionTails.delete(id); } } async function retainGitLock(env, id) { await withGitLockTransition(id, async () => { const held = heldGitLocks.get(id) ?? { refcount: 0, gitLocked: false }; const needsLock = held.refcount === 0 && !held.gitLocked; held.refcount += 1; heldGitLocks.set(id, held); if (!needsLock) return; const record = getRegistryWorktree(env, id); if (!record) return; try { await lockWorktreeForProcess(record); held.gitLocked = true; } catch (error) { heldGitLocks.delete(id); throw new Error(`managed worktree is unusable because its Git removal guard could not be acquired: ${record.path}; repair the checkout or create a new worktree before retrying: ${formatErrorMessage(error)}`, { cause: error }); } }); } async function releaseGitLock(cleanup) { return await withGitLockTransition(cleanup.id, async () => { let held = heldGitLocks.get(cleanup.id); if (!cleanup.refcountReleased) { cleanup.refcountReleased = true; if (held) held.refcount -= 1; } held = heldGitLocks.get(cleanup.id); if (!held) { cleanup.gitUnlockPending = false; return true; } if (held.refcount > 0) { cleanup.gitUnlockPending = false; return true; } if (!held.gitLocked) { heldGitLocks.delete(cleanup.id); cleanup.gitUnlockPending = false; return true; } const record = getRegistryWorktree(cleanup.env, cleanup.id); if (!record) { heldGitLocks.delete(cleanup.id); cleanup.gitUnlockPending = false; return true; } try { await unlockWorktreeImpl(record); } catch (error) { cleanup.gitUnlockPending = true; log.warn(`failed to unlock worktree ${cleanup.id}: ${formatErrorMessage(error)}`); return false; } heldGitLocks.delete(cleanup.id); cleanup.gitUnlockPending = false; return true; }); } async function realpathOrSelf(candidate) { try { return await fs.realpath(candidate); } catch { return path.resolve(candidate); } } async function resolveWorktreeIdForPath(params) { const env = params.env ?? process.env; const boundId = params.sessionEntry?.worktree?.id; if (boundId !== void 0) { const record = getRegistryWorktree(env, boundId); if (!record || record.removedAt !== void 0) throw new Error(`managed worktree was removed: ${record?.path ?? boundId}`); return boundId; } const records = listRegistryWorktrees(env).filter((record) => record.removedAt === void 0); if (records.length === 0) return; const bases = /* @__PURE__ */ new Map(); for (const record of records) bases.set(record.id, await realpathOrSelf(record.path)); const seen = /* @__PURE__ */ new Set(); for (const candidate of params.candidatePaths) { if (!candidate || seen.has(candidate)) continue; seen.add(candidate); const real = await realpathOrSelf(candidate); for (const record of records) { const base = bases.get(record.id); if (base && (real === base || real.startsWith(`${base}${path.sep}`))) return record.id; } } } function deleteRunLeaseRowWithRetries(cleanup) { for (let attempt = 1; attempt <= RELEASE_MAX_ATTEMPTS; attempt += 1) try { releaseRunLeaseRow(cleanup.env, cleanup.id, cleanup.token); return true; } catch (error) { log.warn(`failed to release worktree run lease for ${cleanup.id} (attempt ${attempt}): ${formatErrorMessage(error)}`); } return false; } async function runLeaseCleanup(cleanup) { if (!cleanup.rowDeleted) { if (!deleteRunLeaseRowWithRetries(cleanup)) return false; cleanup.rowDeleted = true; } return await releaseGitLock(cleanup); } async function drainPendingLeaseCleanups() { for (const cleanup of pendingLeaseCleanups) if (await runLeaseCleanup(cleanup)) pendingLeaseCleanups.delete(cleanup); } function ensureExitCleanupRegistered() { if (exitCleanupRegistered) return; exitCleanupRegistered = true; process.on("exit", () => { for (const cleanup of pendingLeaseCleanups) if (!cleanup.rowDeleted) try { releaseRunLeaseRow(cleanup.env, cleanup.id, cleanup.token); } catch {} }); } async function acquireWorktreeRunLease(id, opts = {}) { const env = opts.env ?? process.env; ensureExitCleanupRegistered(); await drainPendingLeaseCleanups(); const token = randomUUID(); const pid = process.pid; const startTime = resolveSelfStartTime(pid); admitWorktreeRunLeaseRow(env, { worktreeId: id, token, pid, startTime, now: Date.now(), checks: ownerChecks, ...opts.exclusive ? { exclusive: true } : {} }); const cleanup = { env, id, token, rowDeleted: false, refcountReleased: false, gitUnlockPending: false }; try { await retainGitLock(env, id); } catch (error) { cleanup.refcountReleased = true; if (!await runLeaseCleanup(cleanup)) pendingLeaseCleanups.add(cleanup); throw error; } let released = false; return { id, token, release: async () => { if (released) return; released = true; if (!await runLeaseCleanup(cleanup)) pendingLeaseCleanups.add(cleanup); } }; } function claimWorktreeRemoval(env, params) { const pid = process.pid; claimWorktreeRemovalRow(env, { ...params, pid, startTime: resolveSelfStartTime(pid), now: Date.now(), checks: ownerChecks }); } function finalizeWorktreeRemoval(env, worktreeId) { finalizeWorktreeRemovalRows(env, worktreeId); } function abortWorktreeRemoval(env, worktreeId, token) { abortWorktreeRemovalRow(env, worktreeId, token); } function hasLiveWorktreeRunLease(env, worktreeId) { return hasLiveWorktreeRunLeaseRow(env, worktreeId, ownerChecks); } const testing = { setProcessStartTimeResolverForTest(resolver) { resolveSelfStartTime = resolver ?? getFileLockProcessStartTime; ownerChecks = { ...ownerChecks, getProcessStartTime: resolver ?? void 0 }; }, setDeadPidResolverForTest(resolver) { ownerChecks = { ...ownerChecks, isPidDefinitelyDead: resolver ?? void 0 }; }, setReleaseRowImplForTest(impl) { releaseRunLeaseRow = impl ?? releaseWorktreeRunLeaseRow; }, setUnlockImplForTest(impl) { unlockWorktreeImpl = impl ?? unlockWorktree; }, async drainPendingCleanupsForTest() { await drainPendingLeaseCleanups(); }, resetForTest() { heldGitLocks.clear(); gitLockTransitionTails.clear(); pendingLeaseCleanups.clear(); ownerChecks = {}; resolveSelfStartTime = getFileLockProcessStartTime; releaseRunLeaseRow = releaseWorktreeRunLeaseRow; unlockWorktreeImpl = unlockWorktree; } }; if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.worktreeRunLeaseTestApi")] = { testing }; //#endregion export { hasLiveWorktreeRunLease as a, lockWorktreeForProcess as c, finalizeWorktreeRemoval as i, unlockWorktree as l, acquireWorktreeRunLease as n, resolveWorktreeIdForPath as o, claimWorktreeRemoval as r, lockState as s, abortWorktreeRemoval as t };