UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

1,368 lines • 70.9 kB
import { randomBytes } from "node:crypto";
import { defineTool, errorResult } from "./tools.js";
import { envDeclaresRetain, hasBackgroundShell, retainsBackgroundProcesses } from "./background-shell.js";
import { shutdownDebug } from "./shutdown-debug.js";
import { canAccessAgentRecord, } from "./background-agent-store.js";
import { delimitUntrusted } from "./untrusted-text.js";
import { createSafeNotifier } from "./safe-notify.js";
import { stripCwdSentinel } from "../tools/fs/fs-shared.js";
import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, stdoutHeadingOf, REPLAY_FACE, SPOOL_REPLAY, INCREMENT_REPLAY, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, activeDelegationHandlesLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
import { toolFace } from "./tool-catalog-entries.js";
import { thrownText } from "./thrown-value.js";
import { agentStateSourceOfHandle, agentStateSourceOfRecord, agentStateSummary, readAgentState } from "./agent-state.js";
export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
function shellRowRetained(h) {
    return retainsBackgroundProcesses({ marked: h.retained === true, env: envDeclaresRetain(h.env) });
}
const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
const BLOCK_MAX_TIMEOUT_MS = 600_000;
function blockDeadline(timeoutMs, now) {
    const t = Math.min(BLOCK_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeoutMs ?? BLOCK_DEFAULT_TIMEOUT_MS)));
    return now + t;
}
const BLOCK_WAIT_WRITEOUT_RESERVE_MS = 5_000;
const TASK_PREFIX = {
    background_bash: "b",
    workflow: "w",
    background_agent: "a",
    monitor: "m",
};
function noTask(taskId, runningAgents) {
    return {
        content: `No task found with ID: ${taskId}${runningAgents ? notFoundRunningAgentsTail(runningAgents) : ""}`,
        details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
        isError: true,
    };
}
function noTaskForStop(taskId, enrich) {
    let content = `No task found with ID: ${taskId}`;
    if (enrich.suggestion !== undefined)
        content += `. Did you mean: ${enrich.suggestion}?`;
    content += notFoundRunningAgentsTail(enrich.runningAgents);
    return {
        content,
        details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
        isError: true,
    };
}
function bashTerminalResult(spool) {
    const out = spool.stdout.slice(-2_000);
    const err = stripCwdSentinel(spool.stderr).stderr.slice(-2_000);
    return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
}
async function drainBashTailBestEffort(handle) {
    if (handle.outputFile === undefined)
        return;
    try {
        const drained = await handle.env.pollBackground(handle.shellId);
        if (drained.ok && handle.spool !== undefined)
            accountDroppedBytes(handle.spool, drained.value);
        if (drained.ok && (drained.value.stdout.length > 0 || drained.value.stderr.length > 0)) {
            if (handle.spool !== undefined) {
                const spool = handle.spool;
                const cap = 2 * TASK_OUTPUT_MAX_CHARS;
                spool.stdout = rollSpoolText(spool, spool.stdout + drained.value.stdout, cap);
                spool.stderr = rollSpoolText(spool, spool.stderr + drained.value.stderr, cap);
                const ap = await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
                if (!ap.ok)
                    spool.mirrorFailed = true;
            }
            else {
                await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
            }
        }
    }
    catch {
    }
}
function bashMirrorGapNote(handle) {
    return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
        ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
        : "";
}
function explicitStopBashTerminalNote(handle, alreadyGone) {
    const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
    const detail = alreadyGone
        ? "killed before completion (stopped via TaskStop; the process was already gone)"
        : "killed before completion (stopped via TaskStop)";
    handle.onTerminal?.({
        task_id: handle.id,
        task_type: "background_bash",
        ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
        ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
        status: "killed",
        ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
        summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "killed")} — ${detail}${bashMirrorGapNote(handle)}${handle.spool !== undefined ? droppedGapNote(handle.spool) : ""}`,
        ...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
        ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
    });
}
function monitorInTimeoutWindow(handle) {
    return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
}
export class TaskRegistry {
    handles = new Map();
    sessionReapHooks = new Set();
    bgQuiescenceWatchers = new Map();
    notifier = createSafeNotifier();
    notifierFailureCounts() {
        return this.notifier.failedSites().map((site) => ({ site, count: this.notifier.failuresAt(site) }));
    }
    core = (() => {
        const self = this;
        return {
            handles: self.handles,
            mintTaskId: (type) => self.mintTaskId(type),
            markStopSource: (id, source) => self.markStopSource(id, source),
            notifyTerminalOnce: (handle, send) => self.notifyTerminalOnce(handle, send),
            pokeBgQuiescence: (owner) => self.pokeBgQuiescence(owner),
            get writerId() { return self.writerId; },
            get reapingHandles() { return self.reapingHandles; },
            get reapedHandles() { return self.reapedHandles; },
            get claimingHandles() { return self.claimingHandles; },
            get durableHeartbeatTimer() { return self.durableHeartbeatTimer; },
            set durableHeartbeatTimer(v) { self.durableHeartbeatTimer = v; },
            get deliverConfirmTimeoutMs() { return self.deliverConfirmTimeoutMs; },
        };
    })();
    durableAgentArmed(id) {
        return durableAgentArmedLane(this.core, id);
    }
    durableAgentRowProbe(id) {
        return durableAgentRowProbeLane(this.core, id);
    }
    beginDurableClaim(id) {
        return beginDurableClaimLane(this.core, id);
    }
    endDurableClaim(id) {
        return endDurableClaimLane(this.core, id);
    }
    async reapDurableAgents(scope, deps, policy) {
        return reapDurableAgentsLane(this.core, scope, deps, policy);
    }
    releaseDurableTranscriptAnchor(id) {
        return releaseDurableTranscriptAnchorLane(this.core, id);
    }
    bindBackgroundAgentSession(id, sessionId) {
        return bindBackgroundAgentSessionLane(this.core, id, sessionId);
    }
    registerBackgroundAgent(input) {
        return registerBackgroundAgentLane(this.core, input);
    }
    recordBackgroundAgentOrgAdmission(id, verdict) {
        return recordBackgroundAgentOrgAdmissionLane(this.core, id, verdict);
    }
    parkBackgroundAgent(id, park) {
        return parkBackgroundAgentLane(this.core, id, park);
    }
    async reconcileParkedAgents(stores, scope, now, opts) {
        return reconcileParkedAgentsLane(this.core, stores, scope, now, opts);
    }
    async claimParkedAgent(stores, handle, access) {
        return claimParkedAgentLane(stores, handle, access);
    }
    async rollbackParkedClaim(stores, ticket) {
        return rollbackParkedClaimLane(this.core, stores, ticket);
    }
    async consumeParkedFlip(id, stores, ticket) {
        return consumeParkedFlipLane(this.core, id, stores, ticket);
    }
    finalizeParkedResume(id) {
        return finalizeParkedResumeLane(this.core, id);
    }
    settleBackgroundAgent(id, outcome) {
        return settleBackgroundAgentLane(this.core, id, outcome);
    }
    noteBackgroundAgentActivity(id, now) {
        return noteBackgroundAgentActivityLane(this.core, id, now);
    }
    reapStaleSessionBackgroundAgents(staleMs, now) {
        return reapStaleSessionBackgroundAgentsLane(this.core, staleMs, now, (note) => {
            this.notifier.notify(() => note(), "taskRegistry.reapTerminalNote");
        });
    }
    abortBackgroundAgentsForOwner(access, opts) {
        return abortBackgroundAgentsForOwnerLane(this.core, access, opts);
    }
    serveDurableAgentRow(row, checkpointSeat) {
        const read = readAgentState(agentStateSourceOfRecord(row), checkpointSeat, Date.now());
        return read instanceof Promise ? read.then((agentState) => serveDurableAgentRowLane(row, agentState)) : serveDurableAgentRowLane(row, read);
    }
    agentStateSource(taskId, access) {
        const h = this.handles.get(taskId);
        if (!h || h.type !== "background_agent" || !canAccess(h, access))
            return undefined;
        return agentStateSourceOfHandle(h);
    }
    resolveBackgroundAgentByName(name, access, opts) {
        return resolveBackgroundAgentByNameLane(this.core, name, access, opts);
    }
    markRetainedContinuation(id) {
        return markRetainedContinuationLane(this.core, id);
    }
    backgroundAgentCycleSeq(id) {
        return backgroundAgentCycleSeqLane(this.core, id);
    }
    activeDelegationHandles(scope, rootSessionId) {
        return activeDelegationHandlesLane(this.core, scope, rootSessionId);
    }
    reviveBackgroundAgent(id, access, abort) {
        return reviveBackgroundAgentLane(this.core, id, access, abort);
    }
    settleRevivedAgent(id, cycle, outcome) {
        return settleRevivedAgentLane(this.core, id, cycle, outcome);
    }
    unmarkRetainedContinuation(id) {
        return unmarkRetainedContinuationLane(this.core, id);
    }
    attachAgentNotify(id, notify, cycle) {
        return attachAgentNotifyLane(this.core, id, notify, cycle);
    }
    attachAgentTerminalNotifier(id, fn) {
        const handle = this.handles.get(id);
        if (!handle || handle.type !== "background_agent")
            return;
        if (handle.status !== "running" && handle.status !== "parked" && handle.status !== "pending")
            return;
        handle.onReapTerminal = fn;
    }
    claimAgentTerminalNotify(id) {
        const handle = this.handles.get(id);
        if (!handle || handle.type !== "background_agent")
            return true;
        if (handle.terminalNotified === true)
            return false;
        handle.terminalNotified = true;
        return true;
    }
    async deliverToRunningAgent(id, access, notification, opts) {
        return deliverToRunningAgentLane(this.core, id, access, notification, opts);
    }
    runningBackgroundAgentLabels(access) {
        return runningBackgroundAgentLabelsLane(this.core, access);
    }
    async pollBackgroundAgent(handle, deadline, signal, oneShot, toolResultStore, sessionId, checkpointSeat) {
        return pollBackgroundAgentLane(handle, deadline, signal, oneShot, toolResultStore, sessionId, checkpointSeat);
    }
    async stopBackgroundAgent(handle) {
        return stopBackgroundAgentLane(this.core, handle);
    }
    writerId = randomBytes(8).toString("hex");
    reapingHandles = new Set();
    reapedHandles = new Set();
    claimingHandles = new Set();
    durableHeartbeatTimer;
    mintTaskId(type) {
        const prefix = TASK_PREFIX[type];
        for (let i = 0; i < 32; i++) {
            const id = `${prefix}${randomBytes(8).toString("hex")}`;
            if (!this.handles.has(id))
                return id;
        }
        throw new Error(`TaskRegistry: exhausted task id mint retries for ${type}`);
    }
    registerBackgroundBash(input) {
        assertOwnership(input, "registerBackgroundBash");
        const id = input.id ?? this.mintTaskId("background_bash");
        if (this.handles.has(id))
            throw new Error(`TaskRegistry: task id already registered: ${id}`);
        const now = input.now ?? Date.now();
        this.handles.set(id, {
            id,
            type: "background_bash",
            status: "running",
            ...(input.sessionScoped ? { sessionScoped: true } : {}),
            ...(input.description !== undefined ? { description: input.description } : {}),
            ...(input.owner !== undefined ? { owner: input.owner } : {}),
            ...(input.scope !== undefined ? { scope: input.scope } : {}),
            ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
            createdAt: now,
            updatedAt: now,
            env: input.env,
            shellId: input.shellId,
            ...(input.onTerminal !== undefined ? { onTerminal: input.onTerminal } : {}),
            ...(input.outputFile !== undefined ? { outputFile: input.outputFile } : {}),
            ...(retainsBackgroundProcesses({ env: envDeclaresRetain(input.env), task: input.retainBackgroundProcesses === true }) ? { retained: true } : {}),
            ...(input.retainBackgroundProcesses !== undefined ? { retainStatedAtLaunch: true } : {}),
        });
        if (input.onTerminal)
            this.startBashWatcher(id, input.onTerminal);
        return id;
    }
    isBackgroundOutputFile(canonicalPath, access, env) {
        for (const handle of this.handles.values()) {
            if (handle.type !== "background_bash" || handle.outputFile === undefined)
                continue;
            if (handle.env !== env)
                continue;
            if (handle.outputFile !== canonicalPath)
                continue;
            if (canAccess(handle, access))
                return true;
        }
        return false;
    }
    getAccessibleTask(taskId, access) {
        const h = this.handles.get(taskId);
        if (!h || !canAccess(h, access))
            return undefined;
        const hb = h;
        return {
            task_id: h.id,
            type: h.type,
            status: h.status,
            ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}),
            ...(hb.sessionScoped ? { sessionScoped: true } : {}),
            ...(hb.description !== undefined ? { description: hb.description } : {}),
            ...(hb.name !== undefined ? { name: hb.name } : {}),
            ...(hb.agentType !== undefined ? { agentType: hb.agentType } : {}),
            ...(hb.owner !== undefined ? { owner: hb.owner } : {}),
            ...(hb.scope !== undefined ? { scope: hb.scope } : {}),
            ...(hb.parentTaskId !== undefined ? { parentTaskId: hb.parentTaskId } : {}),
            ...(hb.parentSessionId !== undefined ? { parentSessionId: hb.parentSessionId } : {}),
            ...(hb.rootSessionId !== undefined ? { rootSessionId: hb.rootSessionId } : {}),
            createdAt: h.createdAt,
        };
    }
    markStopSource(id, source) {
        const handle = this.handles.get(id);
        if (!handle || (handle.type !== "background_agent" && handle.type !== "background_bash" && handle.type !== "monitor"))
            return;
        if (handle.status !== "running" || handle.stopSource !== undefined)
            return;
        handle.stopSource = source;
    }
    markStopSourceForEnvSweep(env, source, except) {
        const keep = except !== undefined && except.length > 0 ? new Set(except.map(String)) : undefined;
        const marked = [];
        for (const [taskId, handle] of this.handles) {
            if (handle.type !== "background_bash")
                continue;
            if (handle.env !== env)
                continue;
            if (handle.status !== "running")
                continue;
            if (handle.shellId !== undefined && keep?.has(String(handle.shellId)))
                continue;
            this.markStopSource(taskId, source);
            marked.push(taskId);
        }
        return marked;
    }
    markStopSourceByShellId(shellId, source, env) {
        if (env === undefined)
            return;
        const sid = String(shellId);
        for (const [id, handle] of this.handles) {
            if (handle.type === "background_bash" && String(handle.shellId) === sid && handle.env === env) {
                this.markStopSource(id, source);
                return;
            }
        }
    }
    clearPendingStopSourceByShellId(shellId, source, env) {
        const sid = String(shellId);
        const clear = (id) => {
            const handle = this.handles.get(id);
            if (!handle || handle.type !== "background_bash")
                return false;
            if (env !== undefined && handle.env !== env)
                return false;
            if (handle.status === "running" && handle.stopSource === source && handle.stoppedBy === undefined) {
                handle.stopSource = undefined;
            }
            return true;
        };
        if (env === undefined)
            return;
        for (const [id, handle] of this.handles) {
            if (handle.type === "background_bash" && String(handle.shellId) === sid && handle.env === env) {
                clear(id);
                return;
            }
        }
    }
    getStopAttribution(id) {
        const handle = this.handles.get(id);
        if (!handle || (handle.type !== "background_agent" && handle.type !== "background_bash" && handle.type !== "monitor"))
            return undefined;
        if (handle.status !== "killed")
            return undefined;
        return handle.stoppedBy ?? handle.stopSource ?? "system";
    }
    getCompletionId(id) {
        return this.handles.get(id)?.completionId;
    }
    markStopSourceForOwner(access, source, opts) {
        let marked = 0;
        for (const handle of this.handles.values()) {
            if (handle.type !== "background_agent" && handle.type !== "background_bash" && handle.type !== "monitor")
                continue;
            if (handle.status !== "running" || !canAccess(handle, access))
                continue;
            if (opts?.skipSessionScoped && handle.sessionScoped)
                continue;
            if (handle.type === "monitor" && monitorInTimeoutWindow(handle))
                continue;
            if ((handle.type === "background_bash" || handle.type === "monitor") && shellRowRetained(handle))
                continue;
            const before = handle.stopSource;
            this.markStopSource(handle.id, source);
            if (before === undefined && handle.stopSource !== undefined)
                marked++;
        }
        return marked;
    }
    async settleKilledForOwner(access, opts) {
        const source = opts?.source ?? "parent";
        const clause = (by) => by === "user" ? "stopped by user" : by === "parent" ? "its parent run ended" : `stopped by ${by}`;
        if (source === "user") {
            for (const handle of this.handles.values()) {
                if (handle.type !== "background_agent" || handle.status !== "running")
                    continue;
                if (!canAccess(handle, { owner: access.owner, scope: access.scope }))
                    continue;
                if (opts?.skipSessionScoped && handle.sessionScoped)
                    continue;
                this.markStopSource(handle.id, source);
            }
        }
        let settled = 0;
        for (const handle of this.handles.values()) {
            if (handle.type !== "background_bash" && handle.type !== "monitor")
                continue;
            if (handle.status !== "running" || !canAccess(handle, access))
                continue;
            if (retainsBackgroundProcesses({ marked: handle.retained === true, env: envDeclaresRetain(handle.env), task: opts?.retainProcesses === true && handle.retainStatedAtLaunch !== true, envDying: opts?.envDying === true })) {
                handle.retained = true;
                continue;
            }
            if (opts?.skipSessionScoped && handle.sessionScoped && !(opts?.envDying === true && handle.type === "background_bash"))
                continue;
            if (handle.type === "monitor" && monitorInTimeoutWindow(handle) && !opts?.envDying)
                continue;
            this.markStopSource(handle.id, source);
            if (handle.type === "background_bash") {
                if (handle.watcher !== undefined) {
                    clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                const kill = await handle.env.killBackground(handle.shellId);
                if (handle.status !== "running")
                    continue;
                const killFailed = !kill.ok && kill.error.code !== "not_found";
                handle.status = "killed";
                handle.stoppedBy = handle.stopSource ?? source;
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                await drainBashTailBestEffort(handle);
                const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
                this.notifyTerminalOnce(handle, () => handle.onTerminal?.({
                    task_id: handle.id,
                    task_type: "background_bash",
                    ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
                    ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
                    status: "killed",
                    stoppedBy: handle.stoppedBy,
                    summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "killed")} — killed before completion (${clause(handle.stoppedBy)})${killFailed ? ` — kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries` : ""}${bashMirrorGapNote(handle)}`,
                    ...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
                    ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                }));
            }
            else {
                if (handle.watcher !== undefined) {
                    handle.timers.clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                const kill = await handle.env.killBackground(handle.shellId);
                if (handle.status !== "running")
                    continue;
                const killFailed = !kill.ok && kill.error.code !== "not_found";
                handle.status = "killed";
                handle.stoppedBy = handle.stopSource ?? source;
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                const lastLines = handle.lineBuf !== "" ? [handle.lineBuf] : [];
                handle.lineBuf = "";
                this.notifyTerminalOnce(handle, () => handle.onEvent?.({
                    task_id: handle.id,
                    task_type: "monitor",
                    ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
                    status: "killed",
                    stoppedBy: handle.stoppedBy,
                    summary: `${terminalTaskSummary("monitor", (handle.description ?? "monitor").slice(0, 200), "killed")} — killed before completion (${clause(handle.stoppedBy)}); watch ended.${killFailed ? ` Kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries.` : ""}`,
                    ...(lastLines.length > 0 ? { lines: lastLines, result: lastLines.join("\n"), partial: true } : {}),
                    ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                }));
            }
            settled++;
        }
        return settled;
    }
    async reportSessionReapKillFailure(kind, handle) {
        try {
            const kill = await handle.env.killBackground(handle.shellId);
            if (!kill.ok && kill.error.code !== "not_found") {
                shutdownDebug("session reap: kill failed", { kind, id: handle.id, shellId: handle.shellId, code: kill.error.code });
            }
        }
        catch (err) {
            shutdownDebug("session reap: kill threw", { kind, id: handle.id, shellId: handle.shellId, error: thrownText(err) });
        }
    }
    reapSessionBackground(sessionId, scope) {
        const access = { owner: sessionId, sessionId, ...(scope !== undefined ? { scope } : {}) };
        let reaped = 0;
        for (const handle of this.handles.values()) {
            if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && shellRowRetained(handle))
                continue;
            if (handle.type === "monitor" && handle.status === "running" && handle.sessionScoped && canAccess(handle, access)) {
                shutdownDebug("session reap: monitor row", { id: handle.id, shellId: handle.shellId });
                this.markStopSource(handle.id, "system");
                if (handle.watcher !== undefined) {
                    handle.timers.clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                void this.reportSessionReapKillFailure("monitor", handle);
                handle.status = "killed";
                handle.stoppedBy = handle.stopSource ?? "system";
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                reaped++;
                continue;
            }
            if (handle.type === "background_bash" && handle.status === "running" && handle.sessionScoped && canAccess(handle, access)) {
                shutdownDebug("session reap: background_bash row", { id: handle.id, shellId: handle.shellId });
                this.markStopSource(handle.id, "system");
                if (handle.watcher !== undefined) {
                    clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                void this.reportSessionReapKillFailure("background_bash", handle);
                handle.status = "killed";
                handle.stoppedBy = handle.stopSource ?? "system";
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                reaped++;
                continue;
            }
            if (handle.type === "workflow" && handle.status === "running" && handle.sessionScoped === true && canAccess(handle, access)) {
                void this.stopWorkflow(handle);
                reaped++;
                continue;
            }
            if (handle.type !== "background_agent" || handle.status !== "running" || !handle.sessionScoped)
                continue;
            if (!canAccess(handle, access))
                continue;
            this.markStopSource(handle.id, "system");
            const reapTerminalNote = handle.onReapTerminal;
            handle.abort.abort();
            this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released", cycle: handle.reviveCycle ?? 0 });
            if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
                handle.terminalNotified = true;
                this.notifier.notify(() => reapTerminalNote(), "taskRegistry.reapTerminalNote");
            }
            reaped++;
        }
        this.pokeBgQuiescence(sessionId);
        for (const hook of this.sessionReapHooks) {
            this.notifier.notify(() => hook(sessionId, scope), "taskRegistry.onSessionReap");
        }
        return reaped;
    }
    reapAllSessionBackground() {
        const pairs = new Map();
        for (const handle of this.handles.values()) {
            if (handle.status !== "running")
                continue;
            if (!handle.sessionScoped)
                continue;
            const sessionKey = handle.type === "workflow" ? handle.originatingSessionId : handle.owner;
            if (sessionKey === undefined)
                continue;
            const key = JSON.stringify([sessionKey, handle.scope ?? null]);
            if (!pairs.has(key))
                pairs.set(key, { owner: sessionKey, ...(handle.scope !== undefined ? { scope: handle.scope } : {}) });
        }
        let reaped = 0;
        for (const p of pairs.values())
            reaped += this.reapSessionBackground(p.owner, p.scope);
        return reaped;
    }
    killAllBackgroundShellsNow(opts) {
        const sessionAccess = opts.sessionId !== undefined ? { owner: opts.sessionId, sessionId: opts.sessionId, scope: opts.scope } : undefined;
        const receipts = [];
        const settledOwners = new Set();
        for (const handle of this.handles.values()) {
            if (handle.type !== "background_bash" && handle.type !== "monitor")
                continue;
            if (sessionAccess !== undefined && !canAccess(handle, sessionAccess))
                continue;
            if (handle.status === "completed")
                continue;
            const retained = shellRowRetained(handle);
            const base = { taskId: handle.id, type: handle.type, ...(handle.owner !== undefined ? { owner: handle.owner } : {}), ...(retained ? { retained: true } : {}) };
            if (retained && !opts.includeRetained && handle.status !== "killed") {
                receipts.push({ ...base, outcome: "retained_skipped" });
                continue;
            }
            let outcome;
            try {
                const seat = handle.env.killBackgroundNow;
                if (seat === undefined)
                    shutdownDebug("hard kill: seat absent", { id: handle.id, shellId: handle.shellId });
                outcome = seat === undefined ? "no_pgid" : seat.call(handle.env, handle.shellId);
                if (seat !== undefined && outcome === "no_pgid")
                    shutdownDebug("hard kill: seat answered no_pgid", { id: handle.id, shellId: handle.shellId });
            }
            catch (err) {
                shutdownDebug("hard kill: seat threw", { id: handle.id, shellId: handle.shellId, error: thrownText(err) });
                outcome = "no_pgid";
            }
            if (outcome !== "killed" && outcome !== "already_gone" && outcome !== "no_pgid")
                outcome = "no_pgid";
            if (outcome === "no_pgid") {
                try {
                    void handle.env.killBackground(handle.shellId).catch(() => undefined);
                }
                catch {
                }
            }
            shutdownDebug("hard kill: row", { id: handle.id, shellId: handle.shellId, outcome, reason: opts.reason ?? "" });
            if (handle.status === "running" && outcome === "killed") {
                this.markStopSource(handle.id, "system");
                if (handle.watcher !== undefined) {
                    if (handle.type === "monitor")
                        handle.timers.clearInterval(handle.watcher);
                    else
                        clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                handle.status = "killed";
                handle.stoppedBy = handle.stopSource ?? "system";
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                if (handle.owner !== undefined)
                    settledOwners.add(handle.owner);
            }
            receipts.push({ ...base, outcome });
        }
        for (const owner of settledOwners)
            this.pokeBgQuiescence(owner);
        return receipts;
    }
    onSessionReap(hook) {
        this.sessionReapHooks.add(hook);
        return () => void this.sessionReapHooks.delete(hook);
    }
    countRunningBackgroundForOwner(owner) {
        let n = 0;
        for (const handle of this.handles.values()) {
            if (handle.type !== "background_bash" && handle.type !== "monitor" && handle.type !== "background_agent")
                continue;
            if (handle.type !== "background_agent" && shellRowRetained(handle))
                continue;
            if (handle.status === "running" && handle.owner === owner)
                n++;
        }
        return n;
    }
    static BG_QUIESCENCE_WATCHERS_MAX = 1_024;
    onBackgroundQuiescence(owner, cb) {
        let set = this.bgQuiescenceWatchers.get(owner);
        if (set === undefined) {
            if (this.bgQuiescenceWatchers.size >= TaskRegistry.BG_QUIESCENCE_WATCHERS_MAX) {
                const oldest = this.bgQuiescenceWatchers.keys().next().value;
                if (oldest !== undefined) {
                    const victims = this.bgQuiescenceWatchers.get(oldest);
                    this.bgQuiescenceWatchers.delete(oldest);
                    queueMicrotask(() => {
                        for (const victim of victims) {
                            this.notifier.notify(() => victim("evicted"), "taskRegistry.bgQuiescence.evicted");
                        }
                    });
                }
            }
            set = new Set();
            this.bgQuiescenceWatchers.set(owner, set);
        }
        set.add(cb);
        return () => {
            const s = this.bgQuiescenceWatchers.get(owner);
            if (s !== undefined) {
                s.delete(cb);
                if (s.size === 0)
                    this.bgQuiescenceWatchers.delete(owner);
            }
        };
    }
    pokeBgQuiescence(owner) {
        if (owner === undefined)
            return;
        const set = this.bgQuiescenceWatchers.get(owner);
        if (set === undefined || set.size === 0)
            return;
        if (this.countRunningBackgroundForOwner(owner) > 0)
            return;
        this.bgQuiescenceWatchers.delete(owner);
        for (const cb of set) {
            this.notifier.notify(() => cb("quiescent"), "taskRegistry.bgQuiescence.quiescent");
        }
    }
    notifyTerminalOnce(handle, send) {
        if (handle.terminalNotified === true)
            return;
        handle.terminalNotified = true;
        this.notifier.notify(() => send(), "taskRegistry.notifyTerminalOnce");
        this.pokeBgQuiescence(handle.owner);
    }
    startBashWatcher(id, onTerminal) {
        const handle = this.handles.get(id);
        if (!handle || handle.type !== "background_bash")
            return;
        handle.spool ??= { stdout: "", stderr: "", rolledChars: 0 };
        const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
        const roll = (s) => rollSpoolText(handle.spool, s, ROLL_CAP);
        let ticking = false;
        const MAX_RETRYABLE_POLL_FAILURES = 3;
        let pollFailures = 0;
        const stop = () => {
            if (handle.watcher !== undefined) {
                clearInterval(handle.watcher);
                handle.watcher = undefined;
            }
        };
        const tick = async () => {
            if (ticking)
                return;
            ticking = true;
            try {
                if (this.handles.get(id) !== handle)
                    return stop();
                if (handle.status !== "running")
                    return stop();
                const r = await handle.env.pollBackground(handle.shellId);
                if (this.handles.get(id) !== handle || handle.status !== "running")
                    return stop();
                if (!r.ok) {
                    if ((r.error.code === "timeout" || r.error.code === "io") && ++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
                        return;
                    handle.status = "failed";
                    handle.updatedAt = Date.now();
                    mintCompletionId(handle);
                    stop();
                    const lostResult = bashTerminalResult(handle.spool);
                    this.notifyTerminalOnce(handle, () => onTerminal({
                        task_id: id,
                        task_type: "background_bash",
                        ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
                        ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
                        status: "failed",
                        summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "failed")} — lost its background process (${r.error.message}); no further output will arrive.${bashMirrorGapNote(handle)}`,
                        ...(lostResult.length > 0 ? { result: lostResult } : {}),
                        ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                    }));
                    return;
                }
                pollFailures = 0;
                const spool = handle.spool;
                accountDroppedBytes(spool, r.value);
                spool.stdout = roll(spool.stdout + r.value.stdout);
                spool.stderr = roll(spool.stderr + r.value.stderr);
                if (handle.outputFile !== undefined && (r.value.stdout.length > 0 || r.value.stderr.length > 0)) {
                    const ap = await handle.env.appendFile(handle.outputFile, r.value.stdout + r.value.stderr);
                    if (!ap.ok)
                        spool.mirrorFailed = true;
                }
                handle.status = statusFromBackground(r.value.status, r.value.exitCode);
                if (handle.status === "killed" && handle.stoppedBy === undefined) {
                    handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
                }
                handle.updatedAt = Date.now();
                if (r.value.status !== "running") {
                    mintCompletionId(handle);
                    spool.exitCode = r.value.exitCode;
                    if (r.value.timedOut === true) {
                        spool.timedOut = true;
                        if (r.value.timeoutSec !== undefined)
                            spool.timeoutSec = r.value.timeoutSec;
                    }
                    stop();
                    const desc = (handle.description ?? "background command").slice(0, 200);
                    const success = r.value.status === "exited" && r.value.exitCode === 0;
                    const resultText = success ? spool.stdout.slice(-2_000) : bashTerminalResult(spool);
                    this.notifyTerminalOnce(handle, () => onTerminal({
                        task_id: id,
                        task_type: "background_bash",
                        ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
                        ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
                        status: handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed",
                        ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
                        ...(r.value.status === "exited" && r.value.exitCode !== undefined ? { exitCode: r.value.exitCode } : {}),
                        summary: (r.value.status === "exited"
                            ? terminalTaskSummary("bash", desc, r.value.exitCode === 0 ? "completed" : "failed", r.value.exitCode)
                            : r.value.timedOut === true
                                ? r.value.timeoutSec !== undefined
                                    ? `${terminalTaskSummary("bash", desc, "killed")} — timed out after ${r.value.timeoutSec}s (its own background time budget); the process was killed`
                                    : `${terminalTaskSummary("bash", desc, "killed")} — timed out (its own background time budget); the process was killed`
                                : `${terminalTaskSummary("bash", desc, handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed")} — ${r.value.status}`) +
                            bashMirrorGapNote(handle) +
                            droppedGapNote(spool),
                        ...(resultText.length > 0 ? { result: resultText } : {}),
                        ...(handle.status === "killed" && resultText.length > 0 ? { partial: true } : {}),
                        ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                    }));
                }
            }
            catch (e) {
                if (this.handles.get(id) !== handle || handle.status !== "running")
                    return stop();
                if (++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
                    return;
                handle.status = "failed";
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                stop();
                const crashedResult = bashTerminalResult(handle.spool);
                this.notifyTerminalOnce(handle, () => onTerminal({
                    task_id: id,
                    task_type: "background_bash",
                    ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
                    ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
                    status: "failed",
                    summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "failed")} — lost its background process (poll failed: ${thrownText(e)}); no further output will arrive.${bashMirrorGapNote(handle)}`,
                    ...(crashedResult.length > 0 ? { result: crashedResult } : {}),
                    ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                }));
            }
            finally {
                ticking = false;
            }
        };
        handle.watcher = setInterval(() => void tick(), 1_000);
        handle.watcher.unref?.();
    }
    runningShellIds(env) {
        const out = [];
        for (const handle of this.handles.values()) {
            if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && handle.env === env) {
                out.push(handle.shellId);
            }
        }
        return out;
    }
    registerMonitor(input) {
        return registerMonitorLane(this.core, input);
    }
    registerWorkflow(input) {
        return registerWorkflowLane(this.core, input);
    }
    async pollTask(id, access, opts = {}) {
        const taskId = id.trim();
        const deadline = opts.block ? blockDeadline(opts.timeoutMs, Date.now()) : undefined;
        const handle = this.resolveInternal(taskId, access);
        if (handle) {
            if (handle.type === "background_bash")
                return this.pollBackgroundBash(handle, taskId, access, opts.filter, deadline, opts.signal);
            if (handle.type === "background_agent")
                return this.pollBackgroundAgent(handle, deadline, opts.signal, opts.oneShot, opts.toolResultStore, access.sessionId, opts.checkpointSeat);
            if (handle.type === "monitor")
                return this.pollMonitor(handle, opts.filter, deadline, opts.signal);
            return this.pollWorkflow(handle, access, deadline, opts.signal);
        }
        if (opts.workflowStore) {
            for (;;) {
                const run = await opts.workflowStore.get(taskId);
                if (!run || !canAccessWorkflowRun(run, access))
                    break;
                if (run.status !== "running" || deadline === undefined || Date.now() >= deadline || opts.signal?.aborted) {
                    const out = formatWorkflowRun(run);
                    if (run.status === "running" && deadline !== undefined && Date.now() >= deadline)
                        out.details.retrieval_status = "timeout";
                    return out;
                }
                await sleepPollStep(deadline, opts.signal);
            }
        }
        if (opts.agentStore && access.scope !== undefined && DURABLE_AGENT_HANDLE_RE.test(taskId)) {
            for (;;) {
                let row;
                try {
                    row = await opts.agentStore.get(taskId, access.scope);
                }
                catch {
                    break;
                }
                if (!row || !canAccessAgentRecord(row, access))
                    break;
                if (row.status === "running" && row.writerId === this.writerId)
                    break;
                if (row.status !== "running")
                    return this.serveDurableAgentRow(row, opts.checkpointSeat);
                if (deadline === undefined || Date.now() >= deadline || opts.signal?.aborted) {
                    const stateRead = readAgentState(agentStateSourceOfRecord(row), opts.checkpointSeat, Date.now());
                    const agentState = stateRead instanceof Promise ? await stateRead : stateRead;
                    const body = `status: unknown (not running on this engine instance)
state: ${agentStateSummary(agentState)}
The durable record is still marked running (last update ${new Date(row.updatedAt).toISOString()}), but this engine instance is not executing it — the outcome is unknown here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles the record as interrupted.`;
                    return {
                        content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
                        isError: true,
                        details: {
                            task_id: row.handle,
                            type: "background_agent",
                            status: "running",
                            retrieval_status: "not_ready",
                            error: "not_local",
                            code: "not_local",
                            agentState,
                        },
                    };
                }
                await sleepPollStep(deadline, opts.signal);
            }
        }
        return noTask(taskId, runningAgentFooterLane(this.core, access));
    }
    async stopTask(id, access, opts = {}) {
        const taskId = id.trim();
        const handle = this.resolveInternal(taskId, access);
        if (handle) {
            if (handle.type === "background_bash")
                return this.stopBackgroundBash(handle);
            if (handle.type === "background_agent")
                return this.stopBackgroundAgent(handle);
            if (handle.type === "monitor")
                return this.stopMonitor(handle);
            return this.stopWorkflow(handle);
        }
        if (opts.workflowStore) {
            const run = await opts.workflowStore.get(taskId);
            if (run && canAccessWorkflowRun(run, access)) {
                return {
                    content: JSON.stringify({
                        task_id: taskId,
                        runId: taskId,
                        status: run.status,
                        stopped: false,
                        error: "workflow task is not attached to this process; cannot stop it here",
                    }),
                    details: {
                        task_id: taskId,
                        type: "workflow",
                        status: run.status,
                        retrieval_status: "success",
                        error: "not_local",
                        code: "not_local",
                    },
                };
            }
        }
        if (opts.agentStore && access.scope !== undefined && DURABLE_AGENT_HANDLE_RE.test(taskId)) {
            let row = null;
            try {
                row = await opts.agentStore.get(taskId, access.scope);
            }
            catch {
                row = null;
            }
            if (row && canAccessAgentRecord(row, access) && !(row.status === "running" && row.writerId === this.writerId)) {
                return {
                    content: row.status === "running"
                        ? `${row.handle} is not attached to this engine instance; cannot stop it here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles it as interrupted.`
                        : row.status === "parked"
                            ? `${row.handle} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`
                            : alreadyTerminalStopNote(row.handle, row.status),
                    details: {
                        task_id: row.handle,
                        type: "background_agent",
                        status: row.status,
                        retrieval_status: row.status === "parked" ? "not_ready" : "success",
                        ...(row.status === "running" ? { error: "not_local", code: "not_local" } : {}),
                        ...(row.status === "parked" ? { error: "parked_pending_approval", code: "parked_pending_approval" } : {}),
                    },
                };
            }
        }
        const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
        if (byName.status === "found")
            return this.stopBackgroundAgent(byName.handle);
        if (byName.status === "ambiguous") {
            return {
                content: byName.message,
                details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
                isError: true,
            };
        }
        return noTaskForStop(taskId, {
            ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}),
            runningAgents: runningAgentFooterLane(this.core, access),
        });
    }
    deliverConfirmTimeoutMs = 10_000;
    lastGcAt = 0;
    list(access, opts = {}) {
        const limit = Math.min(500, Math.max(1, Math.floor(opts.limit ?? 50)));
        const out = [];
        for (const h of this.handles.values()) {
            if (!canAccess(h, access))
                continue;
            out.push({
                task_id: h.id,
                type: h.type,
                status: h.status,
                sessionScoped: h.sessionScoped === true,
                ...((h.type === "background_bash" || h.type === "monitor") && shellRowRetained(h) ? { retained: true } : {}),
                ...(h.description !== undefined ? { description: h.description } : {}),
                ...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}),
                createdAt: h.createdAt,
                updatedAt: h.updatedAt,
            });
        }
        out.sort((a, b) => b.createdAt - a.createdAt);
        return out.slice(0, limit);
    }
    maybeGc(now = Date.now()) {
        if (now - this.lastGcAt < 60_000)
            return 0;
        this.lastGcAt = now;
        return this.gc(now);
    }
    gc(now = Date.now(), terminalTtlMs = 60 * 60 * 1000) {
        let deleted = 0;
        for (const [id, handle] of this.handles) {
            if (handle.status === "running" || handle.status === "pending")
                continue;
            if (this.claimingHandles.has(id) || this.reapingHandles.has(id))
                continue;
            if (now - handle.updatedAt < terminalTtlMs)
                continue;
            this.handles.delete(id);
            if (handle.type === "background_bash" && handle.watcher !== undefined)
                clearInterval(handle.watcher);
            if (handle.type === "monitor" && handle.watcher !== undefined) {
                handle.timers.clearInterval(handle.watcher);
                handle.watcher = undefined;
            }
            deleted++;
        }
        return deleted;
    }
    clearBackgroundForOwner(access) {
        let deleted = 0;
        const evictedRunningOwners = new Set();
        for (const [id, handle] of this.handles) {
            if (handle.type === "monitor" && !handle.sessionScoped && canAccess(handle, access)) {
                if (monitorInTimeoutWindow(handle))
                    continue;
                if (shellRowRetained(handle) && handle.status === "running")
                    continue;
                this.handles.delete(id);
                if (handle.status === "running" && handle.owner !== undefined)
                    evictedRunningOwners.add(handle.owner);
                if (handle.watcher !== undefined) {
                    handle.timers.clearInterval(handle.watcher);
                    handle.watcher = undefined;
                }
                deleted++;
                continue;
            }
            if (handle.type !== "background_bash" || !canAccess(handle, access))
                continue;
            if (handle.sessionScoped)
                continue;
            if (shellRowRetained(handle) && handle.status === "running")
                continue;
            this.handles.delete(id);
            if (handle.status === "running" && handle.owner !== undefined)
                evictedRunningOwners.add(handle.owner);
            if (handle.watcher !== undefined)
                clearInterval(handle.watcher);
            deleted++;
        }
        for (const owner of evictedRunningOwners)
            this.pokeBgQuiescence(owner);
        return deleted;
    }
    resolveInternal(id, access) {
        const handle = this.handles.get(id);
        if (!handle || !canAccess(handle, access))
            return undefined;
        return handle;
    }
    async pollBackgroundBash(handle, requestedId, access, filter, deadline, signal) {
        let rx;
        if (filter !== undefined) {
            try {
                rx = new RegExp(filter);
            }
            catch {
                return {
                    content: `Error (TaskOutput): invalid filter regex: ${filter}`,
                    details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status: "not_ready" },
                    isError: true,
                };
            }
        }
        if (!hasBackgroundShell(handle.env))
            return noTask(requestedId, runningAgentFooterLane(this.core, access));
        if (handle.spool !== undefined) {
            while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
                await sleepPollStep(deadline, signal);
            }
            const spool = handle.spool;
            const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
            const running = handle.status === "running";
            const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
            const body = renderSpoolBody(spool, { running, handleStatus: handle.status, shape, rolledExtraClause: "", drainNote: "" });
            return {
                content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
                details: {
                    task_id: handle.id,
                    type: "background_bash",
                    status: handle.status,
                    retrieval_status: retrieval,
                    ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
                    ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                    details: {
                        status: running ? "running" : handle.status,
                        exitCode: spool.exitCode,
                        replay: SPOOL_REPLAY,
                        ...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
                        ...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
                    },
                },
            };
        }
        const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
        let rolledChars = 0;
        const roll = (s) => {
            if (s.length <= ROLL_CAP)
                return s;
            const half = Math.floor(ROLL_CAP / 2);
            rolledChars += s.length - 2 * half;
            return s.slice(0, half) + s.slice(s.length - half);
        };
        let stdout = "";
        let stderr = "";
        const dropAcct = {};
        for (;;) {
            const r = await handle.env.pollBackground(handle.shellId);
            if (!r.ok) {
                if (r.error.code === "not_found")
                    return noTask(requestedId, runningAgentFooterLane(this.core, access));
                const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
                const carriedOutput = stdout.length > 0 || stderr.length > 0;
                const salvaged = carriedOutput
                    ? `\n--- output accumulated before the error ---\n${clipTaskOutput(stdout, handle.outputFile)}${stderr ? `\n--- stderr ---\n${clipTaskOutput(stderr, handle.outputFile)}` : ""}`
                    : "";
                return {
                    content: `Error (TaskOutput): ${r.error.message}${salvaged}`,
                    details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, ...(carriedOutput ? { details: { replay: INCREMENT_REPLAY } } : {}), error: r.error.code },
                    isError: true,
                };
            }
            stdout = roll(stdout + r.value.stdout);
            stderr = roll(stderr + r.value.stderr);
            accountDroppedBytes(dropAcct, r.value);
            handle.status = statusFromBackground(r.value.status, r.value.exitCode);
            if (handle.status === "killed" && handle.stoppedBy === undefined)
                handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
            handle.updatedAt = Date.now();
            const running = r.value.status === "running";
            if (!running)
                mintCompletionId(handle);
            if (!running || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
                const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
                const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
                const timedOutClause = r.value.timedOut === true ? ` (timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"})` : "";
                const displayStatus = (r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status) + timedOutClause;
                const droppedNote = spoolDropNote(dropAcct) +
                    (rolledChars > 0 ? `[!] ${rolledChars} accumulated char(s) dropped from the middle during the blocked wait (memory bound)\n` : "");
                const body = `status: ${displayStatus}\n${droppedNote}--- ${stdoutHeadingOf(INCREMENT_REPLAY, droppedNote !== "")} ---\n${shape(stdout) || "(none)"}\n` +
                    `--- ${REPLAY_FACE[INCREMENT_REPLAY].stderr} ---\n${shape(stderr) || "(none)"}`;
                return {
                    content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
                    details: {
                        task_id: handle.id,
                        type: "background_bash",
                        status: handle.status,
                        retrieval_status: retrieval,
                        ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
                        ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                        details: {
                            status: r.value.status,
                            exitCode: r.value.exitCode,
                            replay: INCREMENT_REPLAY,
                            bytesDroppedBeforeCursor: dropAcct.droppedBytes ?? 0,
                            ...(r.value.timedOut === true ? { timedOut: true, ...(r.value.timeoutSec !== undefined ? { timeoutSec: r.value.timeoutSec } : {}) } : {}),
                        },
                    },
                };
            }
            await sleepPollStep(deadline, signal);
        }
    }
    async pollWorkflow(handle, access, deadline, signal) {
        return pollWorkflowLane(handle, access, deadline, signal);
    }
    async stopBackgroundBash(handle) {
        const wasRunning = handle.status === "running";
        if (handle.watcher !== undefined) {
            clearInterval(handle.watcher);
            handle.watcher = undefined;
        }
        this.markStopSource(handle.id, "parent");
        const r = await handle.env.killBackground(handle.shellId);
        if (handle.status !== "running") {
            return {
                content: wasRunning ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
                details: {
                    task_id: handle.id,
                    type: "background_bash",
                    status: handle.status,
                    retrieval_status: "success",
                    ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
                    ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                },
            };
        }
        if (!r.ok) {
            if (r.error.code === "not_found") {
                if (handle.stoppedBy === undefined)
                    handle.stoppedBy = handle.stopSource ?? "parent";
                handle.status = "killed";
                handle.updatedAt = Date.now();
                mintCompletionId(handle);
                this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, true));
                return {
                    content: `Terminated ${handle.id} (the process was already gone).`,
                    details: {
                        task_id: handle.id,
                        type: "background_bash",
                        status: "killed",
                        retrieval_status: "success",
                        stoppedBy: handle.stoppedBy,
                        ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
                    },
                };
            }
            const restored = handle.onTerminal !== undefined;
            if (restored && handle.watcher === undefined)
                this.startBashWatcher(handle.id, handle.onTerminal);
            const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
            return {
                content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running.` +
                    (restored ? " Its watcher keeps running: TaskOutput still works and the completion notification will still fire." : " TaskOutput still reflects its live status."),
                details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
                isError: true,
            };
        }
        if (handle.stoppedBy === undefined)
            handle.stoppedBy = handle.stopSource ?? "parent";
        handle.status = "killed";
        handle.updatedAt = Date.now();
        mintCompletionId(handle);
        await drainBashTailBestEffort(handle);
        this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
        return {
            content: `Terminated ${handle.id}.`,
            details: {
                task_id: handle.id,
                type: "background_bash",
                status: "killed",
                retrieval_status: "success",
                stoppedBy: handle.stoppedBy,
                ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
            },
        };
    }
    async pollMonitor(handle, filter, deadline, signal) {
        return pollMonitorLane(handle, filter, deadline, signal);
    }
    async stopMonitor(handle) {
        return stopMonitorLane(this.core, handle);
    }
    async stopWorkflow(handle) {
        return stopWorkflowLane(handle);
    }
}
export const defaultTaskRegistry = new TaskRegistry();
export function createTaskOutputTool(opts) {
    const caps = { ...REGISTRY_TASK_TOOL_CAPS, notification: opts.notificationWired !== false };
    return defineTool({
        name: TASK_OUTPUT_TOOL_NAME,
        ...toolFace("task-output"),
        description: composeTaskOutputDescription(caps),
        parameters: composeTaskOutputParams(caps),
        execute: async (rawArgs, ctx) => {
            const args = rawArgs;
            const id = resolveTaskIdArg(args.task_id);
            if (!id)
                return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
            const requestedMs = typeof args.timeout === "number" ? args.timeout : undefined;
            let effectiveTimeoutMs = requestedMs;
            let waitClampNote = "";
            const wall = opts.deadlineMs?.();
            const requestedWaitMs = Math.max(1, Math.floor(requestedMs ?? BLOCK_DEFAULT_TIMEOUT_MS));
            if (args.block !== false && wall !== undefined) {
                const remainMs = wall - Date.now() - BLOCK_WAIT_WRITEOUT_RESERVE_MS;
                const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS, requestedWaitMs);
                if (remainMs <= 0) {
                    effectiveTimeoutMs = 1;
                    waitClampNote =
                        "\n\nNOTE: wait skipped — the task's wall-clock deadline has been reached. Do NOT wait on this background task again; write out your results now (an unfinished answer beats a hard timeout).";
                }
                else if (remainMs < wantMs) {
                    effectiveTimeoutMs = remainMs;
                    waitClampNote = `\n\nNOTE: wait clamped to ${Math.round(remainMs / 1000)}s (requested ${Math.round(requestedWaitMs / 1000)}s) — the task's wall-clock deadline is near. If the task is still running after this wait, do NOT re-wait: proceed with other work or write out your results now.`;
                }
            }
            const r = await opts.registry.pollTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, {
                workflowStore: opts.workflowStore,
                agentStore: opts.agentStore,
                filter: typeof args.filter === "string" ? args.filter : undefined,
                block: args.block !== false,
                timeoutMs: effectiveTimeoutMs,
                signal: ctx.signal,
                oneShot: opts.oneShot,
                toolResultStore: opts.toolResultStore,
                checkpointSeat: opts.checkpointSeat,
            });
            const { type: taskType, ...rest } = r.details;
            return {
                content: r.content + waitClampNote,
                details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest },
                ...(r.isError ? { isError: true } : {}),
            };
        },
    });
}
export function createTaskStopTool(opts) {
    return defineTool({
        name: TASK_STOP_TOOL_NAME,
        ...toolFace("task-stop"),
        description: composeTaskStopDescription(REGISTRY_TASK_TOOL_CAPS),
        parameters: TASK_STOP_PARAMS,
        execute: async (rawArgs, ctx) => {
            const args = rawArgs;
            const id = resolveTaskIdArg(args.task_id, args.shell_id);
            if (!id)
                return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
            const r = await opts.registry.stopTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, { workflowStore: opts.workflowStore, agentStore: opts.agentStore });
            const { type: taskType, ...rest } = r.details;
            return {
                content: r.content,
                details: { type: "task-stop", message: r.content, task_type: taskType ?? "unknown", ...rest },
                ...(r.isError ? { isError: true } : {}),
            };
        },
    });
}