affaan-m/ECC · critical · anyhow::Error
candidate alias integrity verification failed
Error message
candidate alias integrity verification failed
What it means
Raised by ensure_harness_candidate_aliases in ecc2/src/session/store.rs:985 during the schema/migration integrity pass. For each row in harness_candidate_aliases it reconstructs the CandidateSpec from harness_candidates, recomputes legacy_id and id_for_v2, and bails if the alias row's stored version/target/alias triplet disagrees with the recomputed values, or if an alias_id collides with an existing primary id in harness_candidates. This is a defensive check that the persisted alias table matches deterministic id derivation.
Source
Thrown at ecc2/src/session/store.rs:985
let target = CandidateSpec {
id: target_id.clone(),
canonical_config,
trace_refs: serde_json::from_str(&trace_json)?,
evidence_refs: serde_json::from_str(&evidence_json)?,
};
if version != 2
|| target_id != target.legacy_id()
|| alias_id != target.id_for_v2()?
|| tx
.query_row(
"SELECT 1 FROM harness_candidates WHERE id = ?1",
[&alias_id],
|_| Ok(()),
)
.optional()?
.is_some()
{
anyhow::bail!("candidate alias integrity verification failed");
}
}
tx.commit()?;
Ok(())
}
fn ensure_session_board_columns(&self) -> Result<()> {
if !self.has_column("session_board", "row_label")? {
self.conn
.execute("ALTER TABLE session_board ADD COLUMN row_label TEXT", [])
.context("Failed to add row_label column to session_board table")?;
}
if !self.has_column("session_board", "previous_lane")? {
self.conn
.execute(
"ALTER TABLE session_board ADD COLUMN previous_lane TEXT",
[],View on GitHub (pinned to 01e15490f0)
Solutions
- Back up the sqlite DB, then re-run the migration; if it fails, inspect harness_candidate_aliases vs harness_candidates for the offending alias_id and reconcile the row.
- Ensure all ecc2 instances sharing the DB are on the same version so CandidateSpec::id_for_v2 and legacy_id produce identical results.
- If the DB was partially restored, drop and rebuild harness_candidate_aliases from harness_candidates by re-registering aliases via register_harness_alias (the code path at store.rs:945).
- Stop hand-editing candidate/alias rows; route all writes through the CandidateSpec API.
Example fix
// before: legacy and v2 ids disagree after a partial restore
// harness_candidate_aliases row: alias_id="abc-v2", candidate_id="abc", version=1
// -> ensure_harness_candidate_aliases bails
// after: rebuild aliases deterministically from candidates
for candidate in store.list_candidates()? {
let legacy = candidate.legacy_id();
let v2 = candidate.id_for_v2()?;
if legacy != v2 {
store.register_harness_alias(&tx, &v2, &legacy)?;
}
} Defensive patterns
Strategy: validation
Validate before calling
// Before opening a DB that may have been produced by another build, run a
// read-only integrity probe (do not call the destructive migration path).
fn probe_alias_integrity(conn: &rusqlite::Connection) -> anyhow::Result<()> {
let mut stmt = conn.prepare(
"SELECT alias_id, candidate_id, id_version FROM harness_candidate_aliases",
)?;
let rows: Vec<(String,String,i64)> = stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))?
.collect::<rusqlite::Result<_>>()?;
for (alias_id, candidate_id, version) in rows {
if version != 2 { anyhow::bail!("alias {alias_id} has non-v2 version {version}"); }
if alias_id == candidate_id { anyhow::bail!("alias {alias_id} points at itself"); }
}
Ok(())
}
// Call probe_alias_integrity on a COPY of the DB first; only run the real
// migration if the probe passes. Type guard
// Not applicable: this is a data-integrity predicate over DB rows, not a // language type. Encode it as a function returning Result (above).
Try / catch
// Treat migration-time integrity failure as fatal and surface the offending
// alias row so the operator can reconcile manually rather than lose data.
match store.run_migrations() {
Ok(()) => Ok(()),
Err(e) if e.to_string().contains("alias integrity verification failed") => {
eprintln!("FATAL: harness_candidate_aliases is inconsistent. Back up the DB and reconcile.");
std::process::exit(1);
}
Err(e) => Err(e),
} Prevention
- Run ecc2 on a single version across all writers sharing a DB; id-derivation changes between versions break alias integrity.
- Back up the sqlite file before any upgrade that touches harness_candidates.
- Never edit harness_candidate_aliases or harness_candidates rows by hand.
- After a restore, rebuild aliases via register_harness_alias from the candidates.
When it happens
Trigger: Running a migration against a database whose harness_candidates rows were hand-edited, partially restored from backup, or produced by an older CandidateSpec id-derivation algorithm whose legacy_id/id_for_v2 outputs no longer match the rows on disk; a collision where an alias_id equals a primary candidate id (alias pointing at itself).
Common situations: Downgrading the binary after an id-format upgrade (v1 -> v2 alias scheme changed); restoring a DB snapshot from a different build; concurrent writers from two ecc2 versions with different id-derivation logic; manual SQL edits to harness_candidate_aliases.
Related errors
- candidate alias collision with physical candidate id
- Session not found: {session_id}
- Scheduled task {id} was not found after insert
- Remote dispatch request {id} was not found after insert
- candidate alias collision with different immutable target
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/f08870255df7c3c5.
Report an issue: GitHub.