BigPizzaV3/CodexPlusPlus · critical

Codex app-server 已关闭

Error message

Codex app-server 已关闭{exit_status_suffix}

What it means

The Codex app-server child process closed its stdout stream while the library was reading a JSON-RPC message from it, so the pending read/turn cannot continue. The library marks the connection as no longer running and, when the OS reports the exit status, appends it (e.g. ':exit status: 101') to the message. This is thrown from read_message, the single read path used by both request() and run_turn(), so any turn in flight at process death surfaces this error.

Solutions

  1. Check the exit-status suffix in the message and run `codex app-server` manually to reproduce the crash and see stderr
  2. Re-run the connect flow to respawn the app-server process (the flag `running` is set false, so a restart is required)
  3. Verify the codex binary version matches what CodexPlusPlus expects; upgrade or downgrade codex
  4. Check system resources (memory pressure, OOM-killer logs in dmesg/journalctl) for SIGKILL of the child

Example fix

// before
// assuming the AppServerConnection is still usable after an error
let reply = conn.request(params).await?;
// after
// detect the closed server and respawn before retrying
match conn.request(params).await {
    Err(e) if e.to_string().contains("Codex app-server 已关闭") => {
        let mut conn = AppServerConnection::spawn(&codex_path)?;
        let reply = conn.request(params).await?;
    }
    other => other?,
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before each turn, confirm the child is still alive
fn server_alive(conn: &AppServerConnection) -> bool { conn.is_running() }

Type guard

fn ensure_running(conn: &AppServerConnection) -> anyhow::Result<()> {
    if !conn.is_running() { anyhow::bail!("app-server 未运行,请重新启动连接"); }
    Ok(())
}

Try / catch

match conn.request(params).await {
    Err(e) if e.to_string().contains("Codex app-server 已关闭") => respawn_and_retry(),
    other => other,
}

Prevention

When it happens

Trigger: Calling request() or run_turn() while the spawned `codex app-server` child process has exited or crashes mid-response: the child terminates (panic, bad flag, OOM kill, manual kill) and tokio's next_line() on stdout returns None.

Common situations: The codex binary panics or segfaults during a long turn; the process is killed by the OS (OOM) or by the user; an incompatible codex version crashes on an unsupported request; the child was never launched successfully and died immediately before the first read.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/c613051e68ed9f7d. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/connect/app_server.rs:377

            if response_id(&message) != Some(id) {
                continue;
            }
            if let Some(error) = rpc_error(&message) {
                bail!("Codex app-server {method} 失败:{error}");
            }
            return Ok(message.get("result").cloned().unwrap_or(Value::Null));
        }
    }

    async fn read_message(&mut self, timeout: Duration) -> anyhow::Result<Value> {
        loop {
            let line = tokio::time::timeout(timeout, self.stdout.next_line())
                .await
                .context("等待 Codex app-server 响应超时")??;
            let Some(line) = line else {
                self.running = false;
                let exit = self.child.try_wait().ok().flatten();
                bail!(
                    "Codex app-server 已关闭{}",
                    exit.map(|status| format!(":{status}")).unwrap_or_default()
                );
            };
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            match serde_json::from_str(line) {
                Ok(message) => return Ok(message),
                Err(_) => continue,
            }
        }
    }

    async fn reject_server_request(&mut self, message: &Value) -> anyhow::Result<()> {
        let Some(id) = message.get("id") else {
            return Ok(());

View on GitHub (pinned to b1ed92e5e4)