superlocalmemory 4.0.3 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +19 -13
  3. package/ide/configs/codex-mcp.toml +2 -2
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.mcp.json +1 -0
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +3 -2
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +5 -4
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +7 -6
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-graph/SKILL.md +3 -2
  30. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-profile/SKILL.md +5 -4
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +1 -1
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/brain/__init__.py +5 -0
  41. package/src/superlocalmemory/brain/truth.py +348 -0
  42. package/src/superlocalmemory/cli/commands.py +82 -25
  43. package/src/superlocalmemory/cli/main.py +12 -0
  44. package/src/superlocalmemory/core/context_cache.py +58 -1
  45. package/src/superlocalmemory/core/mutations.py +155 -25
  46. package/src/superlocalmemory/core/recall_pipeline.py +6 -10
  47. package/src/superlocalmemory/core/remember_runtime.py +271 -2
  48. package/src/superlocalmemory/core/store_pipeline.py +100 -38
  49. package/src/superlocalmemory/encoding/consolidator.py +17 -47
  50. package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
  51. package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
  52. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +185 -0
  53. package/src/superlocalmemory/learning/database.py +2 -1
  54. package/src/superlocalmemory/mcp/profiles.py +25 -7
  55. package/src/superlocalmemory/mcp/server.py +7 -2
  56. package/src/superlocalmemory/mcp/tools_brain.py +138 -9
  57. package/src/superlocalmemory/mcp/tools_core.py +88 -3
  58. package/src/superlocalmemory/retrieval/engine.py +7 -10
  59. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
  60. package/src/superlocalmemory/server/routes/brain.py +21 -1
  61. package/src/superlocalmemory/server/routes/memories.py +129 -3
  62. package/src/superlocalmemory/storage/_migration_internals.py +8 -0
  63. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  64. package/src/superlocalmemory/storage/agent_experience.py +26 -4
  65. package/src/superlocalmemory/storage/correction_cases.py +670 -0
  66. package/src/superlocalmemory/storage/database.py +194 -24
  67. package/src/superlocalmemory/storage/external_evidence.py +359 -0
  68. package/src/superlocalmemory/storage/migration_runner.py +12 -0
  69. package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
  70. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
  71. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  72. package/src/superlocalmemory/storage/write_coordinator.py +4 -0
  73. package/src/superlocalmemory/ui/js/brain.js +43 -7
  74. package/src/superlocalmemory/ui/js/od-brain.js +44 -19
