UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

151 lines (150 loc) 7.37 kB
import { a as getChildLogger } from "./logger-DK-iouVT.js"; import { i as openOpenClawStateDatabase } from "./openclaw-state-db-BRTnL-D8.js"; import "./session-accessor-YsytfDtG.js"; import { l as loadSessionEntry } from "./session-accessor.sqlite-entry-CWk3jL7s.js"; import { h as runExclusiveSessionLifecycleMutation } from "./session-lifecycle-admission-CS8v45tk.js"; import { S as collectActiveSessionWorkAdmissionKeys } from "./disk-budget-BsDSCUGD.js"; import { u as runGit } from "./git-BuLvhVpD.js"; import { n as prepareSessionWorkerPlacementMutationCheck } from "./session-placement-lifecycle-7DQHpaT9.js"; import { d as getRegistryWorktree } from "./registry-Dvi_fTvk.js"; import { c as classifyWorktreeRemovalError, l as managedWorktrees, n as ManagedWorktreeService } from "./service-B1SvAvxa.js"; import { t as createWorkerSessionPlacementStore } from "./placement-store-D2ojt5RF.js"; import { existsSync } from "node:fs"; import { isDeepStrictEqual } from "node:util"; //#region src/sessions/session-worktree-lifecycle.ts var SessionWorktreeLifecycleError = class extends Error { constructor(message, reason) { super(message); this.reason = reason; } }; function serviceFor(env) { return env ? new ManagedWorktreeService({ env }) : managedWorktrees; } function belongsToSession(record, sessionKey) { return record.ownerKind === "session" && record.ownerId === sessionKey; } /** The session lifecycle fence remains held until this exact bound checkout finishes cleanup. */ async function removeSessionWorktree(params) { if (!params.id) return; const env = params.env ?? process.env; const record = getRegistryWorktree(env, params.id); if (!record || record.removedAt !== void 0) return; const preserved = (current, reason) => ({ id: current.id, branch: current.branch, path: current.path, reason }); const assertCurrent = () => { params.commitGuard?.(); const current = getRegistryWorktree(env, record.id); if (current && !belongsToSession(current, params.sessionKey)) throw new SessionWorktreeLifecycleError("Session worktree ownership changed; retry cleanup.", "owner-mismatch"); }; try { assertCurrent(); await serviceFor(params.env).remove({ id: record.id, reason: params.reason, commitGuard: assertCurrent }); } catch (error) { params.commitGuard?.(); const current = getRegistryWorktree(env, record.id); if (current && current.removedAt === void 0) { const reason = error instanceof SessionWorktreeLifecycleError && error.reason === "owner-mismatch" ? error.reason : classifyWorktreeRemovalError(error); getChildLogger({ subsystem: "session-worktree" }).warn("Session worktree preserved", { worktreeId: record.id, sessionKey: params.sessionKey, reason }); return preserved(current, reason); } } } /** Snapshot removal and restore never change conversation or session metadata themselves. */ async function synchronizeSessionWorktreeArchive(params) { const { entry, scope } = params; const id = entry.worktree?.id; if (!id) return; const assertCurrent = () => { params.commitGuard?.(); const current = loadSessionEntry(scope); if (current?.sessionId !== entry.sessionId || current?.lifecycleRevision !== entry.lifecycleRevision || current?.archivedAt !== entry.archivedAt || !isDeepStrictEqual(current?.worktree, entry.worktree)) throw new SessionWorktreeLifecycleError("Session changed while preparing its worktree; retry the request.", "session-changed"); const record = getRegistryWorktree(scope.env ?? process.env, id); if (record && !belongsToSession(record, scope.sessionKey)) throw new SessionWorktreeLifecycleError("Session worktree has a different owner; restore the correct binding before retrying.", "owner-mismatch"); }; assertCurrent(); if (params.archived) { const preserved = await removeSessionWorktree({ id, sessionKey: scope.sessionKey, reason: "session-archive", env: scope.env, commitGuard: assertCurrent }); if (preserved) throw new SessionWorktreeLifecycleError(`Session worktree was preserved (${preserved.reason}); resolve the cleanup condition and retry the archive.`, preserved.reason); } else { const record = getRegistryWorktree(scope.env ?? process.env, id); if (!record || record.removedAt !== void 0 && !record.snapshotRef) throw new SessionWorktreeLifecycleError("Session worktree snapshot is missing or expired. The conversation is preserved; start a new worktree task from the source repository to continue.", "restore-failed"); if (record.removedAt !== void 0) try { await serviceFor(scope.env).restore({ id, commitGuard: assertCurrent }); } catch (error) { assertCurrent(); if (error instanceof SessionWorktreeLifecycleError) throw error; if (!existsSync(record.repoRoot)) throw new SessionWorktreeLifecycleError("Session worktree source repository is missing. Restore the original repository and its snapshot refs, then retry; otherwise start a new worktree task. The conversation is preserved.", "restore-failed"); const snapshot = await runGit(record.repoRoot, [ "rev-parse", "--verify", "--end-of-options", `${record.snapshotRef}^{commit}` ]); assertCurrent(); if (snapshot.code !== 0) throw new SessionWorktreeLifecycleError("Session worktree snapshot is missing or unavailable. Restore the original repository snapshot, or start a new worktree task. The conversation is preserved.", "restore-failed"); throw new SessionWorktreeLifecycleError("Session worktree could not be restored. Free disk space if needed, check the source repository, then retry. The conversation and snapshot are preserved.", "restore-failed"); } } assertCurrent(); } /** Maintenance commits archive metadata first; its released writer lane must never retain Git work. */ async function cleanUpAutomaticallyArchivedWorktrees(scope, targets) { for (const target of targets) try { await runExclusiveSessionLifecycleMutation({ scope: target.storePath, identities: [target.sessionKey, target.entry.sessionId], run: async () => { const placements = createWorkerSessionPlacementStore({ database: openOpenClawStateDatabase({ env: scope.env }) }); const assertPlacementCurrent = prepareSessionWorkerPlacementMutationCheck({ context: { workerSessionPlacementService: placements }, sessionId: target.entry.sessionId }); await synchronizeSessionWorktreeArchive({ archived: true, entry: target.entry, scope: { ...scope, sessionKey: target.sessionKey, storePath: target.storePath }, commitGuard: () => { assertPlacementCurrent(); if (collectActiveSessionWorkAdmissionKeys({ storePath: target.storePath, store: { [target.sessionKey]: target.entry } })?.has(target.sessionKey)) throw new SessionWorktreeLifecycleError("Session worktree is still active; cleanup is deferred.", "busy"); } }); } }); } catch (error) { getChildLogger({ subsystem: "session-worktree" }).warn("Automatic archive worktree cleanup deferred", { worktreeId: target.entry.worktree?.id, reason: error instanceof SessionWorktreeLifecycleError ? error.reason : "cleanup-failed" }); } } //#endregion export { synchronizeSessionWorktreeArchive as i, cleanUpAutomaticallyArchivedWorktrees as n, removeSessionWorktree as r, SessionWorktreeLifecycleError as t };