{"record":{"id":"36854d9607d583df","repo":"openai/codex","slug":"app-server-closed-the-control-socket","errorCode":null,"errorMessage":"app-server closed the control socket","messagePattern":"app-server closed the control socket","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"codex-rs/app-server-daemon/src/client.rs","lineNumber":139,"sourceCode":") -> Result<()>\nwhere\n    S: AsyncRead + AsyncWrite + Unpin,\n{\n    websocket\n        .send(Message::Text(serde_json::to_string(message)?.into()))\n        .await?;\n    Ok(())\n}\n\npub(crate) async fn read_message<S>(websocket: &mut WebSocketStream<S>) -> Result<JSONRPCMessage>\nwhere\n    S: AsyncRead + AsyncWrite + Unpin,\n{\n    loop {\n        let frame = websocket\n            .next()\n            .await\n            .ok_or_else(|| anyhow!(\"app-server closed the control socket\"))??;\n        let Message::Text(payload) = frame else {\n            continue;\n        };\n        return serde_json::from_str::<JSONRPCMessage>(&payload)\n            .context(\"failed to parse app-server JSON-RPC message\");\n    }\n}\n\nfn parse_version_from_user_agent(user_agent: &str) -> Result<String> {\n    let (_originator, rest) = user_agent\n        .split_once('/')\n        .ok_or_else(|| anyhow!(\"app-server user-agent omitted version separator\"))?;\n    let version = rest\n        .split_whitespace()\n        .next()\n        .filter(|version| !version.is_empty())\n        .ok_or_else(|| anyhow!(\"app-server user-agent omitted version\"))?;\n    Ok(version.to_string())","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/app-server-daemon/src/client.rs#L121-L157","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: one-shot probe that strands the caller on a dying server\nlet info = client::probe(&socket_path).await?;\n\n// after: treat EOF as 'restart and retry once'\nlet info = match client::probe(&socket_path).await {\n    Ok(info) => info,\n    Err(err) if err.to_string().contains(\"closed the control socket\") => {\n        run(LifecycleCommand::Restart).await?; // respawns a dead managed server\n        client::probe(&socket_path).await?\n    }\n    Err(err) => return Err(err),\n};","handlingStrategy":"retry","validationCode":"// before probing, confirm the recorded pid is still alive\nlet record: serde_json::Value =\n    serde_json::from_str(&tokio::fs::read_to_string(&pid_file).await?)?;\nlet pid = record[\"pid\"].as_u64().unwrap_or_default() as i32;\nif unsafe { libc::kill(pid, 0) } != 0 {\n    run(LifecycleCommand::Start).await?; // respawn before connecting\n}","typeGuard":null,"tryCatchPattern":"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.","preventionTips":["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."],"tags":["rust","codex","daemon","websocket","json-rpc","unix-socket","connection-reset"],"backgroundTag":"connection-closed-unexpectedly","analyzedSha":"339751715c64496cb86246bfb3935f40e309dd3d","analyzedAt":"2026-08-25T05:35:09.876Z","schemaVersion":2},"datasetVersion":"2026-08-25T06:17:31.827Z"}