import { type BackgroundTask, type HarnessAdapter, type HarnessKind } from "../shared/types.js"; import { type LaunchOptsBuilder } from "./session-manager.js"; import type { IngestCredentialProvider } from "./ingest-credentials.js"; export { McpCredentialGenerationChangedError } from "./errors.js"; /** * The slice of node's ChildProcess a task actually uses — injectable so * tests drive fake processes without spawning anything real. */ export interface TaskProcess { pid?: number; stdout: NodeJS.ReadableStream | null; stderr: NodeJS.ReadableStream | null; on(event: "exit", listener: (code: number | null) => void): unknown; on(event: "error", listener: (err: Error) => void): unknown; kill(signal?: NodeJS.Signals): boolean; } export type TaskSpawnFn = (command: string, args: string[], options: { cwd: string; env: Record; stdin?: string; }) => TaskProcess; /** Thrown when the session's harness adapter has no `launchTask` (codex, * today) — the macros router surfaces this as a 400 with this message. */ export declare class TaskNotSupportedError extends Error { constructor(harness: HarnessKind, label: string); } /** Thrown when the same macro is already running against the same target — * two concurrent enrichment runs for one workflow would race on the same * render/cache files. The target is the workflow when both tasks carry one * (so one session CAN enrich two different workflows concurrently, and two * sessions can NEVER double-enrich the same workflow), falling back to the * session for workflow-less tasks. */ export declare class TaskAlreadyRunningError extends Error { constructor(label: string); } export interface RunTaskRequest { macroId: string; label: string; harnessSessionId: string; harness: HarnessKind; cwd: string; prompt: string; /** The workflow the task targets, when it has one — carried onto * BackgroundTask.workflowPath (pane scoping) and used as the dedupe key. */ workflowPath?: string | null; /** Model override for the headless run (LaunchOpts.model). */ model?: string; /** Turn cap for the headless run (LaunchOpts.maxTurns). */ maxTurns?: number; } export interface TaskManagerOptions { adapters: Partial>; /** Base URL the harness server is reachable at, e.g. http://127.0.0.1:4100. */ ingestUrl: string; /** Per-task capability issuer; shared ingest tokens are not supported. */ ingestCredentials: IngestCredentialProvider; collectorUrl?: string; /** Same builder real sessions use — generates the task's own --settings / * --mcp-config / system-prompt files under generated/. */ buildLaunchOpts?: LaunchOptsBuilder; /** Live credential generation checked synchronously immediately before spawn. */ currentCredentialGeneration?: () => number; /** Injectable for tests. Defaults to node:child_process.spawn. */ spawnProcess?: TaskSpawnFn; /** Called once the task's process has exited (either outcome) — the server * wires generated-config-dir removal here, mirroring session exit. */ onCleanup?: (taskId: string) => void; now?: () => string; generateId?: () => string; } export type TaskStatusListener = (task: BackgroundTask) => void; export declare class TaskManager { private readonly adapters; private readonly ingestUrl; private readonly issueIngestToken; private readonly revokeIngestToken; private readonly collectorUrl; private readonly buildLaunchOpts; private readonly currentCredentialGeneration; private readonly spawnProcess; private readonly onCleanup; private readonly now; private readonly generateId; private readonly tasks; /** Runs admitted past the dedupe check but not yet registered in `tasks` * — the check→register window contains an await (buildLaunchOpts), so * without this reservation two near-simultaneous run() calls for the * same target both pass the check and both spawn. */ private readonly pendingRuns; private readonly processes; private readonly mcpCredentialLaunches; private readonly stderrTails; /** The final result event's error text, when the stream produced one — * preferred over a raw stderr tail for failure display. */ private readonly resultErrors; /** The final result event's success text — published on the finished task * as `resultText`, so a structured task's caller can parse the answer. */ private readonly resultTexts; private readonly statusEmitter; /** * Per-task exit promises: each resolves when `finish()` runs for that task * (whether via a normal exit, an error event, or a killAll() escalation). * `killAll()` awaits these to confirm actual death before resolving. */ private readonly exitedPromises; private readonly resolveExited; constructor(options: TaskManagerOptions); list(): BackgroundTask[]; get(id: string): BackgroundTask | undefined; /** Returns true when any registered task with the given macroId is currently * running against the given workflowPath — used by forceRefresh to check * before destroying caches so a second Visualize click is a true no-op. */ isRunning(macroId: string, workflowPath: string): boolean; onStatusChange(listener: TaskStatusListener): () => void; /** * Spawns one background task. Resolves once the process is launched (not * when it finishes — completion arrives as a task.status broadcast). * Throws TaskNotSupportedError / TaskAlreadyRunningError for the two * user-addressable refusals; anything else (spawn failure, config * generation failure) propagates as-is. */ run(req: RunTaskRequest): Promise; kill(id: string): Promise; /** Stop credential-bearing tasks launched no later than the removed generation. */ terminateCredentialBearingTasks(throughGeneration: number): Promise; /** * Kills every still-running task process and returns a Promise that resolves * when all of them have actually exited. Bounded and never hangs: * * 1. SIGTERM all running tasks. * 2. Wait up to `TASK_KILL_ESCALATION_MS` for graceful exits. * 3. SIGKILL any survivors. * 4. Wait up to `TASK_KILL_CONFIRM_MS` for SIGKILL exits. * 5. Synthesize `finish(id, null)` for any still-registered processes — * a zombie that never emits "exit" would otherwise leave allExited * pending forever. finish()'s idempotence guard (status !== "running") * makes a double-call harmless if the exit event arrives concurrently. */ killAll(): Promise; private handleStdoutLine; /** The single running→finished transition — shared by "exit" and "error", * which can both fire for one process, so it's idempotent. */ private finish; /** Drops the oldest finished tasks beyond MAX_FINISHED_TASKS — running * tasks are never dropped. */ private trimFinished; private emitStatus; } //# sourceMappingURL=task-manager.d.ts.map