@@ -0,0 +1,348 @@
1
+ """The versioned, read-only Living Brain truth model.
2
+
3
+ This module is deliberately below every transport surface. CLI, MCP, HTTP,
4
+ and the dashboard can serialize the same dictionary without importing an
5
+ engine, attaching databases, or acquiring a writer lock. Every store is read
6
+ in its own short-lived, SQLite read-only connection; one store failing never
7
+ turns a failed measurement into a fabricated zero for another store.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sqlite3
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ BRAIN_TRUTH_V1 = "superlocalmemory.brain-truth/v1"
18
+
19
+ _EXPLICIT_SIGNAL_TYPES = frozenset(
20
+ {"user_positive", "user_negative", "user_correction", "user_pin", "legacy_feedback"}
21
+ )
22
+
23
+
24
+ class BrainTruthService:
25
+ """Build one honest, profile-scoped observation snapshot.
26
+
27
+ ``memory.db`` and ``learning.db`` are intentionally passed separately.
28
+ This service never uses ``ATTACH``, starts no transaction, and must not be
29
+ used for recall, ranking, routing, correction application, or learning.
30
+ """
31
+
32
+ def __init__(self, *, memory_db_path: str | Path, learning_db_path: str | Path) -> None:
33
+ self._memory_db_path = Path(memory_db_path)
34
+ self._learning_db_path = Path(learning_db_path)
35
+
36
+ def snapshot(self, profile_id: str) -> dict[str, Any]:
37
+ """Return the stable BrainTruth v1 payload for ``profile_id``.
38
+
39
+ The payload intentionally reports unavailable measurements with
40
+ ``None`` values and a reason. A missing table or locked/corrupt file
41
+ therefore cannot be mistaken for a real count of zero.
42
+ """
43
+ if not isinstance(profile_id, str) or not profile_id:
44
+ raise ValueError("profile_id must be a non-empty string")
45
+
46
+ memory = self._read_memory(profile_id)
47
+ learning = self._read_learning(profile_id)
48
+ return {
49
+ "contract": BRAIN_TRUTH_V1,
50
+ "profile_id": profile_id,
51
+ "generated_at": _utc_now(),
52
+ "control_plane": "observation_only",
53
+ "memory_activity": memory["memory_activity"],
54
+ "feedback": learning["feedback"],
55
+ "agent_experience": learning["agent_experience"],
56
+ "external_evidence": learning["external_evidence"],
57
+ "correction_quality": memory["correction_quality"],
58
+ }
59
+
60
+ def _read_memory(self, profile_id: str) -> dict[str, dict[str, Any]]:
61
+ conn, unavailable = _open_read_only(self._memory_db_path, source="memory.db")
62
+ if unavailable is not None:
63
+ return {
64
+ "memory_activity": _unavailable_activity(unavailable),
65
+ "correction_quality": _unavailable_corrections(unavailable),
66
+ }
67
+ assert conn is not None
68
+ try:
69
+ return {
70
+ "memory_activity": _memory_activity(conn, profile_id),
71
+ "correction_quality": _correction_quality(conn, profile_id),
72
+ }
73
+ finally:
74
+ conn.close()
75
+
76
+ def _read_learning(self, profile_id: str) -> dict[str, dict[str, Any]]:
77
+ conn, unavailable = _open_read_only(self._learning_db_path, source="learning.db")
78
+ if unavailable is not None:
79
+ return {
80
+ "feedback": _unavailable_feedback(unavailable),
81
+ "agent_experience": _unavailable_agent_experience(unavailable),
82
+ "external_evidence": _unavailable_external_evidence(unavailable),
83
+ }
84
+ assert conn is not None
85
+ try:
86
+ return {
87
+ "feedback": _feedback(conn, profile_id),
88
+ "agent_experience": _agent_experience(conn, profile_id),
89
+ "external_evidence": _external_evidence(conn, profile_id),
90
+ }
91
+ finally:
92
+ conn.close()
93
+
94
+
95
+ def _open_read_only(
96
+ path: Path, *, source: str
97
+ ) -> tuple[sqlite3.Connection | None, dict[str, str] | None]:
98
+ """Open a single store without creating it or exposing SQLite errors."""
99
+ if not path.exists():
100
+ return None, _unavailable(source, "missing")
101
+ try:
102
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=0.25)
103
+ conn.row_factory = sqlite3.Row
104
+ conn.execute("PRAGMA query_only=ON")
105
+ return conn, None
106
+ except sqlite3.Error:
107
+ return None, _unavailable(source, "read_failed")
108
+
109
+
110
+ def _memory_activity(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
111
+ required = {"atomic_facts": {"profile_id", "lifecycle", "created_at"}}
112
+ if not _schema_has(conn, required):
113
+ return _unavailable_activity(_unavailable("memory.db:atomic_facts", "schema_unavailable"))
114
+ try:
115
+ rows = conn.execute(
116
+ "SELECT lifecycle, COUNT(*) AS count FROM atomic_facts "
117
+ "WHERE profile_id=? GROUP BY lifecycle ORDER BY lifecycle",
118
+ (profile_id,),
119
+ ).fetchall()
120
+ recent = conn.execute(
121
+ "SELECT COUNT(*) AS count FROM atomic_facts WHERE profile_id=? "
122
+ "AND created_at >= datetime('now', '-1 day')",
123
+ (profile_id,),
124
+ ).fetchone()
125
+ except sqlite3.Error:
126
+ return _unavailable_activity(_unavailable("memory.db:atomic_facts", "read_failed"))
127
+ by_lifecycle = {str(row["lifecycle"]): int(row["count"]) for row in rows}
128
+ return {
129
+ "availability": "available",
130
+ "source": "memory.db:atomic_facts",
131
+ "facts_total": sum(by_lifecycle.values()),
132
+ "facts_by_lifecycle": by_lifecycle,
133
+ "facts_created_last_24h": int(recent["count"]) if recent is not None else 0,
134
+ }
135
+
136
+
137
+ def _feedback(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
138
+ required = {"learning_signals": {"profile_id", "signal_type"}}
139
+ if not _schema_has(conn, required):
140
+ return _unavailable_feedback(
141
+ _unavailable("learning.db:learning_signals", "schema_unavailable")
142
+ )
143
+ try:
144
+ rows = conn.execute(
145
+ "SELECT signal_type, COUNT(*) AS count FROM learning_signals "
146
+ "WHERE profile_id=? GROUP BY signal_type ORDER BY signal_type",
147
+ (profile_id,),
148
+ ).fetchall()
149
+ except sqlite3.Error:
150
+ return _unavailable_feedback(_unavailable("learning.db:learning_signals", "read_failed"))
151
+ by_type = {str(row["signal_type"]): int(row["count"]) for row in rows}
152
+ explicit = sum(count for kind, count in by_type.items() if kind in _EXPLICIT_SIGNAL_TYPES)
153
+ implicit = sum(count for kind, count in by_type.items() if kind not in _EXPLICIT_SIGNAL_TYPES)
154
+ return {
155
+ "availability": "available",
156
+ "source": "learning.db:learning_signals",
157
+ "signals_total": sum(by_type.values()),
158
+ "signals_by_type": by_type,
159
+ "explicit_signals": explicit,
160
+ "implicit_signals": implicit,
161
+ }
162
+
163
+
164
+ def _agent_experience(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
165
+ required = {
166
+ "agent_experiences": {"profile_id", "verification_authority"},
167
+ "cognitive_turn_receipts": {"profile_id", "state"},
168
+ }
169
+ if not _schema_has(conn, required):
170
+ return _unavailable_agent_experience(
171
+ _unavailable("learning.db:M040_agent_experience_receipts", "schema_unavailable")
172
+ )
173
+ try:
174
+ claimed = conn.execute(
175
+ "SELECT COUNT(*) AS count FROM agent_experiences WHERE profile_id=?", (profile_id,)
176
+ ).fetchone()
177
+ turn_rows = conn.execute(
178
+ "SELECT state, COUNT(*) AS count FROM cognitive_turn_receipts "
179
+ "WHERE profile_id=? GROUP BY state ORDER BY state",
180
+ (profile_id,),
181
+ ).fetchall()
182
+ except sqlite3.Error:
183
+ return _unavailable_agent_experience(
184
+ _unavailable("learning.db:M040_agent_experience_receipts", "read_failed")
185
+ )
186
+ turns_by_state = {str(row["state"]): int(row["count"]) for row in turn_rows}
187
+ return {
188
+ "availability": "available",
189
+ "source": "learning.db:M040_agent_experience_receipts",
190
+ "claimed_experiences_total": int(claimed["count"]) if claimed is not None else 0,
191
+ # M040 validates a declared authority, but this read-only service has
192
+ # no independent verifier. Calling those claims verified would be a
193
+ # product-quality lie, so this number is deliberately known to be zero.
194
+ "independently_verified_experiences_total": 0,
195
+ "verification_availability": "not_supported_by_read_model",
196
+ "cognitive_turns_total": sum(turns_by_state.values()),
197
+ "cognitive_turns_by_state": turns_by_state,
198
+ }
199
+
200
+
201
+ def _external_evidence(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
202
+ required = {
203
+ "external_evidence_receipts": {
204
+ "profile_id",
205
+ "run_state",
206
+ "demonstration",
207
+ "eligible_for_learning",
208
+ }
209
+ }
210
+ if not _schema_has(conn, required):
211
+ return _unavailable_external_evidence(
212
+ _unavailable("learning.db:M041_external_evidence_receipts", "schema_unavailable")
213
+ )
214
+ try:
215
+ rows = conn.execute(
216
+ "SELECT run_state, COUNT(*) AS count FROM external_evidence_receipts "
217
+ "WHERE profile_id=? GROUP BY run_state ORDER BY run_state",
218
+ (profile_id,),
219
+ ).fetchall()
220
+ demo = conn.execute(
221
+ "SELECT COUNT(*) AS count FROM external_evidence_receipts "
222
+ "WHERE profile_id=? AND demonstration=1",
223
+ (profile_id,),
224
+ ).fetchone()
225
+ eligible = conn.execute(
226
+ "SELECT COUNT(*) AS count FROM external_evidence_receipts "
227
+ "WHERE profile_id=? AND eligible_for_learning=1",
228
+ (profile_id,),
229
+ ).fetchone()
230
+ except sqlite3.Error:
231
+ return _unavailable_external_evidence(
232
+ _unavailable("learning.db:M041_external_evidence_receipts", "read_failed")
233
+ )
234
+ by_state = {str(row["run_state"]): int(row["count"]) for row in rows}
235
+ return {
236
+ "availability": "available",
237
+ "source": "learning.db:M041_external_evidence_receipts",
238
+ "receipts_total": sum(by_state.values()),
239
+ "receipts_by_run_state": by_state,
240
+ "demonstrations_total": int(demo["count"]) if demo is not None else 0,
241
+ "eligible_for_learning_total": int(eligible["count"]) if eligible is not None else 0,
242
+ "control_plane": "observation_only",
243
+ }
244
+
245
+
246
+ def _correction_quality(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
247
+ required = {"correction_cases": {"profile_id", "status"}}
248
+ if not _schema_has(conn, required):
249
+ return _unavailable_corrections(
250
+ _unavailable("memory.db:M042_correction_case_ledger", "schema_unavailable")
251
+ )
252
+ try:
253
+ rows = conn.execute(
254
+ "SELECT status, COUNT(*) AS count FROM correction_cases "
255
+ "WHERE profile_id=? GROUP BY status ORDER BY status",
256
+ (profile_id,),
257
+ ).fetchall()
258
+ except sqlite3.Error:
259
+ return _unavailable_corrections(
260
+ _unavailable("memory.db:M042_correction_case_ledger", "read_failed")
261
+ )
262
+ by_status = {str(row["status"]): int(row["count"]) for row in rows}
263
+ return {
264
+ "availability": "available",
265
+ "source": "memory.db:M042_correction_case_ledger",
266
+ "cases_total": sum(by_status.values()),
267
+ "cases_by_status": by_status,
268
+ # M042 is a ledger. A policy owner must be supplied by a later host
269
+ # integration; this neutral reader cannot manufacture authorization.
270
+ "review_policy": {
271
+ "availability": "not_configured",
272
+ "automatic_application": False,
273
+ "reason": "host-authorized review policy is not attached",
274
+ },
275
+ }
276
+
277
+
278
+ def _schema_has(conn: sqlite3.Connection, required: dict[str, set[str]]) -> bool:
279
+ try:
280
+ for table, columns in required.items():
281
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall() # nosec B608
282
+ if not rows or not columns <= {str(row[1]) for row in rows}:
283
+ return False
284
+ except sqlite3.Error:
285
+ return False
286
+ return True
287
+
288
+
289
+ def _unavailable(source: str, reason: str) -> dict[str, str]:
290
+ return {"availability": "unavailable", "source": source, "reason": reason}
291
+
292
+
293
+ def _unavailable_activity(status: dict[str, str]) -> dict[str, Any]:
294
+ return {
295
+ **status,
296
+ "facts_total": None,
297
+ "facts_by_lifecycle": None,
298
+ "facts_created_last_24h": None,
299
+ }
300
+
301
+
302
+ def _unavailable_feedback(status: dict[str, str]) -> dict[str, Any]:
303
+ return {
304
+ **status,
305
+ "signals_total": None,
306
+ "signals_by_type": None,
307
+ "explicit_signals": None,
308
+ "implicit_signals": None,
309
+ }
310
+
311
+
312
+ def _unavailable_agent_experience(status: dict[str, str]) -> dict[str, Any]:
313
+ return {
314
+ **status,
315
+ "claimed_experiences_total": None,
316
+ "independently_verified_experiences_total": None,
317
+ "verification_availability": "unavailable",
318
+ "cognitive_turns_total": None,
319
+ "cognitive_turns_by_state": None,
320
+ }
321
+
322
+
323
+ def _unavailable_external_evidence(status: dict[str, str]) -> dict[str, Any]:
324
+ return {
325
+ **status,
326
+ "receipts_total": None,
327
+ "receipts_by_run_state": None,
328
+ "demonstrations_total": None,
329
+ "eligible_for_learning_total": None,
330
+ "control_plane": "observation_only",
331
+ }
332
+
333
+
334
+ def _unavailable_corrections(status: dict[str, str]) -> dict[str, Any]:
335
+ return {
336
+ **status,
337
+ "cases_total": None,
338
+ "cases_by_status": None,
339
+ "review_policy": {
340
+ "availability": "unavailable",
341
+ "automatic_application": False,
342
+ "reason": "correction ledger is unavailable",
343
+ },
344
+ }
345
+
346
+
347
+ def _utc_now() -> str:
348
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -345,6 +345,7 @@ def dispatch(args: Namespace) -> None:
345
345
  "forget": cmd_forget,
346
346
  "delete": cmd_delete,
347
347
  "update": cmd_update,
348
+ "review-correction": cmd_review_correction,
348
349
  "status": cmd_status,
349
350
  "brain": cmd_brain,
350
351
  "health": cmd_health,
@@ -1799,21 +1800,64 @@ def cmd_update(args: Namespace) -> None:
1799
1800
  if use_json:
1800
1801
  from superlocalmemory.cli.json_output import json_print
1801
1802
  json_print("update", data={
1802
- "fact_id": fact_id,
1803
- "new_content": new_content[:120],
1803
+ "predecessor_fact_id": result.get("predecessor_fact_id", fact_id),
1804
+ "successor_fact_id": result.get("successor_fact_id"),
1805
+ "correction_case": result.get("correction_case"),
1806
+ "review_required": bool(result.get("review_required", False)),
1804
1807
  }, next_actions=[
1805
1808
  {
1806
- "command": "slm list --json",
1807
- "description": "List recent memories",
1809
+ "command": "slm brain --json",
1810
+ "description": "Inspect the observation-only brain status",
1808
1811
  },
1809
1812
  ])
1810
1813
  else:
1811
- print(f"New: {new_content[:100]}")
1812
- print(f"Updated: {fact_id}")
1814
+ if result.get("review_required"):
1815
+ case = result.get("correction_case", {})
1816
+ print(f"Correction proposed: {case.get('case_id', 'unknown')}")
1817
+ print(f"Predecessor remains current until review: {fact_id}")
1818
+ else:
1819
+ print(f"Unchanged: {fact_id}")
1813
1820
  return
1814
1821
  _daemon_unavailable("update", use_json)
1815
1822
 
1816
1823
 
1824
+ def cmd_review_correction(args: Namespace) -> None:
1825
+ """Apply, reject, or roll back an explicitly reviewed correction case."""
1826
+ from superlocalmemory.core.admission import gate_cli_mutation
1827
+ from superlocalmemory.core.operation_request import OperationKind
1828
+
1829
+ gate_cli_mutation(OperationKind.CORRECT)
1830
+ import urllib.parse
1831
+
1832
+ from superlocalmemory.cli.daemon import daemon_request, ensure_daemon, is_daemon_running
1833
+
1834
+ use_json = getattr(args, "json", False)
1835
+ action = str(args.action).strip().lower()
1836
+ if action not in {"apply", "reject", "rollback"}:
1837
+ raise ValueError("action must be apply, reject, or rollback")
1838
+ if not isinstance(args.expected_version, int) or args.expected_version < 0:
1839
+ raise ValueError("expected_version must be a non-negative integer")
1840
+ if not (is_daemon_running() or ensure_daemon()):
1841
+ _daemon_unavailable("correction review", use_json)
1842
+ return
1843
+ payload: dict[str, object] = {"expected_version": args.expected_version}
1844
+ if getattr(args, "event_valid_until", None):
1845
+ payload["event_valid_until"] = args.event_valid_until
1846
+ path = "/api/corrections/" + urllib.parse.quote(args.case_id, safe="") + "/" + action
1847
+ result = daemon_request("POST", path, payload)
1848
+ if not isinstance(result, dict) or not result.get("success"):
1849
+ _daemon_unavailable("correction review", use_json)
1850
+ return
1851
+ if use_json:
1852
+ from superlocalmemory.cli.json_output import json_print
1853
+
1854
+ json_print("review-correction", data=result)
1855
+ else:
1856
+ case = result.get("correction_case", {})
1857
+ print(f"Correction {action}: {case.get('case_id', args.case_id)}")
1858
+ print(f"State: {case.get('status', 'unknown')}")
1859
+
1860
+
1817
1861
  # -- Diagnostics (all support --json) -------------------------------------
1818
1862
 
1819
1863
 
@@ -2157,7 +2201,8 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
2157
2201
  ("recall", "Semantic + keyword search across memories"),
2158
2202
  ("search", "Exact keyword / full-text search"),
2159
2203
  ("list", "Show recent memories (-n N)"),
2160
- ("update", "Correct an existing memory by id"),
2204
+ ("update", "Propose an immutable correction by id"),
2205
+ ("review-correction", "Apply, reject, or roll back a correction case"),
2161
2206
  ("delete", "Delete a memory by id"),
2162
2207
  ("forget", "Run the decay cycle (preview first)"),
2163
2208
  ("trace", "Recall with a per-channel score breakdown"),
@@ -3690,25 +3735,22 @@ def cmd_session_context(args: Namespace) -> None:
3690
3735
 
3691
3736
 
3692
3737
  def cmd_brain(args: Namespace) -> None:
3693
- """Read the portable, profile-scoped Agent Experience evidence summary.
3738
+ """Read the portable, profile-scoped Living Brain truth snapshot.
3694
3739
 
3695
- This command deliberately opens no memory engine and starts no daemon. It
3696
- is an indexed ``learning.db`` read, so support scripts and non-technical
3697
- users can inspect the Living Brain without affecting recall latency.
3740
+ This command deliberately opens no memory engine and starts no daemon.
3741
+ BrainTruth uses short read-only connections to keep the status view out of
3742
+ recall and writer domains.
3698
3743
  """
