astrid-runtime/astrid · critical

candidate generation for '{id}' exited before signaling read

Error message

candidate generation for '{id}' exited before signaling ready

What it means

This error is thrown when `candidate.wait_ready()` returns `ReadyStatus::Crashed`, meaning the candidate capsule process exited (crashed) before it could signal Ready. It indicates an abnormal termination during startup rather than a timeout.

Source

Thrown at crates/astrid-kernel/src/lib.rs:4722

    use astrid_capsule::capsule::ReadyStatus;

    let activation_timeout = std::time::Duration::from_secs(30);
    let readiness_timeout = std::time::Duration::from_millis(500);
    match astrid_runtime::time::timeout(activation_timeout, candidate.activate()).await {
        Ok(result) => result?,
        Err(_) => anyhow::bail!(
            "candidate generation for '{id}' did not activate within {}ms",
            activation_timeout.as_millis()
        ),
    }
    match candidate.wait_ready(readiness_timeout).await {
        ReadyStatus::Ready => Ok(()),
        ReadyStatus::Timeout => anyhow::bail!(
            "candidate generation for '{id}' did not signal ready within {}ms",
            readiness_timeout.as_millis()
        ),
        ReadyStatus::Crashed => {
            anyhow::bail!("candidate generation for '{id}' exited before signaling ready")
        },
    }
}

/// Attempts to restart a failed capsule, respecting backoff and max retries.
///
/// Records ONE restart attempt (advancing backoff and the retry count) per call
/// when eligible. The count is a measure of CONSECUTIVE health failures: a busy
/// capsule whose restart legitimately leaves a lingering old instance is NOT
/// treated as a failure here — the tracker is pruned by the health monitor the
/// moment the capsule RECOVERS (see the retain in [`spawn_capsule_health_monitor`]),
/// so only a capsule that keeps failing across ticks accumulates toward the cap.
/// This deliberately does not key off the [`RestartOutcome`], which is diagnostic
/// only: keying the cap off "lingering" would let a busy-but-healthy capsule
/// (whose consumer holds a clone for up to its 60s idle grace) exhaust the cap
/// and be permanently disabled.
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
async fn attempt_capsule_restart(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the capsule's logs/stderr for panics, aborts, or OOM kills during startup.
  2. Verify the capsule binary and its runtime dependencies (config, env, native libs) are present and valid.
  3. Reproduce the capsule's startup in isolation to surface the crash before it is managed by the kernel.
  4. Check memory limits on the host/runtime that might kill the process mid-startup.
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-launch sanity: verify the capsule binary exists, is executable, and its
// config/env dependencies resolve before calling activate()

Try / catch

match candidate.wait_ready(readiness_timeout).await {
    ReadyStatus::Crashed => {
        log::error!("capsule exited before ready; collecting stderr/coredump");
        // surface capsule logs, do not blind-retry
    }
    _ => {}
}

Prevention

When it happens

Trigger: `candidate.wait_ready(readiness_timeout)` returns `ReadyStatus::Crashed` — the capsule process died between activation and readiness signaling.

Common situations: Capsule panics or aborts during initialization; missing runtime dependencies (shared libs, config, env vars) causing immediate exit; bad binary/entrypoint; OOM kill of the capsule process during startup.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8e31c014df211b96. Report an issue: GitHub.