Repository navigation
canonical(knowledge): two functions named content_hash return different values — full digest vs 16-char truncation #15722
Description
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
- One definition of the fact dedup key. The three inline computations import it;
_CONTENT_HASH_WIDTHexists once, beside that definition, not in two modules. - 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.
- A guard in the shape of
repo_tests/audio_extension_allowlist_test.py::test_no_fourth_literal_copy_existsfails when the truncation is written as a literal outside its home. autobot_shared/store_authority.py'sknowledge_factsentry 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
-
canonical(state): knowledge_facts dedup keys are advisory — no uniqueness on content_hash or unique_key #15673 — whether this key should be a database-enforced invariant at all (it is 64 bits; a collision would reject a legitimately different fact).
-
Umbrella: canonical-source duplication — concepts with no single definition, where the copies have already drifted #13916 — the umbrella this pattern belongs to.
-
autobot-backend/agent_loop/fingerprint.pycontent_hashdelegates toautobot_shared.content_digest.content_digest(normalize_content(content)), with byte-identical output and an equivalence test. Same primitive, different input, so it is a third copy of the digest. It lands on the canonical(knowledge): two functions named content_hash return different values — full digest vs 16-char truncation #15722 branch after tech-debt(backend): six regex concepts are forked across 2–8 files each, and one fork is a no-op #18093 (which ownsfingerprint.pyuntil it merges) is on main. -
autobot-backend/code_intelligence/pattern_analysis/analyzer.py: the literalEMBEDDING_MODEL = "nomic-embed-text"is removed; the cache key and the generator resolve from one source, the recorded index space (knowledge/index_embedding_space.pyindex_embedding_space("code_patterns")→space_id/model_id). Amended 2026-10-12 by owner ruling: the analyzer is index-bound (its stored vectors must be queried in the space they were built in), so it resolves from the recorded space rather thanactive_embedding_model(); the original wording would reintroduce the fix(knowledge): one active embedding model resolver, model-aware cache keys, and search refuses rather than embed in another space (#18143, #18185) #18230 defect.
Activity
mrveiss commentedon Oct 12, 2026
Closure evidence (merged in merge trains #18346 (constituents #18337, #18327), f194061). Verified against
main.autobot_shared/content_digest.py:33FACT_DEDUP_WIDTH = 16and:43fact_dedup_key, imported byfact_projection.py:39,130andfacts.py:603,1153,1155,1238. No_CONTENT_HASH_WIDTHremains.knowledge/fact_store.py:42definescontent_hash, and it delegates tofact_dedup_key. The connector helper is gone.repo_tests/one_content_digest_15722_test.py:137test_no_local_content_digest_outside_the_canonical_module(shrink-only_UNCONVERTED, with a stale check at :146).autobot_shared/store_authority.py:106-108states that theknowledge_factsdedup index derives fromfact_dedup_keyalone.agent_loop/fingerprint.py:38-40content_hash = content_digest(normalize_content(...)), with an equivalence test atautobot-backend/tests/agent_loop/test_stagnation.py:399.EMBEDDING_MODELliteral 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 ("throughactive_embedding_model()") was amended in the issue body for that reason.