Skip to content

exec: add fork subcommand for non-interactive session forking #17568

@hampsterx

Description

@hampsterx

What feature would you like to see?

Add a codex exec fork <session-id> [prompt] subcommand that creates a forked conversation from an existing session and runs the prompt on the new thread, mirroring the codex exec resume pattern. All server-side infrastructure (thread/fork RPC, ThreadForkParams/ThreadForkResponse types, Thread.forked_from_id metadata) already exists and is consumed by the TUI; only the codex exec CLI layer is missing.

Why

The TUI supports forking sessions via /fork and codex fork, but there is no non-interactive equivalent. External tools and automation that wrap codex exec --json cannot branch from an existing session programmatically:

  • Agent frameworks that want to explore multiple solution paths from the same starting point cannot fork without spawning the full TUI.
  • MCP bridge tools (like codex-mcp-bridge) that expose codex exec as an MCP server cannot offer forkFromSession parameters because the CLI has no fork subcommand.
  • CI/CD pipelines that want to re-run a previous session's context with a different prompt must either resume (mutating the original session) or start fresh (losing context).

The gap is purely at the CLI layer. The app-server already handles thread/fork (see app-server/README.md lines 260-268), the protocol types are stable and used by the TUI, and SessionConfiguredEvent already has a forked_from_id field.

Current state (verified against 3895ddd6)

codex-rs/tui/src/app_server_session.rs
    fork_thread() at line 335: sends ClientRequest::ThreadFork, builds session state
    thread_fork_params_from_config() at line 907: builds ThreadForkParams from Config
    thread_session_state_from_thread_fork_response() at line 1024: maps response

codex-rs/exec/src/lib.rs
    Command enum at line 118: Resume, Review — no Fork variant
    Thread setup block at line 636: handles Resume, falls through to ThreadStart — no Fork branch
    session_configured_from_thread_response() at line 1007: hardcodes forked_from_id: None
        (pre-existing: discards Thread.forked_from_id from start/resume responses too)

The TUI path shows the complete fork wiring. codex exec handles resume through the same thread/list + thread/resume APIs. Fork follows the identical pattern but with ClientRequest::ThreadFork and a hard error on session-not-found (no fallback to new session).

Proposal

Add Fork(ForkArgs) to the Command enum and wire it into the execution flow. Three files, single crate (codex-exec):

cli.rs: New ForkArgs struct and Command::Fork variant.

Command::Fork(ForkArgs)

pub struct ForkArgs {
    pub session_id: String,      // required positional (UUID or thread name)
    pub images: Vec<PathBuf>,    // optional --image/-i
    pub prompt: Option<String>,  // optional positional (reads stdin if omitted, same as resume)
}

Simpler than ResumeArgs: no --last/--all (fork source is always explicit).

lib.rs: Three additions:

  1. Prompt match arm for ExecCommand::Fork (mirrors resume: resolves prompt, builds UserInput items, returns InitialOperation::UserTurn).

  2. Thread setup branch: resolves source thread ID (UUID parse or name lookup via state DB + thread/list), sends ClientRequest::ThreadFork, converts response to SessionConfiguredEvent. If source session not found, hard error (not a fallback to new session).

  3. Helper functions mirroring the existing resume/start pattern:

  • thread_fork_params_from_config: matches TUI's version (sets ephemeral, persist_extended_history: true, same field list).
  • session_configured_from_thread_fork_response: maps ThreadForkResponse through the shared session_configured_from_thread_response.
  • resolve_fork_source_thread_id: UUID parse or name lookup, filters by cwd.

Bonus fix: session_configured_from_thread_response currently hardcodes forked_from_id: None. The patch adds a forked_from_id parameter and updates all call sites (start, resume, fork) to pass response.thread.forked_from_id.clone(). This matches how the TUI already handles it (app_server_session.rs lines 984-1044) and fixes --json mode silently discarding fork metadata from start/resume responses.

cli_tests.rs: Four parsing tests following the existing resume pattern:

  • fork_with_session_id_and_prompt
  • fork_with_session_id_only
  • fork_with_images
  • fork_requires_session_id (clap rejects missing positional)

Scope

Three files, +241 LOC, -3 LOC (the 3 deletions are the hardcoded None in existing call sites), within codex-exec only:

  • codex-rs/exec/src/cli.rs: ForkArgs struct, Command::Fork variant.
  • codex-rs/exec/src/lib.rs: fork execution flow, helpers, forked_from_id plumbing fix.
  • codex-rs/exec/src/cli_tests.rs: four CLI parsing tests.

No changes to codex-core, codex-protocol, codex-app-server, or codex-app-server-protocol. No new crate dependencies.

Backwards compatibility

Purely additive. The fork subcommand is a new Command variant. Existing resume, review, and bare codex exec behavior is unchanged. The forked_from_id fix is also backwards-compatible: it populates a field that was previously always None, so --json consumers that ignored it continue to work and those that wanted it now get it.

Alternatives considered

  • Use resume then manually reset context: mutates the original session. Fork creates a clean branch point without side effects on the source thread.
  • Spawn TUI with codex fork <id> and extract output: requires terminal allocation, defeats the purpose of non-interactive execution, brittle to parse.
  • Add --fork-from flag to bare codex exec: conflates session creation modes. The subcommand pattern (resume, review, fork) is cleaner and matches how the TUI already organizes these operations.
  • Support --last for fork: deferred for v1. Fork source should be explicit to avoid accidentally branching the wrong session. Can be added later if there's demand.

Open questions

  1. Should fork support --all (search globally, not just current cwd) for name-based resolution? Currently scoped to cwd for safety, matching resume's default behavior.
  2. Should a promptless fork be allowed (just create the forked thread and exit without starting a turn)? Current behavior matches resume: reads from stdin if no prompt argument, errors if stdin is empty. A no-op fork could be useful for pre-creating branches.

Implementation

I have a working implementation on a feature branch against 3895ddd6. Happy to open a PR if this is directionally acceptable.

Verification results:

  • cargo check -p codex-exec: clean
  • cargo test -p codex-exec: 61 tests passing (including 4 new CLI parsing tests)
  • cargo clippy -p codex-exec: clean

Context

Driven by codex-mcp-bridge, which wraps codex exec as an MCP server. Adding a forkFromSession parameter to its codex_exec tool depends on codex exec fork existing. Without it, the bridge cannot offer session branching, which is valuable for multi-path exploration and A/B comparison workflows in agent frameworks that sit on top of MCP.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestexecIssues related to the `codex exec` subcommand

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions