/** * process-triggers.ts, one-shot on-exit process-lifecycle triggers. * * GoodVibes launches and supervises a command; exactly one payload fires when * it terminates. The watcher is owned by the daemon, not by the turn that * created it, so a six-hour build does not have to hold an agent turn open * against turn timeouts, context limits and provider interruptions. * * Four rules this module exists to enforce: * * Exit is not success. The payload carries exit code, signal, timed-out flag, * duration and an output tail, and the default prompt template inspects that * termination state rather than announcing a finished build. * * Bind only to processes we launched. A trigger references a tracked-process * record we created, never an arbitrary PID, PIDs are recycled, and by the * time a daemon comes back a remembered PID very likely belongs to somebody * else's process. * * stdin is closed by default. An unattended command that stops to ask for a * password should get EOF and fail, not block forever; a hard max-duration * cap backstops the cases that ignore EOF, and it fires with an explicit * `timed-out` state rather than quietly. * * A trigger never evaporates. If the daemon restarts and the child is gone, * the trigger fires once with an explicit `unknown` / `daemon-restart` state. * Firing with honest uncertainty beats a watcher that silently disappears. */ import type { OnExitTriggerSpec, TerminationMetadata, TrackedProcessRef } from './types.js'; export interface LaunchedProcess { readonly processId: string; readonly pid: number; readonly startedAt: number; } export interface ObservedTermination { readonly running: boolean; readonly exitCode: number | null; readonly signal: string | null; readonly timedOut: boolean; readonly stdoutTail: string; readonly stderrTail: string; readonly endedAt?: number | undefined; } /** * The process effects an on-exit trigger needs. Injected so the whole lifecycle *, launch, poll, cancel, restart reconciliation, is testable without * spawning anything. */ export interface TriggerProcessHost { launch(spec: { readonly command: string; readonly args: readonly string[]; readonly cwd?: string | undefined; readonly env?: Readonly> | undefined; readonly stdin: 'none' | 'empty'; readonly maxDurationMs: number; }): Promise; /** Null when the host has no record of this process at all. */ observe(processId: string): ObservedTermination | null; cancel(processId: string): void; /** * Whether a remembered pid still belongs to the process we launched. Used * only for reporting after a restart, the trigger never re-binds to it. */ isSameProcessAlive(pid: number, startedAt: number): boolean; } export declare const DEFAULT_ON_EXIT_MAX_DURATION_MS = 21600000; export declare const DEFAULT_OUTPUT_TAIL_BYTES = 8192; export declare function tailOf(text: string, bytes: number): string; export interface LaunchOnExitInput { readonly spec: OnExitTriggerSpec; readonly host: TriggerProcessHost; readonly daemonBootId: string; readonly defaults?: { readonly maxDurationMs?: number | undefined; readonly stdin?: 'none' | 'empty' | undefined; } | undefined; } export declare function launchOnExitProcess(input: LaunchOnExitInput): Promise; /** * Builds the termination payload for a child we actually watched exit. * `observed: true`, every field here was measured, not inferred. */ export declare function buildTermination(input: { readonly process: TrackedProcessRef; readonly observed: ObservedTermination; readonly now: number; readonly outputTailBytes?: number | undefined; }): TerminationMetadata; /** * Builds the payload for a child the daemon lost track of across a restart. * Everything that cannot be known is null and `observed` is false, so a prompt * template that inspects the payload can say "I do not know how this ended" * instead of inventing an exit code. */ export declare function buildDaemonRestartTermination(input: { readonly process: TrackedProcessRef; readonly now: number; readonly note?: string | undefined; }): TerminationMetadata; export declare function buildCancelledTermination(input: { readonly process: TrackedProcessRef; readonly now: number; }): TerminationMetadata; /** * Recovery decision for one on-exit record found on disk. * * A record written by a previous daemon boot can never be re-observed: the * subprocess handle, its pipes and its exit status all died with that daemon, * and the pid alone is not evidence of anything. So the answer is always * "fire once, honestly" rather than "adopt and hope". */ export type OnExitRecoveryDecision = { readonly action: 'resume'; readonly reason: string; } | { readonly action: 'fire-unknown'; readonly reason: string; }; export declare function decideOnExitRecovery(input: { readonly process: TrackedProcessRef; readonly currentBootId: string; readonly host?: TriggerProcessHost | undefined; }): OnExitRecoveryDecision; /** * The default agent prompt for an on-exit fire. * * Written to make the agent inspect the termination state rather than assume * success: the state, reason, exit code, signal and timed-out flag all appear * before the output, and the unknown case says outright that the outcome was * not observed. A template that opened with "your build finished" would be * wrong four times out of five states. */ export declare function renderOnExitPrompt(termination: TerminationMetadata, label: string): string; //# sourceMappingURL=process-triggers.d.ts.map