Enable the agent command center in embedded mode (#58239)

## Why

The agent command center previously required a shared background server, preventing embedded sessions from opening it and rejecting `codex agents --no-daemon`.

## What changed

- Allow `codex agents --no-daemon` to use an embedded app server and open the command center without a shared server.
- Route `codex agents` through the normal daemon startup, eligibility, and compatibility checks, including embedded fallback where applicable.
- Return embedded sessions to the command center when canceling directory trust or archived-session confirmation.

## Testing

Add CLI coverage for `--no-daemon` before and after `agents`, plus an integration test that verifies embedded mode is used even when a daemon is running. Update archived-session tests to expect cancellation to return to the command center.

GitOrigin-RevId: 3bdf18a00456eae2e4d38103d12126e039d44119
This commit is contained in:
Eric Traut
2026-10-30 04:01:11 +00:00
committed by copyberry
parent 7689f21b79
commit bf877af418
29 changed files with 88 additions and 432 deletions
+16 -65
View File
@@ -102,7 +102,6 @@ use codex_features::Stage;
use codex_features::is_known_feature_key;
use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_login::is_workload_identity_selected;
use codex_memories_write::clear_memory_roots_contents;
use codex_models_manager::bundled_models_response;
use codex_models_manager::manager::RefreshStrategy;
@@ -144,7 +143,7 @@ struct MultitoolCli {
#[derive(Debug, clap::Subcommand)]
enum Subcommand {
/// Browse all agent sessions on the shared local app-server daemon.
/// Open the agent command center to browse and switch sessions.
Agents(AgentsCommand),
/// Internal: forward a local TCP socket through an HTTP/3 CONNECT proxy.
@@ -337,8 +336,8 @@ struct DebugTraceReduceCommand {
#[derive(Debug, Parser)]
struct AgentsCommand {
/// The agents overview requires a shared server; this option is rejected.
#[arg(long, hide = true)]
/// Run the agents overview with an embedded app server.
#[arg(long)]
no_daemon: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
@@ -771,10 +770,7 @@ fn handle_app_exit(
eprintln!("ERROR: {message}");
true
}
ExitReason::UserRequested
| ExitReason::Archived(_)
| ExitReason::TurnInterrupted
| ExitReason::ThreadRemoved => false,
ExitReason::UserRequested | ExitReason::TurnInterrupted => false,
};
let update_action = exit_info.update_action;
@@ -1131,19 +1127,6 @@ async fn cli_main(
"`codex agents` cannot apply local provider or additional-directory overrides to a remote server"
);
}
if is_workload_identity_selected() {
anyhow::bail!(
"`codex agents` is unavailable while workload identity is active"
);
}
if root_remote.is_none() {
resolve_remote_endpoint(
/*remote*/ None,
root_remote_auth_token_env.clone(),
)?;
#[cfg(not(any(unix, windows)))]
anyhow::bail!("`codex agents` requires `--remote` on this platform");
}
interactive.agents_overview = true;
}
let exit_info = run_interactive_tui(
@@ -2500,17 +2483,10 @@ async fn run_interactive_tui(
remote_auth_token_env: Option<String>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<AppExitInfo> {
if interactive.no_daemon {
if interactive.agents_overview {
return Ok(AppExitInfo::fatal(
"--no-daemon cannot be used with codex agents. The agents overview requires a shared server. Use codex --no-daemon to work without it.",
));
}
if remote.is_some() {
return Ok(AppExitInfo::fatal(
"--no-daemon cannot be used with --remote.",
));
}
if interactive.no_daemon && remote.is_some() {
return Ok(AppExitInfo::fatal(
"--no-daemon cannot be used with --remote.",
));
}
if let Some(prompt) = interactive.prompt.take() {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
@@ -2535,25 +2511,6 @@ async fn run_interactive_tui(
}
}
#[cfg(any(unix, windows))]
if interactive.agents_overview && remote.is_none() {
if !std::io::stdin().is_terminal() {
return Ok(AppExitInfo::fatal("stdin is not a terminal"));
}
if !std::io::stdout().is_terminal() {
return Ok(AppExitInfo::fatal("stdout is not a terminal"));
}
cloud_config::load_config(&interactive.config_overrides, LoaderOverrides::default())
.await
.map_err(std::io::Error::other)?;
let output = codex_app_server_daemon::run(AppServerLifecycleCommand::Start)
.await
.map_err(|err| std::io::Error::other(format!(
"{err:#}\nThe agents overview requires a shared server. Use codex --no-daemon to work without it."
)))?;
interactive.managed_daemon = output.backend.is_some();
}
let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env.clone()) {
Ok(remote_endpoint) => remote_endpoint,
Err(err) if is_remote_auth_usage_error(&err) => {
@@ -2561,6 +2518,14 @@ async fn run_interactive_tui(
}
Err(err) => return Err(err),
};
if interactive.agents_overview && remote_endpoint.is_none() {
if !std::io::stdin().is_terminal() {
return Ok(AppExitInfo::fatal("stdin is not a terminal"));
}
if !std::io::stdout().is_terminal() {
return Ok(AppExitInfo::fatal("stdout is not a terminal"));
}
}
let start_tui = || {
codex_tui::run_main(
interactive.clone(),
@@ -3902,20 +3867,6 @@ enabled = true
insta::assert_snapshot!(lines.join("\n"), @"Session ID: 123e4567-e89b-12d3-a456-426614174000");
}
#[test]
fn format_exit_messages_confirms_archive() {
let mut exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
exit_info.exit_reason = ExitReason::Archived(exit_info.thread_id.unwrap());
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
insta::assert_snapshot!(lines.join("\n"), @"
Token usage: total=2 input=0 output=2
Session archived: 123e4567-e89b-12d3-a456-426614174000
");
}
#[test]
fn format_exit_messages_includes_session_id_for_fatal_exit_without_resume_hint() {
let exit_info = AppExitInfo {