What happens
entities.mention_count is incremented when facts are linked to an entity and never decremented. The only mutation in the codebase is:
hindsight-api-slim/hindsight_api/engine/entity_resolver.py:556
UPDATE entities SET
mention_count = mention_count + $2,
last_seen = GREATEST(last_seen, $3)
WHERE id = $1::uuid
grep -rn "mention_count -" hindsight_api/ returns nothing. Removing the facts that produced a mention — replacing a document, deleting one, invalidating a memory — leaves the counter where it was.
Reproduction
Two documents, both naming Alice, then replace one of them:
await client.aretain(bank_id=bank, content="Alice moved to Berlin.", document_id="d1") # entity: Alice
await client.aretain(bank_id=bank, content="Alice plays cello.", document_id="d2") # entity: Alice
# -> Alice mention_count == 2, correct
await client.aretain(bank_id=bank, content="Alice moved to Lisbon.",
document_id="d1", update_mode="replace")
Observed afterwards:
entities: Alice mention_count = 3
memory_units total: 2
memories filtered by Alice's entity_id: 2 # the links are correct
So the link table is right and the denormalised counter is wrong — it counted the Berlin fact's mention and kept it after that fact was replaced. Berlin itself disappears from the entity list correctly; only the count on the surviving entity is stale.
The same applies to delete_document and to invalidating a memory: nothing gives the count back.
Why it matters
mention_count is not internal bookkeeping — it is read in three places that matter:
- Returned to callers on
list_entities, and rendered in the control plane's entity list.
- Graph node prominence.
get_entity_graph emits it as mentionCount and sizes/colours nodes by it, so a stale-inflated entity looks like the most important thing in the bank.
- Curation ordering —
engine/memories/pg/curation.py:486 sorts ORDER BY mention_count DESC, last_seen DESC, id ASC, so an inflated entity outranks genuinely well-attested ones in whatever that list feeds.
The drift is not a rounding error over time; it is proportional to how often documents are updated. The coding-agents integration re-retains a conversation under the same document_id on every turn (update_mode append/replace), so a long-running session inflates every entity in it by roughly the number of turns. Those banks will show mention_count values with no relationship to how many facts actually mention the entity.
Suggested fix
Either decrement on unlink — wherever fact→entity links are removed (document replace/delete, memory invalidation) — or stop denormalising and derive the count from unit_entities at read time. The derived form is harder to get wrong and the entity list is already paginated; a subquery or a maintained materialised count both work.
If the intended semantics really are "mentions ever seen" rather than "current mentions", then the field is misnamed for what the UI and the curation ordering do with it, and the fix is to document that and stop using it for prominence.
Notes
Found while writing blackbox system tests (#4214). hindsight-system-tests/tests/test_74_entity_links.py asserts the correct behaviour and fails today.
What happens
entities.mention_countis incremented when facts are linked to an entity and never decremented. The only mutation in the codebase is:hindsight-api-slim/hindsight_api/engine/entity_resolver.py:556grep -rn "mention_count -" hindsight_api/returns nothing. Removing the facts that produced a mention — replacing a document, deleting one, invalidating a memory — leaves the counter where it was.Reproduction
Two documents, both naming Alice, then replace one of them:
Observed afterwards:
So the link table is right and the denormalised counter is wrong — it counted the Berlin fact's mention and kept it after that fact was replaced.
Berlinitself disappears from the entity list correctly; only the count on the surviving entity is stale.The same applies to
delete_documentand to invalidating a memory: nothing gives the count back.Why it matters
mention_countis not internal bookkeeping — it is read in three places that matter:list_entities, and rendered in the control plane's entity list.get_entity_graphemits it asmentionCountand sizes/colours nodes by it, so a stale-inflated entity looks like the most important thing in the bank.engine/memories/pg/curation.py:486sortsORDER BY mention_count DESC, last_seen DESC, id ASC, so an inflated entity outranks genuinely well-attested ones in whatever that list feeds.The drift is not a rounding error over time; it is proportional to how often documents are updated. The coding-agents integration re-retains a conversation under the same
document_idon every turn (update_modeappend/replace), so a long-running session inflates every entity in it by roughly the number of turns. Those banks will showmention_countvalues with no relationship to how many facts actually mention the entity.Suggested fix
Either decrement on unlink — wherever fact→entity links are removed (document replace/delete, memory invalidation) — or stop denormalising and derive the count from
unit_entitiesat read time. The derived form is harder to get wrong and the entity list is already paginated; a subquery or a maintained materialised count both work.If the intended semantics really are "mentions ever seen" rather than "current mentions", then the field is misnamed for what the UI and the curation ordering do with it, and the fix is to document that and stop using it for prominence.
Notes
Found while writing blackbox system tests (#4214).
hindsight-system-tests/tests/test_74_entity_links.pyasserts the correct behaviour and fails today.