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
- Check the managed server's pid record and the <pid file>.stderr.log tail — the close reason is usually a crash logged there.
- Re-run start/restart: if the old server is dead, the daemon spawns a fresh one and the next probe succeeds.
- Keep daemon and managed app-server on the same codex release so control-socket frames are understood (probe exists precisely to compare versions).
- 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
- Don't run concurrent stop/start against the same CODEX_HOME from multiple clients.
- Monitor the managed server's stderr log for boot crashes.
- Pin daemon and app-server to the same release so the control protocol matches.
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
- app-server user-agent omitted version separator
- pid-managed app-server shutdown is unsupported on this platf
- pid-managed updater shutdown is unsupported on this platform
- failed to read start time for pid-managed app server {pid}
- pid-managed app server {pid} has no recorded start time
AI-assisted analysis of openai/codex@339751715c (2026-08-25).
Data as JSON: /api/errors/36854d9607d583df.
Report an issue: GitHub.