import type { AgentTool, ExecutionEnv } from "../internal/harness.js"; import type { ToolEffect } from "./types.js"; import { type BackgroundShellCapability, type BackgroundShellId } from "./background-shell.js"; import { type WorkflowRunStore } from "./workflow-run-store.js"; import type { WorkflowHandle } from "../orchestration/workflow.js"; import type { TaskNotificationPayload } from "./task-notification.js"; export type SemaTaskType = "background_bash" | "workflow" | "background_agent" | "monitor"; /** design/134 §3.3 (R3): WHO initiated a kill — recorded at the initiation site (first-marker-wins), * because the settle-side then/catch context has already lost it. OPEN ENUM on the wire, now in the * TYPE too (`(string & {})` keeps literal completion while admitting future values like "timeout" * without a breaking change): consumers must tolerate unknown values and fall back to default copy. * SINGLE SOURCE of the shape — two wire mirrors are INLINED to avoid import cycles (this module sits * above both) and must stay in sync: types.ts `BackgroundChildEvent.stoppedBy` and * task-notification.ts `TaskNotificationPayload.stoppedBy`. */ export type StopSource = "user" | "parent" | "system" | (string & {}); export type SemaTaskStatus = "pending" | "running" | "completed" | "failed" | "killed" | "cancelled"; export type TaskRetrievalStatus = "success" | "not_ready" | "timeout"; export interface UnifiedTaskOutput { task_id: string; type?: SemaTaskType; status?: SemaTaskStatus | string; retrieval_status: TaskRetrievalStatus; content?: string; error?: string; /** design/134 §3.3: only present when `status === "killed"` (open enum — see {@link StopSource}). */ stoppedBy?: StopSource; details?: unknown; } export interface SemaTaskHandle { id: string; type: SemaTaskType; status: SemaTaskStatus; description?: string; owner?: string; scope?: string; toolUseId?: string; createdAt: number; updatedAt: number; outputFile?: string; outputOffset?: number; notified?: boolean; } export interface TaskKind { type: SemaTaskType; effect: ToolEffect; } export interface TaskAccess { owner?: string; scope?: string; /** design/129: the caller's SESSION id — matches session-scoped handles across turns (a fresh per-turn * spec.taskId changes `owner`, but the session is the stable key a background child outlives turns under). */ sessionId?: string; } /** design/135 G2 (Monitor): injectable timer/clock seam for the monitor watcher — the 200ms batch window * and the timeout/storm checks must be drivable by a test without real sleeps (design/87 discipline). * `setInterval` receives an ASYNC tick (the default adapter fires it void; a fake can await it). */ export interface MonitorTimers { setInterval: (fn: () => unknown, ms: number) => unknown; clearInterval: (handle: unknown) => void; now: () => number; } export interface RegisterBackgroundBashInput extends TaskAccess { shellId: BackgroundShellId; env: ExecutionEnv & BackgroundShellCapability; description?: string; toolUseId?: string; id?: string; now?: number; /** 黑板 [636]① (design/129 discipline): session-resident background bash — `owner` MUST be the * sessionId (a later turn's fresh per-turn owner reaches it via `access.sessionId`). Survives the * run teardown (settle/evict skip + dispose except-list); terminal anchor = `reapSessionBackground`. */ sessionScoped?: true; /** design/116 §7 G2b: when set, the registry WATCHES the process (1s poll, increments spooled with a * rolling bound) and fires ONE completion task-notification at the terminal state — the background-bash * half of CC's LocalShellTask completion notify. Also flips TaskOutput to re-readable spool reads. */ onTerminal?: (notification: TaskNotificationPayload) => void; } export interface RegisterMonitorInput extends TaskAccess { shellId: BackgroundShellId; env: ExecutionEnv & BackgroundShellCapability; description?: string; toolUseId?: string; /** Session-level residency: no watcher timeout; reaped by session release (design/129 semantics — * when set, `owner` MUST be the session id and `sessionScoped` should be set too). */ persistent?: boolean; /** design/129 explicit-flag discipline: session-scoped access matching + session-terminal reap. */ sessionScoped?: true; /** Non-persistent watch deadline (ms). The tool clamps to [1, 3_600_000], default 300_000. */ timeoutMs?: number; /** The event sink — each stdout-line batch AND the single terminal state fires exactly one payload. */ onEvent?: (n: TaskNotificationPayload) => void; /** design/87: injectable timers/clock (tests drive ticks + time manually; default = real timers). */ timers?: MonitorTimers; /** Batch window (ms, default 200): lines arriving within one tick coalesce into one notification. */ batchWindowMs?: number; /** Event-storm auto-stop threshold (batches per rolling 60s, default 50). */ maxBatchesPerMinute?: number; now?: number; } export interface RegisterBackgroundAgentInput extends TaskAccess { description?: string; toolUseId?: string; abort: AbortController; now?: number; /** design/129: register as session-scoped (owner MUST be the sessionId; see the handle flag). */ sessionScoped?: true; } export interface RegisterWorkflowInput extends TaskAccess { runId: string; handle: WorkflowHandle; store?: WorkflowRunStore; description?: string; toolUseId?: string; now?: number; /** * 黑板 [405] — fired AT MOST ONCE when a `pollTask` through THIS registry entry (= the originating * session's own lane; cross-session store-fallback polls don't fire it) serves a TERMINAL snapshot. * The RunWorkflow tool wires it to {@link import("../orchestration/run-workflow-tool.js").WorkflowCompletionNotifier.ackServed} * so the deployment can drop the now-redundant completion-inbox entry. Errors are swallowed. */ onServedTerminal?: () => void; } export interface TaskPollOptions { workflowStore?: WorkflowRunStore; filter?: string; /** design/116 W3 (CC TaskOutput `block`): wait until the task reaches a terminal state (or `timeoutMs`) * instead of returning the instantaneous snapshot — saves the model blind re-poll turns. The TOOL layer * defaults this to TRUE (CC 187 `block: default(!0)`, clay 拍 2026-07-02); registry callers pass it explicitly. */ block?: boolean; /** Max wait for `block` (ms). Default 30s, capped at 600s (CC 187). */ timeoutMs?: number; /** Abort signal — a blocked wait must stop when the tool call is cancelled. */ signal?: AbortSignal; } export interface TaskStopOptions { workflowStore?: WorkflowRunStore; } export interface TaskToolOptions extends TaskAccess { registry: TaskRegistry; workflowStore?: WorkflowRunStore; } /** design/135 G2 Monitor defaults (CC schema: 200ms batch window; timeout default 5min / max 60min; * "too many events" auto-stop — threshold is a sema choice, disclosed in the stop notification). */ export declare const MONITOR_BATCH_WINDOW_MS = 200; export declare const MONITOR_DEFAULT_TIMEOUT_MS = 300000; export declare const MONITOR_MAX_TIMEOUT_MS = 3600000; export declare const MONITOR_MAX_BATCHES_PER_MINUTE = 50; /** * CC `FHl` shape (pretty.js:458012-458016) when the task has an on-disk output file: TAIL-keep (a * background command's latest output is the valuable end) behind a `[Truncated. Full output: ]` * pointer so the model can Read the full spool. Without a file (workflow/agent results), keep the * legacy head+tail middle-omission — there is no path to point at, and the head often carries status. */ export declare function clipTaskOutput(s: string, fullOutputPath?: string): string; export declare class TaskRegistry { private handles; /** codex 终审 1.255 F1: observers of the design/129 session terminal anchor — `reapSessionBackground` * fires each hook (swallow-guarded) AFTER reaping, so session-anchored side state (the session-scoped * subagent retain ledger) releases on the SAME deployment call. Registered idempotently per consumer. */ private sessionReapHooks; private legacyToTaskId; mintTaskId(type: SemaTaskType): string; registerBackgroundBash(input: RegisterBackgroundBashInput): string; /** design/115 P3: register a background sub-agent run. The CALLER owns driving the child promise and * calling {@link settleBackgroundAgent} at the end; the registry provides the unified task_id, the * owner/scope guard, poll/stop dispatch, and terminal GC — exactly like the other two kinds. */ registerBackgroundAgent(input: RegisterBackgroundAgentInput): string; /** design/134 §3.3 (R3 single-source): record WHO is about to kill `id`, at the initiation site, * BEFORE the abort()/status flip — the ordering is load-bearing: the guard below refuses markers on a * non-running handle, so a caller that flips first loses its claim and attribution falls back to * "system". First-marker-wins: an earlier marker (e.g. a service-wire "user") is never overwritten. * Applies to background_agent AND background_bash (same class); workflow cancellation is out of scope. */ markStopSource(id: string, source: StopSource): void; /** design/134 KNOWN-ISSUES close-out (legacy env-direct TaskStop): the stop-source annotation face * addressed by the ENV-level shellId — for a kill initiator that holds only the env handle (the legacy * registry-less TaskStop band) and is about to `env.killBackground` directly, i.e. without going * through {@link stopTask}. Resolves the legacy shellId → unified task id and delegates to * {@link markStopSource}, so the running-only + first-marker-wins guards hold unchanged. `env` * identity is part of the match when provided (same discipline as {@link sessionResidentShellIds}): * on a shared registry one env's kill must never claim another env's row. No access guard — this * marks attribution only (never kills); the caller already demonstrated kill capability on the env. */ markStopSourceByShellId(shellId: string, source: StopSource, env?: unknown): void; /** Withdraw a pending stop-source marker after the kill it announced FAILED (小优化批 2026-07-13): * a marker left by a failed legacy kill would win first-marker-wins against the NEXT, real stopper * (e.g. a later user TaskStop reads "parent"). Clears ONLY the exact still-pending marker the caller * minted — same (shellId, env) resolution as {@link markStopSourceByShellId}; a row already settled, * or carrying a different source, is left alone. */ clearPendingStopSourceByShellId(shellId: string, source: StopSource, env?: unknown): void; /** design/134 §3.3: narrow terminal read for consumers (subagent sinkEmit/notify) — only a KILLED row * has an attribution; "system" is the defensive floor for a row that went killed without a landed * stoppedBy (e.g. legacy flips). Returns undefined for running/completed/failed/evicted rows. */ getStopAttribution(id: string): StopSource | undefined; /** Terminal update for a background agent (the spawn-side then/catch calls this exactly once). * design/129-B: returns the WINNING terminal status — the earlier writer's when this settle is refused * (first-writer-wins), `undefined` when the handle is gone (evicted). Callers report THIS in their * notification/terminal event so the push never contradicts the registry row (a completed-notify over a * killed row — the TaskStop-then-late-resolve race — was an observable incoherence). */ settleBackgroundAgent(id: string, outcome: { status: "completed" | "failed" | "killed"; result?: string; error?: string; stoppedBy?: StopSource; }): "completed" | "failed" | "killed" | undefined; /** Abort every background agent belonging to `access` (parent-task teardown — a finished parent must * not leave orphan child runs burning tokens; mirrors clearBackgroundForOwner for bash). */ abortBackgroundAgentsForOwner(access: TaskAccess, opts?: { skipSessionScoped?: boolean; sessionScopedOnly?: boolean; }): number; /** service 黑板 [418]①②: bulk stop-source attribution for one owner — the registry-side primitive that * replaces the service's `list()+markStopSource` per-row loop, which had TWO holes: ① `list()` didn't * expose `sessionScoped`, so on the resume leg (canonical taskId==sessionId, rebuilt taskConfig without * spec.taskId) task-scoped and session-scoped children share the same owner KEY VALUE and can't be told * apart; ② `list()`'s 500-row display cap silently skipped rows on >500-children runs. * Same owner-filter semantics as {@link abortBackgroundAgentsForOwner}: {@link canAccess} + the EXPLICIT * `sessionScoped` flag (design/129 — never inferred from key values), over a DIRECT handle-map walk * (no list() cap in the path). Covers every markable kind ({@link markStopSource}'s own set: * background_agent / background_bash / monitor); each row is delegated to `markStopSource`, so the * running-only + first-marker-wins guards hold unchanged — an existing claim is never overwritten. * Returns how many rows NEWLY took the marker (already-marked / non-running rows don't count). */ markStopSourceForOwner(access: TaskAccess, source: StopSource, opts?: { skipSessionScoped?: boolean; }): number; /** 飞轮 [506]③ — the run-teardown KILLED-receipt producer for the SHELL lanes (background_bash + per-run * monitor). The runner teardown disposes the processes (`disposeBackgroundShells`) and evicts the rows * (`clearBackgroundForOwner`) — after which NO producer can ever mint the killed notification: the bash * watcher's next poll sees a vanished shell ("nothing to notify about") or its evicted handle and just * stops. So a mid-turn abort (or a normal turn end) silently swallowed children the model had been * promised notifications for ("You will be notified when it completes — do not poll"). The AGENT lane * already has its producer (the child promise's settle → notify, [496]④); this is the same receipt for * the shell lanes — call it BEFORE dispose/evict so the sink chain (onTerminal/onEvent → the run's * notification wrapper, whose lane is already down at teardown) PARKS the receipt on the per-session * pending store and the session's next run drains it (the [492]② lane, one shape for all producers). * * Attribution (design/134 §3.3): `markStopSource(source)` first — first-marker-wins, so a deployment's * earlier explicit mark (e.g. a service-wire "user" on abort) is never overwritten; `stoppedBy` then * lands from the marker. Rows without a sink still settle (registry truth), they just have no receipt * to send. Session-scoped rows are skipped under `skipSessionScoped` (they outlive the turn; their * terminal anchor is `reapSessionBackground`). KNOWN small race, recorded: a child that exited within * the last watcher-poll interval (~1s) but whose exit the watcher hasn't observed yet still reads * "running" here and is settled `killed` — the receipt is at worst one status coarser, never silent. * * codex 1.257.3 review (HIGH): the settle EXECUTES the kill itself (await `killBackground`, Result-typed) * before minting the receipt — previously the receipt claimed "killed" while the actual kill was left to * the later `disposeBackgroundShells`, whose failure was swallowed (process alive + registry cleared = * the receipt lied). A failed kill now mints an HONEST receipt naming the failure (the env dispose is * still the idempotent backstop). Returns how many rows were settled. */ settleKilledForOwner(access: TaskAccess, opts?: { source?: StopSource; skipSessionScoped?: boolean; envDying?: boolean; }): Promise; /** design/129: the session-scoped children's TERMINAL anchor — call from your session release/sweep. * Aborts every running session-scoped background child registered under `sessionId` and settles it * `killed` (the child's own catch/then would also classify the abort as killed; this makes the registry * state immediate rather than waiting on the child's promise). Idempotent. */ reapSessionBackground(sessionId: string, scope?: string): number; /** 黑板 [651] (reap 责任裁定 B) — the FULL-registry reap for the ENGINE's own exit path. TOC form: * the engine process IS the session terminal (the shell has no line to call — [650] live evidence: * detached bg children reparent to PID 1 and outlive the engine), so the engine's SIGHUP/SIGTERM/ * normal-exit terminus calls THIS. Enumerates every running session-scoped row's (owner, scope) * pair (owner = the sessionId by design/129 discipline) and runs the per-session reap on each — * retain-declared envs keep their processes (rows settle only), exactly like the per-session anchor. * Timing discipline ([651]①): call at the hardShutdown TERMINUS, never at drain start — an * in-flight turn may still be TaskOutput-following a bg process. Returns total rows reaped. */ reapAllSessionBackground(): number; /** codex 终审 1.255 F1 — subscribe to `reapSessionBackground` (the design/129 session terminal anchor). * Returns an unsubscribe. Hooks are best-effort observers; a throwing hook is swallowed. */ onSessionReap(hook: (sessionId: string, scope?: string) => void): () => void; /** G2b watcher (design/116 §7): 1s poll loop that spools increments (rolling bound) and fires ONE * completion notification at the terminal state. `unref()`d — a watcher never keeps the process alive. * Stops itself on terminal, on a vanished shell (env disposed), or when the handle is evicted. */ private startBashWatcher; /** design/135 G2: register a spawned background process as a MONITOR — the watcher polls at the batch * window (200ms), turns each completed stdout line into an event, batches lines within one tick into * ONE notification, enforces the non-persistent timeout, auto-stops an event storm, and fires exactly * one terminal notification (exit code / timeout / auto-stop / env-side kill). */ /** 飞轮 [492]② — the shells a run teardown must LEAVE ALIVE: running SESSION-scoped monitors of this * session on THIS env (their whole point is outliving the turn; the env-wide dispose would otherwise * orphan the watch — handle alive, process dead, watcher reporting a bogus env-death terminal). Env * identity is part of the match so a shared registry never exempts another env's ids. */ sessionResidentShellIds(sessionId: string, env: unknown): BackgroundShellId[]; /** 飞轮 [511]③ — the OTHER shells a run teardown must leave alive: NON-persistent monitors still inside * their timeout window, on THIS env. CC semantics: Monitor(persistent:false) lives to timeout_ms — the * parent run ending is not an exit (timeout / process exit / TaskStop are). Deliberately NO owner * filter: on a shared env an earlier run's (or another session's) in-window watch must survive this * run's env-wide dispose too. Env identity stays part of the match, same as * {@link sessionResidentShellIds} (a shared registry never exempts another env's ids). */ timeoutResidentShellIds(env: unknown): BackgroundShellId[]; registerMonitor(input: RegisterMonitorInput): string; /** The monitor watcher tick loop. Same zombie-proofing discipline as {@link startBashWatcher} * (re-entrancy guard / stop on eviction / stop on vanished shell / stop on a throwing adapter), * plus the three monitor-only exits: watcher-enforced timeout, event-storm auto-stop, and the * line→batched-event emission path. All time reads go through the INJECTED clock (design/87). */ private startMonitorWatcher; registerWorkflow(input: RegisterWorkflowInput): string; pollTask(id: string, access: TaskAccess, opts?: TaskPollOptions): Promise<{ content: string; details: UnifiedTaskOutput; }>; stopTask(id: string, access: TaskAccess, opts?: TaskStopOptions): Promise<{ content: string; details: UnifiedTaskOutput; }>; /** CC206-B name resolution over the caller-visible background_agent rows (CC `cgo`/`gvy`, 206:575142-575178 * and 206:575199-575210 — sema's single-lane subset: no teammate/name-registry branches). Matching key = * normalized description (the spawn label that IS the agent's name on this face). Running rows are * preferred over terminal ones (CC `gvy`: `o.length > 0 ? o : n`), so a re-used label addresses the live * agent, while a lone terminal match still resolves (stop is then a no-op receipt with its real status). */ private resolveBackgroundAgentByName; /** CC `ugo` (206:575227-575243): `id (description)` rows for the not-found footer — running bg agents the * CALLER can address (canAccess scopes; CC's self/observer/named exclusions have no counterpart rows here). */ private runningBackgroundAgentLabels; private lastGcAt; /** * 预清(飞轮 /tasks + footer pill 的数据源正道): list the caller's OWN tasks as a bounded DISPLAY * projection — never the internal handles (no shellId/env/abort). Same owner/scope guard as poll/stop; * a caller only ever sees its own tasks. Sorted newest-first, capped at `limit` (default 50). * CC parity note: CC's /tasks is a UI/SDK surface, NOT an LLM tool — expose this via SDK/service, * do not mount it as a model tool by default. */ list(access: TaskAccess, opts?: { limit?: number; }): Array<{ task_id: string; type: SemaTaskType; status: SemaTaskStatus; /** service [418]①: design/129 lifetime flag, surfaced for observability — on resume-leg deployments the * canonical taskId==sessionId, so task- and session-scoped rows share an owner KEY VALUE and only this * explicit flag tells them apart. Bulk attribution should use {@link markStopSourceForOwner}, not a * list()-driven loop (the 500-row cap — [418]②). */ sessionScoped: boolean; description?: string; toolUseId?: string; createdAt: number; updatedAt: number; }>; /** Throttled {@link gc} for hot-path callers (design/115 review B5): prepare-task runs once per task, * and a full-map sweep per prepare is pure overhead — sweep at most once per minute. */ maybeGc(now?: number): number; gc(now?: number, terminalTtlMs?: number): number; clearBackgroundForOwner(access: TaskAccess): number; private resolveInternal; private pollBackgroundBash; /** 黑板 [405]: this poll SERVED the terminal state in-band — fire the (once-only) ack hook so the * deployment can drop the redundant completion-inbox entry. Swallow-guarded. */ private fireServedTerminal; private pollWorkflow; private stopBackgroundBash; /** design/135 G2: TaskOutput over a monitor — a RE-READABLE spool snapshot (the watcher owns the env * cursor, exactly the bash G2b spool posture): repeated polls return the same accumulated output. */ private pollMonitor; /** design/135 G2: TaskStop over a monitor — same shape/attribution discipline as stopBackgroundBash * (mark "parent" BEFORE the kill; an earlier service-wire "user" marker wins; watcher torn down first * so a racing tick can't fire a duplicate terminal notification after this receipt). */ private stopMonitor; private pollBackgroundAgent; private stopBackgroundAgent; private stopWorkflow; } export declare const defaultTaskRegistry: TaskRegistry; export declare function createTaskOutputTool(opts: TaskToolOptions): AgentTool; export declare function createTaskStopTool(opts: TaskToolOptions): AgentTool; //# sourceMappingURL=task-registry.d.ts.map