Skip to content

Commit a66a8da

Browse files
author
STARGA Inc
committed
release: v3.10.7 — mm doctor for backend-drift diagnosis + repair
Three modes: - mm doctor (check) — reports PG vs SQLite parity, exits 1 if drifted - --migrate-recall-log — heals pre-2026-04 SQLite missing v2 columns - --rebuild-cache — copies PG-only blocks into SQLite cache JSON output, idempotent (cron-safe). Closes the manual SQL drift-fix loop that v3.10.6 left for users with old workspaces.
1 parent f246adb commit a66a8da

5 files changed

Lines changed: 215 additions & 6 deletions

File tree

ANATOMY.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
> Re-generate with: `anatomy .`
66
77
**Project:** `mind-mem`
8-
**Files:** 740 | **Est. tokens:** ~1,515,098
9-
**Generated:** 2026-05-09 02:01 UTC
8+
**Files:** 740 | **Est. tokens:** ~1,516,943
9+
**Generated:** 2026-05-09 02:34 UTC
1010

1111
## Token Budget Guide
1212

@@ -56,7 +56,7 @@
5656
| `skills/integrity-scan/` | 1 | ~376 |
5757
| `skills/memory-recall/` | 1 | ~549 |
5858
| `src/` | 1 | ~280 |
59-
| `src/mind_mem/` | 155 | ~538,817 |
59+
| `src/mind_mem/` | 155 | ~540,662 |
6060
| `src/mind_mem/api/` | 5 | ~15,751 |
6161
| `src/mind_mem/mcp/` | 3 | ~3,960 |
6262
| `src/mind_mem/mcp/infra/` | 8 | ~6,924 |
@@ -529,7 +529,7 @@
529529
- `mind_ffi.py` (~5481 tok, huge) — mind-mem FFI bridge — loads compiled MIND .so and exposes scoring functions.
530530
- `mind_filelock.py` (~1844 tok, huge) — mind-mem file locking — cross-platform advisory locks. Zero external deps.
531531
- `mind_kernels.py` (~1706 tok, huge) — # Copyright 2026 STARGA, Inc.
532-
- `mm_cli.py` (~20528 tok, huge) — # Copyright 2026 STARGA, Inc.
532+
- `mm_cli.py` (~22373 tok, huge) — # Copyright 2026 STARGA, Inc.
533533
- `model_audit.py` (~4370 tok, huge) — Model checkpoint audit — scan for remote-code hooks, unsafe pickle, tokenizer injection.
534534
- `model_gate.py` (~2549 tok, huge) — Load-gate registry for ``mm audit-model`` checkpoints.
535535
- `model_provenance.py` (~1751 tok, huge) — Provenance allowlist check for ``mm audit-model`` checkpoints.

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@
22

33
All notable changes to MIND-Mem are documented in this file.
44

5+
## v3.10.7 — `mm doctor` for backend-drift diagnosis + repair
6+
7+
Released 2026-05-08. Adds `mm doctor` so users with old workspaces
8+
or cross-backend drift can self-heal without manual SQL.
9+
10+
### Added — `mm doctor`
11+
Three modes:
12+
13+
```bash
14+
mm doctor # check-only; reports drift, exits 1 if any
15+
mm doctor --migrate-recall-log # add intent_type/stage_counts to old SQLite recall.db
16+
mm doctor --rebuild-cache # copy Postgres-only blocks into SQLite recall cache
17+
```
18+
19+
Output is JSON with: workspace, block_store_class, postgres_active_blocks,
20+
sqlite_cache_blocks, pg_only_count, sqlite_only_count, in_sync, and any
21+
actions taken.
22+
23+
### Why
24+
- **`--migrate-recall-log`** fixes the "no such column: intent_type"
25+
warning in recall logs for workspaces created before 2026-04 where
26+
the auto-migrate skipped silently.
27+
- **`--rebuild-cache`** closes the bidirectional-parity gap when
28+
blocks land directly in Postgres (e.g. via `mm propose` or
29+
hooks) without going through the markdown-file indexer that
30+
normally populates SQLite. The SQLite cache stays in sync without
31+
the user knowing about the two-tier storage layer.
32+
33+
### Notes
34+
- Drift is direction-aware: `--rebuild-cache` only copies PG → SQLite
35+
(the documented direction); SQLite-only blocks are reported but
36+
not auto-promoted, since that direction is the markdown-indexer's
37+
job.
38+
- All three modes are idempotent — safe to re-run on cron.
39+
540
## v3.10.6 — `mm install-model` hardening (multi-LLM audit findings)
641

