Hmbown/CodeWhale · error · anyhow::Error

Automation transaction cannot replace its identity

Error message

Automation transaction cannot replace its identity

What it means

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.

Solutions

  1. Preserve record.id in the edit callback — change other fields (name, schedule, prompt) but keep the id
  2. To give the automation a new identity, delete the old record and create a new one instead of editing
  3. Strip or ignore an incoming 'id' field when importing/updating definitions so the existing id is retained

Example fix

// before
let edited = Automation { id: new_id(), ..record.clone() }; // rejected
// after
let edited = Automation { id: record.id, name: "renamed".into(), ..record.clone() };
Defensive patterns

Strategy: try-catch

Validate before calling

if let Some(existing) = load(id)? {
    assert_eq!(incoming.id, existing.id, "edit must not change id");
}

Try / catch

match edit_result {
    Err(e) if e.to_string().contains("cannot replace its identity") => {
        // fall back to delete+create with the new record
        manager.delete_automation(old_id)?;
        manager.create_automation(new_record)?;
    }
    r => r?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/3ffb85403c0d55c5. Report an issue: GitHub.

Appendix: source

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

    }

    /// Short read/modify/write transaction shared with scheduler admission.
    /// Returning None leaves an absent record absent; it does not delete one.
    pub(crate) fn edit_automation(
        &self,
        id: &str,
        edit: impl FnOnce(Option<AutomationRecord>) -> Result<Option<AutomationRecord>>,
    ) -> Result<Option<AutomationRecord>> {
        self.with_transaction(|| {
            let current = if self.automation_path(id)?.try_exists()? {
                Some(self.get_automation(id)?)
            } else {
                None
            };
            let edited = edit(current)?;
            if let Some(record) = &edited {
                if record.id != id {
                    bail!("Automation transaction cannot replace its identity");
                }
                self.save_automation_unlocked(record)?;
            }
            Ok(edited)
        })
    }

    pub fn open(root: PathBuf) -> Result<Self> {
        let automations_dir = root.join("automations");
        let runs_dir = root.join("runs");
        let triggers_dir = root.join("triggers");
        fs::create_dir_all(&automations_dir)
            .with_context(|| format!("Failed to create {}", automations_dir.display()))?;
        fs::create_dir_all(&runs_dir)
            .with_context(|| format!("Failed to create {}", runs_dir.display()))?;
        fs::create_dir_all(&triggers_dir)
            .with_context(|| format!("Failed to create {}", triggers_dir.display()))?;
        Ok(Self {

View on GitHub (pinned to 73e0f67d83)