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
- Delete the orphaned delayed-trigger record so the loop recreates it with a proper dispatch and task binding.
- Re-create the automation/trigger through the normal API so the durable task binding is established before it becomes claimable.
- 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
- Always create delayed triggers through the API that binds dispatch + task_id atomically.
- After crashes or migrations, scan triggers and drop any lacking a durable binding before resuming.
- Never hand-edit automation state files.
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
- HOURLY schedule exceeded its range
- Once schedule has no future run after
- Unable to compute next anchored HOURLY run
- Unable to compute next HOURLY run for BYDAY filter
- Unable to compute next WEEKLY run
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)