affaan-m/ECC · error
candidate alias collision with different immutable target
Error message
candidate alias collision with different immutable target
What it means
Candidate aliases are immutable: once an alias_id maps to a candidate_id it can never be re-pointed. Registration loads any existing mapping for the alias; if the existing target differs from the requested candidate_id it bails 'candidate alias collision with different immutable target'. If it matches, registration is idempotent and returns Ok.
Solutions
- Look up the existing mapping first and only register when the target matches (the API already short-circuits idempotently).
- Derive new alias strings when the target candidate changes (version-suffixed aliases) instead of reusing the old one.
- Centralize alias creation behind one service/lock to avoid concurrent conflicting claims.
- If you truly need to move an alias, add an explicit re-point API with deprecation of the old target — do not overwrite.
Example fix
// before
// blindly re-registering after re-keying the candidate
store.register_candidate_alias(&new_candidate_id, "stable-alias")?; // bails
// after
match store.lookup_candidate_by_alias("stable-alias")? {
Some(id) if id != new_candidate_id => {
let alias = format!("stable-alias@{}", new_candidate_id);
store.register_candidate_alias(&new_candidate_id, &alias)?;
}
_ => store.register_candidate_alias(&new_candidate_id, "stable-alias")?,
} Defensive patterns
Strategy: validation
Validate before calling
if let Some(existing) = store.lookup_candidate_by_alias(alias_id)? {
ensure!(existing == candidate_id, "alias {alias_id} is immutable and points to {existing}");
} // else safe to register (API is idempotent for matching targets) Try / catch
match store.register_candidate_alias(&cid, alias) {
Err(e) if e.to_string().contains("different immutable target") => {
mint_new_versioned_alias(alias) // never re-point
}
other => other,
} Prevention
- Treat alias->candidate mappings as immutable; never reuse an alias for a new candidate.
- Version-suffix aliases when the underlying candidate is re-keyed.
- Funnel alias registration through one service to avoid conflicting concurrent claims.
When it happens
Trigger: registering the same alias_id for two different candidate_ids — e.g. reusing a well-known alias string for a new candidate, retrying registration after re-keying a candidate, or concurrent writers racing with different targets.
Common situations: Re-deploying configs where an alias was reassigned to a renamed candidate; build/tooling systems reusing stable short names across versions; two services independently claiming the same alias for different candidates.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- candidate alias collision with physical candidate id
- a current bound approved draft is required
- a top-level committed transaction is required
- approved hash must be lowercase SHA-256 hexadecimal
- approved text must be valid UTF-8 text
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/bc203556f0449423.
Report an issue: GitHub.
Appendix: source
Thrown at ecc2/src/session/store.rs:5318
"SELECT 1 FROM harness_candidates WHERE id = ?1",
[alias_id],
|_| Ok(()),
)
.optional()?
.is_some()
{
anyhow::bail!("candidate alias collision with physical candidate id");
}
let existing = tx
.query_row(
"SELECT candidate_id FROM harness_candidate_aliases WHERE alias_id = ?1",
[alias_id],
|row| row.get::<_, String>(0),
)
.optional()?;
if let Some(existing) = existing {
if existing != candidate_id {
anyhow::bail!("candidate alias collision with different immutable target");
}
return Ok(());
}
tx.execute(
"INSERT INTO harness_candidate_aliases (alias_id, candidate_id, id_version, created_at) VALUES (?1, ?2, 2, ?3)",
rusqlite::params![alias_id, candidate_id, chrono::Utc::now().to_rfc3339()],
)?;
Ok(())
}
fn resolve_harness_candidate_id(connection: &Connection, candidate_id: &str) -> Result<String> {
if let Some(target) = connection
.query_row(
"SELECT candidate_id FROM harness_candidate_aliases WHERE alias_id = ?1",
[candidate_id],
|row| row.get::<_, String>(0),
)
.optional()?View on GitHub (pinned to 8321021c54)