import { type ChildProcess } from "node:child_process"; import type { ToolCall, ToolResult } from "../types.js"; export type JobStatus = "starting" | "running" | "exited" | "failed" | "stopping" | "killed" | "lost"; export type JobTerminalStatus = Exclude; export interface JobArtifactReceipt { path: string; chunks: string[]; bytes: number; droppedBytes: number; redacted: boolean; sha256: string; } export type JobMonitorMetadata = Record; export interface JobLinkMetadata { taskId?: string | undefined; parentTaskId?: string | undefined; /** * Stable delegation identity minted before launch. It is the * reconciliation key between a job and its responder child task, so a fast * exit or a failed link cannot orphan the child. */ delegationId?: string | undefined; wakeOnCompletion?: boolean | undefined; monitor?: JobMonitorMetadata | undefined; /** Runtime listening lease that authorized this responder delegation. */ responderLeaseId?: string | undefined; /** * Opt-in delegation to the Responder: fire-and-continue, plan subtask + * auto-wake on completion, and inclusion in the Responder inbox/UI. When * false/absent the job is a plain background job the agent polls itself * (shell.jobs/shell.tail) exactly as before Responder existed. */ responder?: boolean | undefined; } /** * durable — shell.start / auto-backgrounded servers (listed by shell.jobs, persisted) * ephemeral — per-tool stall tracking in the agent runner (never listed, never persisted) */ export type JobKind = "durable" | "ephemeral"; export interface BackgroundJob extends JobLinkMetadata { id: string; command: string; commandDisplay: string; cwd: string; pid?: number | undefined; processGroupId?: number | undefined; processIdentity?: string | undefined; status: JobStatus; startedAt: string; heartbeatAt?: string | undefined; endedAt?: string | undefined; exitCode?: number | undefined; signal?: string | undefined; artifactPath: string; stdoutArtifact: string; stderrArtifact: string; artifacts: { stdout: JobArtifactReceipt; stderr: JobArtifactReceipt; }; redactionProfile: string; ownerSessionId: string; /** Default durable for registry records; ephemeral for tool-stall tracking. */ kind?: JobKind | undefined; name?: string | undefined; authorization?: { target: string; expiresAt?: string | undefined; } | undefined; /** Accepted when reading legacy registries but never armed or restored. */ timeoutAt?: string | undefined; } export interface ResponderPollingPolicyInput { call: ToolCall; targetJob?: BackgroundJob | undefined; /** Jobs in the exact shell.jobs display window, in display order. */ recentJobs?: readonly BackgroundJob[] | undefined; } export declare function responderPollingPolicy(input: ResponderPollingPolicyInput): { blocked: boolean; reason?: string | undefined; }; export declare function formatJobElapsed(job: Pick, now?: number): string; export interface SupersededResultRevision { resultRevision: number; resultHash: string; status: JobTerminalStatus; endedAt: string; exitCode?: number | undefined; signal?: string | undefined; deliveredAt?: string | undefined; readAt?: string | undefined; analyzedAt?: string | undefined; acknowledgedAt?: string | undefined; settledAt?: string | undefined; } export interface ResponderNotification { id: string; ownerSessionId: string; jobId: string; taskId?: string | undefined; parentTaskId?: string | undefined; status: JobTerminalStatus; createdAt: string; startedAt: string; endedAt: string; exitCode?: number | undefined; signal?: string | undefined; stdoutArtifact: JobArtifactReceipt; stderrArtifact: JobArtifactReceipt; commandDisplay: string; wakeOnCompletion: boolean; responder: boolean; monitor?: JobMonitorMetadata | undefined; responderLeaseId?: string | undefined; /** A delivery attempt began; not durable consumption. Cleared claims may retry. */ deliveryStartedAt?: string | undefined; deliveredAt?: string | undefined; readAt?: string | undefined; analyzedAt?: string | undefined; acknowledgedAt?: string | undefined; /** User discarded this receipt (cancel/new session); never model analysis. */ discardedAt?: string | undefined; discardReason?: "session-cancelled" | undefined; /** Monotonic revision of the authoritative result this receipt carries. */ resultRevision?: number | undefined; /** Content hash of the authoritative result, used to detect a correction. */ resultHash?: string | undefined; /** Bounded audit trail of revisions this receipt superseded. */ supersededRevisions?: readonly SupersededResultRevision[] | undefined; archivedAt?: string | undefined; settledAt?: string | undefined; } export type JobManagerChange = { type: "job"; jobId: string; } | { type: "notification"; jobId: string; notificationId: string; }; export type JobManagerListener = (change: JobManagerChange) => void; /** Durable projection marker for a terminal result whose plan child is unsettled. */ export interface PendingSettlement { jobId: string; resultRevision: number; attempts: number; firstAttemptAt: string; lastAttemptAt: string; lastReason: string; deadLetteredAt?: string | undefined; } interface TailCursor { stream?: "stdout" | "stderr" | "combined"; offset?: number; bytes?: number; } /** Safe detached process form. stdinText is written once, then stdin is closed. */ export interface BackgroundSpawnSpec { command: string; argv: string[]; stdinText?: string | undefined; /** Non-secret display text persisted in the registry and artifacts. */ display?: string | undefined; } export interface StartJobOptions extends JobLinkMetadata { cwd?: string | undefined; name?: string | undefined; ownerSessionId?: string | undefined; profile?: string | undefined; estimatedSeconds?: number | undefined; /** Legacy compatibility input. Durable jobs no longer have generic deadlines. */ timeoutMs?: number | undefined; authorization?: { target: string; expiresAt?: string | undefined; } | undefined; } export declare class JobManager { private readonly jobsDir; private jobs; private notifications; private readonly claimedNotifications; private processes; private writers; private abortControllers; private authorizationTimers; private responderLeases; private settlementTimers; private pendingSettlements; private consumedResponderResults; private livenessWatchTimer; private finalizations; private listeners; /** * Tracks the first time a live job (with no ChildProcess handle) failed the * liveness check, so we only finalize it as "lost" after a sustained grace * window instead of on a single transient miss. Cleared as soon as the job is * observed alive again or finalized. */ private livenessMisses; private livenessCheckedAt; private registryRetryTimer; private readonly registryPath; private readonly transientV2RegistryPath; constructor(jobsDir?: string); private isDurable; private artifactPathsOf; /** Delete a dropped job's artifact chunks, never one still referenced. */ private removeJobArtifacts; /** Drop a job row plus its per-job caches and artifacts. */ private forgetJob; /** Remove artifact files whose job row no longer exists. */ private sweepOrphanArtifacts; private isLive; private isTerminalStatus; private emit; subscribe(listener: JobManagerListener): () => void; private matchesSession; private clearAuthorizationTimer; private scheduleAuthorizationExpiry; activateResponderLease(sessionId: string): string; getResponderLeaseId(sessionId: string | undefined): string | undefined; releaseResponderLease(sessionId: string, leaseId?: string): void; private cloneReceipt; private notificationForJob; private ensureCompletionNotification; private closeWriters; private finalizeJob; /** Reconcile a restored process that no longer has a ChildProcess close event. */ private refreshJobLiveness; private scheduleTaskSettlement; private completeSettlement; /** * Retry until the projection lands. The marker is durable, so a crash or a * long-lived plan-write conflict cannot silently strand a green child, and an * unrecoverable case is dead-lettered with a visible reason. */ private retrySettlement; /** * One coalesced, unref'd watcher that reconciles restored live jobs without a * UI read. Without it a headless session can leave a finished responder job * "running" forever, because liveness was only refreshed by list/get calls. */ private scheduleLivenessWatch; /** Ownership guard: a receipt may only be mutated by its owning session. */ private ownsNotification; /** Receipt for a launch that was deduplicated by its delegation key. */ private startJobReceipt; /** Terminal results whose plan child could not be settled yet. */ getPendingSettlements(): PendingSettlement[]; /** * Register an in-flight tool for stall tracking only. * Never appears in shell.jobs and never touches the durable registry. */ registerJob(id: string, job: BackgroundJob, ac?: AbortController, child?: ChildProcess): void; updateJobStatus(id: string, status: JobStatus, exitCode?: number): void; startJob(command: string | BackgroundSpawnSpec, options?: StartJobOptions): Promise; /** * List durable background jobs for this session only. * Ephemeral tool-stall rows and other sessions' jobs are excluded. */ listJobs(sessionId?: string): ToolResult; private resolveJobId; getJob(id: string): BackgroundJob | undefined; getPendingNotifications(sessionId?: string): ResponderNotification[]; pendingNotifications(sessionId?: string): ResponderNotification[]; claimNextResponderNotification(sessionId: string, leaseId: string): ResponderNotification | undefined; releaseResponderNotificationClaim(notificationId: string): void; /** * Record that a delivery attempt started. This is deliberately weaker than * `markDelivered`: an aborted or failed analysis turn must remain deliverable. */ markDeliveryStarted(notificationId: string, sessionId?: string): boolean; markDelivered(notificationId: string, sessionId?: string): boolean; markRead(notificationId: string, sessionId: string): boolean; markAnalyzed(notificationId: string, sessionId?: string): boolean; acknowledge(notificationId: string, sessionId?: string): boolean; linkJob(jobId: string, metadata: JobLinkMetadata): BackgroundJob | undefined; waitForJob(id: string, options?: { timeoutMs?: number | undefined; signal?: AbortSignal | undefined; }): Promise; tailJob(id: string, bytesOrCursor?: number | TailCursor): Promise; stopJob(id: string, options?: { signal?: NodeJS.Signals; graceMs?: number; escalate?: boolean; suppressWake?: boolean; }): Promise; cancelAll(sessionId: string): Promise; getRunningJobs(sessionId?: string): BackgroundJob[]; getRecentJobs(limit?: number, sessionId?: string): BackgroundJob[]; /** Drop stale terminal durable jobs and all leftover ephemeral rows. */ pruneTerminalJobs(): void; private loadAndReconcile; /** Only durable jobs and responder notifications are written to disk. */ private registry; private scheduleRegistryRetry; private persistSync; private persist; } export declare const jobManager: JobManager; export {};