openai/codex · error · anyhow::Error

app-server closed the control socket

Error message

app-server closed the control socket

What it means

The daemon's control client wraps the app-server's Unix control socket in a websocket and reads JSON-RPC frames; read_message maps a None from websocket.next() — EOF before any frame — to this bail, meaning the peer closed the connection. initialize(), finish() and read_request() all surface it, and the probe layer additionally bounds it with a 2-second timeout. Practically the managed app-server process died, was killed, or dropped the socket mid-conversation.

Source

Thrown at codex-rs/app-server-daemon/src/client.rs:139

) -> Result<()>
where
    S: AsyncRead + AsyncWrite + Unpin,
{
    websocket
        .send(Message::Text(serde_json::to_string(message)?.into()))
        .await?;
    Ok(())
}

pub(crate) async fn read_message<S>(websocket: &mut WebSocketStream<S>) -> Result<JSONRPCMessage>
where
    S: AsyncRead + AsyncWrite + Unpin,
{
    loop {
        let frame = websocket
            .next()
            .await
            .ok_or_else(|| anyhow!("app-server closed the control socket"))??;
        let Message::Text(payload) = frame else {
            continue;
        };
        return serde_json::from_str::<JSONRPCMessage>(&payload)
            .context("failed to parse app-server JSON-RPC message");
    }
}

fn parse_version_from_user_agent(user_agent: &str) -> Result<String> {
    let (_originator, rest) = user_agent
        .split_once('/')
        .ok_or_else(|| anyhow!("app-server user-agent omitted version separator"))?;
    let version = rest
        .split_whitespace()
        .next()
        .filter(|version| !version.is_empty())
        .ok_or_else(|| anyhow!("app-server user-agent omitted version"))?;
    Ok(version.to_string())

View on GitHub (pinned to 339751715c)

Solutions

  1. Check the managed server's pid record and the <pid file>.stderr.log tail — the close reason is usually a crash logged there.
  2. Re-run start/restart: if the old server is dead, the daemon spawns a fresh one and the next probe succeeds.
  3. Keep daemon and managed app-server on the same codex release so control-socket frames are understood (probe exists precisely to compare versions).
  4. If an external actor (OOM killer, watchdog, manual kill) takes the server down, fix that cause — the EOF is only the symptom.

Example fix

// before: one-shot probe that strands the caller on a dying server
let info = client::probe(&socket_path).await?;

// after: treat EOF as 'restart and retry once'
let info = match client::probe(&socket_path).await {
    Ok(info) => info,
    Err(err) if err.to_string().contains("closed the control socket") => {
        run(LifecycleCommand::Restart).await?; // respawns a dead managed server
        client::probe(&socket_path).await?
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: retry

Validate before calling

// before probing, confirm the recorded pid is still alive
let record: serde_json::Value =
    serde_json::from_str(&tokio::fs::read_to_string(&pid_file).await?)?;
let pid = record["pid"].as_u64().unwrap_or_default() as i32;
if unsafe { libc::kill(pid, 0) } != 0 {
    run(LifecycleCommand::Start).await?; // respawn before connecting
}

Try / catch

Match 'closed the control socket' (or an UnexpectedEof tungstenite error in the chain), restart the managed server via LifecycleCommand::Restart, and retry the exchange exactly once; anything else propagates.

Prevention

When it happens

Trigger: client::probe during Daemon::start (lib.rs:299) or version() while the server exits concurrently; initialize() when the server crashes right after accepting the connection; finish()/read_request() when a concurrent 'stop' SIGTERMs the server between two frames.

Common situations: App-server crashing on boot (invalid settings, unwritable CODEX_HOME); a stop/restart racing another client's probe; version skew where an older managed server closes control frames it does not understand; the server being OOM-killed under memory pressure.

Related errors


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