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

  1. 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.
  2. Ensure all ecc2 instances sharing the DB are on the same version so CandidateSpec::id_for_v2 and legacy_id produce identical results.
  3. 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).
  4. 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

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


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/f08870255df7c3c5. Report an issue: GitHub.