Skip to content

fix: disable SQLite mmap on all connections to prevent 2^30 FTS truncation - #69759

Closed
mr-september wants to merge 2 commits into
NousResearch:mainfrom
mr-september:pr/sqlite-mmap-pin
Closed

fix: disable SQLite mmap on all connections to prevent 2^30 FTS truncation#69759
mr-september wants to merge 2 commits into
NousResearch:mainfrom
mr-september:pr/sqlite-mmap-pin

Conversation

@mr-september

@mr-september mr-september commented Jul 23, 2026

Copy link
Copy Markdown

What does this PR do?

Pins PRAGMA mmap_size=0 on every SessionDB connection (read-only and read-write paths) so SQLite never memory-maps state.db.

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_state.py: add pin_mmap_off(conn) helper that executes PRAGMA mmap_size=0, reads the value back, and logs a warning if it is not 0/NULL (some SQLite builds / read-only connections report no mmap support — treated as the safe unmapped state, never fatal).
  • Call pin_mmap_off(self._conn) on both SessionDB connection paths: the read-only ?mode=ro attach and the read-write path (before apply_wal_with_fallback).
  • tests/test_hermes_state.py: add TestMmapPin — asserts the helper sets mmap_size=0 on a bare connection, that a freshly opened SessionDB connection reports 0, and that the helper is non-fatal on a read-only connection.

How to Test

  1. python -m pytest tests/test_hermes_state.py::TestMmapPin -q → 3 passed.
  2. python -m pytest tests/test_hermes_state.py -q -k "MmapPin or corruption or fts or malformed or reindex or cjk" -q → all upstream FTS/corruption probes still pass (no regression).
  3. Manual: open any state.db; PRAGMA mmap_size returns 0 on every connection.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

pytest tests/test_hermes_state.py::TestMmapPin → 3 passed.

…tion

Pin mmap_size=0 on every SessionDB connection (read-only and read-write
paths). mmap is OFF by default, but a runtime PRAGMA issued against
state.db in a past session enabled it; combined with a long unattended FTS
optimize on the multi-GB monolith this caused the 2026-07-03 DB
truncation/corruption. Pinning makes the safe state explicit and
self-healing even if a future code path raises mmap_size. mmap is never
needed here (search is sub-5ms at 4.2GB); disabling removes the only
code-path-independent way the 2^30 truncation hazard can recur. Read-only/
attached connections may reject the PRAGMA; wrapped in try/except (non-fatal).
@mr-september mr-september changed the title ix: disable SQLite mmap on all connections to prevent 2^30 FTS truncation fix: disable SQLite mmap on all connections to prevent 2^30 FTS truncation Jul 23, 2026
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-repro Bug needs reproduction steps labels Jul 23, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused defensive change. Current main has moved materially since this PR, so the policy needs a current-main repro and the implementation needs to cover the new connection architecture.

Problems

  • The reported unattended FTS mechanism is no longer current: routine maintenance uses bounded merges at hermes_state.py:1766-1780 (df841d342c), and v23 storage optimization is opt-in at hermes_state.py:1984-1990 (9acc4b47f5). The current tree has no mmap_size pragma/configuration to establish a current runtime enablement path.
  • Current SessionDB creates a separate per-thread WAL reader at hermes_state.py:2030-2036; this PR predates it. The changed test only checks db._conn at tests/test_hermes_state.py:7176-7181, so it would not prove the claimed all-connection guarantee after salvage.

Suggested changes

  • Provide a current-main reproduction for the mmap hazard, then wire and test the policy for the constructor's read-only/writer paths and _get_read_conn().

Automated hermes-sweeper review.

# connection, regardless of any prior runtime PRAGMA.
row = db._conn.execute("PRAGMA mmap_size").fetchone()
cur = row[0] if row else None
assert cur == 0, f"SessionDB connection mmap_size={cur}, expected 0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only verifies the writer db._conn. Current main also opens a per-thread WAL mode=ro connection in SessionDB._get_read_conn() (hermes_state.py:2030-2036); when salvaging, please force that path and assert its mmap_size, plus the SessionDB(read_only=True) constructor path.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
@mr-september

Copy link
Copy Markdown
Author

Closing as superseded — upstream's own changes have addressed the concerns since this PR was filed:

PR #71755 (perf(session): set SQLite PRAGMA for large state.db performance) added config-gated database.mmap_size via apply_database_pragmas(), which covers all three connection types (writer, read-only constructor, and the per-thread WAL reader via _get_read_conn()) — addressing the coverage gap @teknium1 pointed out, and doing so in a more flexible, user-configurable way.

The bounded incremental FTS merges (optimize_fts_storage / _merge_fts_incrementally) replaced the full-table optimize cadence that was the original trigger for the corruption, substantially reducing the likelihood of the hazard on current main.

Given these changes, the hard mmap pin proposed here would now conflict with the config-gated approach. Closing in favor of upstream's mechanism.

The remaining hazard surface (large configured mmap + manually-triggered optimize_fts()) is narrow enough that a defensive cap is not warranted.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint needs-repro Bug needs reproduction steps P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants