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
- 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
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
- 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
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
- Bound task identity or schema does not match its durable…
- Duplicate policy identity.
- Duplicate producer incarnation/sequence in evidence bundle…
- Record requires nonempty id, traceId, and name.
- 1
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)