Hmbown/CodeWhale · error
Automation run has no durable task binding
Error message
Automation run has no durable task binding
What it means
This error is thrown by `enqueue_run_task` in the automation manager when an automation run record lacks a durable task binding. Dispatching a run requires BOTH a dispatch handle (`run.dispatch`) and a persisted task id (`run.task_id`); if either is `None`, the caller's contract (bind first, then enqueue) was violated, so the run cannot be scheduled onto the task manager.
Solutions
- Ensure the run is bound before enqueueing: create the task via the task manager, persist the binding, and only then call the code path that enqueues the run
- Check for code paths that set `run.task_id = None` (task removal/cleanup) that later retry the run
- Inspect the persisted run record to confirm the binding survived the restart/reload
- Log `run.dispatch.is_some()` and `run.task_id` at the call site to identify which half of the binding is missing
Example fix
// before
match result {
Ok(task) => { ... }
}
// after
if run.dispatch.is_none() || run.task_id.is_none() {
// bind first: create + persist the task, then re-enter enqueue_run_task
bind_run_task(run, tasks).await?;
}
let result = enqueue_run_task(run, tasks).await; Defensive patterns
Strategy: validation
Validate before calling
fn is_dispatchable(run: &AutomationRunRecord) -> bool {
run.dispatch.is_some() && run.task_id.is_some()
}
if !is_dispatchable(&run) { return Err(anyhow!("run not bound")); } Type guard
fn bound(run: &AutomationRunRecord) -> Option<(&Dispatch, &TaskId)> {
Some((run.dispatch.as_ref()?, run.task_id.as_ref()?))
} Prevention
- Always create and persist the task binding before enqueueing a run
- Model the binding in the type system (e.g. a `BoundRun` newtype) so unbound runs can't reach enqueue
- Audit cleanup paths that clear `task_id` or `dispatch`
- Log binding state when restoring runs from persistence
When it happens
Trigger: Calling `enqueue_run_task` with an `AutomationRunRecord` whose `dispatch` or `task_id` field is `None` — i.e. the run was never bound to a task, or the binding was cleared/lost before enqueueing.
Common situations: Restoring automation state from disk where the task binding failed to persist; a run created but the task registration step failed silently; clearing a task (e.g. after cancellation) and then attempting to re-dispatch the run.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Automation admission belongs to a different task store; it…
- Automation admission execution ownership is unverified or…
- Automation execution ownership is unverified
- Automation name is required
- Automation run schema v
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/ceb08d2516f345a0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/automation_manager.rs:2131
.recover_task_admission(dispatch.request.clone(), task_id.to_owned())
.await?
};
crate::task_manager::validate_bound_task_request(&task, &dispatch.request)?;
dispatch.accepted = true;
dispatch.suppress_report = dispatch.delivery_mode == AutomationDeliveryMode::Watcher
&& task.status == TaskStatus::Completed
&& task
.result_summary
.as_deref()
.is_some_and(|summary| summary.trim() == AUTOMATION_WATCHER_NO_REPORT_SENTINEL);
Ok(task)
}
/// Caller owns dispatch.lock and has already persisted this exact binding.
async fn enqueue_run_task(run: &mut AutomationRunRecord, tasks: &SharedTaskManager) {
let result = match (&mut run.dispatch, &run.task_id) {
(Some(dispatch), Some(task_id)) => dispatch_bound_task(dispatch, task_id, tasks).await,
_ => Err(anyhow::anyhow!(
"Automation run has no durable task binding"
)),
};
match result {
Ok(task) => {
run.error = None;
apply_task_status(run, &task);
}
Err(error) => {
// Keep the same pending identity after uncertain admission. A later
// tick first looks for its canonical task; no new id is allocated.
run.error = Some(format!(
"Automation task admission needs recovery: {error:#}"
));
}
}
}
View on GitHub (pinned to 433685b202)