Skip to content

retain: let a store that owns its vector index skip the Postgres ANN pass - #3478

Draft
nicoloboschi wants to merge 2 commits into
mainfrom
skip-ann-pass-for-self-indexed-stores
Draft

retain: let a store that owns its vector index skip the Postgres ANN pass#3478
nicoloboschi wants to merge 2 commits into
mainfrom
skip-ann-pass-for-self-indexed-stores

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a backend-agnostic capability flag, derives_semantic_links_internally (with a per-bank derives_semantic_links_internally_for), that lets a store which owns its own vector index skip the retain-time Postgres kNN work that populates memory_links.

Why

The default SQL stores have no standing kNN index, so retain finds each unit's semantic neighbours with a pgvector ANN probe and materializes them as semantic memory_links rows, which the SQL graph arm reads back at recall time.

A store that keeps its own vector index can answer the same question from that index (at read time, or from links it derived at fold time), so the Postgres work is redundant for it — and for a store whose memories don't live in memory_units at all it is worse than redundant: it queries a table holding none of its rows and writes memory_links rows nothing on its read path consults.

How

The flag is honoured at three choke points, so every retain path — streaming (full), delta, and bank import — inherits the skip and no caller can reintroduce the query by forgetting to pass a flag down:

  • engine/retain/orchestrator.py
    • _pre_resolve_phase1 — skips the ANN probe (this is the probe delta retain and bank import run).
    • _insert_facts_and_links — skips the memory_links write. Necessary on its own: the within-batch similarities are computed in Python, so skipping the probe alone would still leave rows behind.
    • _run_final_semantic_ann — skips streaming retain's post-commit pass, which reaches pgvector directly rather than through Phase 1.
  • engine/memories/base.py — adds the class attr derives_semantic_links_internally: bool = False and derives_semantic_links_internally_for(bank_id), mirroring the existing writes_memory_rows_in_sql_for / owns_document_store_for pattern.

Setting the flag True is a promise, not just an optimization, and the docstring says so: the store's graph_retriever / graph_direct_links must serve the semantic neighbours itself, because afterwards there are no semantic rows in memory_links for the SQL graph arm to find.

Tests

tests/test_retain_orchestrator_mapping.py drives all three gates through the orchestrator — each test fails if its gate is removed — and covers the per-bank answer (one store, one bank it owns the index for and one it leaves in SQL). tests/test_memories_extension.py keeps the default-False assertion.

Default is False, so there is no behaviour change for existing stores.

…pass

The default SQL stores have no standing kNN index, so retain runs a
pgvector ANN pass (_run_final_semantic_ann) over the committed units to
populate memory_links, which the recall graph arm then reads.

A store that keeps its own vector index can serve those same neighbours
itself (at read time, or from links it derived at fold time), so for it
the pass is redundant: it scans a memory_units table that is empty for
its banks and writes memory_links rows nothing on its read path consults.

Add a backend-agnostic capability flag,
derives_semantic_links_internally (with a per-bank
derives_semantic_links_internally_for), mirroring the existing
writes_memory_rows_in_sql_for / owns_document_store_for pattern, and gate
the final ANN pass on it. Default False, so there is no behaviour change
for existing stores. Tests included.
The gate only covered the streaming full-retain path, so a store that owns
its vector index still paid for the Postgres semantic graph everywhere else:
delta retain and bank import ran the pgvector ANN probe in Phase 1, and Phase 2
wrote the within-batch similarities (computed in Python, so skipping the probe
alone does not stop them) into memory_links — rows the store's read path never
consults.

Move each gate to its single choke point instead: the ANN probe in
_pre_resolve_phase1, the memory_links write in _insert_facts_and_links, and the
post-commit pass inside _run_final_semantic_ann. Every retain path (full, delta,
import) now inherits the skip and no caller can reintroduce the query.

Also state the obligation the flag carries — a store that sets it True must serve
the semantic neighbours from its own graph_retriever, since there will be no
semantic memory_links rows for the SQL graph arm to find — and drop the claim
that the pass "scans" memory_units: for a store whose facts are not in that table
the pass currently degenerates to a lookup that finds none of its units and logs
"(unexpected)" on every retain.

Tests now pin the three gates through the orchestrator (each fails if its gate is
removed) and cover the per-bank answer, replacing a unit test that asserted only
the class attribute.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant