fix(cli): preserve live task-isolation sandboxes on worktree clear
`omp worktree clear` (without `--all`) removed every task-isolation dir under the worktree base, including sandboxes owned by subagents running right now, and the "no live task owns it" reason was asserted from the mere presence of the `m` mount dir with no ownership check. `ensureIsolation` now stamps each sandbox base dir with a pid-bearing ownership marker before the backend materialises `m`, and the worktree scanner classifies a sandbox as live while its owning process is alive, so `clear` reclaims only crashed leftovers. Fixes #6761
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Ownership marker for task-isolation sandboxes under `~/.omp/wt/`.
|
||||
*
|
||||
* Each isolation base dir (`ensureIsolation` in {@link ./worktree}) holds a
|
||||
* compact `m` mount plus this marker file naming the omp process that created
|
||||
* it. `omp worktree clear` consults the marker so it can distinguish a live
|
||||
* subagent's sandbox from a crashed run's leftover instead of deleting both.
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
|
||||
/** Marker file written into a task-isolation base dir identifying its owner. */
|
||||
export const ISOLATION_OWNER_FILE = ".omp-isolation-owner.json";
|
||||
|
||||
/** Recorded owner of a task-isolation sandbox. */
|
||||
export interface IsolationOwner {
|
||||
/** PID of the omp process that created and owns the sandbox. */
|
||||
pid: number;
|
||||
/** Task id the sandbox was materialised for. */
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the current process as owner of the sandbox rooted at `baseDir`.
|
||||
*
|
||||
* Written before the isolation backend materialises `m` so a concurrent
|
||||
* `omp worktree clear` never sees an owner-less sandbox mid-creation.
|
||||
*/
|
||||
export async function writeIsolationOwner(baseDir: string, id: string): Promise<void> {
|
||||
const owner: IsolationOwner = { pid: process.pid, id };
|
||||
await Bun.write(path.join(baseDir, ISOLATION_OWNER_FILE), JSON.stringify(owner));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a live omp process still owns the sandbox at `baseDir`.
|
||||
*
|
||||
* A missing or malformed marker means no verifiable owner — a crashed run or a
|
||||
* sandbox from before markers existed, both safe to reclaim. `process.kill(pid,
|
||||
* 0)` can fail with `EPERM` even when the process is alive, so only an explicit
|
||||
* `ESRCH` ("no such process") counts as dead; any other error is treated as
|
||||
* alive to avoid deleting a sandbox that is actually in use.
|
||||
*/
|
||||
export async function hasLiveIsolationOwner(baseDir: string): Promise<boolean> {
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = await Bun.file(path.join(baseDir, ISOLATION_OWNER_FILE)).json();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (typeof decoded !== "object" || decoded === null || !("pid" in decoded)) return false;
|
||||
const pid = decoded.pid;
|
||||
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return (err as NodeJS.ErrnoException).code !== "ESRCH";
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as natives from "@oh-my-pi/pi-natives";
|
||||
import { getWorktreeDir, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
||||
import * as git from "../utils/git";
|
||||
import * as jj from "../utils/jj";
|
||||
import { writeIsolationOwner } from "./isolation-ownership";
|
||||
import { mapWithConcurrencyLimit } from "./parallel";
|
||||
|
||||
const { IsoBackendKind } = natives;
|
||||
@@ -434,6 +435,13 @@ export async function ensureIsolation(
|
||||
|
||||
for (const candidate of candidates) {
|
||||
await fs.rm(baseDir, { recursive: true, force: true });
|
||||
// Claim ownership before the backend materialises `m`. Backends only
|
||||
// create/replace `mergedDir` (and overlay upper/work), never the base
|
||||
// dir, so the marker survives `isoStart` — and a concurrent
|
||||
// `omp worktree clear` never sees this sandbox without a live owner,
|
||||
// even while a large clone is still in progress.
|
||||
await fs.mkdir(baseDir, { recursive: true });
|
||||
await writeIsolationOwner(baseDir, id);
|
||||
try {
|
||||
await natives.isoStart(candidate, repoRoot, mergedDir);
|
||||
// Sever the isolation's git metadata from the source checkout. Copy
|
||||
|
||||
Reference in New Issue
Block a user