3744
+ from superlocalmemory.brain.truth import BrainTruthService
3699
3745
  from superlocalmemory.core.config import SLMConfig
3700
3746
  from superlocalmemory.infra.data_root import state_path
3701
- from superlocalmemory.storage.agent_experience import get_profile_receipt_summary
3702
3747
 
3703
3748
  config = SLMConfig.load()
3704
3749
  profile_id = config.active_profile
3705
- data = {
3706
- "profile_id": profile_id,
3707
- "agent_experience": get_profile_receipt_summary(
3708
- state_path("learning.db"), profile_id
3709
- ),
3710
- "control_plane": "observation_only",
3711
- }
3750
+ data = BrainTruthService(
3751
+ memory_db_path=state_path("memory.db"),
3752
+ learning_db_path=state_path("learning.db"),
3753
+ ).snapshot(profile_id)
3712
3754
  if getattr(args, "json", False):
3713
3755
  from superlocalmemory.cli.json_output import json_print
3714
3756
 
@@ -3716,18 +3758,33 @@ def cmd_brain(args: Namespace) -> None:
3716
3758
  {"command": "slm dashboard", "description": "Open the Living Brain dashboard"},
3717
3759
  ])
3718
3760
  return
3761
+ memory = data["memory_activity"]
3762
+ feedback = data["feedback"]
3719
3763
  evidence = data["agent_experience"]
