Files
codex/codex-rs/app-server-daemon
Eric Traut adee0b04fa Preserve standalone release pins during daemon updates (#43521)
## Why

Bootstrapping the app-server daemon should preserve an explicitly selected release, even when that version is currently `latest`. Older managed binaries should also be able to serve app-server without supporting the updater command.

## What changed

- Record `latest` selections in `auto-update-version` in both standalone installers and clear the marker for explicit releases.
- Start the daemon updater only for a marked stable release whose binary supports `pid-update-loop`. Existing installs without a marker require a new `latest` installation to enable automatic updates.
- Recheck the selected release under the install lock so an in-flight update cannot overwrite a new pin, including installer calls from older updaters. Recheck selection before restarting app-server or replacing the updater.
- Cancel Unix installer process groups and clean up their owned fallback locks when the updater stops.

## Testing

Add coverage for channel markers, explicit pins of the current latest version, older updater guards, updater command support, and Unix installer cancellation with child-process and lock cleanup.

GitOrigin-RevId: 4d275237bd77d896bf64dc1b85a5bca3608142c7
2026-09-07 17:18:02 +00:00
..

codex-app-server-daemon

codex-app-server-daemon is experimental and its lifecycle contract may change while the remote-management flow is still being developed.

codex-app-server-daemon backs the machine-readable codex app-server lifecycle commands used by remote clients such as the desktop and mobile apps. It is intended for Codex instances launched over SSH, including fresh developer machines that should expose app-server with remote_control enabled.

Platform support

The daemon supports Linux, macOS, and Windows using platform-specific process and file-locking primitives. Windows startup requires a non-elevated terminal whose host permits detached child processes.

Windows automatic attachment requires the canonical socket address to fit the 108-byte AF_UNIX limit (including its terminator). A short junction alias whose resolved address exceeds that limit falls back to the embedded server. Use a shorter CODEX_HOME to share the daemon; discovery does not trust a mutable alias.

Shared clients use the environment inherited when the daemon started. Opening a new terminal or clearing variables there does not clear the running daemon's environment; per-client environment isolation is not provided. An invocation that sets CODEX_EXEC_SERVER_URL skips implicit daemon attachment so its executor selection is preserved. If an implicitly discovered daemon cannot initialize the connection, the TUI starts an embedded server instead. Explicit --remote endpoints remain authoritative and report connection failures.

Commands

codex app-server daemon start
codex app-server daemon restart
codex app-server daemon enable-remote-control
codex app-server daemon disable-remote-control
codex app-server daemon stop
codex app-server daemon version
codex app-server daemon bootstrap --remote-control

On success, every command writes exactly one JSON object to stdout. Consumers should parse that JSON rather than relying on human-readable text. Lifecycle responses report the resolved backend, socket path, local CLI version, and running app-server version when applicable.

Bootstrap flow

For a new Linux or macOS machine:

curl -fsSL https://chatgpt.com/codex/install.sh | sh
$HOME/.codex/packages/standalone/current/codex app-server daemon bootstrap --remote-control

On Windows, use a non-elevated PowerShell terminal whose host allows breakaway:

irm https://chatgpt.com/codex/install.ps1 | iex
$codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME '.codex' }
& "$codexHome\packages\standalone\current\bin\codex.exe" app-server daemon bootstrap --remote-control

bootstrap requires the standalone managed install. It records the daemon settings under CODEX_HOME/app-server-daemon/, starts app-server as a pidfile-backed detached process. It launches a detached updater loop when the installer selected the stable latest channel and the managed binary supports the updater command.

Installation and update cases

The daemon uses the standalone installer (install.sh on Unix, install.ps1 on Windows) and its managed binary under CODEX_HOME/packages/standalone/current: bin/codex or bin/codex.exe, falling back to the legacy flat layout when present.

Situation What starts Does this daemon fetch new binaries? Does a running app-server eventually move to a newer binary on its own?
Installer has run; only start is used Managed binary No No; explicit restart is required.
Latest-channel installer has run; bootstrap is used Managed binary and detached updater when supported Yes; the platform's installer runs hourly. Yes; after a successful update, a running app-server restarts with the new binary before the updater replaces itself.
Installer selected an explicit release; bootstrap is used Managed binary only No; the selected release stays pinned. No; an explicit restart uses the selected binary.
Another tool updates the managed binary Next start or restart uses it Only with bootstrap, on its normal cadence. With bootstrap, the next successful installer pass compares binary contents and refreshes a running app-server before the updater.

Standalone installs

For installs created by either platform's standalone installer:

  • lifecycle commands always use the standalone managed binary path
  • bootstrap is supported
  • bootstrap starts a detached pid-backed updater loop only for a stable latest-channel release whose managed binary supports the updater command
  • the installer records the latest-channel selection alongside current; selecting an explicit release clears it, even if that version is currently latest. The updater checks the selection again while holding the install lock so an in-flight update cannot override a new pin
  • installs made before the installer recorded channel selections need one new latest installation to opt into automatic updates; until then the daemon continues to serve app-server without updating the selected release
  • after a successful refresh, if app-server is running and the managed binary contents changed, the updater restarts app-server with that binary first and only then replaces its own process image
  • the updater loop is not reboot-persistent; it must be started again by rerunning bootstrap after a reboot

Out-of-band updates

This daemon does not watch arbitrary executable files for replacement. If some other tool updates the managed binary path:

  • without bootstrap, a currently running app-server remains on the old executable image until an explicit restart
  • with bootstrap, the detached updater loop notices the changed managed binary on its next successful scheduled installer pass; if app-server is running, it refreshes app-server first and then refreshes itself once that replacement starts successfully

Lifecycle semantics

start is idempotent and returns after app-server is ready to answer the normal JSON-RPC initialize handshake on the Unix control socket.

restart stops any managed daemon and starts it again.

enable-remote-control and disable-remote-control persist the launch setting for future starts. If a managed app-server is already running, they restart it so the new setting takes effect immediately.

Top-level codex remote-control bootstraps with --remote-control when the updater loop is not running. Otherwise it enables remote control and starts the daemon normally.

stop sends a graceful termination request first, then sends a second termination signal after the grace window if the process is still alive.

All mutating lifecycle commands are serialized per CODEX_HOME, so a concurrent start, restart, enable-remote-control, disable-remote-control, stop, or bootstrap does not race another in-flight lifecycle operation.

State

The daemon stores its local state under CODEX_HOME/app-server-daemon/:

  • settings.json for persisted launch settings
  • app-server.pid for the app-server process record
  • app-server-updater.pid for the pid-backed standalone updater loop
  • daemon.lock for daemon-wide lifecycle serialization