import type { DaemonToWorker } from './types.js'; type InitCfg = Extract; /** CLIs that expose the codex-family `app-server --listen` + `--remote resume` * protocol the RPC engine drives. codex + traex are verified identical; coco * diverges (--resume flag) and needs its own verification before inclusion. */ export declare const RPC_CAPABLE_CLIS: Set; /** Retry cadence for native turn/completed → rollout visibility hydration. * Total window is 11.55s: bounded, but intentionally aligned with the 12s * first-turn persistence probe so a fast terminal does not discard fallback * output merely because the rollout filesystem is a few seconds behind. */ export declare const CODEX_RPC_TERMINAL_HYDRATION_DELAYS_MS: readonly [50, 100, 200, 400, 800, 1600, 2400, 3000, 3000]; /** Ordinary transcript ingest must not advance past a turn/start ACK that has * not installed its exact bridge mark yet. Native-terminal hydration for an * older owner is different: it must keep draining that owner's final output * even while a successor waits for ACK. Any successor events reached by that * drain stay in CodexBridgeQueue's unmatched replay buffer until activation. * * A matching awaiting owner still blocks hydration, because consuming its * transcript before the exact mark exists would retire the same logical * delivery against incomplete local ownership. */ export declare function rpcTranscriptIngestBlockedByAwaitingActivation(awaitingOwnerKeys: Iterable, hydrationOwnerKey?: string): boolean; /** Monotonic fence for async app-server engagement. Worker IPC handlers are not * serialized, so restart/close can invalidate an engage while it is awaiting * /readyz, thread creation, or first-turn rollout evidence. Only the lease * returned by the latest begin() may publish process-global engine state. */ export declare class RpcEngagementFence { private epoch; private deadEpoch; begin(): number; invalidate(): void; isCurrent(lease: number): boolean; /** Record an app-server death only for the engagement that owns it. A late * callback from an older engine must not poison a replacement generation. */ markDead(lease: number): void; /** Publication requires both generation ownership and a live app-server. * `onDead` can run after the last awaited RPC response but before the worker * stores the engine globally, so generation freshness alone is insufficient. */ isLive(lease: number): boolean; } /** All fail-closed gates for codex-family RPC input in ONE place so the worker's * pane-branching and engageCodexRpc agree. Every excluded case degrades to the * normal paste path — never a silent capability/security change: * - disableCliBypass: RPC hardcodes approvalPolicy=never + dangerFullAccess, so * engaging it for an approval-gated bot would silently upgrade it to full * access (P1-1). * - startupCommands: /effort etc. must run in the TUI before the first turn, * but the fresh first turn is sent pre-spawn to persist the rollout — RPC * can't honor that ordering, so fail-closed (P1-4). * - wrapperCli / legacy cliPathOverride: the app-server is launched as ` * app-server`, which an ambiguous wrapper/alternate launcher won't satisfy * the same way the TUI's buildArgs does. A structured compatible runtime is * the only path override allowed through this gate (P1-2). * - backendType !== 'tmux': the pane-ownership detection + controlled respawn * are only wired for tmux. On herdr/zellij a surviving dead `--remote` pane * would be misjudged as native and reattached, and pty has no persistent * pane at all — so restrict RPC to tmux until each backend's replace path is * built + verified. */ export interface CodexRpcRuntimeGates { /** Process-wide BOTMUX_SANDBOX=1 force. It is not represented in InitCfg but * must gate RPC too: the app-server owns model execution and otherwise runs * outside the sandbox wrapped around the viewer pane. */ sandboxForced?: boolean; } export declare function codexRpcEligible(cfg: InitCfg, runtime?: CodexRpcRuntimeGates): boolean; /** Positive rollout evidence that THIS turn's user message was persisted (P1-1). * Given a thread's drained rollout events, is there a user turn matching the * prompt? session_meta (written at thread/start) is not a `kind:'user'` event, * so an empty thread yields false — filename existence alone would not. Match is * normalized-equal OR contains (codex may prepend AGENTS.md context to the first * turn); the fresh-thread scope makes a contains-match unambiguous. */ export declare function rolloutUserTurnMatches(events: ReadonlyArray<{ kind: string; text: string; }>, promptText: string): boolean; /** Decide what to do about a codex startup dialog on an RPC `--remote` pane * (P1-3 / P2). An RPC pane has no terminal input path, so a blocking dialog * freezes the viewer. The update menu is disabled at the source (-c * check_for_update_on_startup=false); this is only a fail-safe: * - 'warn-update' — an update menu is present (default may be "Update now") → * NEVER auto-press; warn the user, they dismiss manually. * - 'dismiss-safe' — a plain "press enter to continue" with no menu → safe Enter. * - 'ready' — composer reached, no blocking dialog → stop watching. * - 'wait' — nothing actionable yet. * UPDATE_DIALOG takes precedence, so a screen with both an update menu AND a * "press enter" line is warned, never pressed. */ export declare function decideStartupDialogAction(screen: string, readyPattern?: RegExp): 'warn-update' | 'dismiss-safe' | 'ready' | 'wait'; export interface PaneProbes { panePidOf?: (sessionName: string) => number | undefined; argvOf?: (pid: number) => string[]; commOf?: (pid: number) => string | undefined; childrenOf?: (pid: number) => number[]; } /** Outcome of an engage attempt (exactly-once-priority three-state for fresh + * the resume/setup states): * - 'accepted' — fresh first turn confirmed (ack or rollout evidence), or a * resume that needs its waking prompt queued is 'resumed'. * - 'ambiguous' — fresh first turn dispatched but unconfirmed → engaged, but * the prompt must NOT be resent (P1-1); caller notifies. * - 'resumed' — resume path engaged (no turn sent) → the waking prompt must * be queued for post-ready flush. * - 'not-engaged' — setup failed OR fresh frame never dispatched → paste. */ export type EngageOutcome = 'accepted' | 'ambiguous' | 'resumed' | 'not-engaged'; /** Whether the FRESH first turn should use the normal confirmed pre-mark path * (so the reply is attributed even if the model skips `botmux send`). ONLY * 'accepted' — a confirmed turn whose prompt is not re-queued. The worker also * gives 'ambiguous' its own attribution-only mark plus a fail-closed owner: * structured terminal retires it when visible, while exact engine teardown is * the intentional fallback when no native owner can ever be mapped. That * separate path never resends the prompt and prevents a permanent queue head. * 'not-sent'/'resumed' never reach either pre-mark path; not-sent's paste flush * marks once, and resume flushes its queued prompt. */ export declare function shouldPreMarkFirstTurn(outcome: EngageOutcome): boolean; /** Injected effects for the init-time RPC state machine (real ones wired by the * worker; fakes by tests). */ export interface RpcInitEffects { paneInfo: (sessionId: string) => { name: string; live: boolean; } | null; paneIsRemote: (sessionName: string) => boolean; /** Refresh the session-scoped Skill/MCP generation and start its trusted MCP * host before the app-server starts. For fresh RPC, this also mutates the * first prompt to include the current Skill catalog. */ prepare: () => Promise; engage: () => Promise; killVerify: (sessionName: string) => Promise; teardownEngine: () => void; log: (m: string) => void; notify: (m: string) => void; } /** Whether the initial prompt should be pushed to pendingMessages (the exact * worker wiring, extracted so the P1-1 exactly-once guarantee is unit-testable): * - paste (no RPC engine) → queue as usual. * - RPC RESUME (engine + queuePrompt) → queue the waking prompt for post-ready flush. * - RPC FRESH accepted/ambiguous → engine set + queuePrompt=false → NEVER * queue (the turn was pre-sent or is ambiguous; re-queuing would double-execute). * args-baked first prompts skip the queue unless deferred for startup commands. */ export declare function shouldQueueInitialPrompt(o: { hasPrompt: boolean; rpcEngineActive: boolean; queuePrompt: boolean; passesInitialPromptViaArgs: boolean; deferInitialPrompt: boolean; }): boolean; export interface RpcInitDecision { /** RPC is active → spawnCli will launch the `--remote resume` TUI. */ engaged: boolean; /** The init prompt must be QUEUED (resume path: delivered by flushPending → * sendTurn once the TUI is ready) rather than pre-sent (fresh) or pasted. */ queuePrompt: boolean; /** A stale `--remote` pane could not be replaced — the caller MUST NOT run * spawnCli (it would reattach the dead pane against the fresh-port engine). */ abortSpawn: boolean; } /** Pure init-time state machine for codex-family RPC, extracted so the * fresh/resume/kill-failure ORDERING is unit-testable (the worker only wires * real effects + acts on the decision). Mirrors the four cases: * - not eligible → nothing (paste). * - no live pane (fresh or resume- → prepare Skill/MCP state, then engage; * without a surviving pane) fresh pre-sends the first turn inside * engage, resume must queue it. * - live RPC-owned pane → prepare, engage, then kill+VERIFY the stale pane; * on success respawn (queue the prompt), * on failure tear the engine down + abort * spawn (never attach a stale remote pane * to a fresh-port engine — Codex P0-2). * - live native paste pane → leave it (fail-closed paste, boundary #3). */ export declare function orchestrateCodexRpcInit(cfg: InitCfg, fx: RpcInitEffects, runtime?: CodexRpcRuntimeGates): Promise; export interface PersistentPaneKillEffects { kill: (sessionName: string) => void; /** Tri-state on purpose. A boolean cannot distinguish "the pane is gone" from * "the liveness probe got no answer", and this function's return value is * read as PROOF of absence. `hasSession()`-style helpers collapse a probe * timeout into `false`, which is exactly the wrong direction here. */ probeLive: (sessionName: string) => 'live' | 'gone' | 'unknown'; wait: (ms: number) => Promise; } /** Kill a resolved persistent-session name and verify that exact name is gone. * Keeping the resolved name opaque avoids accidentally applying sessionName() * twice (`bmx-1234` -> `bmx-bmx-`), which would turn every failed kill into a * false success and reattach the stale RPC pane. * * Returns true ONLY on an authoritative 'gone'. An indeterminate probe is not * proof: under host load even a cheap `has-session` (~20ms healthy) can be * killed by its own timeout, and reporting that as a verified kill lets a * surviving dead-`--remote` pane be reattached to the fresh-port engine — the * exact P0 freeze this verification layer exists to prevent. When every * attempt comes back 'unknown' we fail closed: the caller then aborts init * and tells the user, instead of silently proceeding on an unproven kill. */ export declare function killAndVerifyPersistentPane(sessionName: string, fx: PersistentPaneKillEffects, attempts?: number, retryMs?: number): Promise; /** Does the surviving persistent pane run a botmux RPC `--remote` TUI (vs a * native paste codex/traex)? Walks the pane's process tree and inspects the LEAF * argv (Linux /proc + macOS ps, via readCmdline) — NOT tmux * pane_current_command, which only returns `codex`/`node` without argv. Only a * codex-family process carrying `--remote` in argv counts as RPC-owned; a native * `codex resume`, a bare shell, or an unreadable tree fails-closed to false so a * daemon-restart resume never force-respawns a possibly-mid-turn native pane. * This is a live-argv check, not a persisted marker, so there is no stale-marker * hazard. Probes are injectable for tests (defaults hit the real OS/tmux). */ export declare function paneRunsRemoteTui(persistentSessionName: string, probes?: PaneProbes, expectedExecutable?: string): boolean; export {}; //# sourceMappingURL=codex-rpc-lifecycle.d.ts.map