Hmbown/CodeWhale · critical · anyhow::Error

Codewhale runtime thread panicked: {message}

Error message

Codewhale runtime thread panicked: {message}

What it means

crates/tui/src/lib.rs runs the entire async runtime on a dedicated 'codewhale-main' thread with a fixed stack size (CODEWHALE_MAIN_STACK_BYTES). If code inside run_async_main panics, thread::join() returns the panic payload; this site downcasts it to &str or String and rewraps it as 'Codewhale runtime thread panicked: {message}'. The message names the panic value, not the file:line where it happened.

Source

Thrown at crates/tui/src/lib.rs:1681

    // events all share one async owner. Debug builds retain enough stack
    // temporaries that nesting a modal event over the TUI loop can exceed the
    // platform main-thread default (8 MiB on macOS). Give that owner an
    // explicit stack while keeping process hardening and the global panic hook
    // above this boundary, before Tokio or any worker thread exists.
    let runtime_thread = std::thread::Builder::new()
        .name("codewhale-main".to_string())
        .stack_size(CODEWHALE_MAIN_STACK_BYTES)
        .spawn(move || run_async_main(cli, command, plugin_discovery, plugin_registry))
        .context("Failed to start the Codewhale runtime thread")?;
    match runtime_thread.join() {
        Ok(result) => result,
        Err(payload) => {
            let message = payload
                .downcast_ref::<&str>()
                .map(|value| (*value).to_string())
                .or_else(|| payload.downcast_ref::<String>().cloned())
                .unwrap_or_else(|| "unknown panic payload".to_string());
            Err(anyhow!("Codewhale runtime thread panicked: {message}"))
        }
    }
}

fn run_async_main(
    cli: Cli,
    command: Option<Commands>,
    plugin_discovery: Arc<crate::plugins::PluginDiscoveryContext>,
    plugin_registry: Arc<crate::plugins::PluginRegistry>,
) -> Result<()> {
    build_runtime()?.block_on(run_async_main_inner(
        cli,
        command,
        plugin_discovery,
        plugin_registry,
    ))
}

View on GitHub (pinned to 8880682c63)

Solutions

  1. Re-run with RUST_BACKTRACE=1 (or full) — the panic hook prints the real file:line to stderr before this join error is returned
  2. Reproduce with the same subcommand and inputs, then fix the unwrap/expect at the reported location
  3. If depth/recursion related, raise CODEWHALE_MAIN_STACK_BYTES or move the deep work off the fixed-stack thread
  4. Report the panic location and backtrace if it comes from released code
Defensive patterns

Strategy: try-catch

Try / catch

match runtime_thread.join() {
    Ok(result) => result,
    Err(payload) => {
        let message = payload
            .downcast_ref::<&str>()
            .map(|value| (*value).to_string())
            .or_else(|| payload.downcast_ref::<String>().cloned())
            .unwrap_or_else(|| "unknown panic payload".to_string());
        Err(anyhow!("Codewhale runtime thread panicked: {message}"))
    }
}

Prevention

When it happens

Trigger: Any panic inside run_async_main: an unwrap/expect on None or Err, an out-of-bounds index, an assertion, a panic inside a dependency, or 'unknown panic payload' when the payload is not a string type.

Common situations: A startup regression panics on an unexpected config shape or empty model registry; a dependency panics on malformed input; deep recursion exceeds the configured stack size and the runtime aborts into the panic path.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/4cb6d858874ac809. Report an issue: GitHub.