/** * Injected runtime primitives (D-005): the execution layer never imports * child_process, never signals the process directly, and never reads a wall clock - * spawn, clock, and signalling arrive as data so Node/Bun is a real * portability boundary and tests run against fakes deterministically. */ import type { NativeProcessOwner } from "../knowledge/interactive.js"; import type { NativeSettingsInspector } from "./native-settings.js"; export type { NativeProcessOwner } from "../knowledge/interactive.js"; export type ProcessStart = | { readonly kind: "started"; readonly owner: NativeProcessOwner | undefined } | { readonly kind: "not-started"; readonly code: string } | { readonly kind: "uncertain" }; export interface SpawnedProcess { /** OS creation evidence, separate from exit status or native session loading. * Older injected adapters may omit it; callers must treat that as unknown. */ readonly started?: Promise; /** Resolves on asynchronous stdin failure (for example EPIPE). The * adapter always handles the native error event, even without a caller. */ readonly inputError?: Promise; readonly stdout: AsyncIterable; readonly stderr: AsyncIterable; /** Idempotently terminate stdout and stderr reads without signalling the * child. An adapter MUST preserve chunks already buffered when disposal is * requested, then settle the first read beyond that finite snapshot. * Terminal cleanup owns process signalling separately. */ disposeOutput(): void; /** Resolves with the exit code, or null when the process died to a * signal without one. */ readonly exited: Promise; /** After `exited` settles, the spawn-time error message when the child * never started (async ENOENT and the like), else null. */ readonly startupError?: () => string | null; /** Present when the spawner opened a writable stdin (session mode needs * it; one-shot turns close or inherit stdin instead). */ readonly stdin?: { write(data: string): void; end(): void; }; } export interface SpawnOptions { /** Terminal output bypasses normalized streams and never reaches control NDJSON. */ readonly output?: "inherit" | "pipe"; readonly cwd?: string; /** `pipe` opens a writable stdin (sessions require it); `inherit` hands * the child the parent's fd 0; `close` gives it nothing (pi's rule). */ readonly stdin: "inherit" | "close" | "pipe"; /** Merged over the parent environment by the spawn adapter. A key mapped * to "" means DELETE the variable (not an empty string). Validated * against /^[A-Za-z_][A-Za-z0-9_]*$/ and NUL-free. */ readonly env?: Readonly>; } export type TimerHandle = number; export interface Clock { now(): number; /** The local zone's offset east of UTC at that instant, in minutes * (+420 for +07:00). Optional: a clock without it leaves wall-prose * reset times in local form unresolved. The zone is the child's too - * the child inherits this process's environment, so its prose * timestamps render in this zone. */ utcOffsetMinutes?(epochMs: number): number; setTimeout(fn: () => void, ms: number): TimerHandle; clearTimeout(handle: TimerHandle): void; } export type SignalName = "SIGTERM" | "SIGKILL"; /** One structured line per boundary transition - spawn, exit, stall, * redacted argv. Always-on baseline evidence; the host decides where it * goes. */ export type BoundaryLog = (event: Record) => void; export interface RunnerDeps { readonly approvalInput?: { readonly chunks: AsyncIterable; readonly close: () => void; readonly newId: () => string; }; /** Passive source read, required only for a caller-requested verified resume. */ readonly inspectNativeSettings?: NativeSettingsInspector; readonly spawn: (argv: readonly string[], opts: SpawnOptions) => SpawnedProcess; readonly clock: Clock; readonly signal: (proc: SpawnedProcess, sig: SignalName) => void; /** Inactivity budget; arms at every granularity and rearms on any output chunk. */ readonly stallMs?: number; /** Wall-clock ceiling; armed once at spawn and never rearmed. */ readonly turnTimeoutMs?: number; readonly log?: BoundaryLog; /** Host-minted correlation id; without it the runner falls back to a * per-process monotonic counter (collides across processes). */ readonly turnId?: string; }