3764
+ external = data["external_evidence"]
3765
+ corrections = data["correction_quality"]
3720
3766
  print("SuperLocalMemory Living Brain")
3721
3767
  print(f" Profile: {profile_id}")
3722
- print(f" Agent experiences: {evidence['experiences_total']}")
3723
- print(f" Claimed evidence authority: {evidence['claimed_evidence_experiences']}")
3724
- print(f" Cognitive turns: {evidence['turns_total']}")
3725
- if evidence["turns_by_state"]:
3768
+ print(f" Stored facts: {memory['facts_total']}")
3769
+ print(f" Feedback signals: {feedback['signals_total']}")
3770
+ print(f" Claimed agent experiences: {evidence['claimed_experiences_total']}")
3771
+ print(
3772
+ " Independently verified experiences: "
3773
+ f"{evidence['independently_verified_experiences_total']}"
3774
+ )
3775
+ print(f" Cognitive turns: {evidence['cognitive_turns_total']}")
3776
+ if evidence["cognitive_turns_by_state"]:
3726
3777
  states = ", ".join(
3727
- f"{state}: {count}" for state, count in sorted(evidence["turns_by_state"].items())
3778
+ f"{state}: {count}"
3779
+ for state, count in sorted(evidence["cognitive_turns_by_state"].items())
3728
3780
  )
