BigPizzaV3/CodexPlusPlus · error · anyhow::Error

Codex injection failed

Error message

Codex injection failed

What it means

retry_injection (crates/codex-plus-core/src/launcher.rs:2369) tries try_inject up to 20 times with 500 ms sleeps while the Codex app warms up its DevTools target. Every Err iteration stores the error in last_error, so the final Err normally carries the real underlying failure (CDP connect refused, no injectable page target, etc.). The bare 'Codex injection failed' only materializes when the loop recorded no error — structurally unreachable unless the loop body is refactored, so encountering it usually means a forked retry loop or a logic regression.

Source

Thrown at crates/codex-plus-core/src/launcher.rs:2380

            inspector_port,
            extra_args,
        )),
        process_id: None,
    })
}

async fn retry_injection(debug_port: u16, helper_port: u16) -> anyhow::Result<()> {
    let mut last_error = None;
    for _ in 0..20 {
        match try_inject(debug_port, helper_port).await {
            Ok(()) => return Ok(()),
            Err(error) => {
                last_error = Some(error);
                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
            }
        }
    }
    Err(last_error.unwrap_or_else(|| anyhow::anyhow!("Codex injection failed")))
}

pub async fn check_and_reinject_bridge(debug_port: u16, helper_port: u16) -> bool {
    check_and_reinject_bridge_inner(debug_port, helper_port, false, None).await
}

pub fn browser_identity_changed(previous: Option<&str>, current: &str) -> bool {
    previous.is_some_and(|previous| previous != current)
}

fn launcher_target_alive(has_codex_process: bool, cdp_available: bool) -> bool {
    has_codex_process || cdp_available
}

fn should_probe_launcher_cdp(is_windows: bool, has_codex_process: bool) -> bool {
    is_windows && !has_codex_process
}

View on GitHub (pinned to 1f431ae49b)

Solutions

  1. Read the full error chain: in stock code you get the last try_inject error (e.g. 'No injectable Codex page target found') — fix that cause first: ensure Codex fully started and its remote-debugging port is reachable
  2. Increase startup patience by retrying launch/injection at the caller level (check_and_reinject_bridge retries later) rather than editing the 20x500ms budget
  3. If you maintain a fork, preserve `last_error = Some(error)` in the Err arm so the real cause is always reported
  4. Check the debug port: curl http://127.0.0.1:<debug_port>/json/list and look for an app://--/index.html page target with a webSocketDebuggerUrl

Example fix

// before (fork regression that loses the cause)
for _ in 0..20 {
    match try_inject(debug_port, helper_port).await {
        Ok(()) => return Ok(()),
        Err(_) => { tokio::time::sleep(Duration::from_millis(500)).await; }
    }
}
Err(last_error.unwrap_or_else(|| anyhow!("Codex injection failed"))) // fires with no cause

// after
for _ in 0..20 {
    match try_inject(debug_port, helper_port).await {
        Ok(()) => return Ok(()),
        Err(error) => { last_error = Some(error); tokio::time::sleep(Duration::from_millis(500)).await; }
    }
}
Err(last_error.unwrap_or_else(|| anyhow!("Codex injection failed")))
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight the CDP endpoint the injector needs
async fn injectable_target_ready(debug_port: u16) -> bool {
    crate::cdp::list_targets(debug_port).await
        .map(|t| crate::cdp::pick_injectable_codex_page_target(&t).is_ok())
        .unwrap_or(false)
}

Type guard

fn is_injection_failure(e: &anyhow::Error) -> bool {
    let msg = e.to_string();
    msg.contains("Codex injection failed") || msg.contains("No injectable Codex page target found")
}

Try / catch

// Retry at the caller level with backoff; injection is idempotent via install_bridge
let mut attempts = 0;
loop {
    match check_and_reinject_bridge(debug_port, helper_port).await {
        true => break,
        false if attempts < 5 => { attempts += 1; tokio::time::sleep(Duration::from_secs(2)).await; }
        false => { tracing::error!("bridge injection gave up after {attempts} rounds"); break; }
    }
}

Prevention

When it happens

Trigger: The 20-attempt loop exhausts (Codex never exposes an injectable app:// page within ~10 s) — the surfaced error is then the last try_inject error, not this one; this exact message appears only if the match arms were modified so Err no longer sets last_error (e.g. a continue added, or the loop range changed to 0).

Common situations: Slow machines or antivirus-delayed app startup causing all 20 attempts to fail (you see the real cause instead); forks that rewrote retry_injection; race where the app exits during startup and the error slot is bypassed.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@1f431ae49b (2026-08-16). Data as JSON: /api/errors/1545d685c47d78fe. Report an issue: GitHub.