Summary
icm serve --read-only opens the SQLite store with mode=ro&immutable=1 (crates/icm-store/src/store.rs:72, open_readonly_immutable). immutable=1 tells SQLite the file will never change for the lifetime of the connection. But the documented deployment model for --read-only serve is exactly a DB that does keep changing underneath it: hooks and CLI invocations write to the same memories.db while a long-lived read-only MCP server holds its connection.
The result: after the first external write, the server's connection is permanently stale —
- recalls silently return a stale snapshot (memories stored after server start are invisible), and
- once the file has grown/restructured enough, every query on that connection — reads included (
icm_memory_recall, icm_memory_health) — fails with SQLITE_CORRUPT: database error: database disk image is malformed.
The database itself is healthy the whole time (PRAGMA integrity_check, PRAGMA quick_check, and FTS5 integrity-check all return ok; fresh connections work fine). The error message sends operators chasing nonexistent corruption.
- Version: icm 0.10.57 (release binary;
main has the same open path)
- OS: macOS (Darwin 27.0)
- Setup:
icm --db <path> --read-only serve --no-embeddings as an MCP server for Claude Code/Codex, with Claude Code hooks writing to the same DB via the CLI.
Repro
Deterministic, single machine:
# 1. seed
icm --db /tmp/t.sqlite --no-embeddings store -t t1 -c "seed memory alpha" -i low
# 2. start a read-only MCP server and keep it running
icm --db /tmp/t.sqlite --read-only serve --no-embeddings
# (drive it over stdio: initialize, then tools/call icm_memory_recall query="alpha" → returns the seed. OK)
# 3. from another shell, write to the same DB
icm --db /tmp/t.sqlite --no-embeddings store -t t1 -c "second memory alpha beta" -i low
# 4. recall again on the SAME server process
# → still returns only "seed memory alpha"; the new memory is invisible (stale immutable snapshot)
# 5. keep writing (e.g. ~40 stores of a few KB, or anything that grows/restructures the file)
# → queries on the server connection begin failing:
# "database error: database disk image is malformed"
Observed in production as: MCP icm_memory_store/icm_memory_health/recall all returning database disk image is malformed from long-lived Claude Code sessions, while sqlite3 <db> "PRAGMA integrity_check;" says ok and fresh CLI invocations work.
Root cause
open_readonly_immutable's doc comment explains immutable=1 was chosen so a read-only open works even on a chmod -w parent directory (SQLite otherwise wants to create/refresh -shm/-wal). That's the right recipe for a static snapshot, but it also disables all locking and change detection — SQLite caches the page count/schema and serves stale pages, and once the file layout shifts, cached assumptions contradict the file and every statement returns SQLITE_CORRUPT.
Proposed fix
Open read-only connections with plain mode=ro by default — WAL-aware, lock-respecting, sees committed writes; busy_timeout=30000 is already set on this path. Fall back to immutable=1 only when the plain read-only open fails (unwritable directory, true read-only sandbox), or gate it behind an explicit flag. I.e.:
// try: file:{path}?mode=ro (normal case: live DB, writable dir)
// fallback: file:{path}?mode=ro&immutable=1 (read-only sandboxes)
A local build of the icm-v0.10.57 tag with immutable=1 dropped resolves both symptoms (fresh reads after external writes; no corrupt-image errors; store on the read-only server now fails with the honest attempt to write a readonly database).
As a smaller companion fix: surfacing SQLITE_CORRUPT from a read-only connection as "connection stale — reopen" (or auto-reopening) would prevent the misleading corruption diagnosis even in genuinely immutable setups.
Related
Summary
icm serve --read-onlyopens the SQLite store withmode=ro&immutable=1(crates/icm-store/src/store.rs:72,open_readonly_immutable).immutable=1tells SQLite the file will never change for the lifetime of the connection. But the documented deployment model for--read-only serveis exactly a DB that does keep changing underneath it: hooks and CLI invocations write to the samememories.dbwhile a long-lived read-only MCP server holds its connection.The result: after the first external write, the server's connection is permanently stale —
icm_memory_recall,icm_memory_health) — fails withSQLITE_CORRUPT:database error: database disk image is malformed.The database itself is healthy the whole time (
PRAGMA integrity_check,PRAGMA quick_check, and FTS5integrity-checkall returnok; fresh connections work fine). The error message sends operators chasing nonexistent corruption.mainhas the same open path)icm --db <path> --read-only serve --no-embeddingsas an MCP server for Claude Code/Codex, with Claude Code hooks writing to the same DB via the CLI.Repro
Deterministic, single machine:
Observed in production as: MCP
icm_memory_store/icm_memory_health/recall all returningdatabase disk image is malformedfrom long-lived Claude Code sessions, whilesqlite3 <db> "PRAGMA integrity_check;"saysokand fresh CLI invocations work.Root cause
open_readonly_immutable's doc comment explainsimmutable=1was chosen so a read-only open works even on achmod -wparent directory (SQLite otherwise wants to create/refresh-shm/-wal). That's the right recipe for a static snapshot, but it also disables all locking and change detection — SQLite caches the page count/schema and serves stale pages, and once the file layout shifts, cached assumptions contradict the file and every statement returnsSQLITE_CORRUPT.Proposed fix
Open read-only connections with plain
mode=roby default — WAL-aware, lock-respecting, sees committed writes;busy_timeout=30000is already set on this path. Fall back toimmutable=1only when the plain read-only open fails (unwritable directory, true read-only sandbox), or gate it behind an explicit flag. I.e.:A local build of the
icm-v0.10.57tag withimmutable=1dropped resolves both symptoms (fresh reads after external writes; no corrupt-image errors;storeon the read-only server now fails with the honestattempt to write a readonly database).As a smaller companion fix: surfacing
SQLITE_CORRUPTfrom a read-only connection as "connection stale — reopen" (or auto-reopening) would prevent the misleading corruption diagnosis even in genuinely immutable setups.Related
database disk image is malformedstring, and SQLite store corrupts under concurrent writes — no WAL, no backup, no repair path #313's proposedicm doctorintegrity check would help disambiguate the two.