Skip to content

canonical(knowledge): two functions named content_hash return different values — full digest vs 16-char truncation #15722

Closed

Description

@mrveiss

Summary

content_hash exists twice in autobot-backend/knowledge/, with the same name and different return values:

Definition Returns Used for
knowledge/connectors/content_extraction.py:21 full SHA-256 (64 chars) connector change detection
knowledge/fact_store.py:45 SHA-256 truncated to 16 chars the fact deduplication key

A same-name/different-semantics pair is worse than a plain duplicate: a caller who imports the wrong one gets a silently wrong key rather than an error. For the dedup key specifically, a full-digest value never matches an existing content_hash:<h> Redis index entry, so every fact would read as new — the duplicate-creating direction.

The truncation is also still written inline, three times

fact_store.content_hash() was added by #15663 as the canonical computation, but the literal it replaced still stands at three call sites:

  • knowledge/fact_projection.py:132 — hexdigest()[:_CONTENT_HASH_WIDTH] (a second constant for the same 16)
  • knowledge/facts.py:586 — hexdigest()[:16]
  • knowledge/facts.py:1175 — hexdigest()[:16], in _cleanup_fact_mappings, which computes the key to delete it

That last one is the sharp edge: the write path and the cleanup path derive the same Redis key independently. If one width changes, deletes stop matching writes and the dedup index leaks entries pointing at facts that no longer exist — which then resolve as duplicates on the next create.

This is the exact anti-pattern #13916 tracks, left behind by the change that introduced the canonical function. Reported by review of the work rather than found later.

Acceptance criteria

  1. One definition of the fact dedup key. The three inline computations import it; _CONTENT_HASH_WIDTH exists once, beside that definition, not in two modules.
  2. The connector helper and the fact-dedup helper no longer share a name, or are shown to be the same concept and merged — whichever the code supports. Same name, different width is not an option.
  3. A guard in the shape of repo_tests/audio_extension_allowlist_test.py::test_no_fourth_literal_copy_exists fails when the truncation is written as a literal outside its home.
  4. autobot_shared/store_authority.py's knowledge_facts entry states that the dedup index is derived from this one function, since a projection rebuilt from a differently-computed key is not the same projection.

Related

Activity

  1. modified the milestones: Backlog, v0.13.0 on Sep 12, 2026
  2. mrveiss commented on Oct 12, 2026

    @mrveiss
    OwnerAuthor

    Closure evidence (merged in merge trains #18346 (constituents #18337, #18327), f194061). Verified against main.

    • AC1, one dedup-key definition: autobot_shared/content_digest.py:33 FACT_DEDUP_WIDTH = 16 and :43 fact_dedup_key, imported by fact_projection.py:39,130 and facts.py:603,1153,1155,1238. No _CONTENT_HASH_WIDTH remains.
    • AC2, the two helpers no longer share a name: only knowledge/fact_store.py:42 defines content_hash, and it delegates to fact_dedup_key. The connector helper is gone.
    • AC3, guard: repo_tests/one_content_digest_15722_test.py:137 test_no_local_content_digest_outside_the_canonical_module (shrink-only _UNCONVERTED, with a stale check at :146).
    • AC4, store authority: autobot_shared/store_authority.py:106-108 states that the knowledge_facts dedup index derives from fact_dedup_key alone.
    • fingerprint.py: agent_loop/fingerprint.py:38-40 content_hash = content_digest(normalize_content(...)), with an equivalence test at autobot-backend/tests/agent_loop/test_stagnation.py:399.
    • analyzer.py (criterion amended by owner ruling, 2026-10-12): the EMBEDDING_MODEL literal is gone. The cache key (space.space_id, analyzer.py:1013,1035) and the generator (space.model_id, :1029) share one source, index_embedding_space("code_patterns") (:1011). The owner ratified the index-bound design: this pipeline queries stored vectors, so it must use the space they were built in. The original wording ("through active_embedding_model()") was amended in the issue body for that reason.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions