openai/codex · error · anyhow::Error

failed to read start time for pid-managed app server

Error message

failed to read start time for pid-managed app server {pid}

What it means

After spawning the managed app-server (and whenever validating a recorded pid), the backend fingerprints the process by running 'ps -p <pid> -o lstart='; a non-zero exit from ps produces this bail. Most often the freshly spawned child already exited (broken codex binary, immediate crash, unwritable CODEX_HOME, occupied control socket), so ps reports no such process; a ps that lacks lstart support (busybox) also exits non-zero. In the start path (pid.rs:200) the daemon then kills the child, removes the pid file, and appends the child's stderr log tail to the error context.

Solutions

  1. Print the error with the full anyhow chain ({err:#}) — the daemon appends the managed server's stderr tail (<pid file>.stderr.log, last 4KB), which usually names the real crash.
  2. Reproduce the spawn manually with the exact argv from command_args (pid.rs:413): '<codex_bin> app-server --listen unix://' — and fix whatever makes it exit.
  3. Ensure a full procps ps is installed: 'ps -p 1 -o lstart=' must print a date (busybox ps lacks lstart=).
  4. Remove a stale or foreign control socket, or stop the conflicting instance, then retry start.
  5. Retry the lifecycle start after cleanup — the failed child and its pid file are already removed by the error path.

Example fix

// before: error chain left unread, crash reason hidden
run(LifecycleCommand::Start).await?;

// after: surface the context chain (includes 'Managed app-server stderr (...)' tail), then verify the environment
if let Err(err) = run(LifecycleCommand::Start).await {
    eprintln!("{err:#}");
}
// daemon depends on this host's ps:
// ps -p $$ -o lstart=   (must print a start date, not an error)
Defensive patterns

Strategy: retry

Validate before calling

// preflight before LifecycleCommand::Start
let ps = std::process::Command::new("ps")
    .args(["-p", "1", "-o", "lstart="])
    .output()?;
if !ps.status.success() || ps.stdout.trim().is_empty() {
    anyhow::bail!("this host's ps cannot report lstart; daemon start will fail");
}
// and confirm the managed binary starts:
// <CODEX_HOME> managed codex: `codex app-server --help`

Try / catch

if let Err(err) = run(LifecycleCommand::Start).await {
    eprintln!("{err:#}"); // context chain includes the 'Managed app-server stderr (...)' tail naming the real crash
    // fix the cause, then retry exactly once
    run(LifecycleCommand::Start).await?;
}

Prevention

When it happens

Trigger: start() at pid.rs:200 invokes ps immediately after spawn — if the child exits first, ps exits 1 and this fires; the error path cleans up and attaches the stderr tail. Also raised from process_matches_record (pid.rs:582) when revalidating a recorded pid, tolerated there only when the process no longer exists. On slim images, busybox ps rejects '-o lstart=' with a non-zero exit for every pid.

Common situations: Corrupt or mismatched managed codex binary under CODEX_HOME after a partial update; app-server crashing at boot from bad config or a conflicting listener; Docker/CI images without full procps installed; spawn/ps races on very slow or loaded machines.

Related errors


AI-assisted analysis of openai/codex@339751715c (2026-08-25). Data as JSON: /api/errors/5a96d570f43fa805. Report an issue: GitHub.

Appendix: source

Thrown at codex-rs/app-server-daemon/src/backend/pid.rs:708

}

#[cfg(not(unix))]
async fn inspect_empty_pid_reservation(
    _pid_path: &Path,
    _lock_path: &Path,
) -> Result<EmptyPidReservation> {
    Ok(EmptyPidReservation::Stale)
}

#[cfg(unix)]
async fn read_process_start_time(pid: u32) -> Result<String> {
    let output = Command::new("ps")
        .args(["-p", &pid.to_string(), "-o", "lstart="])
        .output()
        .await
        .context("failed to invoke ps for pid-managed app server")?;
    if !output.status.success() {
        bail!("failed to read start time for pid-managed app server {pid}");
    }

    let start_time = String::from_utf8(output.stdout)
        .context("pid-managed app server start time was not utf-8")?;
    let start_time = start_time.trim();
    if start_time.is_empty() {
        bail!("pid-managed app server {pid} has no recorded start time");
    }
    Ok(start_time.to_string())
}

#[cfg(all(test, unix))]
#[path = "pid_tests.rs"]
mod tests;

View on GitHub (pinned to 339751715c)