import type { spawnSync } from "node:child_process"; import { type ShellResolution } from "./shell.js"; import type { AgentNotificationQueue } from "./agent-notifications.js"; export interface BackgroundProcess { id: string; pid: number; command: string; logFile: string; startedAt: number; exitCode: number | null; lastReadOffset: number; /** * Last known size of `logFile` in bytes. Kept current by the progress * watcher, the exit handler and every `readOutput`, so consumers (notably * the pre-stop process gate) can tell "output was never consumed" from * "output was read" without an fs stat per check. */ logSize: number; } export interface StartResult { id: string; pid: number; logFile: string; /** False when wake rules were requested but no notification queue is wired, * so callers must not promise the model a wake that can never fire. */ wakeArmed: boolean; } export interface ReadOutputResult { id: string; isRunning: boolean; exitCode: number | null; output: string; } /** * Model-declared wake conditions for a background task. The agent states up * front what output it cares about (or what silence means), and the watcher * turns exactly that into a steering-path notification — instead of the model * polling `task_output` (measured elsewhere at 71 wasted turns on one build) * or re-reading generic progress checkpoints hoping to spot the signal. */ export interface WakeRules { /** Wake the moment new log output matches this regex. One-shot. */ pattern?: RegExp; /** Wake when the process is still running but has logged nothing for this * many milliseconds (a stalled build/hang detector). One-shot. */ silenceMs?: number; } export interface ProcessManagerOps { platform?: NodeJS.Platform; kill?: typeof process.kill; killProcessTree?: (pid: number) => void; spawnSync?: typeof spawnSync; /** * Push queue for background-process progress checkpoints. When set, a long * build reports progress and its exit code into the agent's next turn * instead of waiting to be polled with `task_output`. */ notifications?: AgentNotificationQueue; /** * Directory for background process logs. Defaults to the real `~/.gg/bg`. * * Injectable because this manager both writes AND prunes here: a test that * calls `start()` without an override operates on the developer's own log * history. That is not hypothetical — running the suite once deleted ~12.7k * real logs off a machine before this parameter existed. */ bgDir?: string; /** * Base delay before the first progress report, in ms. Defaults to * {@link WATCH_INTERVAL_MS}. * * Injectable so tests can assert the watcher's BEHAVIOUR (reports, backoff, * budget, retirement) without waiting out the production 5s/10s/20s cadence. * A test that waits real seconds for a real subprocess is a test whose result * depends on how loaded the machine is: the budget test used to fail under * `pnpm test` (12 packages in parallel) while passing when run alone. */ watchIntervalMs?: number; } export declare class ProcessManager { private readonly ops; private processes; private children; /** Per-process progress timers. Cleared on exit, stop and shutdown. */ private watchers; /** Per-process wake-rule timers (model-declared match/silence conditions). */ private wakeWatchers; private wakeStates; /** Log size at the last emitted checkpoint, so a quiet process stays quiet. */ private watchedSizes; /** Timestamp of the last retention sweep; 0 means "never swept". */ private lastPruneAt; constructor(ops?: ProcessManagerOps); private get bgDir(); /** * Delete background logs whose last write is older than the retention window. * * Deliberately best-effort and never awaited by `start()`: losing an old log * is harmless, but failing to launch the user's process because a stale log * couldn't be unlinked is not. Logs belonging to processes this manager still * tracks are skipped regardless of age — a quiet long-running dev server can * easily go a week without writing a line, and its log must stay readable. */ private pruneOldLogs; start(command: string, cwd: string, launch?: ShellResolution, wake?: WakeRules): Promise; /** * Arm a backing-off, budgeted progress watcher for one background process. * Emits at most one latest-only checkpoint per interval, only when the log * actually grew, and at most {@link WATCH_MAX_REPORTS} times in total — so a * build reports itself early without the agent ever calling `task_output`, * while an idle or long-lived process stops costing context. * * Once the budget is spent the watcher retires completely (no timer, no * further injections). The terminal exit notification is unaffected: it is * produced by the exit handler, not this watcher, so "it finished" always * still reaches the agent. * * Self-rescheduling rather than `setInterval` because the delay changes; a * tick is only scheduled once the previous one has been handled. * * No-op when no notification queue is wired, so hosts that never drain * notifications pay nothing. */ private armWatcher; /** Stat the log once and cache its size on the record. Returns 0 if unreadable. */ private refreshLogSize; /** Emit one progress checkpoint if the log grew. Returns whether it did. */ private emitProgress; private notifyExit; /** Read the trailing bytes of a log without loading the whole file. */ private readTail; /** Read the bytes of a log in `[start, end)` without loading the file. */ private readRange; /** * Wake-rule watcher: evaluates the model's `pattern`/`silenceMs` conditions * every {@link WAKE_INTERVAL_MS} and pushes a steering-path notification the * moment one holds. Each rule is one-shot; once every declared rule has fired * (or the process exits) the watcher retires. Unlike the progress watcher it * never backs off — the agent asked for exactly this signal, however long it * takes, and a late match on a quiet dev server is precisely the point. */ private armWakeWatcher; private evaluateWakeRules; /** Stop and forget a process's wake watcher. */ private disposeWakeWatcher; /** Live wake-watcher ids. Exposed for leak assertions in tests. */ activeWakeWatchers(): string[]; /** Stop and forget a process's watcher. A finished process keeps no timer. */ private disposeWatcher; /** Stop and forget a process's progress watcher only. */ private disposeProgressWatcher; /** Live watcher ids. Exposed for leak assertions in tests. */ activeWatchers(): string[]; readOutput(id: string, fromStart?: boolean): Promise; /** * Write input to a running background process's stdin, enabling interactive * control (answer prompts, drive a REPL, feed a scaffolder). By default a * newline is appended (as if the user pressed Enter). Set `eof` to close * stdin afterwards, signalling end-of-input (Ctrl-D) to the program. */ sendInput(id: string, input: string, opts?: { enter?: boolean; eof?: boolean; }): Promise; stop(id: string): Promise; list(): BackgroundProcess[]; shutdownAll(): void; } //# sourceMappingURL=process-manager.d.ts.map