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
+15 -3
View File
@@ -903,9 +903,15 @@ $codexHome = if ([string]::IsNullOrWhiteSpace($env:CODEX_HOME)) {
} else {
$env:CODEX_HOME
}
$standaloneRoot = Join-Path $codexHome "packages\standalone"
$daemonOnly = $env:CODEX_INSTALL_DAEMON_ONLY -eq "1"
$standaloneRoot = Join-Path $codexHome $(if ($daemonOnly) { "packages\app-server-daemon" } else { "packages\standalone" })
$releasesDir = Join-Path $standaloneRoot "releases"
$currentDir = Join-Path $standaloneRoot "current"
$deferSelection = $env:CODEX_INSTALL_DEFER_SELECTION -eq "1"
if ($deferSelection) {
if (-not $daemonOnly) { throw "Deferred selection requires a daemon-only installation." }
$currentDir = Join-Path $standaloneRoot ".migration-current"
}
$autoUpdateVersion = Join-Path $standaloneRoot "auto-update-version"
$lockPath = Join-Path $standaloneRoot "install.lock"
@@ -933,7 +939,7 @@ if (-not [string]::IsNullOrWhiteSpace($currentVersion) -and $currentVersion -ne
Write-Step "Detected platform: $platformLabel"
Write-Step "Resolved version: $resolvedVersion"
$conflictingInstall = Get-ConflictingInstall -VisibleBinDir $visibleBinDir
$conflictingInstall = if ($daemonOnly) { $null } else { Get-ConflictingInstall -VisibleBinDir $visibleBinDir }
$oldStandaloneBackup = $null
$checksumAsset = "codex-package_SHA256SUMS"
@@ -949,6 +955,7 @@ $guardRejected = $false
try {
Invoke-WithInstallLock -LockPath $lockPath -Script {
$updaterRecord = Join-Path $codexHome "app-server-daemon\app-server-updater.pid"
if ($daemonOnly) { $updaterRecord = Join-Path $codexHome "app-server-daemon\daemon-updater.pid" }
$oldUpdaterParent = $false
if ($Release -eq "latest" -and $env:CODEX_INSTALL_IF_LATEST -ne "1" -and (Test-Path -LiteralPath $updaterRecord)) {
$updaterPid = $null
@@ -997,6 +1004,7 @@ try {
if (-not (Test-ReleaseIsComplete -ReleaseDir $releaseDir -ExpectedVersion $resolvedVersion -ExpectedTarget $target -Layout $installLayout)) {
if (Test-Path -LiteralPath $releaseDir) {
if ($daemonOnly) { throw "Refusing to overwrite existing daemon release $releaseDir." }
Write-WarningStep "Found incomplete existing release at $releaseDir. Reinstalling."
}
@@ -1058,6 +1066,9 @@ try {
}
New-Item -ItemType Directory -Force -Path $standaloneRoot | Out-Null
if ($deferSelection -and (Get-Item -LiteralPath (Join-Path $standaloneRoot "current") -Force -ErrorAction SilentlyContinue)) {
throw "A dedicated daemon is already selected; retry the update."
}
Ensure-Junction -LinkPath $currentDir -TargetPath $releaseDir -InstallerOwnedTargetPrefix $releasesDir
if ($Release -eq "latest") {
$tempMarker = "$autoUpdateVersion.tmp.$PID"
@@ -1069,6 +1080,7 @@ try {
}
}
if ($daemonOnly) { return }
$visibleParent = Split-Path -Parent $visibleBinDir
$currentBinDir = if ($installLayout -eq "Package") {
Join-Path $currentDir "bin"
@@ -1096,7 +1108,7 @@ try {
} finally {
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
}
if ($guardRejected) { return }
if ($guardRejected -or $daemonOnly) { return }
Maybe-HandleConflictingInstall -Conflict $conflictingInstall
+31 -2
View File
@@ -4,6 +4,7 @@ set -eu
RELEASE="${CODEX_RELEASE:-latest}"
NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}"
DAEMON_ONLY="${CODEX_INSTALL_DAEMON_ONLY:-0}"
DEFAULT_PREFER_RELEASES_OPENAI_COM="true"
PREFER_RELEASES_OPENAI_COM="${CODEX_INSTALLER_USE_RELEASES_OPENAI_COM:-$DEFAULT_PREFER_RELEASES_OPENAI_COM}"
RELEASES_BASE_URL="https://releases.openai.com/codex"
@@ -17,8 +18,18 @@ BIN_PATH="$BIN_DIR/codex"
CODE_MODE_HOST_BIN_PATH="$BIN_DIR/codex-code-mode-host"
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
STANDALONE_ROOT="$CODEX_HOME_DIR/packages/standalone"
if [ "$DAEMON_ONLY" = "1" ]; then
STANDALONE_ROOT="$CODEX_HOME_DIR/packages/app-server-daemon"
fi
RELEASES_DIR="$STANDALONE_ROOT/releases"
CURRENT_LINK="$STANDALONE_ROOT/current"
if [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" = "1" ]; then
if [ "$DAEMON_ONLY" != "1" ]; then
echo "Deferred selection requires a daemon-only installation." >&2
exit 1
fi
CURRENT_LINK="$STANDALONE_ROOT/.migration-current"
fi
AUTO_UPDATE_VERSION="$STANDALONE_ROOT/auto-update-version"
LOCK_FILE="$STANDALONE_ROOT/install.lock"
LOCK_DIR="$STANDALONE_ROOT/install.lock.d"
@@ -742,7 +753,7 @@ cleanup_stale_install_artifacts() {
find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -name '.staging.*' -exec rm -rf {} +
find "$STANDALONE_ROOT" -mindepth 1 -maxdepth 1 -name '.current.*' -exec rm -f {} +
if [ -d "$BIN_DIR" ]; then
if [ "$DAEMON_ONLY" != "1" ] && [ -d "$BIN_DIR" ]; then
find "$BIN_DIR" -mindepth 1 -maxdepth 1 -name '.codex.*' -exec rm -f {} +
fi
}
@@ -1140,7 +1151,9 @@ fi
step "Detected platform: $platform_label"
step "Resolved version: $resolved_version"
detect_conflicting_install
if [ "$DAEMON_ONLY" != "1" ]; then
detect_conflicting_install
fi
tmp_dir="$(mktemp -d)"
cleanup() {
@@ -1154,7 +1167,15 @@ trap 'exit 130' INT
trap 'exit 143' TERM
acquire_install_lock
if [ "${CODEX_INSTALL_DEFER_SELECTION:-0}" = "1" ] &&
{ [ -e "$STANDALONE_ROOT/current" ] || [ -L "$STANDALONE_ROOT/current" ]; }; then
echo "A dedicated daemon is already selected; retry the update." >&2
exit 1
fi
updater_record="$CODEX_HOME_DIR/app-server-daemon/app-server-updater.pid"
if [ "$DAEMON_ONLY" = "1" ]; then
updater_record="$CODEX_HOME_DIR/app-server-daemon/daemon-updater.pid"
fi
old_updater_parent="false"
if [ "${CODEX_INSTALL_IF_LATEST:-}" != "1" ] && [ -f "$updater_record" ]; then
updater_pid="$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$updater_record" | head -n 1)"
@@ -1197,6 +1218,10 @@ cleanup_stale_install_artifacts
if ! release_dir_is_complete "$release_dir" "$resolved_version" "$vendor_target" "$install_layout"; then
if [ -e "$release_dir" ] || [ -L "$release_dir" ]; then
if [ "$DAEMON_ONLY" = "1" ]; then
echo "Refusing to overwrite existing daemon release $release_dir." >&2
exit 1
fi
warn "Found incomplete existing release at $release_dir; reinstalling."
fi
@@ -1231,6 +1256,10 @@ if [ "$RELEASE" = "latest" ]; then
else
rm -f "$AUTO_UPDATE_VERSION"
fi
if [ "$DAEMON_ONLY" = "1" ]; then
release_install_lock
exit 0
fi
update_visible_command "$release_dir"
add_to_path
verify_visible_command
+36
View File
@@ -156,6 +156,40 @@ class InstallShTest(unittest.TestCase):
],
)
def test_daemon_install_preserves_visible_cli_and_profile(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir).resolve()
archive, checksum, metadata = create_package_release(root)
options = dict(
metadata_json=metadata,
archive_path=archive,
checksum_path=checksum,
force_macos=True,
)
pinned, _ = run_installer_in(root, VERSION, **options)
self.assertEqual(pinned.returncode, 0, pinned.stderr)
visible = root / "install-bin/codex"
original = visible.resolve()
pending = visible.with_name(".codex.12345")
pending.symlink_to(original)
profile = root / "home/.profile"
profile.write_text("unchanged profile\n")
installed, _ = run_installer_in(root, "latest", daemon_only=True, **options)
self.assertEqual(installed.returncode, 0, installed.stderr)
daemon = root / "codex-home/packages/app-server-daemon"
release_name = f"{VERSION}-aarch64-apple-darwin"
self.assertEqual(
(daemon / "current").resolve(), daemon / "releases" / release_name
)
self.assertEqual((daemon / "auto-update-version").read_text(), release_name)
self.assertEqual(visible.resolve(), original)
self.assertTrue(pending.is_symlink())
self.assertEqual(profile.read_text(), "unchanged profile\n")
self.assertFalse(
(root / "codex-home/packages/standalone/auto-update-version").exists()
)
def test_explicit_release_pins_even_the_current_latest_version(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
@@ -637,6 +671,7 @@ def run_installer_in(
update_guard_from_release: str | None = None,
old_updater_parent_pid: int | None = None,
fail_ps: bool = False,
daemon_only: bool = False,
) -> tuple[subprocess.CompletedProcess[str], list[str]]:
bin_dir = root / "bin"
bin_dir.mkdir(exist_ok=True)
@@ -771,6 +806,7 @@ def run_installer_in(
"CODEX_INSTALL_DIR": str(root / "install-bin"),
"CODEX_NON_INTERACTIVE": "1",
"CODEX_RELEASE": release,
"CODEX_INSTALL_DAEMON_ONLY": "1" if daemon_only else "0",
"CODEX_TEST_ARCHIVE_PATH": str(archive_path or ""),
"CODEX_TEST_CHECKSUM_PATH": str(checksum_path or ""),
"CODEX_TEST_RELEASES_CHECKSUM_PATH": str(