Skip to content

[Bug] skills upsert (INSERT OR REPLACE) skips FTS cleanup trigger under recursive_triggers=0, orphaning skills_fts rows #2363

Description

@chiefmojo

Pre-submission checklist

  • I have searched existing issues and this hasn't been mentioned before
  • I have read the project documentation and confirmed this issue doesn't already exist
  • This issue is specific to MemOS and not a general software issue

Bug Description

Summary

core/storage/repos/skills.ts upserts skills via SQLite INSERT OR REPLACE (onConflict: "replace"). When PRAGMA recursive_triggers is 0 (a common non-default SQLite setting some hosts run), the implicit delete performed internally by OR REPLACE does not fire delete triggers — including the skills_fts cleanup trigger defined in the FTS migration. Every upsert-on-conflict therefore leaves the old skills_fts row behind (orphaned) while inserting a new one, so skills_fts accumulates duplicate/orphaned rows over time relative to the base skills table.

Root cause

// core/storage/repos/skills.ts
const upsert = db.prepare(
  buildInsert({ table: "skills", columns: COLUMNS, onConflict: "replace" }),
);

SQLite's own documentation is explicit about this: "The REPLACE conflict resolution algorithm deletes pre-existing rows that are causing the constraint violation... this algorithm does not invoke the row-delete ON DELETE trigger unless PRAGMA recursive_triggers is set to true." (This is unrelated to ON DELETE foreign-key triggers — it also applies to plain AFTER DELETE triggers used for FTS sync, which is the case here.)

This is host-config-dependent, not universal — hosts running with recursive_triggers at its default (or explicitly ON) won't see it. But recursive_triggers=0 is common enough (SQLCipher and several ORMs set it) that this is a real footgun for anyone deploying with that setting.

Impact

  • skills_fts grows stale/duplicate rows over the lifetime of an install with recursive_triggers=0 — never self-heals.
  • searchByText-style retrieval ranks and LIMITs over the polluted FTS table before joining back to the base table, so this degrades retrieval quality (wrong ranking, phantom/stale hits), not just table bloat.

Suggested Fix

Either:

  1. Don't rely on implicit-trigger-driven REPLACE for tables with FTS sync — do an explicit DELETE + INSERT in the upsert path (guaranteed to fire triggers regardless of recursive_triggers), or
  2. Have MemOS set PRAGMA recursive_triggers = ON itself at connection open, so FTS-trigger correctness doesn't depend on the host's SQLite defaults.

Happy to open a PR for whichever direction the maintainers prefer.

Activity

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

Metadata

Metadata

Labels

ai:pr-readyAI-created PR is ready for review | AI 生成的 PR 已等待评审area:databasegraph_db + vector_db | 图数据库与向量数据库status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions