Skip to content

entities.mention_count is never decremented: replacing or deleting a document leaves the count inflated #4291

Description

@nicoloboschi

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions