fix: evict stale workspace seed generations instead of freezing (#17357)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native runs in remote sandboxes upload the workspace as seed
archives, and a per-company cache keeps recent seeds so a later run with
the same workspace state can reuse them
> - The cache only ever added seeds; at its limits it stopped accepting
new ones and never removed old ones
> - On one instance it had filled to its 8 GiB cap with week-old seeds
and froze, so it held disk without helping new runs
> - This pull request expires seeds unused for 7 days and evicts the
least recently used seeds when a new one needs room
> - The benefit is a cache that stays useful within the same limits and
gives back space held by stale seeds

## Linked Issues or Issue Description

Bug report described in-PR (bug report template).

**What happened?**
On one instance, the company's seed cache
(`native-workspace-sync/generations/<company>/`) sat at its 8 GiB cap:
209 generations across 169 workspaces. 107 generations (8.2 GB) had not
been touched for 7 days or more. Because publishing stops at the limits,
new workspace states were no longer cached, while seeds for old states
and deleted workspaces stayed forever.

**Expected behavior**
Stale seeds are removed, and a new seed can replace the least recently
used ones when the cache is full.

**Steps to reproduce**
1. Run enough native sandbox runs across workspace states to reach a
seed cache limit.
2. New seeds are no longer published, and nothing is ever removed.

**Paperclip version or commit:** `master` at 837add0caa.
**Deployment mode:** any with native remote-sandbox runs.
**Agent adapter(s) involved:** native sandbox runtimes
(`packages/adapter-utils` seed cache).

## What Changed

`packages/adapter-utils/src/workspace-seed-cache.ts`:
- **Use tracking:** on a verified hit, `readWorkspaceSeedGeneration`
takes the company lock, confirms the receipt is still the one it
verified, and touches it, so its mtime records the last use. Eviction
holds the same lock from reading timestamps to removing, so a hit is
either recorded first and protected, or already gone and reported as a
miss. The reader accepts a `companyDirectory` option, which the sandbox
runtime now passes as it already does for publishing.
- **Idle expiry:** `publishWorkspaceSeedGeneration`, under the existing
company lock, first removes generations unused for
`WORKSPACE_SEED_CACHE_EVICTION.idleMs` (7 days).
- **LRU eviction:** if the new seed still does not fit, it evicts the
least recently used generations until it does: within the workspace for
workspace limits, across the company for company limits.
- **What is never evicted:**
- a generation used within `minIdleMs` (1 hour), since a run copies a
cache hit shortly after the lookup;
  - the generation being published;
- abandoned `.pending-*` directories, which still count toward the
limits as before.
- **Removal:** renames to `.evicted-<uuid>` before deleting, and removes
leftovers of an interrupted eviction. All empty workspace directories
are removed, so they no longer hold workspace slots.
- **Identity check:** publication rechecks the company directory's
identity under the lock before evicting, so a directory replaced during
verification is never touched.
- **Verification:** optional verification still runs only when the seed
can fit after eviction. A dry eviction plan decides this before
verification, and the real eviction happens under the lock.
- `publishWorkspaceSeedGeneration` accepts `eviction` and `now` options
for tests; the limits are unchanged.

No code comments were added.

## Verification

- `pnpm exec vitest run src/workspace-seed-cache.test.ts
src/workspace-checkpoint.test.ts` (in `packages/adapter-utils`): 58
passed. With `sandbox-managed-runtime.test.ts`: 168 passed. New cases:
- **Idle expiry:** generations unused for 7 days are removed on publish,
emptied workspaces are removed, newer ones stay.
  - **Use tracking:** a cache hit protects an otherwise idle generation.
- **Workspace LRU:** the least recently used generation of a full
workspace is evicted.
- **Company LRU:** company pressure evicts across workspaces, but never
a generation used within the last hour.
  - **Verification:** it runs only when eviction can make room.
- **Pending directories:** abandoned ones are never evicted, and
verification is skipped when they fill the cache.
  - **Leftovers:** remnants of an interrupted eviction are removed.
- **Concurrent read:** an eviction paused just before removing a seed
makes a concurrent read report a miss, never a hit whose files vanish.
- **Replaced directory:** a nonempty company directory swapped in during
verification keeps its seeds.
- **Empty workspace:** an abandoned empty workspace directory no longer
holds the only workspace slot.
- **Mutation checks:** removing any of these fails at least one case:
  - the touch on read;
  - idle expiry;
  - the recent-use guard;
  - the plan before verification;
  - leftover cleanup;
  - the pending-directory guard;
- the read lock (the concurrent-read case then returns a hit whose files
are gone);
  - the identity recheck;
  - removing only workspaces that had generations.

The existing `workspace-checkpoint` end-to-end test caught an earlier
version that ignored pending directories when deciding whether to
verify.
- `pnpm exec vitest run src` (in `packages/adapter-utils`): all seed
cache and checkpoint tests pass. Two runs on a heavily loaded machine
each had unrelated failures (`github-launcher.test.ts`,
`acpx-engine/execute.test.ts`). One run also failed a seed cache test
once; I could not reproduce that in 21 further runs, including 6 in
parallel.
- `pnpm exec vitest run native-workspace-sync` (in `server/`): 23
passed.
- `pnpm run typecheck` (in `server/`, after building
`packages/adapter-utils`): pass.
- Not run: server build and boot smoke.

## Risks

- **First publish after deploy:** on an instance with a full cache of
old seeds, it removes them, about 8 GB on the instance above. The
removal is a rename plus a recursive delete under the company lock.
- **Eviction during use:** a run that looked up a seed more than an hour
earlier and has not yet copied it could see it evicted. It would fail
that preparation and retry. In practice the copy follows the lookup
within the same preparation.
- **No new settings:** the 7-day and 1-hour thresholds are constants
exported as `WORKSPACE_SEED_CACHE_EVICTION`.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), 1M context, through Claude Code
with tool use (shell, file edits, tests, read-only measurements on a
live instance).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-23 14:05:34 -07:00
1 parent b6e690044a
commit 188bb14ba0
3 files changed
+235 -5

No files matched your search

@@ -1419,7 +1419,8 @@ export async function prepareSandboxManagedRuntime(input: {
const seedGeneration = input.workspaceSeedCacheDirectory && baselineSnapshot && workspaceInboundMode !== "durable_seed"
? await runStepSpan("workspace.seed_generation", async () => workspaceSeedGeneration(baselineSnapshot!, gitSnapshot, repositories)) : null;
const cachedSeed = seedGeneration
? await runStepSpan("workspace.seed_lookup", () => readWorkspaceSeedGeneration(input.workspaceSeedCacheDirectory!, seedGeneration)) : null;
? await runStepSpan("workspace.seed_lookup", () => readWorkspaceSeedGeneration(input.workspaceSeedCacheDirectory!, seedGeneration,
{ companyDirectory: input.workspaceSeedCacheCompanyDirectory })) : null;
const replaySeed = workspaceInboundMode === "durable_seed" ? input.workspaceDurableSeed : cachedSeed;
// Every inbound staging step delegates to the provider through `client.syncIn`:
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import nodeFs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { publishWorkspaceSeedGeneration, readWorkspaceSeedGeneration } from "./workspace-seed-cache.js";
function deferred<T>() {
@@ -163,3 +163,133 @@ it("rejects a company directory replaced during verification", async () => {
expect(await fs.readdir(replacement)).toEqual([]);
expect(await fs.readFile(f.archive, "utf8")).toBe("run recovery bytes");
});
describe("eviction", () => {
const DAY = 24 * 60 * 60 * 1000;
async function lastUsed(directory: string, ms: number) {
await fs.utimes(path.join(directory, "receipt.json"), new Date(ms), new Date(ms));
}
it("expires generations unused for the idle period when publishing, and removes emptied workspaces", async () => {
const f = await fixture(); const now = Date.now(); const options = { companyDirectory: f.company, now: () => now };
const other = path.join(f.company, "other");
expect(await publishWorkspaceSeedGeneration(other, generation(1), f.seed, options)).toBe(true);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(true);
await lastUsed(path.join(other, generation(1)), now - 8 * DAY);
await lastUsed(path.join(f.workspace, generation(2)), now - 6 * DAY);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(3), f.seed, options)).toBe(true);
expect((await fs.readdir(f.company)).sort()).toEqual(["workspace"]);
expect((await fs.readdir(f.workspace)).sort()).toEqual([generation(2), generation(3)]);
expect(await fs.readFile(f.archive, "utf8")).toBe("run recovery bytes");
});
it("counts a cache hit as use, so a recently read generation is not expired", async () => {
const f = await fixture(); const options = { companyDirectory: f.company };
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, options)).toBe(true);
await lastUsed(path.join(f.workspace, generation(1)), Date.now() - 8 * DAY);
expect(await readWorkspaceSeedGeneration(f.workspace, generation(1))).not.toBeNull();
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(true);
expect((await fs.readdir(f.workspace)).sort()).toEqual([generation(1), generation(2)]);
});
it("evicts the least recently used generation of the workspace when it is full", async () => {
const f = await fixture(); const now = Date.now();
const options = { companyDirectory: f.company, limits: { workspaceGenerations: 2 }, now: () => now };
for (const index of [1, 2]) expect(await publishWorkspaceSeedGeneration(f.workspace, generation(index), f.seed, options)).toBe(true);
await lastUsed(path.join(f.workspace, generation(1)), now - 3 * 60 * 60 * 1000);
await lastUsed(path.join(f.workspace, generation(2)), now - 2 * 60 * 60 * 1000);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(3), f.seed, options)).toBe(true);
expect((await fs.readdir(f.workspace)).sort()).toEqual([generation(2), generation(3)]);
});
it("evicts across workspaces under company pressure but never a generation used within the last hour", async () => {
const f = await fixture(); const now = Date.now();
const options = { companyDirectory: f.company, limits: { companyGenerations: 2 }, now: () => now };
const other = path.join(f.company, "other");
expect(await publishWorkspaceSeedGeneration(other, generation(1), f.seed, options)).toBe(true);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(true);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(3), f.seed, options)).toBe(false);
await lastUsed(path.join(other, generation(1)), now - 2 * 60 * 60 * 1000);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(3), f.seed, options)).toBe(true);
expect((await fs.readdir(f.company)).sort()).toEqual(["workspace"]);
expect((await fs.readdir(f.workspace)).sort()).toEqual([generation(2), generation(3)]);
});
it("runs optional verification only when eviction can make room", async () => {
const f = await fixture(); const now = Date.now(); const verify = vi.fn(async () => true);
const options = { companyDirectory: f.company, limits: { workspaceGenerations: 1 }, now: () => now, verify };
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, options)).toBe(true);
verify.mockClear();
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(false);
expect(verify).not.toHaveBeenCalled();
await lastUsed(path.join(f.workspace, generation(1)), now - 2 * 60 * 60 * 1000);
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(true);
expect(verify).toHaveBeenCalledTimes(1);
expect(await fs.readdir(f.workspace)).toEqual([generation(2)]);
});
it("never evicts abandoned pending directories and skips verification when only they fill the cache", async () => {
const f = await fixture(); const now = Date.now(); const verify = vi.fn(async () => true);
await fs.mkdir(path.join(f.workspace, ".pending-old"), { recursive: true });
await fs.utimes(path.join(f.workspace, ".pending-old"), new Date(now - 30 * DAY), new Date(now - 30 * DAY));
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, {
companyDirectory: f.company, limits: { workspaceGenerations: 1 }, now: () => now, verify,
})).toBe(false);
expect(verify).not.toHaveBeenCalled();
expect(await fs.readdir(f.workspace)).toEqual([".pending-old"]);
});
it("never returns a cache hit that a concurrent eviction removes", async () => {
const f = await fixture(); const now = Date.now();
const options = { companyDirectory: f.company, limits: { workspaceGenerations: 1 }, now: () => now };
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, options)).toBe(true);
await lastUsed(path.join(f.workspace, generation(1)), now - 2 * 60 * 60 * 1000);
const paused = deferred<void>(), release = deferred<void>();
const rename = fs.rename.bind(fs);
vi.spyOn(fs, "rename").mockImplementation(async (from, to) => {
if (String(to).includes(".evicted-")) { paused.resolve(); await release.promise; }
return rename(from, to);
});
const publishing = publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options);
await paused.promise;
const reading = readWorkspaceSeedGeneration(f.workspace, generation(1), { companyDirectory: f.company });
await new Promise(resolve => setTimeout(resolve, 200));
release.resolve();
const [hit, published] = await Promise.all([reading, publishing]);
expect(published).toBe(true);
if (hit) expect(await fs.readFile(hit.workspaceArchivePath, "utf8")).toBe("run recovery bytes");
else await expect(fs.access(path.join(f.workspace, generation(1)))).rejects.toMatchObject({ code: "ENOENT" });
});
it("does not evict from a company directory replaced during verification", async () => {
const f = await fixture(); const now = Date.now();
const options = { companyDirectory: f.company, limits: { workspaceGenerations: 1 }, now: () => now };
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, options)).toBe(true);
await lastUsed(path.join(f.workspace, generation(1)), now - 8 * DAY);
const replacement = path.join(f.root, "other-company");
await fs.cp(f.company, replacement, { recursive: true, preserveTimestamps: true });
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, { ...options, verify: async () => {
await fs.rename(f.company, `${f.company}-retained`);
await fs.symlink(replacement, f.company);
return true;
} })).toBe(false);
expect(await fs.readdir(path.join(replacement, "workspace"))).toEqual([generation(1)]);
});
it("removes empty workspace directories so they do not hold workspace slots", async () => {
const f = await fixture(); await fs.mkdir(path.join(f.company, "abandoned"), { recursive: true });
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, {
companyDirectory: f.company, limits: { companyWorkspaces: 1 },
})).toBe(true);
expect(await fs.readdir(f.company)).toEqual(["workspace"]);
});
it("removes leftovers of an interrupted eviction", async () => {
const f = await fixture(); const options = { companyDirectory: f.company };
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(1), f.seed, options)).toBe(true);
await fs.mkdir(path.join(f.workspace, ".evicted-leftover"));
await fs.writeFile(path.join(f.workspace, ".evicted-leftover", "workspace.tar"), "stale");
expect(await publishWorkspaceSeedGeneration(f.workspace, generation(2), f.seed, options)).toBe(true);
expect((await fs.readdir(f.workspace)).sort()).toEqual([generation(1), generation(2)]);
});
});
@@ -20,7 +20,8 @@ async function digest(file: string): Promise<string> {
return hash.digest("hex");
}
export async function readWorkspaceSeedGeneration(root: string, generation: string): Promise<WorkspaceDurableSeedPaths | null> {
export async function readWorkspaceSeedGeneration(root: string, generation: string,
options: { companyDirectory?: string } = {}): Promise<WorkspaceDurableSeedPaths | null> {
const directory = path.join(root, generation);
try {
for (const item of [root, directory]) { const stat = await fs.lstat(item); if (!stat.isDirectory() || stat.isSymbolicLink()) return null; }
@@ -37,6 +38,14 @@ export async function readWorkspaceSeedGeneration(root: string, generation: stri
const stat = await fs.lstat(file);
if (!stat.isFile() || stat.isSymbolicLink() || await digest(file) !== expected) return null;
}
const used = await withDirectoryMergeLock(path.resolve(options.companyDirectory ?? root), async () => {
const current = await fs.lstat(receipt).catch(() => null);
if (!current || current.ino !== receiptStat.ino || current.dev !== receiptStat.dev) return false;
const usedAt = new Date();
await fs.utimes(receipt, usedAt, usedAt);
return true;
});
if (!used) return null;
return { workspaceArchivePath, workspaceArchiveSha256: metadata.workspaceSha256,
gitArchivePath, gitArchiveSha256: metadata.gitSha256 };
} catch { return null; }
@@ -50,6 +59,86 @@ export const WORKSPACE_SEED_CACHE_LIMITS = Object.freeze({
});
type SeedCacheLimits = { [Key in keyof typeof WORKSPACE_SEED_CACHE_LIMITS]: number };
export const WORKSPACE_SEED_CACHE_EVICTION = Object.freeze({
idleMs: 7 * 24 * 60 * 60 * 1000,
minIdleMs: 60 * 60 * 1000,
});
type SeedCacheEviction = { [Key in keyof typeof WORKSPACE_SEED_CACHE_EVICTION]: number };
type CachedGeneration = { directory: string; workspace: string; bytes: number; usedAt: number; evictable: boolean };
async function removeCacheEntry(directory: string) {
const retired = path.join(path.dirname(directory), `.evicted-${randomUUID()}`);
await fs.rename(directory, retired);
await fs.rm(retired, { recursive: true, force: true });
}
async function cachedGenerations(company: string, root: string, cleanup: boolean): Promise<{ workspaces: string[]; generations: CachedGeneration[] }> {
const workspaces = root === company ? [company]
: (await fs.readdir(company)).map(name => path.join(company, name));
const generations: CachedGeneration[] = [];
for (const workspace of workspaces) {
const workspaceStat = await fs.lstat(workspace).catch(() => null);
if (!workspaceStat?.isDirectory() || workspaceStat.isSymbolicLink()) continue;
for (const name of await fs.readdir(workspace)) {
const directory = path.join(workspace, name);
if (name.startsWith(".evicted-")) { if (cleanup) await fs.rm(directory, { recursive: true, force: true }); continue; }
const stat = await fs.lstat(directory).catch(() => null);
if (!stat) continue;
const evictable = /^[a-f0-9]{64}$/.test(name) && stat.isDirectory() && !stat.isSymbolicLink();
if (!stat.isDirectory() || stat.isSymbolicLink()) { generations.push({ directory, workspace, bytes: stat.size, usedAt: stat.mtimeMs, evictable }); continue; }
let bytes = 0, usedAt = stat.mtimeMs;
for (const child of await fs.readdir(directory)) {
const file = await fs.lstat(path.join(directory, child)).catch(() => null);
if (!file?.isFile()) continue;
bytes += file.size;
if (child === "receipt.json") usedAt = file.mtimeMs;
}
generations.push({ directory, workspace, bytes, usedAt, evictable });
}
}
return { workspaces, generations };
}
async function evictSeedGenerations(company: string, root: string, input: {
generation: string; incomingBytes: number; limits: SeedCacheLimits; eviction: SeedCacheEviction; now: number; apply: boolean;
}): Promise<boolean> {
const { workspaces: workspaceDirectories, generations } = await cachedGenerations(company, root, input.apply);
const remove = async (directory: string) => { if (input.apply) await removeCacheEntry(directory); };
const target = path.join(root, input.generation);
const live: CachedGeneration[] = [];
for (const entry of generations) {
if (entry.evictable && entry.directory !== target && entry.usedAt < input.now - input.eviction.idleMs) await remove(entry.directory);
else live.push(entry);
}
const over = () => {
const inWorkspace = live.filter(entry => entry.workspace === root);
const workspaces = new Set([...live.map(entry => entry.workspace), root]).size;
return {
workspace: inWorkspace.length + 1 > input.limits.workspaceGenerations
|| inWorkspace.reduce((sum, entry) => sum + entry.bytes, 0) + input.incomingBytes > input.limits.workspaceBytes,
company: live.length + 1 > input.limits.companyGenerations
|| live.reduce((sum, entry) => sum + entry.bytes, 0) + input.incomingBytes > input.limits.companyBytes
|| workspaces > input.limits.companyWorkspaces,
};
};
for (let pressure = over(); pressure.workspace || pressure.company; pressure = over()) {
const victim = live
.filter(entry => entry.evictable && entry.directory !== target && entry.usedAt < input.now - input.eviction.minIdleMs
&& (pressure.company || entry.workspace === root))
.sort((a, b) => a.usedAt - b.usedAt)[0];
if (!victim) return false;
await remove(victim.directory);
live.splice(live.indexOf(victim), 1);
}
if (input.apply && root !== company) {
for (const workspace of workspaceDirectories) {
if (workspace !== root && !live.some(entry => entry.workspace === workspace)) await fs.rmdir(workspace).catch(() => undefined);
}
}
return true;
}
async function cacheUsage(root: string, limits: SeedCacheLimits): Promise<{ generations: number; bytes: number } | null> {
let generations = 0, bytes = 0;
for await (const entry of await fs.opendir(root)) {
@@ -71,9 +160,11 @@ async function cacheUsage(root: string, limits: SeedCacheLimits): Promise<{ gene
export async function publishWorkspaceSeedGeneration(
root: string, generation: string, seed: WorkspaceDurableSeedPaths,
options: { companyDirectory?: string; limits?: Partial<SeedCacheLimits>; verify?: () => Promise<boolean> } = {},
options: { companyDirectory?: string; limits?: Partial<SeedCacheLimits>; eviction?: Partial<SeedCacheEviction>;
now?: () => number; verify?: () => Promise<boolean> } = {},
): Promise<boolean> {
const limits = { ...WORKSPACE_SEED_CACHE_LIMITS, ...options.limits };
const eviction = { ...WORKSPACE_SEED_CACHE_EVICTION, ...options.eviction };
if (!/^[a-f0-9]{64}$/.test(generation)
|| Object.values(limits).some(value => !Number.isSafeInteger(value) || value < 0)) return false;
root = path.resolve(root);
@@ -117,9 +208,17 @@ export async function publishWorkspaceSeedGeneration(
return "available";
};
const initialCapacity = await capacity();
if (initialCapacity !== "available") return initialCapacity === "existing";
if (initialCapacity === "existing") return true;
let incomingBytes = 4096;
for (const archive of [seed.workspaceArchivePath, seed.gitArchivePath]) if (archive) incomingBytes += (await fs.lstat(archive)).size;
const plan = { generation, incomingBytes, limits, eviction, now: options.now?.() ?? Date.now() };
if (initialCapacity === false && !await evictSeedGenerations(company, root, { ...plan, apply: false })) return false;
if (options.verify && !await options.verify()) return false;
return await withDirectoryMergeLock(company, async () => {
const lockedCompany = await fs.lstat(company);
if (!lockedCompany.isDirectory() || lockedCompany.isSymbolicLink()
|| lockedCompany.dev !== companyStat.dev || lockedCompany.ino !== companyStat.ino) return false;
await evictSeedGenerations(company, root, { ...plan, apply: true });
const currentCapacity = await capacity();
if (currentCapacity !== "available") return currentCapacity === "existing";
await fs.mkdir(root, { recursive: true, mode: 0o700 });