/** * Cross-OS control channel for async subagent runs. * * Background runs are detached OS processes. The original control path delivered * an interrupt with `process.kill(pid, SIGUSR2|SIGBREAK)`, but Windows cannot * deliver those signals cross-process via `process.kill` and throws `ENOSYS`, * which left async runs uninterruptible (no stop, no live steer) on Windows. * * This module adds a portable, file-based control inbox inside the run directory. * The parent drops an interrupt request file; the runner watches the inbox and * routes the request into its existing graceful `interruptRunner()` (pause + * resumable), identically on every platform. The OS signal is kept only as an * opportunistic fast-path; its failure is non-fatal because the file inbox is * authoritative. */ import * as fs from "node:fs"; /** * Opportunistic fast-path interrupt signal. On Unix `SIGUSR2` is trapped by the * runner; on Windows `process.kill(pid, "SIGBREAK")` is not deliverable * cross-process and throws `ENOSYS`, so the file inbox below is the real channel. */ export declare const INTERRUPT_SIGNAL: NodeJS.Signals; export type ControlChannelFs = Pick; export type ControlChannelTimers = { setInterval: typeof setInterval; clearInterval: typeof clearInterval; }; type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => unknown; export interface InterruptRequest { type: "interrupt"; ts?: number; source?: string; reason?: string; } export interface TimeoutRequest { type: "timeout"; ts?: number; source?: string; reason?: string; } export interface StopRequest { type: "stop"; ts?: number; source?: string; reason?: string; } export interface CheckpointDecisionRequest { type: "approve-checkpoint" | "reject-checkpoint"; ts?: number; source?: string; reason?: string; } export type SteerDeliveryMode = "steer" | "follow_up" | "auto"; export type SteerDeliveryStatus = "delivered" | "queued"; export interface SteerRequest { type: "steer"; id: string; ts: number; message: string; mode?: SteerDeliveryMode; targetIndex?: number; targetIndexes?: number[]; source?: string; } export interface SteerCapability { type: "steer-capability"; protocolVersion: 1; index: number; pid: number; readyAt: number; supported: boolean; } export interface SteerAck { type: "steer-ack"; protocolVersion: 1; requestId: string; index: number; ts: number; state: "delivered" | "queued" | "failed"; deliveryStatus?: SteerDeliveryStatus; message: string; } export declare const MAX_STEER_QUEUE_SIZE = 20; /** Control inbox directory inside an async run dir. */ export declare function controlInboxDir(asyncDir: string): string; /** Path of the portable interrupt request file. */ export declare function interruptRequestPath(asyncDir: string): string; /** Path of the portable timeout request file. */ export declare function timeoutRequestPath(asyncDir: string): string; /** Path of the portable manual stop request file. */ export declare function stopRequestPath(asyncDir: string): string; export declare function approveCheckpointRequestPath(asyncDir: string): string; export declare function rejectCheckpointRequestPath(asyncDir: string): string; /** Directory of parent-to-runner steering requests. */ export declare function steerRequestsDir(asyncDir: string): string; export declare function steerInboxClosedPath(asyncDir: string): string; export declare function closeSteerInbox(asyncDir: string, state: string): void; /** Per-child inbox consumed by the child prompt runtime inside the Pi process. */ export declare function stepSteerInboxDir(asyncDir: string, index: number): string; export declare function steerCapabilitiesDir(asyncDir: string): string; export declare function steerCapabilityPath(asyncDir: string, index: number): string; export declare function steerAcksDir(asyncDir: string, index: number): string; export declare function steerAckPathFromDir(dir: string, requestId: string): string; export declare function writeSteerRequestToDir(dir: string, request: SteerRequest): string; export declare function writeSteerCapabilityAt(filePath: string, capability: Omit): string; export declare function writeSteerCapability(asyncDir: string, capability: Omit): string; export declare function writeSteerAckAt(filePath: string, ack: Omit): string; export declare function writeSteerAck(asyncDir: string, ack: Omit): string; /** * Parent side: drop a portable interrupt request the runner's inbox watcher will * pick up regardless of OS. Written atomically (temp + rename), dir auto-created. */ export declare function requestAsyncInterrupt(asyncDir: string, payload?: Omit, deps?: { now?: () => number; }): string; export declare function requestAsyncTimeout(asyncDir: string, payload?: Omit, deps?: { now?: () => number; }): string; export declare function requestAsyncStop(asyncDir: string, payload?: Omit, deps?: { now?: () => number; }): string; export declare function requestAsyncCheckpointDecision(asyncDir: string, type: CheckpointDecisionRequest["type"], payload?: Omit, deps?: { now?: () => number; }): string; export declare function requestAsyncSteer(asyncDir: string, payload: { message: string; mode?: SteerDeliveryMode; targetIndex?: number; targetIndexes?: number[]; source?: string; id?: string; ts?: number; }, deps?: { now?: () => number; randomId?: () => string; }): string; export declare function enqueueStepSteer(asyncDir: string, index: number, request: SteerRequest): string; export declare function readSteerCapability(asyncDir: string, index: number): SteerCapability | undefined; export declare function consumeSteerCapabilities(asyncDir: string, fsImpl?: Pick): SteerCapability[]; export declare function consumeSteerAcks(asyncDir: string, fsImpl?: Pick): SteerAck[]; export declare function consumeSteerRequestsFromDir(dir: string, fsImpl?: Pick): SteerRequest[]; export declare function consumeSteerRequests(asyncDir: string, fsImpl?: Pick): SteerRequest[]; export declare function queueRevivalBrief(asyncDir: string, request: SteerRequest): string; export declare function readRevivalBriefs(asyncDir: string): Array<{ request: SteerRequest; path: string; }>; /** * Runner side: consume a pending interrupt request. Idempotent — removes the file * so each distinct request fires exactly once. Returns whether one was pending. */ export declare function consumeInterruptRequest(asyncDir: string, fsImpl?: Pick): boolean; export declare function consumeTimeoutRequest(asyncDir: string, fsImpl?: Pick): boolean; export declare function consumeStopRequest(asyncDir: string, fsImpl?: Pick): boolean; export declare function consumeCheckpointDecisionRequest(asyncDir: string, fsImpl?: Pick): "approved" | "rejected" | undefined; /** * Parent side: portable interrupt = authoritative file request + best-effort OS * signal. The signal is only a latency optimization on Unix; ENOSYS on Windows * is swallowed because the file inbox is authoritative there. Other signal * failures are surfaced because they usually mean the runner is not alive to * consume the request. */ export declare function deliverInterruptRequest(input: { asyncDir: string; pid?: number; kill?: KillFn; signal?: NodeJS.Signals; now?: () => number; source?: string; }): void; export declare function deliverTimeoutRequest(input: { asyncDir: string; pid?: number; kill?: KillFn; signal?: NodeJS.Signals; now?: () => number; source?: string; }): void; export declare function deliverStopRequest(input: { asyncDir: string; pid?: number; kill?: KillFn; signal?: NodeJS.Signals; now?: () => number; source?: string; }): void; export declare function deliverCheckpointDecisionRequest(input: { asyncDir: string; decision: "approved" | "rejected"; now?: () => number; source?: string; reason?: string; }): void; /** * Runner side: watch the control inbox and route interrupt requests into * `onInterrupt`. Uses `fs.watch` when available plus an interval poll as a * portable safety net (covers filesystems/platforms where `fs.watch` is * unreliable). Fires once per distinct request. Returns a disposer. */ export declare function watchAsyncControlInbox(asyncDir: string, opts: { onInterrupt: () => void; onTimeout?: () => void; onStop?: () => void; onSteer?: (request: SteerRequest) => void; onCheckpointDecision?: (decision: "approved" | "rejected") => void; onSteerCapability?: (capability: SteerCapability) => void; onSteerAck?: (ack: SteerAck) => void; pollIntervalMs?: number; fs?: ControlChannelFs; timers?: ControlChannelTimers; }): () => void; export {}; //# sourceMappingURL=control-channel.d.ts.map