fix(mine): validate FTS5 at end of mine (#1537)#1548
Merged
igorls merged 1 commit intoMay 24, 2026
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces post-mine FTS5/SQLite integrity validation to detect database corruption early. It adds a new MineValidationError exception, a validation helper that runs PRAGMA quick_check, and integrates these checks into the mining processes for both projects and conversations. The CLI is updated to catch these validation errors and display a consistent recovery banner. Additionally, a new test suite is included to verify the validation logic and error handling. I have no feedback to provide as there are no review comments.
d3adba8 to
fcef78b
Compare
Wires _validate_palace_fts5_after_mine into all three mine entry points so corrupted-FTS5 palaces cannot silently exit 0 from any of them: - _mine_impl (mempalace/miner.py) — project file miner - mine_convos (mempalace/convo_miner.py) — conversation exports - mine_formats (mempalace/format_miner.py) — binary office documents via --mode extract, introduced on develop by MemPalace#1555 (3.3.6 release) between this PR's open date and its rebase cmd_mine surfaces MineValidationError as exit 1 + the same print_sqlite_integrity_abort banner cmd_repair already prints, appended with a mine-specific stderr note that hedges attribution (quick_check cannot tell pre-existing corruption from corruption this mine produced). 17 tests in tests/test_miner_fts5_validation.py cover the helper, the three call sites, dry-run / KeyboardInterrupt skip semantics, and the MineValidationError constructor invariants. Closes MemPalace#1537. Co-authored-by: Caleb Wells <15988028+calebcwells@users.noreply.github.com>
fcef78b to
0ecf0e5
Compare
arnoldwender
pushed a commit
to arnoldwender/mempalace
that referenced
this pull request
May 24, 2026
Bumps version 3.3.5 → 3.3.6 across pyproject.toml, version.py, plugin manifests (.claude-plugin/plugin.json, .claude-plugin/marketplace.json, .codex-plugin/plugin.json), README badge, and uv.lock. Flips CHANGELOG.md from ``[Unreleased]`` to ``[3.3.6] — 2026-05-24`` and backfills the major user-facing entries that landed without changelog entries during the cycle: Features: - MemPalace#1555 office-document mining via --mode extract + virtual line numbers - MemPalace#1584 surgical closet pointers with date+line locators (Tier 6a) - MemPalace#1558 + MemPalace#1560 within-wing hallways (entity co-occurrence graph) - MemPalace#1565 cross-wing tunnels auto-promoted from hallways - MemPalace#1578 Hebbian potentiation + Ebbinghaus decay on hallways/tunnels - MemPalace#1236 API-tool transcripts auto-route to wing_api - MemPalace#711 hooks.auto_save toggle for silent-mode sessions - MemPalace#1605 COCA content-word filter for entity detection - MemPalace#1557 case-insensitive entity matching at mine time - MemPalace#1483 multilingual embeddings (embeddinggemma-300m) by default Bug Fixes (selected, user-visible): - MemPalace#1540 silent data loss in three unchunked upsert sites - MemPalace#1538 paragraph chunker oversized chunks - MemPalace#1554 per-file chunk cap too low for transcripts - MemPalace#1562 Windows hook subprocess/ChromaDB deadlock - MemPalace#1529 create_tunnel corrupted hyphenated wing names - MemPalace#1424 save-hook truncated hyphenated project folders - MemPalace#1383 KG cache duplicated graphs for symlinked/cased paths - MemPalace#1466 silent symlink skip now logged - MemPalace#1441 macOS stock-bash 3.2 hook compatibility - MemPalace#1500 / MemPalace#1513 structured JSON-RPC errors on bad MCP input - MemPalace#1523 VACUUM + FTS5 rebuild after repair - MemPalace#1548 FTS5 validation at end of mine - plus MemPalace#1216, MemPalace#1408, MemPalace#1438, MemPalace#1439, MemPalace#1445, MemPalace#1452, MemPalace#1459, MemPalace#1461, MemPalace#1466, MemPalace#1470, MemPalace#1477, MemPalace#1485, MemPalace#1500, MemPalace#1513, MemPalace#1528, MemPalace#1532, MemPalace#1543, MemPalace#1546, MemPalace#1585 Performance: - MemPalace#1474 convo miner pre-fetches mined-set - MemPalace#1487 rebuild_index progress callback - MemPalace#1530 MCP cold-start diagnostics + opt-in warmup Lint passes (ruff 0.15.14); mempalace-mcp entry point alignment verified per RELEASING.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes #1537.
mempalace minecurrently printsDone.and exits 0 even when the resultingchroma.sqlite3left FTS5 in a malformed state. Automation, hooks, and CI then proceed against a palace whose wing-filtered searches will fail withError finding id. The siblingrepair --yespath already runsPRAGMA quick_checkand refuses on the same failure (closed via #1216 / #1523); this PR closes the same gap on theminepath.Change shape
mempalace/palace.py: newMineValidationError(RuntimeError)plus_validate_palace_fts5_after_mine(palace_path)helper. Helper composes two existing repair-side primitives:_close_chroma_handles(palace_path, backend=_DEFAULT_BACKEND)to release the live writer's handles (Windows mmap guard) andsqlite_integrity_errors(palace_path)to runPRAGMA quick_checkon a fresh read-only connection. RaisesMineValidationError(palace_path, errors)on any non-okrow.mempalace/miner.py: validator call after the topic-tunnels block, before theDone.banner, insideif not dry_run:. A new explicitexcept MineValidationError: raiseclause sits before the existingexcept Exception as exc:partial-progress handler. Without it the operator would see the misleading "Mine aborted by exception. files_processed: N. last_file: ." banner before the recovery banner, since the loop completed cleanly and validation ran post-loop.mempalace/convo_miner.py: symmetric call site.mine_convoshas no outertry/except(pre-existing), so the exception bubbles directly to the CLI handler.mempalace/format_miner.py: symmetric call site inmine_formats(the--mode extractentry point), placed in the post-loopelse:branch after_compute_topic_tunnels_for_wing. Closes a gap that opened on develop after this PR was rebased (see next section).mempalace/cli.py:cmd_mineadds anexcept MineValidationErrorarm that printsprint_sqlite_integrity_abort(the exact recovery bannercmd_repairalready prints), appends a mine-specific note to stderr, and exits 1. The arm wraps all three mode branches (project/convos/extract).Post-#1555 extract-mode coverage
When this PR was opened on 2026-05-18,
_mine_implwas the single mine entry point and the validator there covered every mine path. Merge #1555 (3.3.6 release, 2026-05-21) introducedmempalace/format_miner.py::mine_formatsas a third mine entry point, wired intocli.pyaselif args.mode == "extract":. Without an additional validator call,mempalace mine <dir> --mode extractwould have exited 0 on a corrupted palace exactly as the original reporter scenario describes, just from a different operator-facing command. The rebase in this branch resolves the develop conflict in_mine_impl(preserving the upstream hallways + entity-tunnels additions while keeping the validator call at the end) and extends the same_validate_palace_fts5_after_minehelper intomine_formats'selse:branch so all three modes share the same end-of-mine integrity contract.Why reuse repair-side primitives
The same
PRAGMA quick_checkfailure can be reached from two paths: a corrupted-by-repair sqlite (#1517, fixed in #1216 / #1523) and a corrupted-by-mine sqlite (this issue). Operators should see the same authoritative recovery banner regardless of which command surfaced it. Reusingprint_sqlite_integrity_abortkeeps the wording singular and the recovery steps coherent.Threaded
backend=_DEFAULT_BACKEND_close_chroma_handleswithout abackendargument falls back to a transientChromaBackend()instance whose_clientsdict is empty, so it never closes the live writer'sPersistentClient. The helper passespalace._DEFAULT_BACKENDso the cached client for the just-mined palace actually gets closed and WAL flushes before the read-only sqlite3 re-open. Matches thebackend=backendpatterncmd_repairalready uses (mempalace/repair.py:838,:851;mempalace/cli.py:972).Wording note
The mine-specific stderr note explicitly hedges attribution: "the corruption may pre-date the mine itself."
quick_checkruns once at end-of-mine, so it cannot distinguish corruption produced by this mine from pre-existing corruption surfaced by it.mempalace repair --yesrebuilds the FTS5 virtual table either way.How to test
End-to-end against the actual
mempalacebinary:The corrupted second run prints
ABORT: SQLite-layer corruption detected before repair rebuildplusquick_check output: - malformed inverted index for FTS5 table main.embedding_fulltext_search, verbatim the operator-facing message reporter cited. The--mode extractand--mode convospaths surface the same banner via the sharedcmd_minehandler.Test coverage
17 new cases in
tests/test_miner_fts5_validation.py:MineValidationErroron a page-mangled sqlite (corruption recipe mirrorstests/test_repair.py).pytest.skipif chromadb renames the shadow table in a future version.cmd_mineproject mode surfacesMineValidationErroras exit 1 + recovery banner.cmd_mineconvos mode symmetric._close_chroma_handlesruns beforesqlite_integrity_errors(Windows guard order)._mine_impl(no monkeypatch): mine, corrupt FTS5, re-mine, assert validator was the explicit raise-source via spy._mine_impldoes NOT print the "Mine aborted by exception" partial-progress banner onMineValidationError(the new explicitexcept MineValidationError: raiseclause bypasses the generic handler).MineValidationErrorconstructor rejects empty errors list and blank palace_path (defense-in-depth against future mis-construction).MineValidationError.errorsis a tuple (immutable forensic snapshot; handlers cannot mutate).chroma.sqlite3(palace dir without sqlite file).mine_convos(dry_run=True)skips the validator (mirrors project-miner guarantee).cmd_mine --mode extractsurfacesMineValidationErroras exit 1 + recovery banner (post-feat(3.3.6): virtual line numbering + format coverage via --mode extract #1555 third entry point).mine_formats(dry_run=True)skips the validator (matches the other two miners).mine_formatsonKeyboardInterruptskips the validator: the per-fileexcept Exceptiondoes not catchBaseException, so the interrupt propagates past theelse:branch where validation sits.mine_formats(no monkeypatch on the validator itself): build palace, corrupt FTS5, invokemine_formats, assert the helper raisedMineValidationErrorwith the corrupt path inerrors.Out of scope
A codebase-wide audit found additional write paths that print success and exit 0 without validating FTS5. They are deliberately not touched here: each is a separate user-visible surface, and the reporter scope is
mempalace minespecifically. A follow-up issue will track adopting the same_validate_palace_fts5_after_minehelper across these sites.CLI surface:
cmd_sweep(sweeper.sweep/sweep_directory)cmd_sync --apply(sync_palace; deletes can leave FTS5 shadow tables stale)cmd_compress(comp_col.upsertof compressed closets)dedup_palace(deletes drawers)closet_llm.regenerate_closetsdiary_ingest(drawers_col.upsert+upsert_closet_lines)migrate.migrate(col.addbatch into temp palace, then swap; no post-swap quick_check)repair.rebuild_from_sqlite(multi-collection upsert; preflight covers the legacyrebuild_indexpath but not this sub-mode)Library surface:
sources/context.py:PalaceContext.upsert_drawer(shared helper used by source adapters)MCP surface:
tool_add_drawer,tool_diary_write,tool_delete_drawer(mcp_server.py:1171). Different surface: JSON-RPC responses rather than CLI exit codes.The recovery path this PR's stderr note directs operators to (
mempalace repair --yes→rebuild_index) is itself safe: it preflights viasqlite_integrity_errorsand post-#1523 rebuilds the FTS5 virtual table.Three pre-existing structural asymmetries that this PR observes but does not address:
mine_convosdoes not wrap its body inmine_palace_lockthe waymine()does. The validator therefore runs outside the palace lock in convos mode, which means a concurrent writer (e.g. MCP server) could in principle slip in between the last upsert and the validator. The new stderr note hedges attribution rather than falsely blaming the mine.mine_formats(introduced by feat(3.3.6): virtual line numbering + format coverage via --mode extract #1555) also lacksmine_palace_lock, with the same concurrent-writer caveat asmine_convos. The validator wired in by this PR runs in the same lock-free zone. Locking parity across the three miners is a separate refactor.mine_convosandmine_formatshave no outertry/exceptwrapping theirDone.banners, so project mode prints a partial-progress summary onMineValidationErrorwhile convos and extract modes print only the recovery banner. Recovering symmetry is a separate refactor.Checklist
uv run pytest tests/ --ignore=tests/benchmarks): 2076 passed, 3 skippeduvx --from 'ruff==0.15.9' ruff check .+ruff format --check)Closes #1537.
Co-authored-by: Caleb Wells 15988028+calebcwells@users.noreply.github.com