affaan-m/ECC · error · anyhow::Error

candidate id collision with different immutable content

Error message

candidate id collision with different immutable content

What it means

A v2 candidate id is content-addressed. After INSERT ... ON CONFLICT(id) DO NOTHING the store reads back the persisted (canonical_config_json, trace_refs_json, evidence_refs_json) and compares to the expected triple. A mismatch means two different contents resolved to the same id — a hash collision is not expected, so this indicates corruption, a serialization bug, or a hash-function drift.

Source

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

                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");
        }
        Ok(())
    }

    pub fn activate_initial_harness(&self, candidate_id: &str, evidence_ref: &str) -> Result<()> {
        if candidate_id.len() != 64 || evidence_ref.trim().is_empty() || evidence_ref.len() > 4096 {
            anyhow::bail!(
                "valid candidate id and bounded activation evidence reference are required"
            );
        }
        let tx = self.conn.unchecked_transaction()?;
        let stored_candidate_id = Self::resolve_harness_candidate_id(&tx, candidate_id)?;
        if tx
            .query_row(
                "SELECT candidate_id FROM active_harness_config WHERE slot = 'default'",
                [],
                |row| row.get::<_, String>(0),
            )

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Guarantee candidate.id is computed from exactly the canonical_config/trace_refs/evidence_refs being stored (recompute and compare before insert).
  2. Use a canonical, deterministic JSON serializer (sorted keys, no extra whitespace) for all three fields before hashing.
  3. If the stored row is the wrong one, delete it and re-insert the correct triple.
Defensive patterns

Strategy: validation

Validate before calling

// Recompute the id from exactly what you are about to store, and confirm it is stable.
let expected_id = candidate.id_for_v2()?;
if expected_id != candidate.id {
    return Err(anyhow::anyhow!("candidate.id does not match recomputed content id"));
}
// Build the expected stored triple with the SAME canonical serializer the store uses.
let expected = (candidate.canonical_config.clone(), trace_json.clone(), evidence_json.clone());
// (the store compares SELECT ... WHERE id = candidate.id against `expected`)

Type guard

fn candidate_id_is_stable(candidate: &CandidateSpec) -> bool {
    candidate.id_for_v2().ok().as_deref() == Some(candidate.id.as_str())
}

Try / catch

match store.register_candidate(&candidate) {
    Ok(()) => { /* ok */ }
    Err(e) if e.to_string().contains("candidate id collision with different immutable content") => {
        // two different contents hashed to the same id — investigate serialization/hash,
        // do not retry with the same inputs.
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: register_candidate where candidate.id already exists in harness_candidates with different canonical_config_json/trace_refs_json/evidence_refs_json than the expected triple being inserted.

Common situations: JSON key ordering or whitespace changed between when candidate.id was hashed and when it is re-serialized for storage; two code paths compute candidate.id with different rules; a manual DB edit changed one column.

Related errors


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