From 07583bc2e8508f0aecc459443798b925e8f0ffec Mon Sep 17 00:00:00 2001 From: R4vager Date: Wed, 20 May 2026 01:30:57 -0400 Subject: [PATCH 1/2] Mammillary Bodies + Papez Circuit Phase 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Papez circuit hub. Damage produces Korsakoff anterograde amnesia — mammillary-body / ATN damage is the canonical lesion, not MD-thalamus. brainctl now logs Papez transit (hippocampus → MB → ATN → cingulate → hippocampus) so memories that have made it through the full loop can be distinguished from hippocampus-only ones in Phase 3. - Migration 081: 2 tables (state + transit log). - 4 MCP tools: status, log_transit, memory_history, reset_24h. - memory_history.consolidated = (full_loop_count >= 1). - 6 tests. Phase 2 auto-logs on consolidation_run. Phase 3 boosts retrieval confidence for Papez-completed memories. Co-Authored-By: Claude Opus 4.7 (1M context) --- db/migrations/081_mammillary.sql | 65 +++++++ src/agentmemory/mcp_tools_mammillary.py | 222 ++++++++++++++++++++++++ tests/test_mcp_tools_mammillary.py | 99 +++++++++++ 3 files changed, 386 insertions(+) create mode 100644 db/migrations/081_mammillary.sql create mode 100644 src/agentmemory/mcp_tools_mammillary.py create mode 100644 tests/test_mcp_tools_mammillary.py diff --git a/db/migrations/081_mammillary.sql b/db/migrations/081_mammillary.sql new file mode 100644 index 0000000..b5be20c --- /dev/null +++ b/db/migrations/081_mammillary.sql @@ -0,0 +1,65 @@ +-- Migration 081: mammillary bodies + Papez circuit — Phase 1 schema +-- +-- The mammillary bodies are the Papez-circuit hub: +-- hippocampus → fornix → mammillary bodies → ATN (anterior thalamus) +-- → cingulate → hippocampus +-- +-- Damage produces Korsakoff syndrome — dense anterograde amnesia. +-- ATN-DAMAGE > MD-thalamus for that pattern. Mammillary bodies are +-- thus a specific bottleneck in episodic memory consolidation. +-- +-- Existing brainctl has the broad hippocampus subsystem + (now) CA1 +-- + Subiculum + anterior-thalamus-analog inside the thalamus module. +-- What's missing is the explicit Papez-loop transport: which memories +-- have made it through the (hippocampus → MB → ATN → cingulate) +-- circuit vs. which are still hippocampus-only. +-- +-- Phase 1 ships: +-- mammillary_transit_log — log of episodic memories whose +-- consolidation has passed through the Papez +-- circuit at least once +-- mammillary_state — single row tracking transit count + recent rate +-- +-- Phase 2 will auto-log Papez transit on consolidation_run for +-- episodic memories. Phase 3 will let Papez-completed memories surface +-- with higher confidence in retrieval (proxy for "consolidated into +-- declarative knowledge"). +-- +-- Rollback: +-- DROP TABLE IF EXISTS mammillary_transit_log; +-- DROP TABLE IF EXISTS mammillary_state; +-- DELETE FROM schema_version WHERE version = 81; +-- +-- IDEMPOTENT. + +CREATE TABLE IF NOT EXISTS mammillary_state ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total_transits INTEGER NOT NULL DEFAULT 0, + transits_24h INTEGER NOT NULL DEFAULT 0, + last_transit_at TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now')) +); +INSERT OR IGNORE INTO mammillary_state (id) VALUES (1); + +CREATE TABLE IF NOT EXISTS mammillary_transit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + transited_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now')), + memory_id INTEGER NOT NULL, + agent_id TEXT, + direction TEXT NOT NULL CHECK(direction IN ( + 'hippocampus_to_atn', -- forward leg of Papez + 'atn_to_cingulate', -- top-down + 'cingulate_to_hippocampus', -- closing the loop + 'full_loop' -- single full Papez circuit completion + )), + transit_strength REAL NOT NULL DEFAULT 1.0 CHECK(transit_strength BETWEEN 0.0 AND 1.0), + notes TEXT +); +CREATE INDEX IF NOT EXISTS idx_mtl_recent ON mammillary_transit_log(transited_at); +CREATE INDEX IF NOT EXISTS idx_mtl_memory ON mammillary_transit_log(memory_id, transited_at); +CREATE INDEX IF NOT EXISTS idx_mtl_direction ON mammillary_transit_log(direction, transited_at); + +INSERT OR IGNORE INTO schema_version (version, description, applied_at) +VALUES (81, 'mammillary bodies + Papez circuit Phase 1: 2 tables (state + transit log)', + strftime('%Y-%m-%dT%H:%M:%S', 'now')); diff --git a/src/agentmemory/mcp_tools_mammillary.py b/src/agentmemory/mcp_tools_mammillary.py new file mode 100644 index 0000000..cf38d85 --- /dev/null +++ b/src/agentmemory/mcp_tools_mammillary.py @@ -0,0 +1,222 @@ +"""brainctl MCP tools — mammillary bodies + Papez circuit. + +Phase 1: log episodic-memory transits through the Papez loop +(hippocampus → MB → ATN → cingulate → hippocampus). Phase 2 will +auto-log on consolidation_run. Phase 3 lets Papez-completed memories +surface with higher confidence (proxy for "consolidated to +declarative"). +""" +from __future__ import annotations + +import sqlite3 +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +from mcp.types import Tool + +from agentmemory.lib.mcp_helpers import open_db +from agentmemory.paths import get_db_path + +DB_PATH: Path = get_db_path() + +VALID_DIRECTIONS = { + "hippocampus_to_atn", "atn_to_cingulate", + "cingulate_to_hippocampus", "full_loop", +} + + +def _db() -> sqlite3.Connection: + return open_db(str(DB_PATH)) + + +def _rows(rows: Iterable[sqlite3.Row]) -> list[dict[str, Any]]: + return [dict(r) for r in rows] + + +def _require_schema(conn: sqlite3.Connection) -> str | None: + for t in ("mammillary_state", "mammillary_transit_log"): + if not conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (t,) + ).fetchone(): + return f"mammillary schema missing: {t}. Run `brainctl migrate` (081)." + return None + + +def tool_mammillary_status(**_kw: Any) -> dict[str, Any]: + with _db() as conn: + conn.row_factory = sqlite3.Row + err = _require_schema(conn) + if err: + return {"error": err} + state = conn.execute("SELECT * FROM mammillary_state WHERE id = 1").fetchone() + last_5 = _rows(conn.execute( + "SELECT * FROM mammillary_transit_log ORDER BY id DESC LIMIT 5" + ).fetchall()) + agg = conn.execute( + """ + SELECT COUNT(*) AS n, + SUM(CASE WHEN direction='full_loop' THEN 1 ELSE 0 END) AS n_full, + SUM(CASE WHEN direction='hippocampus_to_atn' THEN 1 ELSE 0 END) AS n_h2a, + COUNT(DISTINCT memory_id) AS unique_memories + FROM mammillary_transit_log + WHERE transited_at >= datetime('now', '-24 hours') + """ + ).fetchone() + return { + "ok": True, + "state": dict(state) if state else None, + "last_5_transits": last_5, + "aggregate_24h": dict(agg) if agg else {}, + } + + +def tool_mammillary_log_transit( + memory_id: int, direction: str, + transit_strength: float = 1.0, + agent_id: str | None = None, + notes: str | None = None, + **_kw: Any, +) -> dict[str, Any]: + if direction not in VALID_DIRECTIONS: + return {"error": f"invalid direction {direction!r}; expected {sorted(VALID_DIRECTIONS)}"} + if not 0.0 <= transit_strength <= 1.0: + return {"error": "transit_strength must be in [0, 1]"} + with _db() as conn: + conn.row_factory = sqlite3.Row + err = _require_schema(conn) + if err: + return {"error": err} + cur = conn.execute( + """ + INSERT INTO mammillary_transit_log + (memory_id, agent_id, direction, transit_strength, notes) + VALUES (?, ?, ?, ?, ?) + """, + (int(memory_id), agent_id, direction, float(transit_strength), notes), + ) + transit_id = cur.lastrowid + conn.execute( + """ + UPDATE mammillary_state SET + total_transits = total_transits + 1, + transits_24h = transits_24h + 1, + last_transit_at = strftime('%Y-%m-%dT%H:%M:%S', 'now'), + updated_at = strftime('%Y-%m-%dT%H:%M:%S', 'now') + WHERE id = 1 + """ + ) + conn.commit() + return { + "ok": True, "transit_id": transit_id, + "memory_id": int(memory_id), "direction": direction, + "transit_strength": float(transit_strength), + } + + +def tool_mammillary_memory_history(memory_id: int, limit: int = 20, **_kw: Any) -> dict[str, Any]: + limit = max(1, min(int(limit), 200)) + with _db() as conn: + conn.row_factory = sqlite3.Row + err = _require_schema(conn) + if err: + return {"error": err} + rows = conn.execute( + """ + SELECT * FROM mammillary_transit_log + WHERE memory_id = ? ORDER BY id DESC LIMIT ? + """, + (int(memory_id), limit), + ).fetchall() + full_loops = conn.execute( + "SELECT COUNT(*) FROM mammillary_transit_log WHERE memory_id = ? AND direction = 'full_loop'", + (int(memory_id),), + ).fetchone()[0] + return { + "ok": True, "memory_id": int(memory_id), + "transits": _rows(rows), + "full_loop_count": full_loops, + "consolidated": full_loops > 0, + } + + +def tool_mammillary_reset_24h(**_kw: Any) -> dict[str, Any]: + """Reset transits_24h. Phase 1 manual; Phase 2 daemon will + auto-roll this on a 24h schedule.""" + with _db() as conn: + conn.row_factory = sqlite3.Row + err = _require_schema(conn) + if err: + return {"error": err} + prior = conn.execute("SELECT transits_24h FROM mammillary_state WHERE id = 1").fetchone() + conn.execute( + "UPDATE mammillary_state SET transits_24h = 0, updated_at = strftime('%Y-%m-%dT%H:%M:%S', 'now') WHERE id = 1" + ) + conn.commit() + return {"ok": True, "prior_24h_count": int(prior["transits_24h"]) if prior else 0} + + +TOOLS: list[Tool] = [ + Tool( + name="mammillary_status", + description="Mammillary + Papez state + last 5 transits + 24h aggregate.", + inputSchema={"type": "object", "properties": {}}, + ), + Tool( + name="mammillary_log_transit", + description=( + "Log one Papez-circuit transit for an episodic memory. direction ∈ " + "{hippocampus_to_atn, atn_to_cingulate, cingulate_to_hippocampus, full_loop}. " + "full_loop = single call completes one full Papez cycle (use when a " + "consolidation_run pass fully processes a memory)." + ), + inputSchema={ + "type": "object", + "properties": { + "memory_id": {"type": "integer"}, + "direction": {"type": "string", "enum": sorted(VALID_DIRECTIONS)}, + "transit_strength": {"type": "number", "default": 1.0}, + "agent_id": {"type": "string"}, + "notes": {"type": "string"}, + }, + "required": ["memory_id", "direction"], + }, + ), + Tool( + name="mammillary_memory_history", + description=( + "Per-memory transit history with full_loop count + consolidated flag (true " + "iff ≥1 full_loop transit recorded)." + ), + inputSchema={ + "type": "object", + "properties": { + "memory_id": {"type": "integer"}, + "limit": {"type": "integer", "default": 20}, + }, + "required": ["memory_id"], + }, + ), + Tool( + name="mammillary_reset_24h", + description="Reset transits_24h counter. Phase 1 manual; Phase 2 daemon-driven.", + inputSchema={"type": "object", "properties": {}}, + ), +] + + +_MAMM_TOOLS = { + "mammillary_status": tool_mammillary_status, + "mammillary_log_transit": tool_mammillary_log_transit, + "mammillary_memory_history": tool_mammillary_memory_history, + "mammillary_reset_24h": tool_mammillary_reset_24h, +} + +DISPATCH: dict[str, Any] = { + name: (lambda _func=func, **kw: _func(**kw)) + for name, func in _MAMM_TOOLS.items() +} + + +def register_tools() -> tuple[list[Tool], dict[str, Any]]: + return TOOLS, DISPATCH diff --git a/tests/test_mcp_tools_mammillary.py b/tests/test_mcp_tools_mammillary.py new file mode 100644 index 0000000..a44831a --- /dev/null +++ b/tests/test_mcp_tools_mammillary.py @@ -0,0 +1,99 @@ +"""Tests for mcp_tools_mammillary — Phase 1.""" +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +MIGRATION_081 = REPO_ROOT / "db" / "migrations" / "081_mammillary.sql" + + +def _bootstrap(conn): + conn.executescript( + "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, description TEXT, applied_at TEXT);" + ) + + +def _apply(db_path): + conn = sqlite3.connect(str(db_path)) + try: + _bootstrap(conn) + conn.executescript(MIGRATION_081.read_text()) + conn.commit() + finally: + conn.close() + + +def _make_db(tmp_path, monkeypatch): + db = tmp_path / "brain.db" + _apply(db) + from agentmemory import mcp_tools_mammillary as mod + monkeypatch.setattr(mod, "DB_PATH", db) + return mod + + +def test_migration_with_defaults(tmp_path): + db = tmp_path / "brain.db" + _apply(db) + conn = sqlite3.connect(str(db)) + try: + state = conn.execute( + "SELECT total_transits, transits_24h, enabled FROM mammillary_state" + ).fetchone() + assert state == (0, 0, 1) + finally: + conn.close() + + +def test_log_transit(tmp_path, monkeypatch): + mod = _make_db(tmp_path, monkeypatch) + out = mod.tool_mammillary_log_transit( + memory_id=42, direction="full_loop", agent_id="a1", + ) + assert out["ok"] is True + assert out["direction"] == "full_loop" + status = mod.tool_mammillary_status() + assert status["state"]["total_transits"] == 1 + + +def test_log_transit_validates(tmp_path, monkeypatch): + mod = _make_db(tmp_path, monkeypatch) + assert "error" in mod.tool_mammillary_log_transit(memory_id=1, direction="bogus") + assert "error" in mod.tool_mammillary_log_transit(memory_id=1, direction="full_loop", transit_strength=1.5) + + +def test_memory_history_consolidated_flag(tmp_path, monkeypatch): + mod = _make_db(tmp_path, monkeypatch) + # Memory 99 has only a partial leg (hippocampus_to_atn) + mod.tool_mammillary_log_transit(memory_id=99, direction="hippocampus_to_atn") + hist = mod.tool_mammillary_memory_history(memory_id=99) + assert hist["full_loop_count"] == 0 + assert hist["consolidated"] is False + # Add a full_loop event + mod.tool_mammillary_log_transit(memory_id=99, direction="full_loop") + hist = mod.tool_mammillary_memory_history(memory_id=99) + assert hist["full_loop_count"] == 1 + assert hist["consolidated"] is True + + +def test_status_aggregates_24h(tmp_path, monkeypatch): + mod = _make_db(tmp_path, monkeypatch) + for mid in [1, 2, 1, 3]: + mod.tool_mammillary_log_transit(memory_id=mid, direction="full_loop") + status = mod.tool_mammillary_status() + assert status["aggregate_24h"]["n"] == 4 + assert status["aggregate_24h"]["unique_memories"] == 3 + assert status["aggregate_24h"]["n_full"] == 4 + + +def test_reset_24h(tmp_path, monkeypatch): + mod = _make_db(tmp_path, monkeypatch) + mod.tool_mammillary_log_transit(memory_id=1, direction="full_loop") + mod.tool_mammillary_log_transit(memory_id=2, direction="full_loop") + out = mod.tool_mammillary_reset_24h() + assert out["ok"] is True + assert out["prior_24h_count"] == 2 + status = mod.tool_mammillary_status() + assert status["state"]["transits_24h"] == 0 + # total_transits NOT reset + assert status["state"]["total_transits"] == 2 From d86f5c058d304a78d2665018a8a373680a1e3f2f Mon Sep 17 00:00:00 2001 From: R4vager Date: Wed, 20 May 2026 01:31:17 -0400 Subject: [PATCH 2/2] mammillary: register mcp_server import (fixup) --- src/agentmemory/mcp_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agentmemory/mcp_server.py b/src/agentmemory/mcp_server.py index 95e5f45..f4eab27 100755 --- a/src/agentmemory/mcp_server.py +++ b/src/agentmemory/mcp_server.py @@ -60,6 +60,7 @@ mcp_tools_insula, mcp_tools_knowledge, mcp_tools_lifecycle, + mcp_tools_mammillary, mcp_tools_meb, mcp_tools_merge, mcp_tools_neuro, @@ -103,6 +104,7 @@ mcp_tools_insula, mcp_tools_knowledge, mcp_tools_lifecycle, + mcp_tools_mammillary, mcp_tools_meb, mcp_tools_merge, mcp_tools_neuro,