Files
codex/codex-rs/app-server-daemon/README.md
Eric Traut a51608398d Make the managed app-server shutdown grace period configurable (#43572)
## Why

Managed app-server shutdowns use a fixed 60-second grace period. Allow users to adjust how long shutdown waits before forcing the process to exit.

## What changed

- Add `shutdownGraceSeconds` to daemon settings, accepting integers from 0 through 300 and defaulting to 60. Zero requests graceful shutdown and then forces termination immediately.
- Apply the setting to managed app-server stops and restarts, including updater-triggered restarts, and extend the lifecycle lock timeout to accommodate the maximum grace period.
- Keep `stop` usable with unreadable or invalid settings by falling back to the default grace period.

## Testing

Add tests for setting defaults and bounds, invalid-value fallback, preservation when saving remote-control settings, graceful process exit, and forced termination with zero or finite grace periods.

GitOrigin-RevId: 8627b391323cf27b40df96935a82e66aa715f572
2026-09-07 19:51:53 +00:00

180 lines
9.1 KiB
Markdown

# 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
```sh
codex app-server daemon start
codex app-server daemon restart
codex app-server daemon update
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.
Standalone-managed daemons check for updates after five minutes, then hourly by
default. Edit `CODEX_HOME/app-server-daemon/settings.json` to change this:
```json
{"remoteControlEnabled": false,
"shutdownGraceSeconds": 60,
"updater": {"autoUpdateEnabled": false, "updateIntervalMinutes": 120}}
```
Positive minute intervals have no configured cap. `daemon restart` applies the
enabled state; the next updater wait reads a new interval. The preference does
not affect an explicit `codex update` command or `daemon update`.
`daemon update` checks the latest stable release once, even with automatic
updates disabled. It requires a Codex installer-owned latest-channel standalone
install. JSON reports `updated`, `noUpdate`, or `unsupported`, with installed
and running versions. The updater owns scheduled and manual installs. A manual
update restarts a running managed daemon, so active or queued work may be
interrupted.
Accepted updates continue if the CLI exits. Installer errors return nonzero.
The updater uses saved network settings; CLI `-c` overrides do not reach it.
For all managed app-server shutdowns, including explicit stop and restart and
updater-triggered restarts, `shutdownGraceSeconds` defaults to 60 and accepts
an integer from 0 through 300. Zero forces shutdown immediately after requesting
a graceful exit; the five-minute maximum bounds the wait even if a turn is still
running.
## Bootstrap flow
For a new Linux or macOS machine:
```sh
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:
```powershell
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
automatic updates are enabled, 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? |
| --- | --- | --- | --- |
| Latest-channel installer has run; `start` or `bootstrap` is used with automatic updates enabled | Managed binary and detached updater when supported | When supported, the platform's installer runs on the configured cadence. | When supported, the running 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 | A fresh start or explicit restart uses it; a running server is reused. | Yes, when a latest-channel updater is running, on the configured cadence. | An updater that was running through the change compares binary contents on its next successful installer pass and refreshes the server first. |
### Standalone installs
For installs created by either platform's standalone installer:
- lifecycle commands always use the standalone managed binary path
- `bootstrap` is supported
- managed `start`, `restart`, and `bootstrap` ensure a single detached pid-backed
updater loop only when automatic updates are enabled 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; a managed start after reboot
starts it again
### Out-of-band updates
This daemon does not watch arbitrary executable files for replacement. If some
other tool updates the managed binary path:
- an updater that was already running notices a 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
- if the updater was absent during a same-version binary replacement, a later
managed start recovers it but cannot infer the running server's previous
executable identity; use `codex app-server daemon restart` to refresh the server
## 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 start` enables and persists remote control for
the managed daemon, overriding a saved disabled value. It starts or bootstraps
the daemon as needed. Plain `codex remote-control` runs a separate foreground
server and does not change daemon settings; `codex remote-control stop` stops
the managed daemon without clearing its saved remote-control preference.
`daemon start` and `daemon restart` use that saved preference. `daemon bootstrap`
sets it according to `--remote-control` (disabled when omitted).
`stop` sends a graceful termination request first, then force-terminates the
process after the configured grace window if it 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 remote-control launch settings and updater preferences
- `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