742
Released 2026-05-08. Three external code reviewers (Mistral Large,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mind-mem"
3-
version = "3.10.6"
3+
version = "3.10.7"
44
description = "Drop-in memory for Claude Code, OpenClaw, and any MCP-compatible agent."
55
readme = "README.md"
66
license = { text = "Apache-2.0" }

src/mind_mem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
)
4444
from .storage import get_block_store
4545

46-
__version__ = "3.10.6"
46+
__version__ = "3.10.7"
4747

4848
# Best-effort import-time integrity check. Fails open unless
4949
# MIND_MEM_INTEGRITY=strict, so editable installs and source checkouts

src/mind_mem/mm_cli.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,159 @@ def _cmd_audit_pinned(args: argparse.Namespace) -> int:
15671567
return 0 if report.passed else 1
15681568

15691569

1570+
def _cmd_doctor(args: argparse.Namespace) -> int:
1571+
"""Diagnose + repair common workspace drifts.
1572+
1573+
`mm doctor` (no flag) → read-only check; reports drift but does
1574+
not modify state. Add a flag to actually repair.
1575+
1576+
--rebuild-cache: copy any Postgres-only blocks into the SQLite
1577+
recall cache so both stores carry the same id set. Safe; does
1578+
not touch Postgres.
1579+
1580+
--migrate-recall-log: add the v2-schema columns (`intent_type`,
1581+
`stage_counts`) to the SQLite `retrieval_log` table for
1582+
workspaces created before 2026-04 where the auto-migrate fired
1583+
and silently skipped. No-op on fresh DBs.
1584+
"""
1585+
import os as _os
1586+
import sqlite3 as _sqlite3
1587+
import json as _json
1588+
1589+
ws = _workspace()
1590+
report: dict[str, Any] = {"workspace": ws, "actions": []}
1591+
sq_path = _os.path.join(ws, ".mind-mem-index", "recall.db")
1592+
sq_exists = _os.path.exists(sq_path)
1593+
report["sqlite_recall_db"] = {"path": sq_path, "exists": sq_exists}
1594+
1595+
# Always: count PG vs SQLite blocks if both backends are wired.
1596+
try:
1597+
from mind_mem.storage import get_block_store
1598+
1599+
bs = get_block_store(ws)
1600+
store_class = type(bs).__name__
1601+
report["block_store_class"] = store_class
1602+
except Exception as exc:
1603+
report["block_store_error"] = str(exc)
1604+
store_class = ""
1605+
bs = None
1606+
1607+
pg_ids: set[str] = set()
1608+
if store_class == "PostgresBlockStore" and bs is not None:
1609+
try:
1610+
import psycopg
1611+
1612+
with psycopg.connect(bs._dsn) as conn, conn.cursor() as cur:
1613+
cur.execute(f"SELECT id FROM {bs._schema}.blocks WHERE active")
1614+
pg_ids = {r[0] for r in cur.fetchall()}
1615+
report["postgres_active_blocks"] = len(pg_ids)
1616+
except Exception as exc:
1617+
report["postgres_count_error"] = str(exc)
1618+
1619+
sq_ids: set[str] = set()
1620+
if sq_exists:
1621+
try:
1622+
sq = _sqlite3.connect(sq_path)
1623+
sq.row_factory = _sqlite3.Row
1624+
sq_ids = {r[0] for r in sq.execute("SELECT id FROM blocks").fetchall()}
1625+
report["sqlite_cache_blocks"] = len(sq_ids)
1626+
sq.close()
1627+
except Exception as exc:
1628+
report["sqlite_count_error"] = str(exc)
1629+
1630+
pg_only = pg_ids - sq_ids
1631+
sq_only = sq_ids - pg_ids
1632+
report["pg_only_count"] = len(pg_only)
1633+
report["sqlite_only_count"] = len(sq_only)
1634+
report["in_sync"] = (len(pg_only) == 0 and len(sq_only) == 0)
1635+
1636+
# --migrate-recall-log: schema-drift fix
1637+
if args.migrate_recall_log and sq_exists:
1638+
try:
1639+
sq = _sqlite3.connect(sq_path)
1640+
cols = {r[1] for r in sq.execute("PRAGMA table_info(retrieval_log)").fetchall()}
1641+
added: list[str] = []
1642+
if "intent_type" not in cols:
1643+
sq.execute("ALTER TABLE retrieval_log ADD COLUMN intent_type TEXT DEFAULT ''")
1644+
added.append("intent_type")
1645+
if "stage_counts" not in cols:
1646+
sq.execute("ALTER TABLE retrieval_log ADD COLUMN stage_counts TEXT DEFAULT '{}'")
1647+
added.append("stage_counts")
1648+
try:
1649+
sq.execute("CREATE INDEX IF NOT EXISTS idx_rlog_intent ON retrieval_log(intent_type)")
1650+
except _sqlite3.OperationalError:
1651+
pass
1652+
sq.commit()
1653+
sq.close()
1654+
report["actions"].append({"migrate_recall_log": {"added_columns": added}})
1655+
except Exception as exc:
1656+
report["actions"].append({"migrate_recall_log": {"error": str(exc)}})
1657+
1658+
# --rebuild-cache: copy PG-only blocks into SQLite
1659+
if args.rebuild_cache and pg_only and bs is not None and sq_exists:
1660+
try:
1661+
import psycopg
1662+
1663+
written = 0
1664+
errors = 0
1665+
sq = _sqlite3.connect(sq_path)
1666+
with psycopg.connect(bs._dsn) as conn, conn.cursor() as cur:
1667+
cur.execute(
1668+
f"SELECT id, file_path, content, metadata, created_at FROM {bs._schema}.blocks "
1669+
f"WHERE active AND id = ANY(%s)",
1670+
(list(pg_only),),
1671+
)
1672+
for bid, fpath, content, metadata, created_at in cur.fetchall():
1673+
md = metadata or {}
1674+
if isinstance(md, str):
1675+
try:
1676+
md = _json.loads(md)
1677+
except _json.JSONDecodeError:
1678+
md = {}
1679+
btype_raw = md.get("type") or md.get("Type") or (bid.split("-")[0] if "-" in bid else "block")
1680+
btype = {"D": "decision", "I": "incident", "C": "code", "A": "adr", "P": "perf"}.get(
1681+
btype_raw[:1].upper(), btype_raw.lower() or "block"
1682+
)
1683+
json_blob = _json.dumps(
1684+
{
1685+
"id": bid,
1686+
"content": content or "",
1687+
"metadata": md,
1688+
"created_at": created_at.isoformat() if created_at else "",
1689+
}
1690+
)
1691+
try:
1692+
sq.execute(
1693+
"INSERT OR REPLACE INTO blocks "
1694+
"(id, type, file, line, status, date, speaker, tags, dia_id, parent_id, json_blob) "
1695+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1696+
(
1697+
bid,
1698+
btype,
1699+
fpath or "",
1700+
0,
1701+
md.get("Status", md.get("status", "active")) or "active",
1702+
created_at.strftime("%Y-%m-%d") if created_at else "",
1703+
md.get("Speaker", "") or "",
1704+
_json.dumps(md.get("Tags", md.get("tags", []))) or "",
1705+
md.get("DiaId", "") or "",
1706+
md.get("ParentId", "") or "",
1707+
json_blob,
1708+
),
1709+
)
1710+
written += 1
1711+
except _sqlite3.OperationalError:
1712+
errors += 1
1713+
sq.commit()
1714+
sq.close()
1715+
report["actions"].append({"rebuild_cache": {"written": written, "errors": errors}})
1716+
except Exception as exc:
1717+
report["actions"].append({"rebuild_cache": {"error": str(exc)}})
1718+
1719+
print(json.dumps(report, indent=2, default=str))
1720+
return 0 if report.get("in_sync", False) or args.rebuild_cache or args.migrate_recall_log else 1
1721+
1722+
15701723
def _cmd_verify_model(args: argparse.Namespace) -> int:
15711724
from mind_mem.model_signing import ED25519_PUBLIC_KEY_BYTES, verify_model
15721725

