Hmbown/CodeWhale · error · anyhow::Error

Trigger '{trigger_id}' cannot be canceled (status: {:?})

Error message

Trigger '{trigger_id}' cannot be canceled (status: {:?})

What it means

cancel_trigger_for_owner loads the record through the owner-scoped lookup and then refuses to cancel anything whose status is not Pending. DelayedTriggerStatus is a one-way state machine (Pending -> Fired | Canceled | Failed), so this error means the trigger already left the pending state; the message embeds the actual status so callers can tell 'already fired' from 'already canceled' or 'failed to enqueue'.

Source

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

        owner_session_id: &str,
    ) -> Result<Vec<DelayedTriggerRecord>> {
        let mut records = self.list_triggers(status_filter, None)?;
        records.retain(|record| record.owner_session_id.as_deref() == Some(owner_session_id));
        if let Some(limit) = limit {
            records.truncate(limit);
        }
        Ok(records)
    }

    /// Cancel a pending trigger owned by the given session.
    pub fn cancel_trigger_for_owner(
        &self,
        trigger_id: &str,
        owner_session_id: &str,
    ) -> Result<DelayedTriggerRecord> {
        let mut record = self.get_trigger_for_owner(trigger_id, owner_session_id)?;
        if !matches!(record.status, DelayedTriggerStatus::Pending) {
            bail!(
                "Trigger '{trigger_id}' cannot be canceled (status: {:?})",
                record.status
            );
        }
        record.status = DelayedTriggerStatus::Canceled;
        self.save_trigger(&record)?;
        Ok(record)
    }

    /// Return all pending triggers whose `fire_at` is at or before `now`.
    pub fn collect_due_triggers(&self, now: DateTime<Utc>) -> Result<Vec<DelayedTriggerRecord>> {
        let pending = self.list_triggers(Some(DelayedTriggerStatus::Pending), None)?;
        Ok(pending
            .into_iter()
            .filter(|trigger| trigger.owner_session_id.is_some() && trigger.fire_at <= now)
            .collect())
    }
}

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Treat the error as information, not a retryable failure: re-read the record with get_trigger_for_owner and show its terminal state.
  2. Make cancel idempotent in the caller by only attempting it when the fetched status is still Pending.
  3. If the trigger fired unexpectedly early, inspect the enqueued task instead of retrying the cancel.

Example fix

// before
let record = manager.cancel_trigger_for_owner(trigger_id, session_id)?;

// after: only cancel what is still pending
let current = manager.get_trigger_for_owner(trigger_id, session_id)?;
let record = if matches!(current.status, DelayedTriggerStatus::Pending) {
    manager.cancel_trigger_for_owner(trigger_id, session_id)?
} else {
    current // already fired, canceled, or failed
};
Defensive patterns

Strategy: validation

Validate before calling

let record = manager.get_trigger_for_owner(trigger_id, session_id)?;
if !matches!(record.status, DelayedTriggerStatus::Pending) {
    return Ok(record); // nothing left to cancel
}
manager.cancel_trigger_for_owner(trigger_id, session_id)

Prevention

When it happens

Trigger: Calling cancel_trigger_for_owner on a trigger that collect_due_triggers has already fired; submitting the same cancel twice; canceling a trigger whose enqueue previously failed (status Failed).

Common situations: Double-clicking a cancel button or duplicating a UI action; retrying a cancel request that actually succeeded; the due-trigger scheduler firing between the user opening a confirmation dialog and confirming.

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


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/9067e9795840b1e2. Report an issue: GitHub.