/** * @license * Copyright 2026 Steven Roussey * SPDX-License-Identifier: Apache-2.0 */ import type { RunEvent } from "../run-events/RunEventTypes"; import type { RunState } from "../ui/model/runRowModel"; import { type WebInvocation } from "./argv"; export interface RunEventRecord { readonly seq: number; readonly event: RunEvent; } export interface WebRun { readonly id: string; readonly cli: string; readonly invocation: WebInvocation; readonly startedAt: number; state: "running" | RunState; endedAt: number | undefined; exitCode: number | undefined; readonly events: RunEventRecord[]; } export interface RunRegistryOptions { /** How to start the CLI, e.g. `["bun", "/path/to/workglow.ts"]`. */ readonly binary: readonly string[]; readonly cwd: string; readonly logDir: string; readonly binaryName?: string; /** Events retained in memory per run; the log on disk keeps everything. */ readonly maxEvents?: number; } type Listener = (record: RunEventRecord) => void; /** * Runs each invocation as a child of the same binary. * * A child rather than an in-process call, for three reasons that all showed up * in practice: the argv is exactly the line the page shows, so what you read is * what ran; cancellation is SIGINT, which every task already handles; and an * env-var model override belongs to one run instead of to the server, so two * runs cannot observe each other's. */ export declare class RunRegistry { private readonly options; private readonly runs; private readonly children; private readonly listeners; private readonly logs; private readonly maxEvents; constructor(options: RunRegistryOptions); start(invocation: WebInvocation): WebRun; get(id: string): WebRun | undefined; list(): readonly WebRun[]; /** * Asks the run to stop the way Ctrl-C does, then insists. A task graph * cancels cooperatively, so the polite signal is the one that leaves the * database and the caches consistent. */ abort(id: string): boolean; /** * Resolves to whether the answer actually reached the child. * * The write is asynchronous, so the liveness check above it proves nothing: * a child that exits in between fails the write with EPIPE, and only the * write callback knows that happened. Reporting delivery before the flush * tells the page an answer landed when nobody read it. (The EPIPE itself * arrives as an `error` event on a stream nobody else listens to, which Node * throws — `start` installs the listener that keeps it from taking the * console down.) */ answerHuman(id: string, response: unknown): Promise; /** Replays what the subscriber missed, then streams the rest. */ subscribe(id: string, afterSeq: number, listener: Listener): () => void; closeAll(): void; private nextSeq; private record; private readEvents; private readLogs; /** * A child that dies before reporting still has to end the page's spinner, so * a missing `run_end` is synthesized from the exit code rather than left for * the client to time out on. */ private finish; } export {};