UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,475 lines • 59.5 kB
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, stat, unlink, writeFile, } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { createInterface } from "node:readline";
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritEnvPolicyProblem, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
import { physicalPathOfAsync } from "../../core/physical-path.js";
import { isUncSpelling } from "../../tools/fs/safety.js";
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
import { BackgroundShellError } from "../../core/background-shell.js";
import { SchedulerError } from "../../core/scheduler.js";
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
function resolvePath(cwd, path) {
    return isAbsolute(path) ? path : resolve(cwd, path);
}
export function resolveExecTimeoutMs(timeoutSeconds) {
    if (typeof timeoutSeconds !== "number" ||
        !Number.isFinite(timeoutSeconds) ||
        timeoutSeconds <= 0) {
        return undefined;
    }
    const milliseconds = Math.floor(timeoutSeconds * 1000);
    if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
        return 1;
    }
    return Math.min(milliseconds, MAX_TIMER_TIMEOUT_MS);
}
function fileKindFromStats(stats) {
    if (stats.isFile()) {
        return "file";
    }
    if (stats.isDirectory()) {
        return "directory";
    }
    if (stats.isSymbolicLink()) {
        return "symlink";
    }
    return undefined;
}
const HAS_POSIX_MODE_FACE = process.platform !== "win32";
export function anchorAtLinkDir(linkDir, target, separator) {
    const endsInSeparator = linkDir.endsWith(separator) || (separator === "\\" && linkDir.endsWith("/"));
    return endsInSeparator ? `${linkDir}${target}` : `${linkDir}${separator}${target}`;
}
const SYMLINK_CHAIN_HOP_BOUND = 64;
function fileInfoFromStats(path, stats) {
    const kind = fileKindFromStats(stats);
    if (!kind) {
        return err(new FileError("invalid", "Unsupported file type", path));
    }
    return ok({
        name: (() => {
            const t = path.replace(/[\\/]+$/, "");
            return t.slice(Math.max(t.lastIndexOf("/"), t.lastIndexOf("\\")) + 1) || path;
        })(),
        path,
        kind,
        size: stats.size,
        mtimeMs: stats.mtimeMs,
        ...(HAS_POSIX_MODE_FACE && typeof stats.mode === "number" ? { mode: stats.mode & 0o7777 } : {}),
    });
}
function isNodeError(error) {
    return error instanceof Error && "code" in error;
}
function toFileError(error, path) {
    if (error instanceof FileError) {
        return error;
    }
    const cause = toError(error);
    if (isNodeError(error)) {
        const message = error.message;
        switch (error.code) {
            case "ABORT_ERR":
                return new FileError("aborted", message, path, cause);
            case "ENOENT":
                return new FileError("not_found", message, path, cause);
            case "EACCES":
            case "EPERM":
                return new FileError("permission_denied", message, path, cause);
            case "ENOTDIR":
                return new FileError("not_directory", message, path, cause);
            case "EISDIR":
                return new FileError("is_directory", message, path, cause);
            case "EINVAL":
                return new FileError("invalid", message, path, cause);
            default:
                break;
        }
    }
    return new FileError("unknown", cause.message, path, cause);
}
function abortResult(signal, path) {
    return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined;
}
function isFsSupportError(error) {
    const code = isNodeError(error) ? error.code : undefined;
    return code === "EINVAL" || code === "ENOTSUP" || code === "EPERM" || code === "ENOSYS";
}
const RENAME_FALLBACK_CODES = new Set(["EXDEV", "EPERM", "EEXIST", "EBUSY"]);
async function pathExists(path) {
    try {
        await access(path, constants.F_OK);
        return true;
    }
    catch {
        return false;
    }
}
async function runCommand(command, args, timeoutMs) {
    return await new Promise((resolveLocal) => {
        let stdout = "";
        let child;
        try {
            child = spawn(command, args, {
                stdio: ["ignore", "pipe", "ignore"],
                windowsHide: true,
                env: scrubSecretEnv(process.env),
            });
        }
        catch {
            resolveLocal({ stdout: "", status: null });
            return;
        }
        const timeout = setTimeout(() => {
            if (child.pid) {
                killProcessTree(child.pid, { force: true });
            }
        }, timeoutMs);
        child.stdout?.setEncoding("utf8");
        child.stdout?.on("data", (chunk) => {
            stdout += chunk;
        });
        child.stdout?.on("error", () => { });
        child.on("error", () => {
            clearTimeout(timeout);
            resolveLocal({ stdout: "", status: null });
        });
        child.on("close", (status) => {
            clearTimeout(timeout);
            resolveLocal({ stdout, status });
        });
    });
}
export function isWslBashLauncher(p) {
    return /[/\\](system32|windowsapps)[/\\]bash\.exe$/i.test(p);
}
async function findBashOnPath() {
    const result = process.platform === "win32"
        ? await runCommand("where", ["bash.exe"], 5000)
        : await runCommand("which", ["bash"], 5000);
    if (result.status !== 0 || !result.stdout) {
        return null;
    }
    const lines = result.stdout.trim().split(/\r?\n/);
    const firstMatch = process.platform === "win32" ? lines.find((l) => !isWslBashLauncher(l)) : lines[0];
    return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null;
}
export async function getShellConfig(customShellPath) {
    if (customShellPath) {
        if (await pathExists(customShellPath)) {
            return ok({ shell: customShellPath, args: ["-c"] });
        }
        return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`));
    }
    if (process.platform === "win32") {
        const candidates = [];
        const programFiles = process.env.ProgramFiles;
        if (programFiles) {
            candidates.push(`${programFiles}\\Git\\bin\\bash.exe`);
        }
        const programFilesX86 = process.env["ProgramFiles(x86)"];
        if (programFilesX86) {
            candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
        }
        for (const candidate of candidates) {
            if (await pathExists(candidate)) {
                return ok({ shell: candidate, args: ["-c"] });
            }
        }
        const bashOnPath = await findBashOnPath();
        if (bashOnPath) {
            return ok({ shell: bashOnPath, args: ["-c"] });
        }
        return err(new ExecutionError("shell_unavailable", "No bash shell found"));
    }
    if (await pathExists("/bin/bash")) {
        return ok({ shell: "/bin/bash", args: ["-c"] });
    }
    const bashOnPath = await findBashOnPath();
    if (bashOnPath) {
        return ok({ shell: bashOnPath, args: ["-c"] });
    }
    return ok({ shell: "sh", args: ["-c"] });
}
const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
let secretScrubSeatDefectAnnounced = false;
export function __resetSecretScrubAnnouncement() {
    secretScrubSeatDefectAnnounced = false;
    __resetSecretEnvScrubNoticeAnnouncement();
}
function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub, onNotice) {
    const scrubbed = [];
    const inherited = inheritedHostEnv(inheritEnv, scrubbed);
    const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
    if (findings.length > 0) {
        if (typeof onScrub === "function") {
            try {
                const r = onScrub(findings);
                if (typeof r?.then === "function") {
                    r.then(undefined, () => {
                    });
                }
            }
            catch {
            }
        }
        else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
            secretScrubSeatDefectAnnounced = true;
            try {
                console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
            }
            catch {
            }
        }
        if (!(typeof onScrub === "function" && onNotice === undefined)) {
            announceSecretEnvScrub(onNotice, findings);
        }
    }
    return {
        ...inherited,
        ...baseEnv,
        ...extraEnv,
    };
}
const EXIT_SETTLE_DRAIN_MS = 500;
const EXEC_SPOOL_PUMP_MS = 200;
const EXEC_SPOOL_ROTATE_BYTES = 8 * 1024 * 1024;
const EXEC_SPOOL_FINAL_TAIL_BYTES = 8 * 1024 * 1024;
const EXEC_SPOOL_HARD_CAP_BYTES = 16 * 1024 * 1024;
export function decideSpoolReclaim(args) {
    const rotate = args.rotateBytes ?? EXEC_SPOOL_ROTATE_BYTES;
    const hardCap = args.hardCapBytes ?? EXEC_SPOOL_HARD_CAP_BYTES;
    if (args.writerLive)
        return { reclaim: false };
    if (args.cursor < rotate)
        return { reclaim: false };
    if (args.size === args.cursor)
        return { reclaim: true, skippedBytes: 0 };
    if (args.size > args.cursor && args.size >= hardCap)
        return { reclaim: true, skippedBytes: args.size - args.cursor };
    return { reclaim: false };
}
const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
export function openSpoolPair(base) {
    const outPath = `${base}.out.log`;
    const errPath = `${base}.err.log`;
    const fdOut = openSync(outPath, "ax", 0o600);
    let fdErr;
    try {
        fdErr = openSync(errPath, "ax", 0o600);
    }
    catch (e) {
        try {
            closeSync(fdOut);
        }
        catch {
        }
        throw e;
    }
    return { outPath, errPath, fds: [fdOut, fdErr] };
}
export class NodeExecutionEnv {
    cwd;
    get homeDir() {
        return getShellEnv(this.inheritEnv, this.shellEnv, undefined, () => { }).HOME;
    }
    shellPath;
    shellEnv;
    inheritEnv;
    scheduler;
    bgShells = new Map();
    bgCounter = 0;
    retainBackgroundProcesses;
    onSecretEnvScrub;
    onNotice;
    bgSpoolDir;
    backgroundCapabilities;
    constructor(options) {
        this.cwd = options.cwd;
        this.shellPath = options.shellPath;
        this.shellEnv = options.shellEnv;
        const inheritEnvProblem = inheritEnvPolicyProblem(options.inheritEnv);
        if (inheritEnvProblem !== undefined)
            throw new Error(`NodeExecutionEnv: ${inheritEnvProblem}`);
        this.inheritEnv = Array.isArray(options.inheritEnv) ? [...options.inheritEnv] : (options.inheritEnv ?? "scrub");
        this.scheduler = options.scheduler;
        this.retainBackgroundProcesses = options.retainBackgroundProcesses ?? false;
        this.onSecretEnvScrub = options.onSecretEnvScrub;
        this.onNotice = options.onNotice;
        this.backgroundCapabilities = {
            supported: true,
            maxConcurrent: 8,
            defaultBgTimeoutSec: 60 * 60,
            maxBgTimeoutSec: 2 * 60 * 60,
            supportsDetach: true,
            retainBackgroundProcesses: this.retainBackgroundProcesses,
        };
    }
    get schedulerCapabilities() {
        return (this.scheduler?.schedulerCapabilities ?? {
            supported: false,
            maxScheduledPerScope: 0,
            minDelaySec: 0,
            minCronIntervalSec: 0,
            maxScheduledHorizonSec: 0,
        });
    }
    async schedule(intent, ctx) {
        if (!this.scheduler)
            return err(new SchedulerError("unsupported", "No scheduler backend wired on this environment."));
        return this.scheduler.schedule(intent, ctx);
    }
    async cancel(id, ctx) {
        if (!this.scheduler)
            return err(new SchedulerError("unsupported", "No scheduler backend wired on this environment."));
        return this.scheduler.cancel(id, ctx);
    }
    async list(ctx) {
        if (!this.scheduler)
            return err(new SchedulerError("unsupported", "No scheduler backend wired on this environment."));
        return this.scheduler.list(ctx);
    }
    async absolutePath(path) {
        return ok(resolvePath(this.cwd, path));
    }
    async joinPath(parts) {
        return ok(join(...parts));
    }
    async exec(command, options) {
        if (options?.abortSignal?.aborted) {
            return err(new ExecutionError("aborted", "aborted"));
        }
        const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;
        const shellConfig = await getShellConfig(this.shellPath);
        if (!shellConfig.ok) {
            return shellConfig;
        }
        return await new Promise((resolvePromise) => {
            const stdoutTail = new RollingTailBuffer();
            const stderrTail = new RollingTailBuffer();
            let settled = false;
            let timedOut = false;
            let terminationInFlight = false;
            let callbackError;
            let child;
            const timeoutRef = {};
            const forceSettleRef = {};
            const spoolPumpRef = {};
            const execSpoolRef = {};
            const adoptOnTimeoutRef = {};
            const pumpSpoolRef = {};
            const drainSpoolRef = {};
            const writerLive = () => !settled && child !== undefined && child.exitCode === null && child.signalCode === null;
            const armForceSettle = (result) => {
                if (forceSettleRef.current || settled)
                    return;
                forceSettleRef.current = setTimeout(() => {
                    try {
                        child?.stdout?.destroy();
                        child?.stderr?.destroy();
                    }
                    catch {
                    }
                    settle(result);
                }, EXEC_FORCE_SETTLE_GRACE_MS);
                forceSettleRef.current.unref?.();
            };
            const partialOutput = () => {
                drainSpoolRef.current?.();
                const so = stdoutTail.result();
                const se = stderrTail.result();
                return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
            };
            const onAbort = () => {
                terminationInFlight = true;
                if (child?.pid) {
                    const abortTarget = child;
                    killProcessTree(child.pid, { stillRunning: () => abortTarget.exitCode === null && abortTarget.signalCode === null });
                }
                if (callbackError) {
                    const p = partialOutput();
                    callbackError.partialStdout = p.stdout;
                    callbackError.partialStderr = p.stderr;
                    armForceSettle(err(callbackError));
                    return;
                }
                armForceSettle(err(new ExecutionError("aborted", "aborted", undefined, partialOutput())));
            };
            const settle = (result) => {
                if (timeoutRef.current) {
                    clearTimeout(timeoutRef.current);
                }
                if (forceSettleRef.current) {
                    clearTimeout(forceSettleRef.current);
                }
                if (options?.abortSignal) {
                    options.abortSignal.removeEventListener("abort", onAbort);
                }
                if (settled) {
                    return;
                }
                settled = true;
                if (spoolPumpRef.current)
                    clearInterval(spoolPumpRef.current);
                if (execSpoolRef.current && !(result.ok && result.value.detached !== undefined)) {
                    for (const path of [execSpoolRef.current.outPath, execSpoolRef.current.errPath]) {
                        try {
                            unlinkSync(path);
                        }
                        catch {
                        }
                    }
                    execSpoolRef.current = undefined;
                }
                resolvePromise(result);
            };
            let execSpoolFds;
            if (process.platform !== "win32") {
                try {
                    if (!this.bgSpoolDir)
                        this.bgSpoolDir = mkdtempSync(join(tmpdir(), "sema-bg-"));
                    const pair = openSpoolPair(join(this.bgSpoolDir, `fg_${randomUUID()}`));
                    execSpoolRef.current = { outPath: pair.outPath, errPath: pair.errPath };
                    execSpoolFds = pair.fds;
                }
                catch {
                    execSpoolRef.current = undefined;
                    execSpoolFds = undefined;
                }
            }
            const closeSpoolFds = () => {
                if (!execSpoolFds)
                    return;
                for (const fd of execSpoolFds) {
                    try {
                        closeSync(fd);
                    }
                    catch {
                    }
                }
                execSpoolFds = undefined;
            };
            try {
                child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
                    cwd,
                    detached: process.platform !== "win32",
                    env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub, this.onNotice),
                    stdio: execSpoolFds ? ["ignore", execSpoolFds[0], execSpoolFds[1]] : ["ignore", "pipe", "pipe"],
                    windowsHide: true,
                });
            }
            catch (error) {
                closeSpoolFds();
                const cause = toError(error);
                settle(err(new ExecutionError("spawn_error", cause.message, cause)));
                return;
            }
            closeSpoolFds();
            const timeoutMs = resolveExecTimeoutMs(options?.timeout);
            timeoutRef.current =
                timeoutMs === undefined
                    ? undefined
                    : setTimeout(() => {
                        if (options?.autoBackgroundOnTimeout && !terminationInFlight && adoptOnTimeoutRef.current) {
                            adoptOnTimeoutRef.current();
                            return;
                        }
                        terminationInFlight = true;
                        timedOut = true;
                        if (child?.pid) {
                            const timeoutTarget = child;
                            killProcessTree(child.pid, { stillRunning: () => timeoutTarget.exitCode === null && timeoutTarget.signalCode === null });
                        }
                        armForceSettle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
                    }, timeoutMs);
            if (options?.abortSignal) {
                if (options.abortSignal.aborted) {
                    onAbort();
                }
                else {
                    options.abortSignal.addEventListener("abort", onAbort, { once: true });
                }
            }
            const spoolCursor = { out: 0, err: 0 };
            const readSpoolIncrement = (path, cursor) => {
                try {
                    const size = statSync(path).size;
                    if (size <= spoolCursor[cursor])
                        return undefined;
                    const fd = openSync(path, "r");
                    try {
                        const want = Math.min(size - spoolCursor[cursor], 8 * 1024 * 1024 + 4096);
                        const buf = Buffer.alloc(want);
                        const got = readSync(fd, buf, 0, want, spoolCursor[cursor]);
                        if (got <= 0)
                            return undefined;
                        spoolCursor[cursor] += got;
                        return buf.subarray(0, got);
                    }
                    finally {
                        closeSync(fd);
                    }
                }
                catch {
                    return undefined;
                }
            };
            const feed = (tail, cb, buf) => {
                tail.push(buf);
                if (cb === undefined || settled)
                    return;
                try {
                    cb(buf.toString("utf8"));
                }
                catch (error) {
                    const cause = toError(error);
                    callbackError = new ExecutionError("callback_error", cause.message, cause);
                    onAbort();
                }
            };
            const spoolLanes = () => {
                const sp = execSpoolRef.current;
                if (sp === undefined)
                    return [];
                return [
                    { path: sp.outPath, cursor: "out", tail: stdoutTail, cb: options?.onStdout },
                    { path: sp.errPath, cursor: "err", tail: stderrTail, cb: options?.onStderr },
                ];
            };
            const skipAhead = (lane, keep) => {
                try {
                    const behind = statSync(lane.path).size - spoolCursor[lane.cursor];
                    if (behind <= keep)
                        return;
                    lane.tail.recordSkippedBytes(behind - keep);
                    spoolCursor[lane.cursor] += behind - keep;
                }
                catch {
                }
            };
            const reclaimSpool = (lane) => {
                if (spoolCursor[lane.cursor] < EXEC_SPOOL_ROTATE_BYTES)
                    return;
                try {
                    const d = decideSpoolReclaim({ size: statSync(lane.path).size, cursor: spoolCursor[lane.cursor], writerLive: writerLive() });
                    if (!d.reclaim)
                        return;
                    if (d.skippedBytes > 0)
                        lane.tail.recordSkippedBytes(d.skippedBytes);
                    truncateSync(lane.path, 0);
                    spoolCursor[lane.cursor] = 0;
                }
                catch {
                }
            };
            const pumpSpool = () => {
                for (const lane of spoolLanes()) {
                    skipAhead(lane, EXEC_SPOOL_ROTATE_BYTES);
                    const inc = readSpoolIncrement(lane.path, lane.cursor);
                    if (inc)
                        feed(lane.tail, lane.cb, inc);
                    reclaimSpool(lane);
                }
            };
            pumpSpoolRef.current = pumpSpool;
            const drainSpoolFinal = () => {
                for (const lane of spoolLanes()) {
                    skipAhead(lane, EXEC_SPOOL_FINAL_TAIL_BYTES);
                    for (let i = 0; i < 4; i++) {
                        const inc = readSpoolIncrement(lane.path, lane.cursor);
                        if (!inc)
                            break;
                        feed(lane.tail, lane.cb, inc);
                    }
                }
            };
            drainSpoolRef.current = drainSpoolFinal;
            if (execSpoolRef.current) {
                spoolPumpRef.current = setInterval(pumpSpool, EXEC_SPOOL_PUMP_MS);
                spoolPumpRef.current.unref?.();
            }
            const onDetach = (cause) => {
                if (settled || terminationInFlight || !child)
                    return false;
                pumpSpoolRef.current?.();
                if (settled || terminationInFlight)
                    return false;
                const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
                if (shellId === undefined)
                    return false;
                if (spoolPumpRef.current)
                    clearInterval(spoolPumpRef.current);
                if (timeoutRef.current)
                    clearTimeout(timeoutRef.current);
                const dso = stdoutTail.result();
                const dse = stderrTail.result();
                settle(ok({ stdout: markTruncated(dso.text, dso.droppedBytes), stderr: markTruncated(dse.text, dse.droppedBytes), exitCode: 0, detached: { shellId, ...(cause !== undefined ? { cause } : {}) } }));
                return true;
            };
            adoptOnTimeoutRef.current = () => {
                if (onDetach("timeout"))
                    return;
                if (terminationInFlight)
                    return;
                terminationInFlight = true;
                timedOut = true;
                if (child?.pid) {
                    const adoptFallbackTarget = child;
                    killProcessTree(child.pid, { stillRunning: () => adoptFallbackTarget.exitCode === null && adoptFallbackTarget.signalCode === null });
                }
                armForceSettle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
            };
            if (options?.detachSignal) {
                if (options.detachSignal.aborted)
                    onDetach();
                else {
                    const dl = () => onDetach();
                    options.detachSignal.addEventListener("abort", dl, { once: true });
                }
            }
            if (!execSpoolRef.current) {
                child.stdout?.setEncoding("utf8");
                child.stderr?.setEncoding("utf8");
                child.stdout?.on("data", (chunk) => {
                    stdoutTail.push(Buffer.from(chunk, "utf8"));
                    try {
                        options?.onStdout?.(chunk);
                    }
                    catch (error) {
                        const cause = toError(error);
                        callbackError = new ExecutionError("callback_error", cause.message, cause);
                        onAbort();
                    }
                });
                child.stderr?.on("data", (chunk) => {
                    stderrTail.push(Buffer.from(chunk, "utf8"));
                    try {
                        options?.onStderr?.(chunk);
                    }
                    catch (error) {
                        const cause = toError(error);
                        callbackError = new ExecutionError("callback_error", cause.message, cause);
                        onAbort();
                    }
                });
            }
            child.stdout?.on("error", () => { });
            child.stderr?.on("error", () => { });
            child.on("error", (error) => {
                if (options?.abortSignal?.aborted) {
                    settle(err(new ExecutionError("aborted", "aborted", undefined, partialOutput())));
                    return;
                }
                settle(err(new ExecutionError("spawn_error", error.message, error)));
            });
            const settleFromTermination = (code, signal) => {
                drainSpoolFinal();
                if (callbackError) {
                    const p = partialOutput();
                    callbackError.partialStdout = p.stdout;
                    callbackError.partialStderr = p.stderr;
                    settle(err(callbackError));
                    return;
                }
                if (timedOut) {
                    settle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
                    return;
                }
                if (options?.abortSignal?.aborted) {
                    settle(err(new ExecutionError("aborted", "aborted", undefined, partialOutput())));
                    return;
                }
                const so = stdoutTail.result();
                const se = stderrTail.result();
                if (code === null && signal) {
                    const signum = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGSEGV: 11, SIGPIPE: 13, SIGTERM: 15 };
                    settle(ok({
                        stdout: markTruncated(so.text, so.droppedBytes),
                        stderr: `${markTruncated(se.text, se.droppedBytes)}${se.text.endsWith("\n") || se.text === "" ? "" : "\n"}[process terminated by ${signal}]`,
                        exitCode: 128 + (signum[signal] ?? 9),
                    }));
                    return;
                }
                settle(ok({ stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes), exitCode: code ?? 0 }));
            };
            child.on("close", (code, signal) => settleFromTermination(code, signal));
            child.on("exit", (code, signal) => {
                if (execSpoolRef.current === undefined)
                    return;
                const drain = setTimeout(() => {
                    settleFromTermination(code, signal);
                }, EXIT_SETTLE_DRAIN_MS);
                drain.unref?.();
                child.once("close", () => clearTimeout(drain));
            });
        });
    }
    async readTextFile(path, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        try {
            return ok(await readFile(resolved, { encoding: "utf8", signal: abortSignal }));
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async readTextLines(path, options) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(options?.abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        if (options?.maxLines !== undefined && options.maxLines <= 0) {
            return ok([]);
        }
        let stream;
        let lineReader;
        try {
            stream = createReadStream(resolved, { encoding: "utf8", signal: options?.abortSignal });
            lineReader = createInterface({ input: stream, crlfDelay: Infinity });
            const lines = [];
            for await (const line of lineReader) {
                const loopAbort = abortResult(options?.abortSignal, resolved);
                if (loopAbort) {
                    return loopAbort;
                }
                lines.push(line);
                if (options?.maxLines !== undefined && lines.length >= options.maxLines) {
                    break;
                }
            }
            const afterReadAbort = abortResult(options?.abortSignal, resolved);
            if (afterReadAbort) {
                return afterReadAbort;
            }
            return ok(lines);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
        finally {
            lineReader?.close();
            stream?.destroy();
        }
    }
    async readBinaryFile(path, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        try {
            return ok(await readFile(resolved, { signal: abortSignal }));
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async writeFile(path, content, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        try {
            await mkdir(resolve(resolved, ".."), { recursive: true });
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
        const afterMkdirAbort = abortResult(abortSignal, resolved);
        if (afterMkdirAbort) {
            return afterMkdirAbort;
        }
        let target = resolved;
        let existingMode;
        let st;
        try {
            st = await lstat(resolved);
        }
        catch {
            st = undefined;
        }
        if (st !== undefined) {
            if (st.isSymbolicLink()) {
                let hop;
                try {
                    const link = await readlink(resolved);
                    const parentReal = await physicalPathOfAsync(resolve(resolved, "..")).catch(() => resolve(resolved, ".."));
                    hop = isAbsolute(link) ? link : resolve(parentReal, link);
                }
                catch {
                    hop = undefined;
                }
                if (hop === undefined)
                    return await this.writeFileInPlace(resolved, content, abortSignal);
                let hopStat;
                try {
                    hopStat = await lstat(hop);
                }
                catch {
                    hopStat = undefined;
                }
                if (hopStat !== undefined && !hopStat.isFile()) {
                    return await this.writeFileInPlace(resolved, content, abortSignal);
                }
                target = hop;
                existingMode = hopStat !== undefined ? hopStat.mode & 0o7777 : undefined;
            }
            else if (!st.isFile()) {
                return await this.writeFileInPlace(resolved, content, abortSignal);
            }
            else {
                existingMode = st.mode & 0o7777;
            }
        }
        const tmp = `${target}.tmp.${process.pid}.${randomUUID().replace(/-/g, "").slice(0, 12)}`;
        let tmpDurable = false;
        try {
            const handle = await open(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0));
            try {
                await handle.writeFile(content, { signal: abortSignal });
                if (existingMode !== undefined) {
                    try {
                        await handle.chmod(existingMode);
                    }
                    catch (error) {
                        if (!isFsSupportError(error))
                            throw error;
                    }
                }
                try {
                    await handle.sync();
                }
                catch (error) {
                    if (!isFsSupportError(error))
                        throw error;
                }
                tmpDurable = true;
            }
            finally {
                await handle.close().catch(() => { });
            }
        }
        catch (error) {
            await unlink(tmp).catch(() => { });
            if (isNodeError(error) && error.code === "EACCES" && existingMode !== undefined) {
                return await this.writeFileInPlace(resolved, content, abortSignal);
            }
            return err(toFileError(error, resolved));
        }
        const preRenameAbort = abortResult(abortSignal, resolved);
        if (preRenameAbort) {
            await unlink(tmp).catch(() => { });
            return preRenameAbort;
        }
        try {
            await rename(tmp, target);
            return ok(undefined);
        }
        catch (error) {
            const code = isNodeError(error) ? error.code : undefined;
            if (tmpDurable && code !== undefined && RENAME_FALLBACK_CODES.has(code)) {
                const inPlace = await this.writeFileInPlace(resolved, content, abortSignal);
                if (inPlace.ok) {
                    await unlink(tmp).catch(() => { });
                    return inPlace;
                }
                return err(new FileError("unknown", `write to ${resolved} failed after the atomic rename was refused (${code}): ${inPlace.error.message}. The new content was preserved at ${tmp}`, resolved, inPlace.error));
            }
            await unlink(tmp).catch(() => { });
            return err(toFileError(error, resolved));
        }
    }
    async writeFileInPlace(resolved, content, abortSignal) {
        try {
            await writeFile(resolved, content, { signal: abortSignal });
            return ok(undefined);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async writeFileExclusive(path, content, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        try {
            await mkdir(resolve(resolved, ".."), { recursive: true });
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
        const afterMkdirAbort = abortResult(abortSignal, resolved);
        if (afterMkdirAbort) {
            return afterMkdirAbort;
        }
        try {
            await writeFile(resolved, content, { signal: abortSignal, flag: "wx" });
            return ok(undefined);
        }
        catch (error) {
            if (error?.code === "EEXIST") {
                return err(new FileError("already_exists", `file already exists: ${resolved}`, resolved, error instanceof Error ? error : undefined));
            }
            return err(toFileError(error, resolved));
        }
    }
    async appendFile(path, content) {
        const resolved = resolvePath(this.cwd, path);
        try {
            await mkdir(resolve(resolved, ".."), { recursive: true });
            await appendFile(resolved, content);
            return ok(undefined);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async fileInfo(path) {
        const resolved = resolvePath(this.cwd, path);
        try {
            return fileInfoFromStats(resolved, await lstat(resolved));
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async setFileMode(path, mode, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        if (!HAS_POSIX_MODE_FACE) {
            return err(new FileError("not_supported", "This platform has no POSIX permission model", resolved));
        }
        try {
            const handle = await open(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
            try {
                await handle.chmod(mode & 0o7777);
            }
            finally {
                await handle.close().catch(() => { });
            }
            return ok(undefined);
        }
        catch (error) {
            if (isNodeError(error) && error.code === "ELOOP") {
                return err(new FileError("invalid", "Refusing to change the mode of a symlink (the target is not the addressed path)", resolved));
            }
            return err(toFileError(error, resolved));
        }
    }
    async listDir(path, abortSignal) {
        const resolved = resolvePath(this.cwd, path);
        const aborted = abortResult(abortSignal, resolved);
        if (aborted) {
            return aborted;
        }
        try {
            const entries = await readdir(resolved, { withFileTypes: true });
            const infos = [];
            for (const entry of entries) {
                const loopAbort = abortResult(abortSignal, resolved);
                if (loopAbort) {
                    return loopAbort;
                }
                const entryPath = resolve(resolved, entry.name);
                try {
                    const info = fileInfoFromStats(entryPath, await lstat(entryPath));
                    if (info.ok) {
                        infos.push(info.value);
                    }
                }
                catch (error) {
                    return err(toFileError(error, entryPath));
                }
            }
            return ok(infos);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async canonicalPath(path) {
        const resolved = resolvePath(this.cwd, path);
        try {
            return ok(await physicalPathOfAsync(resolved));
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async fileIdentity(path) {
        const resolved = resolvePath(this.cwd, path);
        try {
            const s = await stat(resolved, { bigint: true });
            return ok(`${s.dev}:${s.ino}`);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async readLink(path) {
        const resolved = resolvePath(this.cwd, path);
        try {
            return ok(await readlink(resolved));
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async canonicalChain(path) {
        const resolved = resolvePath(this.cwd, path);
        const hops = [];
        const seen = new Set([resolved]);
        let cur = resolved;
        for (let hop = 0; hop < SYMLINK_CHAIN_HOP_BOUND; hop++) {
            let target;
            try {
                target = await readlink(cur);
            }
            catch (error) {
                if (isNodeError(error) && (error.code === "EINVAL" || error.code === "ENOENT" || error.code === "ENOTDIR"))
                    return ok({ hops });
                return err(toFileError(error, cur));
            }
            const next = isAbsolute(target) ? target : anchorAtLinkDir(dirname(cur), target, sep);
            if (seen.has(next))
                break;
            hops.push(next);
            if (process.platform === "win32" && isUncSpelling(next))
                break;
            seen.add(next);
            cur = next;
        }
        return ok({ hops });
    }
    async exists(path) {
        const result = await this.fileInfo(path);
        if (result.ok) {
            return ok(true);
        }
        if (result.error.code === "not_found") {
            return ok(false);
        }
        return err(result.error);
    }
    async createDir(path, options) {
        const resolved = resolvePath(this.cwd, path);
        try {
            await mkdir(resolved, { recursive: options?.recursive ?? true });
            return ok(undefined);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async remove(path, options) {
        const resolved = resolvePath(this.cwd, path);
        try {
            await rm(resolved, {
                recursive: options?.recursive ?? false,
                force: options?.force ?? false,
            });
            return ok(undefined);
        }
        catch (error) {
            return err(toFileError(error, resolved));
        }
    }
    async createTempDir(prefix = "tmp-") {
        try {
            return ok(await mkdtemp(join(tmpdir(), prefix)));
        }
        catch (error) {
            return err(toFileError(error));
        }
    }
    async createTempFile(options) {
        const dir = await this.createTempDir("tmp-");
        if (!dir.ok) {
            return dir;
        }
        const filePath = join(dir.value, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`);
        try {
            await writeFile(filePath, "");
            return ok(filePath);
        }
        catch (error) {
            return err(toFileError(error, filePath));
        }
    }
    adoptRunningChild(child, stdoutSoFar, stderrSoFar, handover) {
        const liveCount = [...this.bgShells.values()].filter((e) => e.status === "running").length;
        if (liveCount >= this.backgroundCapabilities.maxConcurrent)
            return undefined;
        const shellId = `bg_${++this.bgCounter}_${randomUUID()}`;
        const entry = {
            child,
            stdout: { ...newStreamCursorState(), totalBytes: 0 },
            stderr: { ...newStreamCursorState(), totalBytes: 0 },
            status: "running",
            ...(handover !== undefined
                ? { spool: { outPath: handover.spool.outPath, errPath: handover.spool.errPath, fileCursor: { ...handover.cursors }, ephemeral: true } }
                : {}),
        };
        const seedOut = Buffer.from(stdoutSoFar.text, "utf8");
        const seedErr = Buffer.from(stderrSoFar.text, "utf8");
        entry.stdout.tail.recordSkippedBytes(stdoutSoFar.droppedBytes);
        entry.stderr.tail.recordSkippedBytes(stderrSoFar.droppedBytes);
        entry.stdout.tail.push(seedOut);
        entry.stderr.tail.push(seedErr);
        entry.stdout.totalBytes += stdoutSoFar.droppedBytes + seedOut.length;
        entry.stderr.totalBytes += stderrSoFar.droppedBytes + seedErr.length;
        entry.stdout.acceptedBytes += seedOut.length;
        entry.stderr.acceptedBytes += seedErr.length;
        child.stdout?.on("data", (chunk) => {
            const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
            entry.stdout.tail.push(buf);
            entry.stdout.totalBytes += buf.length;
            entry.stdout.acceptedBytes += buf.length;
        });
        child.stderr?.on("data", (chunk) => {
            const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
            entry.stderr.tail.push(buf);
            entry.stderr.totalBytes += buf.length;
            entry.stderr.acceptedBytes += buf.length;
        });
        child.on("error", () => {
            if (entry.timer)
                clearTimeout(entry.timer);
            if (entry.status === "running")
                entry.status = "failed";
        });
        child.on("close", (code, signal) => {
            if (entry.timer)
                clearTimeout(entry.timer);
            this.reclaimEphemeralSpool(entry);
            if (entry.status === "killed")
                return;
            entry.status = "exited";
            entry.exitCode = code === null && signal ? 128 + (SIGNUM[signal] ?? 9) : (code ?? 0);
        });
        if (child.exitCode !== null || child.signalCode !== null) {
            entry.status = "exited";
            entry.exitCode = child.exitCode ?? (child.signalCode ? 128 + (SIGNUM[child.signalCode] ?? 9) : 0);
            this.reclaimEphemeralSpool(entry);
        }
        else {
            const timeoutSec = this.backgroundCapabilities.defaultBgTimeoutSec;
            entry.timeoutSecApplied = timeoutSec;
            entry.timer = setTimeout(() => {
                entry.status = "killed";
                entry.killCause = "timeout";
                if (child.pid)
                    killProcessTree(child.pid, { force: true });
            }, timeoutSec * 1000);
            entry.timer.unref?.();
        }
        if (entry.spool?.ephemeral) {
            entry.spoolPump = setInterval(() => this.syncSpool(entry), 1000);
            entry.spoolPump.unref?.();
        }
        this.bgShells.set(shellId, entry);
        return shellId;
    }
    async spawnBackground(command, options) {
        const liveCount = [...this.bgShells.values()].filter((e) => e.status === "running").length;
        if (liveCount >= this.backgroundCapabilities.maxConcurrent) {
            return err(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.backgroundCapabilities.maxConcurrent}); TaskStop one first.`));
        }
        const shellConfig = await getShellConfig(this.shellPath);
        if (!shellConfig.ok) {
            return err(new BackgroundShellError("spawn_failed", shellConfig.error.message, shellConfig.error));
        }
        let spool;
        let spoolFds;
        if (this.retainBackgroundProcesses || process.platform !== "win32") {
            try {
                if (!this.bgSpoolDir)
                    this.bgSpoolDir = mkdtempSync(join(tmpdir(), "sema-bg-"));
                const pair = openSpoolPair(join(this.bgSpoolDir, `bg_${this.bgCounter + 1}_${randomUUID()}`));
                spool = {
                    outPath: pair.outPath,
                    errPath: pair.errPath,
                    fileCursor: { out: 0, err: 0 },
                    ...(this.retainBackgroundProcesses ? {} : { ephemeral: true }),
                };
                spoolFds = pair.fds;
            }
            catch {
                spool = undefined;
                spoolFds = undefined;
            }
        }
        let child;
        try {
            child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
                cwd: options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd,
                detached: process.platform !== "win32",
                env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub, this.onNotice),
                stdio: spoolFds ? ["ignore", spoolFds[0], spoolFds[1]] : ["ignore", "pipe", "pipe"],
                windowsHide: true,
            });
        }
        catch (error) {
            if (spoolFds) {
                for (const fd of spoolFds) {
                    try {
                        closeSync(fd);
                    }
                    catch {
                    }
                }
            }
            const cause = toError(error);
            return err(new BackgroundShellError("spawn_failed", cause.message, cause));
        }
        if (spoolFds) {
            for (const fd of spoolFds) {
                try {
                    closeSync(fd);
                }
                catch {
                }
            }
        }
        const shellId = `bg_${++this.bgCounter}_${randomUUID()}`;
        const entry = {
            child,
            stdout: { ...newStreamCursorState(), totalBytes: 0 },
            stderr: { ...newStreamCursorState(), totalBytes: 0 },
            status: "running",
            ...(spool ? { spool } : {}),
        };
        child.stdout?.setEncoding("utf8");
        child.stderr?.setEncoding("utf8");
        child.stdout?.on("data", (chunk) => {
            const buf = Buffer.from(chunk, "utf8");
            entry.stdout.tail.push(buf);
            entry.stdout.totalBytes += buf.length;
            entry.stdout.acceptedBytes += buf.length;
        });
        child.stderr?.on("data", (chunk) => {
            const buf = Buffer.from(chunk, "utf8");
            entry.stderr.tail.push(buf);
            entry.stderr.totalBytes += buf.length;
            entry.stderr.acceptedBytes += buf.length;
        });
        child.stdout?.on("error", () => { });
        child.stderr?.on("error", () => { });
        child.on("error", () => {
            if (entry.timer)
                clearTimeout(entry.timer);
            if (entry.status === "running")
                entry.status = "failed";
        });
        child.on("close", (code, signal) => {
            if (entry.timer)
                clearTimeout(entry.timer);
            this.reclaimEphemeralSpool(entry);
            if (entry.status === "killed")
                return;
            entry.status = "exited";
            entry.exitCode = code === null && signal ? 128 + (SIGNUM[signal] ?? 9) : (code ?? 0);
        });
        const timeoutSec = Math.min(options?.timeout ?? this.backgroundCapabilities.defaultBgTimeoutSec, this.backgroundCapabilities.maxBgTimeoutSec);
        entry.timeoutSecApplied = timeoutSec;
        entry.timer = setTimeout(() => {
            if (child.pid && entry.status === "running") {
                entry.status = "killed";
                entry.killCause = "timeout";
                shutdownDebug("bg hard-wall timeout", { shellId, pid: child.pid, timeoutSec });
                killProcessTree(child.pid, { force: true });
            }
        }, timeoutSec * 1000);
        entry.timer.unref?.();
        if (entry.spool?.ephemeral) {
            entry.spoolPump = setInterval(() => this.syncSpool(entry), 1000);
            entry.spoolPump.unref?.();
        }
        this.bgShells.set(shellId, entry);
        return ok({ shellId });
    }
    async pollBackground(shellId) {
        const entry = this.bgShells.get(shellId);
        if (!entry) {
            return err(new BackgroundShellError("not_found", `Unknown background shell: ${shellId}`));
        }
        this.syncSpool(entry);
        const terminal = entry.status !== "running";
        const out = sliceStreamIncrement(entry.stdout, terminal);
        const er = sliceStreamIncrement(entry.stderr, terminal);
        const droppedBeforeCursor = out.droppedBeforeCursor + er.droppedBeforeCursor;
        return ok({
            stdout: out.inc,
            stderr: er.inc,
            status: entry.status,
            ...(entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
            ...(droppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: droppedBeforeCursor } : {}),
            ...(entry.killCause === "timeout"
                ? { timedOut: true, ...(entry.timeoutSecApplied !== undefined ? { timeoutSec: entry.timeoutSecApplied } : {}) }
                : {}),
            bytesFromStart: entry.stdout.totalBytes + entry.stderr.totalBytes,
        });
    }
    async killBackground(shellId) {
        const entry = this.bgShells.get(shellId);
        if (!entry) {
            return err(new BackgroundShellError("not_found", `Unknown background shell: ${shellId}`));
        }
        if (entry.status === "running" && entry.child.pid) {
            entry.status = "killed";
            if (entry.timer)
                clearTimeout(entry.timer);
            shutdownDebug("killBackground", { shellId, pid: entry.child.pid });
            killProcessTree(entry.child.pid, { force: true });
        }
        return ok(undefined);
    }
    killBackgroundNow(shellId) {
        const entry = this.bgShells.get(shellId);
        if (!entry || entry.hardKilled)
            return "already_gone";
        const child = entry.child;
        if (child.exitCode !== null || child.signalCode !== null)
            return "already_gone";
        const pid = child.pid;
        if (pid === undefined || pid <= 0)
            return "no_pgid";
        if (process.platform === "win32")
            return "no_pgid";
        shutdownDebug("killBackgroundNow", { shellId, pid });
        try {
            process.kill(-pid, "SIGKILL");
        }
        catch (e) {
            return e.code === "ESRCH" ? "already_gone" : "no_pgid";
        }
        entry.hardKilled = true;
        if (entry.timer)
            clearTimeout(entry.timer);
        if (entry.status === "running")
            entry.status = "killed";
        return "killed";
    }
    syncSpool(entry) {
        const spool = entry.spool;
        if (!spool)
            return;
        const streams = [
            [entry.stdout, spool.outPath, "out"],
            [entry.stderr, spool.errPath, "err"],
        ];
        for (const [stream, path, lane] of streams) {
            try {
                const size = statSync(path).size;
                if (spool.ephemeral) {
                    const behind = size - spool.fileCursor[lane];
                    if (behind > EXEC_SPOOL_ROTATE_BYTES) {
                        const skipped = behind - EXEC_SPOOL_ROTATE_BYTES;
                        stream.tail.recordSkippedBytes(skipped);
                        stream.totalBytes += skipped;
                        spool.fileCursor[lane] += skipped;
                    }
                }
                if (size > spool.fileCursor[lane]) {
                    const fd = openSync(path, "r");
                    try {
                        const want = Math.min(size - spool.fileCursor[lane], 8 * 1024 * 1024 + 4096);
                        const buf = Buffer.alloc(want);
                        const got = readSync(fd, buf, 0, want, spool.fileCursor[lane]);
                        if (got > 0) {
                            stream.tail.push(buf.subarray(0, got));
                            stream.totalBytes += got;
                            stream.acceptedBytes += got;
                            spool.fileCursor[lane] += got;
                        }
                    }
                    finally {
                        closeSync(fd);
                    }
                }
                if (spool.ephemeral && spool.fileCursor[lane] >= EXEC_SPOOL_ROTATE_BYTES) {
                    const d = decideSpoolReclaim({
                        size: statSync(path).size,
                        cursor: spool.fileCursor[lane],
                        writerLive: entry.status === "running" || (entry.child.exitCode === null && entry.child.signalCode === null),
                    });
                    if (d.reclaim) {
                        if (d.skippedBytes > 0) {
                            stream.tail.recordSkippedBytes(d.skippedBytes);
                            stream.totalBytes += d.skippedBytes;
                        }
                        truncateSync(path, 0);
                        spool.fileCursor[lane] = 0;
                    }
                }
            }
            catch {
            }
        }
    }
    reclaimEphemeralSpool(entry) {
        if (entry.spoolPump !== undefined) {
            clearInterval(entry.spoolPump);
            entry.spoolPump = undefined;
        }
        const spool = entry.spool;
        if (!spool?.ephemeral)
            return;
        this.syncSpool(entry);
        entry.spool = undefined;
        for (const path of [spool.outPath, spool.errPath]) {
            try {
                unlinkSync(path);
            }
            catch {
            }
        }
    }
    async disposeBackgroundShells(opts) {
        if (this.retainBackgroundProcesses)
            return;
        const keep = opts?.except !== undefined && opts.except.length > 0 ? new Set(opts.except) : undefined;
        shutdownDebug("disposeBackgroundShells", { entries: this.bgShells.size, except: opts?.except?.length ?? 0 });
        for (const [id, entry] of this.bgShells) {
            if (keep?.has(id))
                continue;
            try {
                if (entry.timer)
                    clearTimeout(entry.timer);
                shutdownDebug("dispose entry", { id, status: entry.status, pid: entry.child.pid, action: entry.status === "running" ? "group-kill" : "spare" });
                if (entry.status === "running" && entry.child.pid) {
                    entry.status = "killed";
                    killProcessTree(entry.child.pid, { force: true });
                }
                this.reclaimEphemeralSpool(entry);
            }
            catch {
            }
            if (keep !== undefined)
                this.bgShells.delete(id);
        }
        if (keep === undefined)
            this.bgShells.clear();
    }
    async cleanup() {
        await this.disposeBackgroundShells();
    }
}