3729
3781
  print(f" Turn states: {states}")
3730
- print(" Retrieval control: observation only")
3782
+ print(f" Bounded Loop observations: {external['receipts_total']}")
3783
+ print(f" Correction cases: {corrections['cases_total']}")
3784
+ print(
3785
+ " Retrieval control: observation only; does not change recall, "
3786
+ "ranking, or model routing"
3787
+ )
3731
3788
 
3732
3789
 
3733
3790
  def cmd_observe(args: Namespace) -> None:
@@ -484,6 +484,18 @@ def main() -> None:
484
484
  update_p.add_argument("content", help="New content for the memory")
485
485
  update_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
486
486
 
487
+ correction_p = sub.add_parser(
488
+ "review-correction", help="Apply, reject, or roll back a reviewed correction case"
489
+ )
490
+ correction_p.add_argument("case_id", help="Correction case ID returned by slm update")
491
+ correction_p.add_argument("action", choices=("apply", "reject", "rollback"))
492
+ correction_p.add_argument("expected_version", type=int, help="Current case version (CAS guard)")
493
+ correction_p.add_argument(
494
+ "--event-valid-until",
495
+ help="Optional reviewer-approved RFC3339 event-time boundary (apply only)",
496
+ )
497
+ correction_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
498
+
487
499
  list_p = sub.add_parser("list", help="List recent memories chronologically (shows IDs for delete/update)")
488
500
  list_p.add_argument(
489
501
  "--limit", "-n", type=int, default=20, help="Number of entries (default 20)",
@@ -385,6 +385,7 @@ def read_entry_fast(
385
385
  db_path: Path | None = None,
386
386
  home_dir: Path | None = None,
387
387
  profile_id: str | None = None,
388
+ require_current_admission: bool = False,
388
389
  ) -> CacheEntry | None:
389
390
  """Hot-path reader used by the UserPromptSubmit hook.
390
391
 
@@ -465,7 +466,7 @@ def read_entry_fast(
465
466
  except (ValueError, TypeError):
466
467
  fact_ids = []
467
468
 
468
- return CacheEntry(
469
+ entry = CacheEntry(
469
470
  session_id=session_id,
470
471
  topic_sig=topic_sig,
471
472
  content=row[0],
@@ -474,10 +475,66 @@ def read_entry_fast(
474
475
  computed_at=int(row[3]),
475
476
  byte_size=int(row[4]),
476
477
  )
478
+ if require_current_admission and not _cache_fact_ids_are_current(home, entry.fact_ids):
479
+ return None
480
+ return entry
477
481
  except Exception: # pragma: no cover — last-resort fail-open
478
482
  return None
479
483
 
480
484
 
485
+ def _cache_fact_ids_are_current(home: Path, fact_ids: list[str]) -> bool:
486
+ """Prove cached source IDs remain current without loading the engine.
487
+
488
+ This is a cache-hit-only backstop for review-gated corrections. It uses a
489
+ small read-only SQLite query and treats any unavailable or malformed memory
490
+ state as a cache miss. Normal recall remains the authority after a miss.
491
+ """
492
+ if not fact_ids or not all(isinstance(fact_id, str) and fact_id for fact_id in fact_ids):
493
+ return False
494
+ memory_path = home / "memory.db"
495
+ if not memory_path.exists():
496
+ return False
497
+ try:
498
+ conn = sqlite3.connect(f"file:{memory_path}?mode=ro", uri=True, timeout=0.25)
499
+ try:
500
+ placeholders = ",".join("?" for _ in fact_ids)
501
+ tables = {
502
+ row[0]
503
+ for row in conn.execute(
504
+ "SELECT name FROM sqlite_master WHERE type='table' "
505
+ "AND name IN ('atomic_facts', 'fact_temporal_validity', 'correction_cases')"
506
+ )
507
+ }
508
+ if {"atomic_facts", "fact_temporal_validity"} - tables:
509
+ return False
510
+ correction_join = ""
511
+ correction_bad = "0"
512
+ if "correction_cases" in tables:
513
+ correction_join = (
514
+ " LEFT JOIN correction_cases cc ON cc.successor_fact_id=f.fact_id "
515
+ "AND cc.status IN ('proposed', 'rejected', 'rolled_back')"
516
+ )
517
+ correction_bad = "cc.successor_fact_id IS NOT NULL"
518
+ rows = conn.execute(
519
+ "SELECT f.fact_id, tv.system_expired_at, " + correction_bad + " AS correction_blocked "
520
+ "FROM atomic_facts f "
521
+ "LEFT JOIN fact_temporal_validity tv ON tv.fact_id=f.fact_id "
522
+ "AND tv.profile_id=f.profile_id "
523
+ + correction_join
524
+ + f" WHERE f.fact_id IN ({placeholders})",
525
+ tuple(fact_ids),
526
+ ).fetchall()
527
+ finally:
528
+ conn.close()
529
+ except sqlite3.Error:
530
+ return False
531
+ found = {str(row[0]) for row in rows}
532
+ return (
533
+ found == set(fact_ids)
534
+ and all(row[1] is None and not bool(row[2]) for row in rows)
535
+ )
536
+
537
+
481
538
  def purge_profile_from_cache_db(db_path: Path, profile_id: str) -> int:
482
539
  """Delete all ``context_entries`` rows for *profile_id* from a cache DB file.
483
540