@@ -1762,6 +1915,27 @@ def build_parser() -> argparse.ArgumentParser:
17621915
p_install_model.add_argument("--dry-run", action="store_true")
17631916
p_install_model.set_defaults(func=_cmd_install_model)
17641917

1918+
# doctor — diagnose + repair common workspace drifts
1919+
p_doctor = sub.add_parser(
1920+
"doctor",
1921+
help=(
1922+
"Diagnose workspace state (block-store parity, recall-log "
1923+
"schema drift). Add --rebuild-cache or --migrate-recall-log "
1924+
"to actually repair."
1925+
),
1926+
)
1927+
p_doctor.add_argument(
1928+
"--rebuild-cache",
1929+
action="store_true",
1930+
help="Copy Postgres-only blocks into the SQLite recall cache.",
1931+
)
1932+
p_doctor.add_argument(
1933+
"--migrate-recall-log",
1934+
action="store_true",
1935+
help="Add v2-schema columns (intent_type, stage_counts) to the SQLite retrieval_log.",
1936+
)
1937+
p_doctor.set_defaults(func=_cmd_doctor)
1938+
17651939
# vault namespace
17661940
p_vault = sub.add_parser("vault", help="Vault sync subcommands.")
17671941
vsub = p_vault.add_subparsers(dest="vault_cmd", required=True)

0 commit comments

Comments
 (0)