Skip to content

fix(fts): scope memories_fts update triggers to indexed columns#166

Open
J0nd1sk wants to merge 1 commit into
TSchonleber:mainfrom
J0nd1sk:fix/152-fts-trigger-scope
Open

fix(fts): scope memories_fts update triggers to indexed columns#166
J0nd1sk wants to merge 1 commit into
TSchonleber:mainfrom
J0nd1sk:fix/152-fts-trigger-scope

Conversation

@J0nd1sk

@J0nd1sk J0nd1sk commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Fixes #152. memories_fts (external-content FTS5) is maintained by two unscoped AFTER UPDATE ON memories triggers, so every metadata-only write fires the delete+reinsert pair. cmd_memory_search calls _retrieval_practice_boost on each hit (bumping confidence/recalled_count), so every search erodes the inverted index: docsize bleeds out and search progressively returns fewer rows. q_value, trust_score, ewc_importance and other metadata writes do the same.

Fix

Scope both update triggers to AFTER UPDATE OF content, category, tags, indexed, retired_at. Metadata churn no longer touches FTS; indexed and retired_at stay in scope so promotion and retire still maintain the index. A dedicated AFTER UPDATE OF retired_at purge trigger was tried and rejected in migration 048 due to FTS5 statement-level batching; this keeps the single delete leg and only narrows its trigger columns, so retire firing is unchanged.

Reaches every install shape:

  • init_schema.sql for fresh DBs
  • migration 083 drops and recreates the scoped triggers on migrated DBs, plus a one-time rebuild to repair indexes already corrupted in the field
  • a startup rescope in mcp_server for pip-wheel installs that do not ship db/migrations/

Also fixes a latent bug surfaced by the new tests: _ensure_fts_index_consistent returned None on a healthy index instead of False (its documented contract).

Impact

The repo's own search-quality bench was degraded by this bug. The fix raises overall p_at_1 from 0.60 to 0.80 and recall@10 from 0.51 to 0.85 (baseline refreshed).

Tests

tests/test_memories_fts_trigger_scope.py: metadata updates no longer erode docsize (raw SQL and via the real _retrieval_practice_boost); content edit, retire, and delete still maintained. Full suite: 2519 passed; the 6 remaining failures pre-exist on main in my environment and are unrelated.

@J0nd1sk J0nd1sk changed the title fix(fts): scope memories_fts update triggers to indexed columns (#152) fix(fts): scope memories_fts update triggers to indexed columns Jul 1, 2026
`memories_fts` is external-content FTS5 maintained by two `AFTER UPDATE ON
memories` triggers with no column scope. Every metadata-only write fired
the delete+reinsert pair, so each `cmd_memory_search` (via
`_retrieval_practice_boost` bumping `confidence`/`recalled_count` on every
hit) eroded the inverted index. Searches progressively lost rows; `docsize`
bled out. `q_value`, `trust_score`, `ewc_importance` and other metadata
writes did the same.

Scope both update triggers to `AFTER UPDATE OF content, category, tags,
indexed, retired_at`. Metadata churn no longer touches FTS; `indexed` and
`retired_at` stay in scope so promotion and retire still maintain the index.

- `init_schema.sql`: scoped triggers
- migration `083`: drop and recreate scoped on existing DBs, plus a one-time
  rebuild to repair indexes already corrupted in the field
- `mcp_server` startup: rescope legacy triggers on installs that never run
  migrations (pip wheels do not ship `db/migrations/`)
- `_ensure_fts_index_consistent` now returns `False` on a healthy index
  instead of `None`, matching its docstring (surfaced by the new tests)

The search-quality bench was itself degraded by this bug; the fix raises
overall `p_at_1` from 0.60 to 0.80 and `recall@10` from 0.51 to 0.85, so the
baseline is refreshed.
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.

[Bug] FTS Index Corruption on memory_search

1 participant