import type { AgentProgress, AgentSource, LocalErrorSummary } from "../task/types"; export interface AsyncJob { id: string; readonly generation: string; type: "bash" | "task"; status: "running" | "completed" | "failed" | "cancelled" | "paused"; startTime: number; /** * Wall-clock ms when the job left the `running` state (completed, failed, * cancelled, or paused). Undefined while running. Frozen on the first * terminal/pause transition so elapsed-time renderers stop counting once a * job is no longer active instead of growing forever against `Date.now()`. */ endTime?: number; label: string; abortController: AbortController; promise: Promise; resultText?: string; errorText?: string; /** Safe, bounded cause when session setup failed before the LLM began work. */ setupFailureSummary?: string; /** Safe, bounded summary of a terminal local (non-provider) failure kind. */ localErrorSummary?: LocalErrorSummary; metadata?: AsyncJobMetadata; /** * Registry id of the agent that registered the job (e.g. "0-Main", * "3-AuthLoader"). Used by scoped cancel/list APIs so a subagent's teardown * does not cancel its parent's jobs. Undefined for callers that don't * supply an id (e.g. legacy tests, SDK consumers without an agent context). */ ownerId?: string; } /** * Elapsed wall-clock ms for a job, frozen once it stops running. While the job * is active (`endTime` undefined) this counts against `now`; after it stops it * returns the fixed `endTime - startTime` span so status renderers do not keep * incrementing a completed job's timer. */ export declare function jobElapsedMs(job: Pick, now?: number): number; export interface AsyncJobMetadata { subagent?: { id: string; agent: string; agentSource: AgentSource; description?: string; assignment?: string; duplicateIdentity?: string; duplicateDisposition?: "warned" | "superseded"; }; /** True when this bash job was started by the `monitor` tool (vs plain async bash). */ monitor?: boolean; } /** * Typed outcome a subagent task run may produce. A `paused` outcome is * non-terminal and non-delivering: the run suspended at a safe boundary and the * subagent can be resumed from its persisted sessionFile. `completed` always * wins a race with a late pause because the run returns it once it has actually * finished. A `failed` outcome retains a safe setup diagnostic for receipt rendering. */ export type SubagentRunOutcome = { kind: "completed"; text: string; } | { kind: "failed"; text: string; setupFailureSummary?: string; localErrorSummary?: LocalErrorSummary; } | { kind: "paused"; note?: string; }; /** Canonical lifecycle of a subagent across pause/resume cycles. */ export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled"; /** Maximum time allowed to prove owned subagents have stopped before replacement. */ export declare const OWNER_SUBAGENT_SHUTDOWN_TIMEOUT_MS = 5000; export declare class OwnerSubagentShutdownError extends Error { readonly code = "owner_shutdown_in_progress"; constructor(); } export interface OwnerSubagentShutdownTarget { subagentId: string; jobId: string | null; source: "record" | "metadata_job"; } export interface OwnerSubagentShutdownLease { ownerId: string; id: string; targets: readonly OwnerSubagentShutdownTarget[]; } export interface OwnerSubagentShutdownProof { ownerId: string; leaseId: string; confirmed: boolean; reason: "confirmed" | "deadline_exceeded" | "missing_terminal_evidence" | "lease_lost"; targets: readonly OwnerSubagentShutdownTarget[]; terminalIds: readonly string[]; unresolvedIds: readonly string[]; } /** * Live, executor-owned control handle for a RUNNING subagent. Registered when a * subagent run starts and removed on pause/terminal so a paused subagent retains * no live `AgentSession` reference (leak-free). */ export interface SubagentLiveHandle { /** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */ requestPause(): void; /** Inject a steering message into the live session. */ injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn", opts?: { fromAgentId?: string; }): Promise; } /** * Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob` * eviction so resume stays addressable by subagent id, and is the single source * of truth for control-plane status and identity. */ export interface SubagentRecord { subagentId: string; ownerId?: string; /** Current live/last AsyncJob id; null while queued with no active job. */ currentJobId: string | null; historicalJobIds: string[]; status: SubagentLifecycle; sessionFile: string | null; /** * Explicit veto, not a complete availability result. False always denies; * true still requires an owner-compatible descriptor or non-blank session * file, followed by a separately available runner (`no_runner` otherwise). */ resumable: boolean; queued?: { ownerId?: string; seq: number; message?: string; resumeToolCallId?: string; createdAt: number; }; /** Last queued-resume seq for a CANCELLED queued resume (rec.queued is * cleared on cancel): retained on the record so owned settlement's second * proof can still see the generation as provably cancelled, without a * separate FIFO-capped evidence set that could evict an in-flight * settlement's generation (review thread P2). */ terminalQueuedSeq?: number; /** Resolved model the subagent was asked to use, e.g. "openai-codex/gpt-5.5". */ requestedModel?: string; /** Model actually used after auth fallback (#985); equals requestedModel when no fallback. */ effectiveModel?: string; /** True when the requested model lacked credentials and the subagent fell back to the parent model. */ modelFellBack?: boolean; /** True when the effective subagent provider is in fast mode. */ fastMode?: boolean; duplicateIdentity?: string; duplicateDisposition?: "warned" | "superseded"; terminalGeneration?: string; /** Generation of currentJobId, preventing stale ID reuse from mutating this record. */ currentJobGeneration?: string; } /** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */ export interface ResumeDescriptor { subagentId: string; ownerId?: string; data: unknown; } /** * In-memory resume runner bound to the session that originally launched a * subagent. Never serialized: process restart drops it so resume fails closed. */ export type ResumeRunner = (subagentId: string, message?: string, descriptor?: ResumeDescriptor, resumeToolCallId?: string) => string | undefined; export interface AsyncJobManagerOptions { onJobComplete: (jobId: string, text: string, job?: AsyncJob) => void | Promise; maxRunningJobs?: number; retentionMs?: number; } export interface AsyncJobDisposeDiagnostics { stuckJobIds: string[]; deliveriesDrained: boolean; } export interface AsyncJobDeliveryState { queued: number; delivering: boolean; nextRetryAt?: number; pendingJobIds: string[]; deadLettered: number; } export interface AsyncJobLifecycleCleanup { onCancel?: (job: AsyncJob) => void; onTerminal?: (job: AsyncJob) => void; onEvict?: (job: AsyncJob) => void; /** * Idempotent residual cleanup invoked by a post-eviction tombstone purge * (e.g. a late `job cancel` after the job left the registry). Kept distinct * from the at-most-once lifecycle phases so a tombstone purge never has to * re-invoke a phase hook. Must be safe to call repeatedly. */ onTombstonePurge?: (job: AsyncJob) => void; } export interface MonitorTombstone { jobId: string; ownerId?: string; status: AsyncJob["status"]; expiresAt: number; purge: () => unknown; } export interface AsyncJobRegisterOptions { id?: string; /** Registry id of the agent that owns this job; used to scope cancelAll. */ ownerId?: string; /** Structured metadata for tool-specific control surfaces. */ metadata?: AsyncJobMetadata; onProgress?: (text: string, details?: Record) => void | Promise; lifecycle?: AsyncJobLifecycleCleanup; } /** * Filter applied to job query/cancel APIs. With `ownerId`, results are * restricted to jobs registered by that agent (registry id from * `AgentRegistry`, e.g. "0-Main", "3-AuthLoader"). */ export interface AsyncJobFilter { ownerId?: string; } export type AsyncJobWaitCondition = "all_terminal" | "any_terminal"; export type AsyncJobWaitOutcome = "completed" | "timed_out_wait" | "interrupted"; export interface AsyncJobWaitTarget { targetId: string; jobId: string | null; subagentId?: string; generation: string; ownerId?: string; initialStatus: AsyncJob["status"] | "queued" | "not_found"; } export interface AsyncJobWaitResult { outcome: AsyncJobWaitOutcome; condition: AsyncJobWaitCondition; terminalJobIds: string[]; pendingJobIds: string[]; } export interface AsyncJobWaitHandle { readonly token: string; readonly result: Promise; acknowledge(targetIds?: readonly string[]): { acknowledged: boolean; jobIds: string[]; }; close(): void; } export interface AsyncJobWatchHandle { close(): number; } /** * A slice of process-stream output for a background job, as recorded by * `appendOutput` / read by `readOutputSince`. * * The cursor model is monotonic UTF-8 byte offsets. `nextOffset` is the offset * to pass to the next read to receive only fresh bytes; `startOffset` is the * first byte the manager still retains for this job. When the requested offset * is older than `startOffset`, the manager returns the retained tail and sets * `truncated: true`. */ export interface AsyncJobOutputSlice { jobId: string; status: AsyncJob["status"]; text: string; startOffset: number; nextOffset: number; truncated: boolean; } /** Default retention cap for per-job captured output. ~512 KiB matches the * bash tail-buffer order of magnitude without dominating session memory. */ export declare const DEFAULT_JOB_OUTPUT_RETENTION_BYTES: number; export declare class AsyncJobManager { #private; /** Process-global instance shared by internal URL protocol handlers and tools. */ static instance(): AsyncJobManager | undefined; /** Register a manager for a top-level session endpoint so concurrent * sessions' owned work can be settled in the ABORTING endpoint's manager * instead of the last-created process-global instance (review thread P1). * Returns TRUE when the mapping was installed (or already held by this * manager); returns FALSE when the endpoint id is already held by a * FOREIGN live manager — a second top-level session constructed or * resumed under that endpoint must then fail construction instead of * silently replacing the first manager, which would make the first * session's tools resolve the second manager and let same-id jobs be * queried, registered, or cancelled across sessions (review thread P1). */ static registerForEndpoint(endpointId: string, manager: AsyncJobManager): boolean; static forEndpoint(endpointId: string | undefined): AsyncJobManager | undefined; static unregisterForEndpoint(endpointId: string): void; /** Reverse lookup: the endpoint this manager is registered under, if any * (review thread P1 — endpoint-scoped lineage resolution). */ static endpointIdOf(manager: AsyncJobManager | undefined): string | undefined; /** Re-register a manager under a successor endpoint id after a committed * session-identity transition (newSession / switchSession / handoff). * Lineage bindings made after the transition use the successor id, so the * process-global endpoint registry must follow — otherwise * `endpointIdOf()` keeps resolving the predecessor and a queued subagent * resume can neither resolve its lineage nor register its owned tuple * (review thread P1). Returns TRUE when the mapping was moved (or no move * was needed); returns FALSE when the successor endpoint is owned by a * FOREIGN live manager — the transition must then abort or roll back * BEFORE retiring predecessor state, because leaving this manager under * the predecessor while tools resolve the successor to the foreign * manager sends jobs to the wrong session and owned aborts lose their * causal set (review thread P1). */ static rekeyForEndpoint(predecessorEndpointId: string, successorEndpointId: string, manager: AsyncJobManager | undefined): boolean; /** Remove every endpoint registration owned by the manager. Disposal-safe: * covers registrations whose key drifted from the session's current id * (provider session ids, mid-transition teardown) and stale predecessor * keys, so teardown cannot leave a dead manager behind. */ static unregisterManager(manager: AsyncJobManager | undefined): void; /** Install or clear the process-global instance. */ static setInstance(value: AsyncJobManager | undefined): void; /** Reset the process-global instance. Test-only. */ static resetForTests(): void; resolveSubagentWaitTarget(id: string, filter?: AsyncJobFilter): AsyncJobWaitTarget | undefined; subscribeTerminalWait(targets: readonly AsyncJobWaitTarget[], condition?: AsyncJobWaitCondition): AsyncJobWaitHandle; constructor(options: AsyncJobManagerOptions); /** * Subscribe to live-job-set change events. Returns an unsubscribe function. * Listener errors are isolated so one bad subscriber cannot break others. */ onChange(cb: () => void): () => void; register(type: "bash" | "task", label: string, run: (ctx: { jobId: string; signal: AbortSignal; reportProgress: (text: string, details?: Record) => Promise; }) => Promise, options?: AsyncJobRegisterOptions): string; /** * Cancel a single job by id. When `filter.ownerId` is set and does not * match the job's owner, the call is treated as not-found (returns false) * so cross-agent cancellation is rejected at the manager level. */ cancel(id: string, filter?: AsyncJobFilter): boolean; getMonitorTombstone(jobId: string, filter?: AsyncJobFilter): MonitorTombstone | undefined; purgeMonitorTombstone(jobId: string, filter?: AsyncJobFilter): { found: boolean; status?: AsyncJob["status"]; }; /** Register or replace the canonical record for a subagent. */ registerSubagentRecord(record: SubagentRecord): void; /** * Patch model/runtime metadata onto an existing subagent record (best-effort; no-op if * unknown). Every field is optional and omitting one preserves its current value, so a * narrow patch like `{ fastMode: true }` cannot erase model identity recorded earlier. * A field therefore cannot be cleared back to `undefined` through this method. */ updateSubagentModel(subagentId: string, model: { requestedModel?: string; effectiveModel?: string; modelFellBack?: boolean; fastMode?: boolean; }): void; getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined; getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[]; registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void; getLiveHandle(subagentId: string): SubagentLiveHandle | undefined; removeLiveHandle(subagentId: string): void; /** * Retain the latest live `AgentProgress` for a subagent (deep-cloned so later * mutation of the live object cannot corrupt retained state). Read by the * `subagent` await panel; cleared on terminal/cancel/purge/dispose. * * Ignored for ids without a canonical `SubagentRecord` (e.g. foreground/inline * task runs that share the executor path) so the map only holds detached * subagent progress and never accumulates untracked foreground task state. */ recordSubagentProgress(subagentId: string, progress: AgentProgress): void; getSubagentProgress(subagentId: string): AgentProgress | undefined; /** * True only when a live, in-session progress producer exists for this id: a * canonical registered record with a live handle or an in-memory running job. * False for `SubagentTool` backward-compat job synthesis and resumed-from-disk * records, which have no live producer to stream from. */ hasLiveSubagent(subagentId: string, filter?: AsyncJobFilter): boolean; /** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */ setResumeRunner(runner: ResumeRunner): void; registerResumeDescriptor(descriptor: ResumeDescriptor, runner?: ResumeRunner): void; getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined; beginOwnerSubagentShutdown(ownerId: string): OwnerSubagentShutdownLease | undefined; runOwnerProducerCleanups(filter?: AsyncJobFilter): void; runOwnerProducerCleanupsStrict(filter?: AsyncJobFilter): void; cancelAndProveOwnerSubagents(lease: OwnerSubagentShutdownLease, options?: { timeoutMs?: number; }): Promise; finishOwnerSubagentShutdown(lease: OwnerSubagentShutdownLease, outcome: "commit" | "release"): void; /** Request a graceful safe-boundary pause of a running subagent. */ pauseSubagent(subagentId: string, filter?: AsyncJobFilter): { ok: boolean; status?: SubagentLifecycle; reason?: string; }; /** * Resume a non-running subagent from retained context: an owner-compatible * descriptor or a legacy session file. Workflow routing keeps `not_found`, * `context_unavailable`, `no_runner`, and `resume_failed` distinct. */ resumeSubagent(subagentId: string, filter?: AsyncJobFilter, message?: string, resumeToolCallId?: string): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string; }; /** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */ cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean; getJob(id: string): AsyncJob | undefined; /** * The EXECUTION promise of the job record whose generation matches the * captured generation, or undefined when the record is gone or rebound. * The promise settles only when the job's function actually unwinds — the * eagerly-updated cancel status is not proof of quiescence (review P1). */ getJobPromise(id: string, generation: string): Promise | undefined; getRunningJobs(filter?: AsyncJobFilter): AsyncJob[]; getRecentJobs(limit?: number, filter?: AsyncJobFilter): AsyncJob[]; getAllJobs(filter?: AsyncJobFilter): AsyncJob[]; /** * Append a sanitized process-stream chunk for a background job. Called from * the unthrottled bash-executor capture hook (`onRawChunk`) so monitor sees * every chunk even when preview/progress callbacks are throttled. * * Offsets are in UTF-8 bytes. Storing chunk metadata avoids unsafe byte * slicing across multibyte characters at read time. The retention window is * a per-job rolling cap (`DEFAULT_JOB_OUTPUT_RETENTION_BYTES`); when it * overflows, oldest whole chunks are evicted and `startOffset` advances — * subsequent reads from a stale offset get `truncated: true`. */ appendOutput(jobId: string, chunk: string): void; /** * Read fresh process-stream output for a job since `offset` (in UTF-8 * bytes). Returns `undefined` when the job does not exist or when an * `ownerId` filter is set and the job belongs to a different owner — this * mirrors the manager-level "not found" pattern used by `cancel`. * * - `offset < startOffset` returns the retained tail with `truncated: true`. * - `offset > nextOffset` clamps to `nextOffset` and returns an empty text * slice with `truncated: false`. * - Assembled text slices the leading retained chunk at a UTF-8 codepoint * boundary when needed, so multibyte characters cannot be split. */ readOutputSince(jobId: string, offset: number, filter?: AsyncJobFilter): AsyncJobOutputSlice | undefined; /** * Register an owner-scoped cleanup callback. Returns an unregister function. * * Used by Cron* tools to clear session-scoped timers when the owning agent * is torn down. Invoked by `runOwnerCleanups({ ownerId })` before * `cancelAll({ ownerId })` so timers cannot register new jobs during * teardown. */ registerOwnerCleanup(ownerId: string, cleanup: () => void): () => void; /** Run producer cleanups, then perform the legacy destructive subagent purge. */ runOwnerCleanups(filter?: AsyncJobFilter): void; getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState; hasPendingDeliveries(filter?: AsyncJobFilter): boolean; watchJobGenerations(jobIds: string[]): AsyncJobWatchHandle; watchJobs(jobIds: string[]): number; unwatchJobs(jobIds: string[]): number; acknowledgeDeliveries(jobIds: string[]): number; /** * Cancel running jobs. With `filter.ownerId` set, cancels only jobs the * matching agent registered; with no filter, cancels every running job * (used by `dispose()` to nuke the manager's state). */ cancelAll(filter?: AsyncJobFilter): void; waitForOwnerInFlightDeliveries(ownerId: string, options?: { timeoutMs?: number; }): Promise; cancelAndSettleOwnerJobs(ownerId: string, options?: { timeoutMs?: number; }): Promise; getLastDisposeDiagnostics(): AsyncJobDisposeDiagnostics; waitForAll(): Promise; drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter; }): Promise; dispose(options?: { timeoutMs?: number; }): Promise; isDeliverySuppressed(jobId: string, generation?: string): boolean; }