How one-shot codex subprocesses are executed.
Command.run/3 routes every synchronous command (Exec, ExecResume,
Review, CodexWrapper.exec/2) through the configured runner. The
default is CodexWrapper.Runner.Port, the /bin/sh-with-closed-stdin
Port the library has always used.
For leak-free execution -- where a timeout or BEAM death kills the whole
codex process group (the CLI and every stdio MCP server it spawned)
rather than abandoning it -- add
forcola to your deps and select its
runner:
# mix.exs
{:forcola, "~> 0.3.5 or ~> 0.4.0"}
# config/config.exs
config :codex_wrapper, runner: CodexWrapper.Runner.ForcolaRunner.Forcola only compiles when forcola is present, so the
dependency stays optional. See #48.
Contract
run/4 returns {:ok, {stdout, exit_code}} on completion (a non-zero
exit is not an error -- callers decide what an exit code means),
{:error, :timeout} when the timeout elapsed, and other {:error, reason} tuples for spawn/signal/io failures.
stream_lines/4 returns a lazy Enumerable of stdout lines for the
NDJSON paths (Exec.stream/2, ExecResume.stream/2, Review.stream/2,
and Session.stream/3 through the first two). Lines arrive without
their trailing newline, stderr is never merged into them, and the
stream ends when the process exits. A deadline emits a final
{:error, {:timeout, ms}} or {:error, {:idle_timeout, ms}} item.
A non-zero exit ends the stream without raising, on either runner --
the same silent-halt the streaming paths have always had. Callers that
need the exit code should use execute/2 rather than stream/2.
Summary
Types
Runner error reasons. :timeout is common to every runner.
Execution options, as produced by CodexWrapper.Config.cmd_opts/1
Callbacks
The timeout the runner will actually enforce for a caller's timeout.
Run binary with args and stream its stdout as lines.
Functions
effective_timeout/1 on runner, falling back to timeout unchanged
when the runner does not implement the optional callback.
The configured runner module, CodexWrapper.Runner.Port by default.
stream_lines/4 on the configured runner.
Types
Runner error reasons. :timeout is common to every runner.
@type opts() :: keyword()
Execution options, as produced by CodexWrapper.Config.cmd_opts/1:
:cd-- working directory (string):env-- list of{name, value}string tuples:stderr_to_stdout-- merge stderr into stdout
Callbacks
The timeout the runner will actually enforce for a caller's timeout.
Callers pass nil for "no timeout", but a runner may substitute a bound
of its own (Runner.Forcola does, since forcola requires a finite one).
Command.run/3 uses this to report the timeout that actually elapsed.
Optional; defaults to the caller's timeout unchanged.
@callback stream_lines( binary :: String.t(), args :: [String.t()], opts :: opts(), timeout :: timeout() | nil ) :: Enumerable.t()
Run binary with args and stream its stdout as lines.
Returns a lazy Enumerable of lines without their trailing newlines.
Opening the process is deferred until the stream is consumed, and the
process is terminated when the stream halts (including an early
Enum.take/2).
timeout bounds the whole run. The independent
opts[:idle_timeout_ms] bounds gaps between output frames; nil
disables the idle bound. When timeout is nil, Runner.Port has no
whole-run bound and Runner.Forcola uses its configurable default.
Both built-in runners default to a 300,000 ms idle bound.
Optional. CodexWrapper.Runner.stream_lines/4 warns and falls back to
Runner.Port for runners that do not implement it.
Functions
effective_timeout/1 on runner, falling back to timeout unchanged
when the runner does not implement the optional callback.
@spec impl() :: module()
The configured runner module, CodexWrapper.Runner.Port by default.
Set with a module:
config :codex_wrapper, runner: CodexWrapper.Runner.Forcolaor with the :task / :forcola shorthand for the two built-in runners:
config :codex_wrapper, runner: :forcola:forcola resolves to Runner.Port when the forcola dependency is
absent, so the shorthand is safe to set unconditionally.
@spec stream_lines(String.t(), [String.t()], opts(), timeout() | nil) :: Enumerable.t()
stream_lines/4 on the configured runner.
Falls back to CodexWrapper.Runner.Port with a warning when the
configured runner cannot be loaded or does not implement the optional
callback, so a runner written against the one-shot run/4 contract
alone keeps working. A configured runner is loaded before checking the
callback.