mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Allow restarting managed background servers from the TUI (#53173)
## Why Startup configuration warnings can reflect settings from when the background server started, even after those settings have been fixed. A restart lets users refresh the server without leaving the TUI. ## What changed - Add a confirmed restart action to `/daemon`, warning that active or queued work in other chats may be interrupted. Reconnect automatically and let an in-progress restart finish if the TUI exits. - Verify that Codex manages a local server before enabling restart, including servers discovered with auto-start disabled and those started by `codex agents`. - Show a restart hint on the first startup configuration warning for managed servers, preserving the original diagnostic for copying and dismissal. - Repaint the full surface of centered dialogs to restore background colors over cached blank cells. ## Testing Add CLI integration coverage for agents startup and restarting a discovered daemon with auto-start disabled. Extend TUI tests for confirmation, cancellation, unmanaged servers, warning hints and dismissal, and dialog background rendering. GitOrigin-RevId: 8e4319fab84f13bf6e7335deb93364fccaff14a7
This commit is contained in:
@@ -2444,11 +2444,12 @@ async fn run_interactive_tui(
|
||||
cloud_config::load_config(&interactive.config_overrides, LoaderOverrides::default())
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
codex_app_server_daemon::run(AppServerLifecycleCommand::Start)
|
||||
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()) {
|
||||
|
||||
@@ -14,6 +14,18 @@ async fn auto_daemon_start_attaches_to_shared_server() -> Result<()> {
|
||||
daemon_startup("start").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn agents_can_offer_restart_for_managed_daemon() -> Result<()> {
|
||||
daemon_startup("agents").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn discovered_managed_daemon_can_restart_with_auto_start_disabled() -> Result<()> {
|
||||
daemon_startup("discovered").await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn incompatible_daemon_can_cancel() -> Result<()> {
|
||||
@@ -159,8 +171,14 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
suppress_unstable_features_warning = true\nanalytics.enabled = false\n\
|
||||
windows.sandbox = \"unelevated\"\n\
|
||||
tui.disable_paste_burst = true\n\
|
||||
{}\n\
|
||||
notice.model_migrations.\"gpt-5.6-terra\" = \"gpt-6-sol\"\n\
|
||||
[projects.{}]\ntrust_level = \"trusted\"\n",
|
||||
if command == "discovered" {
|
||||
"tui.status_line=[\"model\",\"thread-id\"]"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
serde_json::to_string(&workspace_path)?,
|
||||
),
|
||||
)?;
|
||||
@@ -196,7 +214,11 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
env.insert("TERM".into(), "xterm-256color".into());
|
||||
let mut args = vec!["--no-alt-screen".to_string()];
|
||||
let mut steps: VecDeque<(&str, &[u8])> = VecDeque::new();
|
||||
if matches!(command, "start" | "bedrock-running" | "restrictive-job") || mismatch {
|
||||
if matches!(
|
||||
command,
|
||||
"start" | "agents" | "discovered" | "bedrock-running" | "restrictive-job"
|
||||
) || mismatch
|
||||
{
|
||||
// A selected package with a stopped daemon avoids installing a release.
|
||||
let managed = home
|
||||
.path()
|
||||
@@ -223,7 +245,9 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
}
|
||||
let pid_file = home.path().join("app-server-daemon/daemon.pid");
|
||||
let result = async {
|
||||
let mut existing_daemon = if command == "bedrock-running" || (mismatch && !disabling) {
|
||||
let mut existing_daemon = if matches!(command, "bedrock-running" | "discovered")
|
||||
|| (mismatch && !disabling)
|
||||
{
|
||||
let started = Command::new(&codex)
|
||||
.env_clear()
|
||||
.envs(&env)
|
||||
@@ -243,6 +267,19 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
// The draft header is visible before the session's command composer is ready.
|
||||
steps.push_back(("GPT-5.6-Terra", b"/status\r"));
|
||||
"Server:Localbackgroundserver"
|
||||
} else if command == "agents" {
|
||||
args.push("agents".into());
|
||||
steps.push_back(("Notasksyet", b"\x1b"));
|
||||
steps.push_back(("GPT-5.6-Terra", b"/daemon\r"));
|
||||
steps.push_back(("Restartbackgroundserver", b"\x1b[F\r"));
|
||||
"Restartbackgroundserver?"
|
||||
} else if command == "discovered" {
|
||||
args.extend(["--disable".into(), "daemon_auto_start".into()]);
|
||||
steps.push_back(("GPT-5.6-Terra", b"/daemon\r"));
|
||||
steps.push_back(("Restartbackgroundserver", b"2"));
|
||||
steps.push_back(("Restartbackgroundserver?", b"2\r"));
|
||||
steps.push_back(("GPT-5.6-Terra", b"/status\r"));
|
||||
"Server:Localbackgroundserver"
|
||||
} else if elevated_launch || command == "restrictive-job" && elevated {
|
||||
if let Some(action) = command.strip_prefix("elevated-") {
|
||||
args.push(action.into());
|
||||
@@ -344,6 +381,27 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
if let Some((ready, input)) = steps.front()
|
||||
&& text.contains(ready)
|
||||
{
|
||||
if command == "discovered" {
|
||||
if *ready == "Restartbackgroundserver?" {
|
||||
existing_daemon = Some(fs::read(&pid_file)?);
|
||||
} else if *ready == "GPT-5.6-Terra" {
|
||||
// The header renders before thread/start completes. Wait for the
|
||||
// live status line to confirm there is a session to reconnect.
|
||||
if !screen.screen().contents().split_whitespace().any(|part| {
|
||||
codex_protocol::ThreadId::from_string(part).is_ok()
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
// The old header can linger while the daemon stops. Do not send
|
||||
// /status until the new process is up and reconnect has finished.
|
||||
if steps.len() == 1
|
||||
&& (!fs::read(&pid_file).is_ok_and(|pid| Some(pid) != existing_daemon)
|
||||
|| text.contains("Reconnecting"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if disabling && *ready == "Backgroundserverhasincompatiblefeaturesettings" {
|
||||
existing_daemon = Some(fs::read(&pid_file)?);
|
||||
let settings: serde_json::Value = serde_json::from_slice(&fs::read(home.path().join("app-server-daemon/settings.json"))?)?;
|
||||
@@ -379,6 +437,9 @@ async fn daemon_startup(command: &str) -> Result<()> {
|
||||
} else if command == "start" {
|
||||
ensure!(text.contains("Server:Localbackgroundserver"));
|
||||
ensure!(home.path().join("app-server-daemon/daemon.pid").exists());
|
||||
} else if command == "discovered" {
|
||||
let previous_pid = existing_daemon.as_ref().context("missing original daemon PID")?;
|
||||
ensure!(fs::read(&pid_file)? != *previous_pid, "daemon PID did not change");
|
||||
} else if let Some(existing_daemon) = &existing_daemon {
|
||||
ensure!(fs::read(&pid_file)? == *existing_daemon);
|
||||
} else if elevated_launch {
|
||||
|
||||
Reference in New Issue
Block a user