Move daemon packages out of the standalone CLI installation (#45546)

## Why

The daemon shares the standalone CLI package, coupling daemon updates to the visible CLI installation. A dedicated package lets daemon updates preserve the CLI selection and shell profile.

## What changed

- Add dedicated daemon package selection and separate daemon and updater PID files, with diagnostics for the new files.
- Make `codex app-server daemon update` migrate eligible legacy stable latest-channel installations to a dedicated package. Prepare and validate the release and its package-ownership support before switching; preserve settings and whether the daemon was running. Scheduled updates keep legacy installations in place.
- Add daemon-only and deferred-selection modes to the shell and PowerShell installers, leaving the visible CLI installation unchanged.
- Coordinate migration with installer locks, refresh package selection after acquiring lifecycle locks, and reject package changes during update downloads.

## Testing

Add coverage for running and stopped migrations, incompatible installers and releases, package-selection races, daemon-only updates, and a capability probe that does not start an updater. Add a shell installer regression test verifying that daemon installation preserves the visible CLI and shell profile.

GitOrigin-RevId: 1ec0f358f8f6ebde7daddb74e880cc8d2235d647
This commit is contained in:
Eric Traut
2026-09-14 23:43:59 +00:00
committed by copyberry
parent 2f1583b411
commit 923c6028b6
17 changed files with 1057 additions and 106 deletions
+11
View File
@@ -55,6 +55,16 @@ pub(super) async fn background_server_check(config: &Config) -> DoctorCheck {
"update-loop pid file",
&state_dir.join(UPDATE_PID_FILE_NAME),
);
push_file_detail(
&mut details,
"dedicated pid file",
&state_dir.join("daemon.pid"),
);
push_file_detail(
&mut details,
"dedicated update-loop pid file",
&state_dir.join("daemon-updater.pid"),
);
push_configured_updater(&mut details, &state_dir.join(SETTINGS_FILE_NAME));
let socket_path = match codex_app_server::app_server_control_socket_path(&config.codex_home) {
@@ -322,6 +332,7 @@ mod tests {
detail.starts_with("automatic updates:")
|| detail.starts_with("update interval:")
|| detail.starts_with("update-loop pid file:")
|| detail.starts_with("dedicated")
})
.map(|detail| {
detail
@@ -1,8 +1,9 @@
---
source: cli/src/doctor/background.rs
assertion_line: 332
expression: details
---
update-loop pid file: CODEX_HOME/app-server-daemon/app-server-updater.pid (missing)
dedicated pid file: CODEX_HOME/app-server-daemon/daemon.pid (missing)
dedicated update-loop pid file: CODEX_HOME/app-server-daemon/daemon-updater.pid (missing)
automatic updates: disabled (configured)
update interval: 90 minutes (configured)
+21 -8
View File
@@ -809,7 +809,11 @@ enum AppServerDaemonSubcommand {
/// [internal] Run the detached pid-backed standalone updater loop.
#[clap(hide = true)]
PidUpdateLoop,
PidUpdateLoop {
/// Check support for daemon-owned packages without starting the updater.
#[arg(long, hide = true)]
check_package_ownership: bool,
},
}
#[derive(Debug, Args)]
@@ -1392,10 +1396,6 @@ async fn cli_main(
AppServerDaemonSubcommand::Restart => {
print_app_server_daemon_output(AppServerLifecycleCommand::Restart).await?;
}
AppServerDaemonSubcommand::Update => {
let output = codex_app_server_daemon::update().await?;
println!("{}", serde_json::to_string(&output)?);
}
AppServerDaemonSubcommand::EnableRemoteControl => {
print_app_server_remote_control_output(AppServerRemoteControlMode::Enabled)
.await?;
@@ -1412,7 +1412,13 @@ async fn cli_main(
AppServerDaemonSubcommand::Version => {
print_app_server_daemon_output(AppServerLifecycleCommand::Version).await?;
}
AppServerDaemonSubcommand::PidUpdateLoop => {
AppServerDaemonSubcommand::PidUpdateLoop {
check_package_ownership: true,
} => return Ok(()),
AppServerDaemonSubcommand::Update
| AppServerDaemonSubcommand::PidUpdateLoop {
check_package_ownership: false,
} => {
let cli_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
@@ -1422,7 +1428,14 @@ async fn cli_main(
.await
.map_err(anyhow::Error::from);
let http_client_factory = updater_http_client_factory(config);
codex_app_server_daemon::run_pid_update_loop(http_client_factory).await?;
if matches!(daemon_cli.subcommand, AppServerDaemonSubcommand::Update) {
let output =
codex_app_server_daemon::update(http_client_factory).await?;
println!("{}", serde_json::to_string(&output)?);
} else {
codex_app_server_daemon::run_pid_update_loop(http_client_factory)
.await?;
}
}
},
Some(AppServerSubcommand::Proxy(proxy_cli)) => {
@@ -2643,7 +2656,7 @@ fn app_server_subcommand_name(subcommand: Option<&AppServerSubcommand>) -> &'sta
}
AppServerDaemonSubcommand::Stop => "app-server daemon stop",
AppServerDaemonSubcommand::Version => "app-server daemon version",
AppServerDaemonSubcommand::PidUpdateLoop => "app-server daemon pid-update-loop",
AppServerDaemonSubcommand::PidUpdateLoop { .. } => "app-server daemon pid-update-loop",
},
Some(AppServerSubcommand::Proxy(_)) => "app-server proxy",
Some(AppServerSubcommand::GenerateTs(_)) => "app-server generate-ts",