{"record":{"id":"1545d685c47d78fe","repo":"BigPizzaV3/CodexPlusPlus","slug":"codex-injection-failed","errorCode":null,"errorMessage":"Codex injection failed","messagePattern":"Codex injection failed","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/launcher.rs","lineNumber":2380,"sourceCode":"            inspector_port,\n            extra_args,\n        )),\n        process_id: None,\n    })\n}\n\nasync fn retry_injection(debug_port: u16, helper_port: u16) -> anyhow::Result<()> {\n    let mut last_error = None;\n    for _ in 0..20 {\n        match try_inject(debug_port, helper_port).await {\n            Ok(()) => return Ok(()),\n            Err(error) => {\n                last_error = Some(error);\n                tokio::time::sleep(std::time::Duration::from_millis(500)).await;\n            }\n        }\n    }\n    Err(last_error.unwrap_or_else(|| anyhow::anyhow!(\"Codex injection failed\")))\n}\n\npub async fn check_and_reinject_bridge(debug_port: u16, helper_port: u16) -> bool {\n    check_and_reinject_bridge_inner(debug_port, helper_port, false, None).await\n}\n\npub fn browser_identity_changed(previous: Option<&str>, current: &str) -> bool {\n    previous.is_some_and(|previous| previous != current)\n}\n\nfn launcher_target_alive(has_codex_process: bool, cdp_available: bool) -> bool {\n    has_codex_process || cdp_available\n}\n\nfn should_probe_launcher_cdp(is_windows: bool, has_codex_process: bool) -> bool {\n    is_windows && !has_codex_process\n}\n","sourceCodeStart":2362,"sourceCodeEnd":2398,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/launcher.rs#L2362-L2398","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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","Increase startup patience by retrying launch/injection at the caller level (check_and_reinject_bridge retries later) rather than editing the 20x500ms budget","If you maintain a fork, preserve `last_error = Some(error)` in the Err arm so the real cause is always reported","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"],"exampleFix":"// before (fork regression that loses the cause)\nfor _ in 0..20 {\n    match try_inject(debug_port, helper_port).await {\n        Ok(()) => return Ok(()),\n        Err(_) => { tokio::time::sleep(Duration::from_millis(500)).await; }\n    }\n}\nErr(last_error.unwrap_or_else(|| anyhow!(\"Codex injection failed\"))) // fires with no cause\n\n// after\nfor _ in 0..20 {\n    match try_inject(debug_port, helper_port).await {\n        Ok(()) => return Ok(()),\n        Err(error) => { last_error = Some(error); tokio::time::sleep(Duration::from_millis(500)).await; }\n    }\n}\nErr(last_error.unwrap_or_else(|| anyhow!(\"Codex injection failed\")))","handlingStrategy":"retry","validationCode":"// Pre-flight the CDP endpoint the injector needs\nasync fn injectable_target_ready(debug_port: u16) -> bool {\n    crate::cdp::list_targets(debug_port).await\n        .map(|t| crate::cdp::pick_injectable_codex_page_target(&t).is_ok())\n        .unwrap_or(false)\n}","typeGuard":"fn is_injection_failure(e: &anyhow::Error) -> bool {\n    let msg = e.to_string();\n    msg.contains(\"Codex injection failed\") || msg.contains(\"No injectable Codex page target found\")\n}","tryCatchPattern":"// Retry at the caller level with backoff; injection is idempotent via install_bridge\nlet mut attempts = 0;\nloop {\n    match check_and_reinject_bridge(debug_port, helper_port).await {\n        true => break,\n        false if attempts < 5 => { attempts += 1; tokio::time::sleep(Duration::from_secs(2)).await; }\n        false => { tracing::error!(\"bridge injection gave up after {attempts} rounds\"); break; }\n    }\n}","preventionTips":["Rely on the watchdog's later re-injection (check_and_reinject_bridge) instead of failing hard on first-launch injection","Verify http://127.0.0.1:<debug_port>/json/list shows an app://--/index.html page before expecting injection to succeed","If maintaining a fork, always preserve last_error assignment in retry loops so real causes surface","Ensure the Codex app is fully started (remote debugging port listening) before injecting"],"tags":["rust","cdp","devtools-injection","retry-loop","bridge"],"backgroundTag":"devtools-injection-failed","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}