affaan-m/ECC · error

legacy candidate id collision with different immutable…

Error message

legacy candidate id collision with different immutable content

What it means

When registering a harness candidate, the store checks whether an entry already exists under the candidate's legacy (v1) id. If a stored legacy candidate's derived v2 id differs from the new candidate's v2 id while both map to the same legacy id, the same legacy identifier refers to two different sets of immutable content, which is treated as an unrecoverable identity collision and the registration aborts.

Solutions

  1. Change the candidate's immutable content (canonical_config, trace_refs, or evidence_refs) so it matches what is stored under the legacy id, or use a new legacy id.
  2. Query the harness_candidates table by legacy id and compare the stored canonical_config_json/trace_refs_json/evidence_refs_json with the candidate you are submitting to find the exact field that diverged.
  3. If the stored legacy row is stale or from a different dataset, remove it deliberately and re-register (understanding this invalidates the legacy identity).
  4. Check that the same candidate is not being registered twice from two code paths with slightly different canonical serialization.

Example fix

// before: candidate edited after first registration, legacy id unchanged
let candidate = CandidateSpec { id: derived_v2_id, canonical_config: edited_config, .. };
store.persist_candidate(&candidate)?; // legacy id collision

// after: recompute ids from current content, or restore original content
let candidate = CandidateSpec::from_config(original_config)?; // id matches stored legacy row
store.persist_candidate(&candidate)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_legacy_consistent(store: &Store, candidate: &CandidateSpec) -> anyhow::Result<()> {
    let legacy_id = candidate.legacy_id();
    if let Some(stored) = store.lookup_candidate(&legacy_id)? {
        let stored_v2 = CandidateSpec::from_stored(&legacy_id, &stored)?.id_for_v2()?;
        anyhow::ensure!(stored_v2 == candidate.id, "legacy id maps to different content; regenerate ids or restore original content");
    }
    Ok(())
}

Prevention

When it happens

Trigger: Calling the candidate-registration (persist) path with a candidate whose canonical_config/trace_refs/evidence_refs produce a legacy_id already stored in harness_candidates, but whose id_for_v2() differs from the stored legacy candidate's derived v2 id.

Common situations: Re-registering a candidate after editing its canonical config or evidence refs while keeping the same legacy id; restoring an old database where a legacy row was written with different content; a migration/replay tool reusing legacy ids across divergent candidate definitions.

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


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/88a2b467a0d12843. Report an issue: GitHub.

Appendix: source

Thrown at ecc2/src/session/store.rs:5376

                [&legacy_id],
                |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?)),
            )
            .optional()?;
        let expected = (
            candidate.canonical_config.clone(),
            trace_json.clone(),
            evidence_json.clone(),
        );
        if let Some(stored) = legacy {
            let legacy_candidate = CandidateSpec {
                id: legacy_id.clone(),
                canonical_config: stored.0,
                trace_refs: serde_json::from_str(&stored.1)?,
                evidence_refs: serde_json::from_str(&stored.2)?,
            };
            legacy_candidate.verify_persisted_id(&legacy_id)?;
            if legacy_candidate.id_for_v2()? != candidate.id {
                anyhow::bail!("legacy candidate id collision with different immutable content");
            }
            let tx = self.conn.unchecked_transaction()?;
            Self::register_harness_alias(&tx, &candidate.id, &legacy_id)?;
            tx.commit()?;
            return Ok(());
        }
        self.conn.execute(
            "INSERT INTO harness_candidates (id, canonical_config_json, trace_refs_json, evidence_refs_json, created_at)
             VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(id) DO NOTHING",
            rusqlite::params![candidate.id, candidate.canonical_config, trace_json, evidence_json, chrono::Utc::now().to_rfc3339()],
        )?;
        let stored: (String, String, String) = self.conn.query_row(
            "SELECT canonical_config_json, trace_refs_json, evidence_refs_json FROM harness_candidates WHERE id = ?1",
            [&candidate.id],
            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
        )?;
        if stored != expected {
            anyhow::bail!("candidate id collision with different immutable content");

View on GitHub (pinned to 8321021c54)