openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
146 lines (145 loc) • 6.71 kB
JavaScript
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 };