diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 0b5b0b4b6d..753484d8bb 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -95,7 +95,7 @@ pub struct ResumeArgs { pub session_id: Option, /// Resume the most recent recorded session (newest) without specifying an id. - #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] + #[arg(long = "last", default_value_t = false)] pub last: bool, /// Prompt to send after resuming the session. If `-` is used, read from stdin. diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a9cf6b2c6d..dff0cd3a11 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -76,7 +76,21 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let prompt_arg = match &command { // Allow prompt before the subcommand by falling back to the parent-level prompt // when the Resume subcommand did not provide its own prompt. - Some(ExecCommand::Resume(args)) => args.prompt.clone().or(prompt), + Some(ExecCommand::Resume(args)) => { + let resume_prompt = args + .prompt + .clone() + // When using `resume --last `, clap still parses the first positional + // as `session_id`. Reinterpret it as the prompt so the flag works with JSON mode. + .or_else(|| { + if args.last { + args.session_id.clone() + } else { + None + } + }); + resume_prompt.or(prompt) + } None => prompt, }; diff --git a/codex-rs/exec/tests/suite/resume.rs b/codex-rs/exec/tests/suite/resume.rs index 24b8cb0bce..3d992af55a 100644 --- a/codex-rs/exec/tests/suite/resume.rs +++ b/codex-rs/exec/tests/suite/resume.rs @@ -123,6 +123,60 @@ fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> { Ok(()) } +#[test] +fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<()> { + let test = test_codex_exec(); + let fixture = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cli_responses_fixture.sse"); + + // 1) First run: create a session with a unique marker in the content. + let marker = format!("resume-last-json-{}", Uuid::new_v4()); + let prompt = format!("echo {marker}"); + + test.cmd() + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg(&prompt) + .assert() + .success(); + + // Find the created session file containing the marker. + let sessions_dir = test.home_path().join("sessions"); + let path = find_session_file_containing_marker(&sessions_dir, &marker) + .expect("no session file found after first run"); + + // 2) Second run: resume the most recent file and pass the prompt after --last. + let marker2 = format!("resume-last-json-2-{}", Uuid::new_v4()); + let prompt2 = format!("echo {marker2}"); + + test.cmd() + .env("CODEX_RS_SSE_FIXTURE", &fixture) + .env("OPENAI_BASE_URL", "http://unused.local") + .arg("--skip-git-repo-check") + .arg("-C") + .arg(env!("CARGO_MANIFEST_DIR")) + .arg("--json") + .arg("resume") + .arg("--last") + .arg(&prompt2) + .assert() + .success(); + + let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2) + .expect("no resumed session file containing marker2"); + assert_eq!( + resumed_path, path, + "resume --last should append to existing file" + ); + let content = std::fs::read_to_string(&resumed_path)?; + assert!(content.contains(&marker)); + assert!(content.contains(&marker2)); + Ok(()) +} + #[test] fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> { let test = test_codex_exec();