openai/codex · error · anyhow::Error

app-server user-agent omitted version separator

Error message

app-server user-agent omitted version separator

What it means

After initialize, the daemon reads InitializeResponse.user_agent and extracts the server version by splitting on the first '/' (expected shape 'codex_app_server/0.42.0 (…)'). A user_agent containing no slash at all fails here. It means whatever answered on the control socket is not a codex app-server of the expected vintage — a foreign or custom JSON-RPC server on that path, or a build whose agent string does not follow the originator/version convention.

Source

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

    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())
}

#[cfg(all(test, unix))]
mod tests {
    use pretty_assertions::assert_eq;

    use super::parse_version_from_user_agent;

    #[test]
    fn parses_version_from_codex_user_agent() {
        assert_eq!(
            parse_version_from_user_agent(

View on GitHub (pinned to 339751715c)

Solutions

  1. Restart the daemon so it spawns its own managed app-server instead of probing whatever currently owns the socket path.
  2. Verify who owns the socket ('ss -x | grep <path>') and remove or redirect the foreign listener.
  3. Run daemon and app-server from the same codex release so the user_agent convention holds.
  4. If you maintain a custom app-server, format user_agent as 'name/version …' so the daemon can parse it.

Example fix

// custom app-server: send a parseable user agent
// before
user_agent: "my-server".into(),
// after
user_agent: "my-server/1.0.0 (custom build)".into(),
Defensive patterns

Strategy: fallback

Type guard

fn has_version_separator(user_agent: &str) -> bool {
    user_agent.contains('/')
}

Try / catch

let info = match probe(&path).await {
    Ok(info) => info,
    Err(err) if err.to_string().contains("user-agent") => {
        ProbeInfo { app_server_version: "unknown".into() } // degrade, then restart the managed server
    }
    Err(err) => return Err(err),
};

Prevention

When it happens

Trigger: probe() (client.rs:59 feeding parse_version_from_user_agent) against a socket served by a non-codex or custom server; an app-server build whose user_agent omits '<name>/<version>'; a protocol change that deserializes user_agent to an unrelated value.

Common situations: Another process bound to the daemon's expected socket path; development forks of app-server that tweak the user_agent string; mixing a new daemon with an old managed binary from a different branch.

Related errors


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