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"}
# 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 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 is enforced as each runner can: Runner.Port applies it as
an idle bound between output frames (what the streaming paths have
always done), Runner.Forcola as forcola's whole-run bound with a
process-group kill. nil means each runner's own default.
Optional. CodexWrapper.Runner.stream_lines/4 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 when the configured runner
does not implement the optional callback, so a runner written against
the one-shot run/4 contract alone keeps working.