{"record":{"id":"3ffb85403c0d55c5","repo":"Hmbown/CodeWhale","slug":"automation-transaction-cannot-replace-its-identity","errorCode":null,"errorMessage":"Automation transaction cannot replace its identity","messagePattern":"Automation transaction cannot replace its identity","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/automation_manager.rs","lineNumber":1064,"sourceCode":"    }\n\n    /// Short read/modify/write transaction shared with scheduler admission.\n    /// Returning None leaves an absent record absent; it does not delete one.\n    pub(crate) fn edit_automation(\n        &self,\n        id: &str,\n        edit: impl FnOnce(Option<AutomationRecord>) -> Result<Option<AutomationRecord>>,\n    ) -> Result<Option<AutomationRecord>> {\n        self.with_transaction(|| {\n            let current = if self.automation_path(id)?.try_exists()? {\n                Some(self.get_automation(id)?)\n            } else {\n                None\n            };\n            let edited = edit(current)?;\n            if let Some(record) = &edited {\n                if record.id != id {\n                    bail!(\"Automation transaction cannot replace its identity\");\n                }\n                self.save_automation_unlocked(record)?;\n            }\n            Ok(edited)\n        })\n    }\n\n    pub fn open(root: PathBuf) -> Result<Self> {\n        let automations_dir = root.join(\"automations\");\n        let runs_dir = root.join(\"runs\");\n        let triggers_dir = root.join(\"triggers\");\n        fs::create_dir_all(&automations_dir)\n            .with_context(|| format!(\"Failed to create {}\", automations_dir.display()))?;\n        fs::create_dir_all(&runs_dir)\n            .with_context(|| format!(\"Failed to create {}\", runs_dir.display()))?;\n        fs::create_dir_all(&triggers_dir)\n            .with_context(|| format!(\"Failed to create {}\", triggers_dir.display()))?;\n        Ok(Self {","sourceCodeStart":1046,"sourceCodeEnd":1082,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/automation_manager.rs#L1046-L1082","documentation":"Automation edits run inside a transaction that takes the existing record by id, applies an edit function, and saves the result. If the edited record's id differs from the original id, the transaction refuses to save — identity changes must go through delete+create, not an in-place edit — because save_automation_unlocked would otherwise overwrite a different automation's slot.","triggerScenarios":"An edit/apply callback that sets record.id to a new value (or replaces the record wholesale with one carrying a different id) inside with_transaction edit of an existing automation.","commonSituations":"Importing a JSON definition that includes a different id; generating ids inside the edit closure instead of preserving the existing one; renaming an automation by changing its id rather than its name field.","solutions":["Preserve record.id in the edit callback — change other fields (name, schedule, prompt) but keep the id","To give the automation a new identity, delete the old record and create a new one instead of editing","Strip or ignore an incoming 'id' field when importing/updating definitions so the existing id is retained"],"exampleFix":"// before\nlet edited = Automation { id: new_id(), ..record.clone() }; // rejected\n// after\nlet edited = Automation { id: record.id, name: \"renamed\".into(), ..record.clone() };","handlingStrategy":"try-catch","validationCode":"if let Some(existing) = load(id)? {\n    assert_eq!(incoming.id, existing.id, \"edit must not change id\");\n}","typeGuard":null,"tryCatchPattern":"match edit_result {\n    Err(e) if e.to_string().contains(\"cannot replace its identity\") => {\n        // fall back to delete+create with the new record\n        manager.delete_automation(old_id)?;\n        manager.create_automation(new_record)?;\n    }\n    r => r?,\n}","preventionTips":["Never assign a fresh id inside an edit callback — clone record.id","When importing definitions, ignore any embedded id and keep the target's id","Model renames as a name-field change, not an id change"],"tags":["transaction","validation","identity"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}