Repository navigation
Conversation
…en sync The four fts5 indexes in this repo were standalone tables kept in sync by hand from Rust, across 10 write sites in 4 files. Every base-table write path was a chance to desync -- not hypothetically: #334/#337 was exactly that bug, a soft-delete that forgot its store_docs_fts row and left stale documents matchable. Any new purge path (#339) would have inherited the same rule. Each index is now external-content with AFTER INSERT/DELETE/UPDATE triggers, following src/memory/schema.rs. The FTS write is a property of the base table, so a call site can no longer forget it, and the index no longer stores its own copy of the indexed text. The store_docs_fts triggers are guarded on deleted_at rather than being straight mirrors: soft-deleted documents stay out of the index (what the manual code did), and on an external-content table a 'delete' naming an unindexed row corrupts it -- which an unguarded AFTER UPDATE would do on every resurrecting upsert. AFTER UPDATE is scoped to the indexed columns so the optional-field UPDATEs in doc_upsert_with_opts and the per-load bandit writes in skill-registry don't re-tokenize whole documents. The two registries have no migration framework, so open_db drops a legacy standalone index and refills from the base table in the same open -- waiting for the next rebuild would leave search empty in between. Adds Store::doc_fts_rebuild as the repair path: store_documents has a TEXT primary key, so VACUUM (which #339 proposes) may renumber the implicit rowids this index and doc_search's JOIN are both keyed on. Also fixes three assertions this change would otherwise have defanged: `SELECT count(*)` on an external-content fts5 table is answered from the content table, so it counts base-table rows whether or not they are indexed. Index state has to be asserted through MATCH.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe change converts document, gateway, and skill FTS5 indexes to external-content tables synchronized by SQLite triggers. Manual FTS writes are removed, legacy indexes are migrated on open, and rebuild and integration tests validate live-row searchability and soft-delete behavior. ChangesExternal-content FTS migration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Store
participant store_documents
participant store_docs_fts
Store->>store_documents: Insert or update indexed document
store_documents->>store_docs_fts: Trigger updates external-content index
Store->>store_documents: Soft-delete document
store_documents->>store_docs_fts: Trigger removes searchable entry
Store->>store_docs_fts: Execute rebuild SQL when requested
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
crates/agentflare-store/src/migrations.rs (1)
119-123: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConsider wrapping the rebuild in a transaction.
execute_batchautocommits each statement, so a failure after'delete-all'leaves the index empty until someone reruns the rebuild.BEGIN; ... COMMIT;makes it all-or-nothing. (Notedoc_fts_rebuildis also called on a connection that may already be in a transaction — if so, keep as-is.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/agentflare-store/src/migrations.rs` around lines 119 - 123, Update FTS_REBUILD_SQL to wrap the delete-all and repopulation statements in BEGIN/COMMIT so rebuilding is atomic. Ensure doc_fts_rebuild preserves the existing behavior when called on a connection already inside a transaction, avoiding nested transaction errors.crates/agentflare-store/src/documents.rs (1)
813-837: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAssertion doesn't actually test what the comment claims.
fts_hits == 1holds whether or not the nine optional-column UPDATEs fire the trigger — an unscopedAFTER UPDATEwould still leave exactly one entry. To pin theUPDATE OF content, deleted_atscoping, assert on the trigger definition (e.g. thatsqlite_master.sqlforstore_docs_fts_aucontainsUPDATE OF content, deleted_at), or do a seconddoc_upsert_with_optsthat changes only optional fields and confirm the index is untouched.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/agentflare-store/src/documents.rs` around lines 813 - 837, The test optional_field_updates_do_not_disturb_the_index does not verify trigger scoping because a single FTS hit passes even with an unscoped trigger. Strengthen it by inspecting sqlite_master for the store_docs_fts_au trigger and asserting its SQL contains UPDATE OF content, deleted_at, or perform a second optional-only doc_upsert_with_opts and verify the index remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/gateway-registry/src/db.rs`:
- Around line 38-62: Update apply_schema so the legacy tools_fts drop, SCHEMA
recreation, and backfill INSERT execute within one transaction, committing only
after all steps succeed and rolling back on failure. Preserve the existing
legacy detection and avoid skipping recovery when the migration cannot complete.
---
Nitpick comments:
In `@crates/agentflare-store/src/documents.rs`:
- Around line 813-837: The test optional_field_updates_do_not_disturb_the_index
does not verify trigger scoping because a single FTS hit passes even with an
unscoped trigger. Strengthen it by inspecting sqlite_master for the
store_docs_fts_au trigger and asserting its SQL contains UPDATE OF content,
deleted_at, or perform a second optional-only doc_upsert_with_opts and verify
the index remains unchanged.
In `@crates/agentflare-store/src/migrations.rs`:
- Around line 119-123: Update FTS_REBUILD_SQL to wrap the delete-all and
repopulation statements in BEGIN/COMMIT so rebuilding is atomic. Ensure
doc_fts_rebuild preserves the existing behavior when called on a connection
already inside a transaction, avoiding nested transaction errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 8387028f-cab9-4f77-b188-4ad47d8175a5
📒 Files selected for processing (6)
crates/agentflare-store/src/documents.rscrates/agentflare-store/src/migrations.rscrates/flare-docs/src/store.rscrates/flare-docs/tests/real_db_migration.rscrates/gateway-registry/src/db.rscrates/skill-registry/src/db.rs
CodeRabbit's finding on #347: the drop/recreate/backfill in both registries was three separate statements, and the halfway state is not self-correcting. With the index dropped but not yet refilled, the next open finds no such table, reads `legacy` as 0, and recreates an empty index over a populated base table -- search silently missing everything until something forces a full rebuild. One transaction each; SQLite makes DDL transactional. Same treatment for doc_fts_rebuild's clear-then-refill. Also splits optional_field_updates_do_not_disturb_the_index: its assertion held whether or not the trigger was scoped, since an unscoped AFTER UPDATE produces an identical index, just nine times over. Scoping is a property of the DDL, so it is now asserted on the trigger definition. And runs cargo fmt, which the first commit missed.
#573) Same architectural gap as skill-registry (#519/PR #572), which gateway-registry's own doc comments say it mirrors: hand-rolled apply_schema() with no user_version tracking. Audited the git history (#104 -> #158 -> #347) -- the tools table's columns have never changed since creation, so there's no live "no such column" bug today, but the next column addition would hit the identical class of bug. Migrates to agentflare-db-kit's open_file/open_memory with a real migration list: 0001_initial replays the original (#104) narrow schema, 0002_fts_triggers unconditionally drops and recreates tools_fts as the external-content shape with sync triggers plus a backfill. No ALTER TABLE/migration hook needed here (unlike #519) since tools's columns are stable -- DROP ... IF EXISTS before a fresh CREATE is correct regardless of which pre-migration shape existed. Added GatewayError::DbInit for db_kit::open::Error and its error_kind match arm. All 60 gateway-registry unit tests pass, including the existing legacy-standalone-FTS conversion test; clippy (with the CI gate flags) and fmt are clean; the full agentflare binary compiles. Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/520-fix-gateway-registry-adopt-agentflare-db Agentflare-Item: 520
…g-column schema drift apply_schema() hand-rolled CREATE TABLE IF NOT EXISTS DDL, which is a no-op against an existing table -- any skills.db created before #302 added body/neg_text/last_used_at/bandit_alpha/bandit_beta stayed permanently stuck without them, while the FTS5 triggers added later (#347) reference old.body/new.body etc. and throw "no such column: old.body" the first time a DELETE or qualifying UPDATE fires (e.g. rebuild()'s DELETE FROM skills). Reproduced live via skill_detect against a real skills.db from before #302. Migrate to agentflare-db-kit's open_file/open_memory with a real migration list, matching the pattern already used by agentflare-backend/agentflare-store/agentflare-artifacts/flare-docs/ flare-workflow/agentflare-jobs: 0001_initial replays the original (#92) narrow schema, 0002_ranking_and_fts adds the ranking columns via a migration hook (ALTER TABLE ADD COLUMN isn't idempotent, so it's guarded by a PRAGMA table_info check first) plus the external-content FTS5 table and sync triggers, drop-and-recreated unconditionally so it's correct regardless of which pre-migration shape the database was in. Verified against a copy of the actual broken skills.db from this machine in addition to the new unit test that reproduces the bug synthetically. Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/519-fix-skill-registry-adopt-agentflare-db-k Agentflare-Item: 519
…g-column schema drift (#572) * fix(skill-registry): adopt agentflare-db-kit migrations to fix missing-column schema drift apply_schema() hand-rolled CREATE TABLE IF NOT EXISTS DDL, which is a no-op against an existing table -- any skills.db created before #302 added body/neg_text/last_used_at/bandit_alpha/bandit_beta stayed permanently stuck without them, while the FTS5 triggers added later (#347) reference old.body/new.body etc. and throw "no such column: old.body" the first time a DELETE or qualifying UPDATE fires (e.g. rebuild()'s DELETE FROM skills). Reproduced live via skill_detect against a real skills.db from before #302. Migrate to agentflare-db-kit's open_file/open_memory with a real migration list, matching the pattern already used by agentflare-backend/agentflare-store/agentflare-artifacts/flare-docs/ flare-workflow/agentflare-jobs: 0001_initial replays the original (#92) narrow schema, 0002_ranking_and_fts adds the ranking columns via a migration hook (ALTER TABLE ADD COLUMN isn't idempotent, so it's guarded by a PRAGMA table_info check first) plus the external-content FTS5 table and sync triggers, drop-and-recreated unconditionally so it's correct regardless of which pre-migration shape the database was in. Verified against a copy of the actual broken skills.db from this machine in addition to the new unit test that reproduces the bug synthetically. Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/519-fix-skill-registry-adopt-agentflare-db-k Agentflare-Item: 519 * fix(skill-registry): open_or_repair must not delete the db on migration/SchemaAhead errors CodeRabbit review on PR #572: the Err(_) arm deleted db_path for every open_db failure, including db_kit::open::Error::Migration (a real bug in a migration -- deleting the file would silently destroy skill_impressions/ranking state the filesystem can't reconstruct, hiding the bug instead of surfacing it) and Error::SchemaAhead (means a newer build already migrated this file; its own error message says not to touch it by hand, let alone delete it). Now only deletes-and-retries on Error::Sqlite -- a genuine open/read failure (corruption, not a valid SQLite file). Migration/SchemaAhead propagate as real errors. Added regression tests for both paths, plus documented FTS_AND_TRIGGERS as frozen migration-0002 history per the review's nitpick. Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/519-fix-skill-registry-adopt-agentflare-db-k Agentflare-Item: 519 * fix(skill-registry): don't treat a failed integrity_check query as corruption integrity_check() previously folded a real corruption verdict from SQLite and a failure to even run the check (locked db, permission denied, I/O error) into the same Some(String), and open_or_repair() deleted the file on either. A transient failure to run the check is not evidence of corruption -- propagate it instead of destroying the database. Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/519-fix-skill-registry-adopt-agentflare-db-k Agentflare-Item: 519 * chore: regenerate Cargo.lock Agentflare-Agent: claude-code_2-1-237_agent Agentflare-Branch: task/519-fix-skill-registry-adopt-agentflare-db-k Agentflare-Item: 519
Proposal file isn't in the tree — reconstructing the spec from the item, PR #884, and code evidence. Context gathered. Writing the spec now. Wrote `docs/superpowers/plans/2026-10-06-cross-instance-pr-identity-and-claims.md`. Summary: design spec for item #347 (cross-instance PR identity and claims). Documents the observed bug (hand-opened PRs #868/#870/#872 adopted by the race winner `flared:c997d745ae66`; sequence-only `item_owns_branch` in `src/worktree/mod.rs`; write-only `beacon:` labels), then specifies P1–P6: origin stamp at PR creation, local-evidence branch ownership, tighter discovery marker matching, ownership-gated supervisor actions, readable beacon, claim-protocol hardening. 4-phase rollout with phase 1 mapped to already-merged PR #884; includes the P4 supervisor call-site audit checklist (self-repair, review-repair, branch update, mark-ready, merge/promotion, stray-heal, label writes), per-phase test plan, and risks. Phases 2–4 explicitly marked not implemented. No application code touched, no PR opened. Agentflare-Branch: task/347-cross-instance-pr-identity-and-claims-st Agentflare-Item: 347
… evidence of the branch (#347 phase 1) (#884) * fix(discovery): attach a PR to an item by branch name only with local evidence of the branch (#347 phase 1) Agentflare-Agent: claude-code Agentflare-Branch: task/347-cross-instance-pr-identity-and-claims-st Agentflare-Item: 347 Agentflare-Session: 0b6a7478-1b0c-4629-9b30-7cba0dd0ea41 * Writing your design doc — first mapping the proposal to the codebase. Proposal file isn't in the tree — reconstructing the spec from the item, PR #884, and code evidence. Context gathered. Writing the spec now. Wrote `docs/superpowers/plans/2026-10-06-cross-instance-pr-identity-and-claims.md`. Summary: design spec for item #347 (cross-instance PR identity and claims). Documents the observed bug (hand-opened PRs #868/#870/#872 adopted by the race winner `flared:c997d745ae66`; sequence-only `item_owns_branch` in `src/worktree/mod.rs`; write-only `beacon:` labels), then specifies P1–P6: origin stamp at PR creation, local-evidence branch ownership, tighter discovery marker matching, ownership-gated supervisor actions, readable beacon, claim-protocol hardening. 4-phase rollout with phase 1 mapped to already-merged PR #884; includes the P4 supervisor call-site audit checklist (self-repair, review-repair, branch update, mark-ready, merge/promotion, stray-heal, label writes), per-phase test plan, and risks. Phases 2–4 explicitly marked not implemented. No application code touched, no PR opened. Agentflare-Branch: task/347-cross-instance-pr-identity-and-claims-st Agentflare-Item: 347
Closes #341.
What
Converts all four fts5 indexes from standalone tables kept in sync by hand to
external-content tables driven by triggers, following the
observations_ftspattern insrc/memory/schema.rs.store_docs_ftsagentflare-storestore_docs_ftsflare-docsskills_ftsskill-registrytools_ftsgateway-registryWhy the
store_docs_ftstriggers are guarded rather than straight mirrorsSoft-deleted documents stay out of the index — that is what the manual code
did, and it is what stops #339's future purge from being the only thing
keeping the index from growing without bound.
That constraint is load-bearing, not cosmetic. On an external-content table a
'delete'command naming a row that is not in the index corrupts it. Aresurrecting upsert (
doc_upserton a soft-deleted path) is anUPDATEwhoseoldrow was already unindexed, so an unguardedAFTER UPDATEwould issueexactly that corrupting delete on every resurrection. Hence
WHERE old.deleted_at IS NULL/WHERE new.deleted_at IS NULLon each arm.resurrecting_a_soft_deleted_document_indexes_it_exactly_onceasserts thisvia fts5's own
'integrity-check', which is what actually fails on theunguarded version.
AFTER UPDATEis scoped to the indexed columns (OF content, deleted_at;OF name, description, body, tags, neg_text).doc_upsert_with_optsfollowsits content write with up to nine single-column UPDATEs, and skill-registry
writes
last_used_at+ bandit counters on every skill load — a bareAFTER UPDATEwould re-tokenize whole documents for each.Upgrade path
agentflare-storehasrusqlite_migration, so it is a newM::up.open_dbdetects a legacy standalone index(
sqlite_master.sql NOT LIKE '%content=%'), drops it, and refills from thebase table in the same open — waiting for the next
rebuildwould leaveskill_search/ gateway tool search empty in between.Repair path
Adds
Store::doc_fts_rebuild.store_documentshas a TEXT primary key, soits rowids are implicit and
VACUUMmay renumber them — which would desyncthis index and
doc_search's rowid JOIN (that coupling predates this PR).Anything that VACUUMs, including the eviction work in #339, must call it
afterwards. It repopulates explicitly rather than using fts5
'rebuild',which would index the soft-deleted rows the triggers deliberately keep out.
A trap worth flagging for reviewers
SELECT count(*)on an external-content fts5 table is answered from thecontent table, so it reports every base-table row whether or not it is
indexed. Three pre-existing assertions named "fts stays in sync" were
measuring nothing once converted; they now go through
MATCH, which isindex-driven, and assert the negative case too (a replaced/deleted row must
stop matching). This is also why the first draft of the real-database test
"failed" at 10858 vs 10857.
Verification
cargo test --workspace— green. One unrelated flake,flare-git-core worktree::run_output_timeout_kills_the_child_not_just_abandons_it,which passes in isolation on this branch and on clean master and passed on
a prior full run; it is a process-kill timing test under parallel load.
cargo clippy --workspace --all-targets --all-features— no new findings(the 21 pre-existing local-toolchain findings from feat: add supervisor auto-dispatch loop (item #408) #376 are unchanged).
~/.agentflare/flare-docs.db(
crates/flare-docs/tests/real_db_migration.rs, skipped when no such fileexists): applies cleanly, passes fts5
'integrity-check', every sampledlive document stays matchable and the soft-deleted one does not.
Not included
The issue also suggests surfacing
PRAGMA integrity_checkand an FTS rebuildescape hatch through
agentflare doctor.doctortoday is entirely aboutagent config wiring (hosts, rules) with no database surface, so that is a
separate change; this PR ships the repair primitive it would call.
Summary by CodeRabbit