@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,368 lines • 70.9 kB
JavaScript
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 } : {}),
};
},
});
}