UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

146 lines (145 loc) • 6.71 kB
import { a as WorktreeRemovalLockError } from "./errors-CgO78Orl.mjs"; import { c as resolveSessionStorePathCore } from "./paths-Bonddxfk.mjs"; import { _ as isSessionWorkAdmissionActive, g as isSessionLifecycleMutationActive, v as runExclusiveSessionLifecycleMutation } from "./session-lifecycle-admission-B5CJ-emz.mjs"; import { a as readResolvedSessionEntriesInWorker, s as resolveSessionEntryAccessTarget } from "./session-accessor.entry-xtqpYxWo.mjs"; import { a as prepareSessionWorkerPlacementMutationCheck } from "./session-placement-lifecycle-D16TB6AJ.mjs"; import { t as resolveSessionWorkerPlacementContext } from "./session-worker-placement-context-CvBn6ZZM.mjs"; import "./service-C8uzIOqd.mjs"; import "./removal-git-Bb5PZJ40.mjs"; import { AsyncLocalStorage } from "node:async_hooks"; import { isDeepStrictEqual } from "node:util"; import { scheduler } from "node:timers/promises"; //#region src/agents/worktrees/owner-protection.ts function createManagedWorktreeOwnerPolicy(cfg, now = Date.now) { const placementChecks = /* @__PURE__ */ new Map(); let preparedOwners = /* @__PURE__ */ new Map(); const cleanupOwner = new AsyncLocalStorage(); const state = (ownerKind, ownerId, prepared) => { if (ownerKind !== "session") return "other"; try { const target = prepared?.target ?? resolveSessionEntryAccessTarget({ cfg, sessionKey: ownerId }, { projection: "worktree" }); const entry = target.entry; const activityAt = Math.max(entry?.lastInteractionAt ?? 0, entry?.updatedAt ?? 0); if (entry?.archivedAt === void 0 && activityAt > 0 && now() - activityAt <= 6048e5) return "active"; const scope = resolveSessionStorePathCore(cfg.session?.store, { agentId: target.agentId }); const identities = [ target.canonicalKey, ownerId, entry?.sessionId ]; const cleanup = cleanupOwner.getStore(); const ownsCleanup = cleanup?.ownerId === ownerId; if (ownsCleanup && (cleanup.scope !== scope || cleanup.entry?.sessionId !== entry?.sessionId || cleanup.entry?.lifecycleRevision !== entry?.lifecycleRevision || cleanup.entry?.archivedAt !== entry?.archivedAt || !isDeepStrictEqual(cleanup.entry?.worktree, entry?.worktree))) return "active"; if (isSessionWorkAdmissionActive(scope, identities) || !(ownsCleanup && cleanup.lifecycleHeld) && isSessionLifecycleMutationActive(scope, identities)) return "active"; const placementCheckCache = prepared?.checks ?? placementChecks; let placementCheck = placementCheckCache.get(target.canonicalKey); if (placementCheck && placementCheck.sessionId !== entry?.sessionId) return "active"; if (!placementCheck) { const context = prepared?.context ?? resolveSessionWorkerPlacementContext(); const store = context.workerSessionPlacementService; if (!store?.listForReconcile) return "active"; const related = () => store.listForReconcile(target.canonicalKey).map((placement) => placement.sessionId).toSorted(); const initial = related(); const checks = [.../* @__PURE__ */ new Set([...initial, ...entry?.sessionId ? [entry.sessionId] : []])].map((sessionId) => prepareSessionWorkerPlacementMutationCheck({ context, sessionId })); const assertCurrent = () => { if (JSON.stringify(related()) !== JSON.stringify(initial)) throw new Error("worktree worker placement changed during cleanup"); for (const check of checks) check(); }; placementCheck = { sessionId: entry?.sessionId, assertCurrent }; placementCheckCache.set(target.canonicalKey, placementCheck); } placementCheck.assertCurrent(); return !entry || entry.archivedAt !== void 0 ? "retired" : "idle"; } catch { return "active"; } }; return { prepareOwners: async (records) => { preparedOwners = /* @__PURE__ */ new Map(); const ownerIds = [...new Set(records.flatMap((record) => record.removedAt === void 0 && record.ownerKind === "session" && record.ownerId ? [record.ownerId] : []))]; const states = /* @__PURE__ */ new Map(); try { const context = resolveSessionWorkerPlacementContext(); if (!context.workerSessionPlacementService?.listAsync) throw new Error("Worker placement census is unavailable"); const [targets, placements] = await Promise.all([readResolvedSessionEntriesInWorker({ cfg, sessionKeys: ownerIds }, "worktree"), context.workerSessionPlacementService.listAsync()]); preparedOwners = targets; const byId = new Map(placements.map((placement) => [placement.sessionId, placement])); const byKey = /* @__PURE__ */ new Map(); for (const placement of placements) if (placement.state !== "local" && placement.state !== "reclaimed") { const related = byKey.get(placement.sessionKey) ?? []; related.push(placement); byKey.set(placement.sessionKey, related); } const prepared = { checks: /* @__PURE__ */ new Map(), context: { ...context, workerSessionPlacementService: { getMany: () => byId, listForReconcile: (key) => byKey.get(key ?? "") ?? [] } } }; for (const [index, id] of ownerIds.entries()) { if (index % 32 === 0) await scheduler.yield(); const target = targets.get(id); states.set(id, target ? state("session", id, { ...prepared, target }) : "active"); } } catch {} return { readOwnerState: (kind, id) => kind === "session" ? states.get(id) ?? "active" : "other" }; }, readOwnerState: (kind, id) => state(kind, id), withOwnerCleanup: async (record, run, signal) => { if (record.ownerKind !== "session" || !record.ownerId) return await run((mutation) => mutation()); const ownerId = record.ownerId; const target = preparedOwners.get(ownerId) ?? (await readResolvedSessionEntriesInWorker({ cfg, sessionKeys: [ownerId] }, "worktree")).get(ownerId); signal?.throwIfAborted(); if (!target) throw new WorktreeRemovalLockError("busy", "worktree owner could not be read for cleanup"); const scope = resolveSessionStorePathCore(cfg.session?.store, { agentId: target.agentId }); const entry = target.entry; const owner = { ownerId, scope, entry: entry && { ...entry, worktree: entry.worktree && { ...entry.worktree } } }; const identities = [ target.canonicalKey, ownerId, owner.entry?.sessionId ]; return await cleanupOwner.run(owner, () => run((mutation, options) => runExclusiveSessionLifecycleMutation("worktree-cleanup", { scope, identities, signal: options?.settle ? void 0 : signal, run: () => cleanupOwner.run({ ...owner, lifecycleHeld: true }, mutation) }))); } }; } //#endregion export { createManagedWorktreeOwnerPolicy as t };