affaan-m/ECC · error

candidate id collision with different immutable content

Error message

candidate id collision with different immutable content

What it means

After an INSERT ... ON CONFLICT(id) DO NOTHING into harness_candidates, the store re-reads the stored row and compares it byte-for-byte against the expected canonical config and ref JSON. If a row already existed under the candidate's v2 id with different immutable content, the write is treated as a content-address id collision and the call fails rather than silently overwriting.

Solutions

  1. Regenerate the candidate id from the current immutable content so a modified candidate gets a fresh id.
  2. Diff the stored canonical_config_json/trace_refs_json/evidence_refs_json against the submitted candidate to identify which field drifted, then make the content match the stored row.
  3. If the pre-existing row is wrong or abandoned, delete it explicitly and re-register the candidate.
  4. Ensure all writers use the same canonicalization (field order, JSON formatting) so identical content yields identical stored tuples.

Example fix

// before: content changed but id reused
let candidate = CandidateSpec { id: old_id, trace_refs: new_refs, .. };
store.persist_candidate(&candidate)?; // stored != expected -> collision

// after: derive id from current content
let id = candidate.id_for_v2()?; // fresh 64-char id reflecting new_refs
let candidate = CandidateSpec { id, trace_refs: new_refs, .. };
store.persist_candidate(&candidate)?;
Defensive patterns

Strategy: validation

Validate before calling

fn ensure_id_content_match(store: &Store, candidate: &CandidateSpec) -> anyhow::Result<()> {
    if let Some(stored) = store.lookup_candidate(&candidate.id)? {
        anyhow::ensure!(
            stored.canonical_config_json == candidate.canonical_config
                && stored.trace_refs_json == serde_json::to_string(&candidate.trace_refs)?
                && stored.evidence_refs_json == serde_json::to_string(&candidate.evidence_refs)?,
            "id already registered with different content"
        );
    }
    Ok(())
}

Prevention

When it happens

Trigger: Calling the candidate persist/registration API with a candidate whose 64-char v2 id already exists in harness_candidates but whose canonical_config_json, trace_refs_json, or evidence_refs_json differ from the stored tuple.

Common situations: A hash/id computation returning the same id for modified candidate content (e.g. after tweaking trace refs or evidence refs); two environments sharing one database with divergent candidate definitions under the same id; replaying an old registration after content changed.

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/c20a24144414cc0e. Report an issue: GitHub.

Appendix: 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 8321021c54)