CodexWrapper.Runner.Forcola (CodexWrapper v0.8.1)

Copy Markdown View Source

Process-group runner backed by forcola.

Evidenced one-shot execution requires Forcola 0.7's reviewed PR #97 run/2 contract and matching shim. Forcola 0.6 cannot establish these facts and is rejected before invoking run_evidenced/5. Legacy run/stream callbacks remain available. New options pass through without global environment or application changes. Native exit, cancellation reason, transport, cleanup confirmation/scope and guard close are retained independently. Missing evidence remains unknown. A confirmed process group never proves containment of escaped descendants.

Every codex invocation runs under forcola's Rust shim, which places the CLI in its own process group and signals the whole group on timeout or when the BEAM dies. One-shot runs send SIGINT first, then SIGTERM after interrupt_grace_ms (default 2,000), then SIGKILL after kill_grace_ms. Codex handles SIGINT by stopping the tool command it is running; on SIGTERM alone it exits and leaves that command behind, because codex puts each tool command and stdio MCP server in its own process group (#119). Pass interrupt_signal: :int and interrupt_grace_ms in runner_options to change the grace.

Streaming (stream_lines/4) still sends SIGTERM, then SIGKILL, with no SIGINT step: Forcola.Stream does not accept interrupt options yet (joshrotenberg/forcola#93). A codex tool command running when a stream is halted or times out can outlive codex. The default CodexWrapper.Runner.Port sends SIGINT first on both paths, but signals only the codex process. Neither runner stops descendants that leave codex's process group or ignore these signals; on Linux, forcola's cgroup option is the backstop.

forcola closes the child's stdin immediately after spawn (forcola 0.3.4, forcola#67), so codex sees EOF naturally -- the /bin/sh ... < /dev/null wrapper the default runner needs is unnecessary here. Before 0.3.4 forcola left the child's stdin open, and codex exec would print "Reading additional input from stdin..." and hang until the timeout elapsed. The ~> 0.7.0 dependency floor includes this fix, raw output observation and bounded one-shot termination evidence.

forcola requires a finite whole-run bound. Synchronous commands with no :timeout use forcola_default_timeout_ms (five minutes by default); streams use forcola_default_stream_timeout_ms (one hour by default), so a long active turn is not cut off after five minutes.

stream_lines/4 is backed by Forcola.Stream.lines/2, so the NDJSON paths (Exec.stream/2 and friends) get the same group kill on halt, timeout, or BEAM death. Unlike Forcola.Stream.lines/2, it does not raise on a non-zero exit -- it ends the stream, which is what CodexWrapper.Runner.Port has always done and what the CodexWrapper.Runner contract specifies.

This module compiles only when forcola is a dependency. Select it with config :codex_wrapper, runner: CodexWrapper.Runner.Forcola (or the :forcola shorthand). forcola is POSIX-only.

Summary

Functions

The bound forcola will enforce for timeout.

Functions

effective_timeout(timeout)

The bound forcola will enforce for timeout.

A caller's nil ("no timeout") becomes config :codex_wrapper, forcola_default_timeout_ms: <ms> (default 300000), since forcola requires a finite bound.