Hmbown/CodeWhale · error · anyhow::Error

Claimed delayed trigger has no durable task binding

Error message

Claimed delayed trigger has no durable task binding

What it means

When a delayed trigger is claimed (status transitions to claimed), the runtime requires a durable binding: the claimed record must carry both an AutomationDispatch and a task_id. This invariant check bails when either is missing, because without them the trigger can never be executed or resumed durably.

Solutions

  1. Delete the orphaned delayed-trigger record so the loop recreates it with a proper dispatch and task binding.
  2. Re-create the automation/trigger through the normal API so the durable task binding is established before it becomes claimable.
  3. Inspect the automation state file for the trigger and repair/complete its dispatch and task_id fields if you can do so safely.
Defensive patterns

Strategy: validation

Validate before calling

fn claimable(t: &DelayedTrigger) -> bool {
    t.dispatch.is_some() && t.task_id.is_some()
}

Type guard

fn has_durable_binding(t: &DelayedTrigger) -> Option<(&AutomationDispatch, &str)> {
    Some((t.dispatch.as_ref()?, t.task_id.as_deref()?))
}

Try / catch

match claim_delayed_trigger(&mut manager, now).await {
    Ok(Some(t)) => run(t).await,
    Ok(None) => {},
    Err(e) if e.to_string().contains("no durable task binding") => purge_orphan_trigger(),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Claiming a DelayedTrigger whose current.dispatch is None or current.task_id is None — typically a trigger row created or migrated by a code path that skipped durable task binding, then later picked up by the claim loop.

Common situations: Manually editing or pruning automation state files; upgrading from a schema where delayed triggers carried no task binding; a crash between trigger creation and dispatch binding leaving a half-written record.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f3ae3e25b5879e8a. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/automation_manager.rs:2421

            || (candidate.status == DelayedTriggerStatus::Pending && candidate.fire_at <= now))
        {
            continue;
        }
        let claimed = {
            let manager = automations.lock().await;
            match manager.with_transaction(|| {
                let mut current = manager.get_trigger(&candidate.trigger_id)?;
                if current.status == DelayedTriggerStatus::Dispatching {
                    if !manager.eligible_scope(
                        current
                            .dispatch
                            .as_ref()
                            .and_then(|d| d.execution_scope.as_deref()),
                    ) {
                        return Ok(None);
                    }
                    if current.dispatch.is_none() || current.task_id.is_none() {
                        bail!("Claimed delayed trigger has no durable task binding");
                    }
                    return Ok(Some(current));
                }
                if !manager.eligible_scope(current.execution_scope.as_deref())
                    || current.status != DelayedTriggerStatus::Pending
                    || current.fire_at > now
                    || current.owner_session_id.is_none()
                {
                    return Ok(None);
                }
                current.schema_version = CURRENT_TRIGGER_SCHEMA_VERSION;
                current.status = DelayedTriggerStatus::Dispatching;
                current.task_id = Some(crate::task_manager::TaskManager::new_task_id());
                current.dispatch = Some(AutomationDispatch {
                    execution_scope: current.execution_scope.clone(),
                    request: NewTaskRequest {
                        prompt: current.message.clone(),
                        name: None,

View on GitHub (pinned to 73e0f67d83)