{"record":{"id":"9067e9795840b1e2","repo":"Hmbown/CodeWhale","slug":"trigger-trigger-id-cannot-be-canceled-status","errorCode":null,"errorMessage":"Trigger '{trigger_id}' cannot be canceled (status: {:?})","messagePattern":"Trigger '(.+?)' cannot be canceled \\(status: (.+?)\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":1460,"sourceCode":"        owner_session_id: &str,\n    ) -> Result<Vec<DelayedTriggerRecord>> {\n        let mut records = self.list_triggers(status_filter, None)?;\n        records.retain(|record| record.owner_session_id.as_deref() == Some(owner_session_id));\n        if let Some(limit) = limit {\n            records.truncate(limit);\n        }\n        Ok(records)\n    }\n\n    /// Cancel a pending trigger owned by the given session.\n    pub fn cancel_trigger_for_owner(\n        &self,\n        trigger_id: &str,\n        owner_session_id: &str,\n    ) -> Result<DelayedTriggerRecord> {\n        let mut record = self.get_trigger_for_owner(trigger_id, owner_session_id)?;\n        if !matches!(record.status, DelayedTriggerStatus::Pending) {\n            bail!(\n                \"Trigger '{trigger_id}' cannot be canceled (status: {:?})\",\n                record.status\n            );\n        }\n        record.status = DelayedTriggerStatus::Canceled;\n        self.save_trigger(&record)?;\n        Ok(record)\n    }\n\n    /// Return all pending triggers whose `fire_at` is at or before `now`.\n    pub fn collect_due_triggers(&self, now: DateTime<Utc>) -> Result<Vec<DelayedTriggerRecord>> {\n        let pending = self.list_triggers(Some(DelayedTriggerStatus::Pending), None)?;\n        Ok(pending\n            .into_iter()\n            .filter(|trigger| trigger.owner_session_id.is_some() && trigger.fire_at <= now)\n            .collect())\n    }\n}","sourceCodeStart":1442,"sourceCodeEnd":1478,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/automation_manager.rs#L1442-L1478","documentation":"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'.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Treat the error as information, not a retryable failure: re-read the record with get_trigger_for_owner and show its terminal state.","Make cancel idempotent in the caller by only attempting it when the fetched status is still Pending.","If the trigger fired unexpectedly early, inspect the enqueued task instead of retrying the cancel."],"exampleFix":"// before\nlet record = manager.cancel_trigger_for_owner(trigger_id, session_id)?;\n\n// after: only cancel what is still pending\nlet current = manager.get_trigger_for_owner(trigger_id, session_id)?;\nlet record = if matches!(current.status, DelayedTriggerStatus::Pending) {\n    manager.cancel_trigger_for_owner(trigger_id, session_id)?\n} else {\n    current // already fired, canceled, or failed\n};","handlingStrategy":"validation","validationCode":"let record = manager.get_trigger_for_owner(trigger_id, session_id)?;\nif !matches!(record.status, DelayedTriggerStatus::Pending) {\n    return Ok(record); // nothing left to cancel\n}\nmanager.cancel_trigger_for_owner(trigger_id, session_id)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Refresh the trigger status immediately before confirming a cancel.","Disable cancel affordances once status leaves Pending.","Never auto-retry a cancel that failed with a status error."],"tags":["rust","state-machine","automation","idempotency","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}