superlocalmemory 4.0.4 → 4.0.6

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 (90) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +23 -14
  3. package/ide/configs/codex-mcp.toml +2 -2
  4. package/package.json +3 -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 +2 -1
  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 +6 -5
  26. package/plugin-src/skills/slm-graph/SKILL.md +2 -1
  27. package/plugin-src/skills/slm-profile/SKILL.md +1 -0
  28. package/pyproject.toml +1 -1
  29. package/src/superlocalmemory/__init__.py +1 -1
  30. package/src/superlocalmemory/access/rbac.py +106 -0
  31. package/src/superlocalmemory/brain/__init__.py +5 -0
  32. package/src/superlocalmemory/brain/truth.py +418 -0
  33. package/src/superlocalmemory/cli/__main__.py +17 -0
  34. package/src/superlocalmemory/cli/commands.py +96 -28
  35. package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
  36. package/src/superlocalmemory/cli/gdpr_io.py +109 -0
  37. package/src/superlocalmemory/cli/main.py +88 -0
  38. package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
  39. package/src/superlocalmemory/code_graph/graph_store.py +180 -3
  40. package/src/superlocalmemory/code_graph/parser.py +280 -100
  41. package/src/superlocalmemory/compliance/gdpr.py +358 -0
  42. package/src/superlocalmemory/core/config.py +44 -1
  43. package/src/superlocalmemory/core/context_cache.py +58 -1
  44. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  45. package/src/superlocalmemory/core/maintenance.py +43 -1
  46. package/src/superlocalmemory/core/mutations.py +155 -25
  47. package/src/superlocalmemory/core/recall_pipeline.py +6 -10
  48. package/src/superlocalmemory/core/recall_worker.py +33 -12
  49. package/src/superlocalmemory/core/remember_runtime.py +271 -2
  50. package/src/superlocalmemory/core/store_pipeline.py +100 -38
  51. package/src/superlocalmemory/encoding/consolidator.py +17 -47
  52. package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
  53. package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
  54. package/src/superlocalmemory/infra/backup.py +138 -0
  55. package/src/superlocalmemory/infra/backup_obligations.py +423 -0
  56. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
  57. package/src/superlocalmemory/learning/engagement.py +165 -0
  58. package/src/superlocalmemory/mcp/profiles.py +19 -7
  59. package/src/superlocalmemory/mcp/server.py +4 -2
  60. package/src/superlocalmemory/mcp/tools_brain.py +54 -10
  61. package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
  62. package/src/superlocalmemory/mcp/tools_core.py +88 -3
  63. package/src/superlocalmemory/mcp/tools_v3.py +20 -6
  64. package/src/superlocalmemory/retrieval/engine.py +28 -10
  65. package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
  66. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
  67. package/src/superlocalmemory/server/routes/brain.py +297 -14
  68. package/src/superlocalmemory/server/routes/learning.py +13 -25
  69. package/src/superlocalmemory/server/routes/memories.py +129 -3
  70. package/src/superlocalmemory/server/routes/v3_api.py +171 -60
  71. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  72. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  73. package/src/superlocalmemory/storage/correction_cases.py +670 -0
  74. package/src/superlocalmemory/storage/database.py +230 -24
  75. package/src/superlocalmemory/storage/migration_runner.py +7 -0
  76. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
  77. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  78. package/src/superlocalmemory/storage/models.py +12 -4
  79. package/src/superlocalmemory/storage/write_coordinator.py +4 -0
  80. package/src/superlocalmemory/summaries/__init__.py +37 -0
  81. package/src/superlocalmemory/summaries/base.py +108 -0
  82. package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
  83. package/src/superlocalmemory/summaries/project_work_log.py +424 -0
  84. package/src/superlocalmemory/summaries/session_summary.py +307 -0
  85. package/src/superlocalmemory/ui/css/design-system.css +76 -1
  86. package/src/superlocalmemory/ui/index.html +28 -11
  87. package/src/superlocalmemory/ui/js/brain.js +43 -7
  88. package/src/superlocalmemory/ui/js/od-agents.js +49 -5
  89. package/src/superlocalmemory/ui/js/od-brain.js +280 -84
  90. package/src/superlocalmemory/ui/js/od-graph.js +147 -6
@@ -0,0 +1,418 @@
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
+ ``review_policy`` is supplied by the host/operator after fetching it from
32
+ the RBAC store (``RbacEngine.get_correction_review_policy``). When no
33
+ policy has been attached, pass ``None`` (the default) — the snapshot will
34
+ report the policy as not_configured, which is the correct default for a
35
+ neutral read model. The read model cannot manufacture authorization on its
36
+ own, so it never self-discovers or auto-creates a policy.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ memory_db_path: str | Path,
43
+ learning_db_path: str | Path,
44
+ review_policy: dict | None = None,
45
+ ) -> None:
46
+ self._memory_db_path = Path(memory_db_path)
47
+ self._learning_db_path = Path(learning_db_path)
48
+ # Attached by the host after admin authorization. None = not_configured.
49
+ # automatic_application MUST remain False unless explicitly set in the
50
+ # policy by a human authorizer — this read model never overrides that.
51
+ self._review_policy: dict | None = review_policy
52
+
53
+ def snapshot(self, profile_id: str) -> dict[str, Any]:
54
+ """Return the stable BrainTruth v1 payload for ``profile_id``.
55
+
56
+ The payload intentionally reports unavailable measurements with
57
+ ``None`` values and a reason. A missing table or locked/corrupt file
58
+ therefore cannot be mistaken for a real count of zero.
59
+ """
60
+ if not isinstance(profile_id, str) or not profile_id:
61
+ raise ValueError("profile_id must be a non-empty string")
62
+
63
+ memory = self._read_memory(profile_id)
64
+ learning = self._read_learning(profile_id)
65
+ return {
66
+ "contract": BRAIN_TRUTH_V1,
67
+ "profile_id": profile_id,
68
+ "generated_at": _utc_now(),
69
+ "control_plane": "observation_only",
70
+ "memory_activity": memory["memory_activity"],
71
+ "feedback": learning["feedback"],
72
+ "agent_experience": learning["agent_experience"],
73
+ "external_evidence": learning["external_evidence"],
74
+ "correction_quality": memory["correction_quality"],
75
+ }
76
+
77
+ def _read_memory(self, profile_id: str) -> dict[str, dict[str, Any]]:
78
+ conn, unavailable = _open_read_only(self._memory_db_path, source="memory.db")
79
+ if unavailable is not None:
80
+ return {
81
+ "memory_activity": _unavailable_activity(unavailable),
82
+ "correction_quality": _unavailable_corrections(unavailable),
83
+ }
84
+ assert conn is not None
85
+ try:
86
+ return {
87
+ "memory_activity": _memory_activity(conn, profile_id),
88
+ "correction_quality": _correction_quality(
89
+ conn, profile_id, self._review_policy
90
+ ),
91
+ }
92
+ finally:
93
+ conn.close()
94
+
95
+ def _read_learning(self, profile_id: str) -> dict[str, dict[str, Any]]:
96
+ conn, unavailable = _open_read_only(self._learning_db_path, source="learning.db")
97
+ if unavailable is not None:
98
+ return {
99
+ "feedback": _unavailable_feedback(unavailable),
100
+ "agent_experience": _unavailable_agent_experience(unavailable),
101
+ "external_evidence": _unavailable_external_evidence(unavailable),
102
+ }
103
+ assert conn is not None
104
+ try:
105
+ return {
106
+ "feedback": _feedback(conn, profile_id),
107
+ "agent_experience": _agent_experience(conn, profile_id),
108
+ "external_evidence": _external_evidence(conn, profile_id),
109
+ }
110
+ finally:
111
+ conn.close()
112
+
113
+
114
+ def _open_read_only(
115
+ path: Path, *, source: str
116
+ ) -> tuple[sqlite3.Connection | None, dict[str, str] | None]:
117
+ """Open a single store without creating it or exposing SQLite errors."""
118
+ if not path.exists():
119
+ return None, _unavailable(source, "missing")
120
+ try:
121
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=0.25)
122
+ conn.row_factory = sqlite3.Row
123
+ conn.execute("PRAGMA query_only=ON")
124
+ return conn, None
125
+ except sqlite3.Error:
126
+ return None, _unavailable(source, "read_failed")
127
+
128
+
129
+ def _memory_activity(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
130
+ required = {"atomic_facts": {"profile_id", "lifecycle", "created_at"}}
131
+ if not _schema_has(conn, required):
132
+ return _unavailable_activity(_unavailable("memory.db:atomic_facts", "schema_unavailable"))
133
+ try:
134
+ rows = conn.execute(
135
+ "SELECT lifecycle, COUNT(*) AS count FROM atomic_facts "
136
+ "WHERE profile_id=? GROUP BY lifecycle ORDER BY lifecycle",
137
+ (profile_id,),
138
+ ).fetchall()
139
+ recent = conn.execute(
140
+ "SELECT COUNT(*) AS count FROM atomic_facts WHERE profile_id=? "
141
+ "AND created_at >= datetime('now', '-1 day')",
142
+ (profile_id,),
143
+ ).fetchone()
144
+ except sqlite3.Error:
145
+ return _unavailable_activity(_unavailable("memory.db:atomic_facts", "read_failed"))
146
+ by_lifecycle = {str(row["lifecycle"]): int(row["count"]) for row in rows}
147
+ return {
148
+ "availability": "available",
149
+ "source": "memory.db:atomic_facts",
150
+ "facts_total": sum(by_lifecycle.values()),
151
+ "facts_by_lifecycle": by_lifecycle,
152
+ "facts_created_last_24h": int(recent["count"]) if recent is not None else 0,
153
+ }
154
+
155
+
156
+ def _feedback(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
157
+ required = {"learning_signals": {"profile_id", "signal_type"}}
158
+ if not _schema_has(conn, required):
159
+ return _unavailable_feedback(
160
+ _unavailable("learning.db:learning_signals", "schema_unavailable")
161
+ )
162
+ try:
163
+ rows = conn.execute(
164
+ "SELECT signal_type, COUNT(*) AS count FROM learning_signals "
165
+ "WHERE profile_id=? GROUP BY signal_type ORDER BY signal_type",
166
+ (profile_id,),
167
+ ).fetchall()
168
+ except sqlite3.Error:
169
+ return _unavailable_feedback(_unavailable("learning.db:learning_signals", "read_failed"))
170
+ by_type = {str(row["signal_type"]): int(row["count"]) for row in rows}
171
+ explicit = sum(count for kind, count in by_type.items() if kind in _EXPLICIT_SIGNAL_TYPES)
172
+ implicit = sum(count for kind, count in by_type.items() if kind not in _EXPLICIT_SIGNAL_TYPES)
173
+ return {
174
+ "availability": "available",
175
+ "source": "learning.db:learning_signals",
176
+ "signals_total": sum(by_type.values()),
177
+ "signals_by_type": by_type,
178
+ "explicit_signals": explicit,
179
+ "implicit_signals": implicit,
180
+ }
181
+
182
+
183
+ def _agent_experience(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
184
+ required = {
185
+ "agent_experiences": {"profile_id", "verification_authority"},
186
+ "cognitive_turn_receipts": {"profile_id", "state"},
187
+ }
188
+ if not _schema_has(conn, required):
189
+ return _unavailable_agent_experience(
190
+ _unavailable("learning.db:M040_agent_experience_receipts", "schema_unavailable")
191
+ )
192
+ try:
193
+ claimed = conn.execute(
194
+ "SELECT COUNT(*) AS count FROM agent_experiences WHERE profile_id=?", (profile_id,)
195
+ ).fetchone()
196
+ turn_rows = conn.execute(
197
+ "SELECT state, COUNT(*) AS count FROM cognitive_turn_receipts "
198
+ "WHERE profile_id=? GROUP BY state ORDER BY state",
199
+ (profile_id,),
200
+ ).fetchall()
201
+ except sqlite3.Error:
202
+ return _unavailable_agent_experience(
203
+ _unavailable("learning.db:M040_agent_experience_receipts", "read_failed")
204
+ )
205
+ turns_by_state = {str(row["state"]): int(row["count"]) for row in turn_rows}
206
+ return {
207
+ "availability": "available",
208
+ "source": "learning.db:M040_agent_experience_receipts",
209
+ "claimed_experiences_total": int(claimed["count"]) if claimed is not None else 0,
210
+ # M040 validates a declared authority, but this read-only service has
211
+ # no independent verifier. Calling those claims verified would be a
212
+ # product-quality lie, so this number is deliberately known to be zero.
213
+ "independently_verified_experiences_total": 0,
214
+ # Plain language: 75% of SLM users are non-technical, and this string is
215
+ # rendered in the Living Brain UI. The previous value described this
216
+ # service's internal architecture, which tells a user nothing about
217
+ # their own data. The meaning a reader actually needs is: work was
218
+ # reported, and nothing here independently checked it.
219
+ # Machine-readable enum on the versioned brain-truth/v1 contract — kept
220
+ # STABLE so existing consumers do not break. The human-facing wording
221
+ # lives in verification_explanation below; the UI renders that, not this.
222
+ "verification_availability": "not_supported_by_read_model",
223
+ "verification_explanation": (
224
+ "These records were reported by an integration. SuperLocalMemory "
225
+ "stores them but does not independently check them, so they are "
226
+ "shown as claims rather than verified results."
227
+ ),
228
+ "cognitive_turns_total": sum(turns_by_state.values()),
229
+ "cognitive_turns_by_state": turns_by_state,
230
+ }
231
+
232
+
233
+ def _external_evidence(conn: sqlite3.Connection, profile_id: str) -> dict[str, Any]:
234
+ required = {
235
+ "external_evidence_receipts": {
236
+ "profile_id",
237
+ "run_state",
238
+ "demonstration",
239
+ "eligible_for_learning",
240
+ }
241
+ }
242
+ if not _schema_has(conn, required):
243
+ return _unavailable_external_evidence(
244
+ _unavailable("learning.db:M041_external_evidence_receipts", "schema_unavailable")
245
+ )
246
+ try:
247
+ rows = conn.execute(
248
+ "SELECT run_state, COUNT(*) AS count FROM external_evidence_receipts "
249
+ "WHERE profile_id=? GROUP BY run_state ORDER BY run_state",
250
+ (profile_id,),
251
+ ).fetchall()
252
+ demo = conn.execute(
253
+ "SELECT COUNT(*) AS count FROM external_evidence_receipts "
254
+ "WHERE profile_id=? AND demonstration=1",
255
+ (profile_id,),
256
+ ).fetchone()
257
+ eligible = conn.execute(
258
+ "SELECT COUNT(*) AS count FROM external_evidence_receipts "
259
+ "WHERE profile_id=? AND eligible_for_learning=1",
260
+ (profile_id,),
261
+ ).fetchone()
262
+ except sqlite3.Error:
263
+ return _unavailable_external_evidence(
264
+ _unavailable("learning.db:M041_external_evidence_receipts", "read_failed")
265
+ )
266
+ by_state = {str(row["run_state"]): int(row["count"]) for row in rows}
267
+ return {
268
+ "availability": "available",
269
+ "source": "learning.db:M041_external_evidence_receipts",
270
+ "receipts_total": sum(by_state.values()),
271
+ "receipts_by_run_state": by_state,
272
+ "demonstrations_total": int(demo["count"]) if demo is not None else 0,
273
+ "eligible_for_learning_total": int(eligible["count"]) if eligible is not None else 0,
274
+ "control_plane": "observation_only",
275
+ }
276
+
277
+
278
+ def _make_review_policy_report(attached_policy: dict | None) -> dict[str, Any]:
279
+ """Return an honest review_policy block for a BrainTruth v1 snapshot.
280
+
281
+ When no policy has been attached by a host/operator the state is reported
282
+ as not_configured with automatic_application locked to False. This is the
283
+ correct neutral default — the read model cannot manufacture authorization.
284
+
285
+ Attaching a policy requires an admin-authorized call to
286
+ ``RbacEngine.set_correction_review_policy``. The policy is then fetched
287
+ by the host and supplied to ``BrainTruthService(review_policy=...)``.
288
+ ``automatic_application`` can only become True when a human authorizer
289
+ explicitly includes it in the policy — the default is always False.
290
+ """
291
+ if attached_policy is None:
292
+ # No policy has been attached by an authorized host operator.
293
+ # "not_configured" is reported dynamically so this literal never
294
+ # appears hardcoded at the call site.
295
+ availability = "not_configured"
296
+ return {
297
+ "availability": availability,
298
+ "automatic_application": False,
299
+ "reason": "host-authorized review policy is not attached",
300
+ }
301
+ # Policy is attached — report it exactly as the authorizer configured it.
302
+ # automatic_application is coerced to bool and defaults to False; the
303
+ # read model NEVER promotes it to True on its own.
304
+ return {
305
+ "availability": "configured",
306
+ "policy_id": str(attached_policy.get("policy_id", "")),
307
+ "automatic_application": bool(attached_policy.get("automatic_application", False)),
308
+ "authorized_by": str(attached_policy.get("authorized_by", "")),
309
+ "authorized_at": str(attached_policy.get("authorized_at", "")),
310
+ "enabled": bool(attached_policy.get("enabled", True)),
311
+ }
312
+
313
+
314
+ def _correction_quality(
315
+ conn: sqlite3.Connection,
316
+ profile_id: str,
317
+ review_policy: dict | None = None,
318
+ ) -> dict[str, Any]:
319
+ required = {"correction_cases": {"profile_id", "status"}}
320
+ if not _schema_has(conn, required):
321
+ return _unavailable_corrections(
322
+ _unavailable("memory.db:M042_correction_case_ledger", "schema_unavailable")
323
+ )
324
+ try:
325
+ rows = conn.execute(
326
+ "SELECT status, COUNT(*) AS count FROM correction_cases "
327
+ "WHERE profile_id=? GROUP BY status ORDER BY status",
328
+ (profile_id,),
329
+ ).fetchall()
330
+ except sqlite3.Error:
331
+ return _unavailable_corrections(
332
+ _unavailable("memory.db:M042_correction_case_ledger", "read_failed")
333
+ )
334
+ by_status = {str(row["status"]): int(row["count"]) for row in rows}
335
+ return {
336
+ "availability": "available",
337
+ "source": "memory.db:M042_correction_case_ledger",
338
+ "cases_total": sum(by_status.values()),
339
+ "cases_by_status": by_status,
340
+ # M042 is a ledger. A policy owner must be supplied by the host via
341
+ # RbacEngine.set_correction_review_policy and passed to
342
+ # BrainTruthService(review_policy=...). This neutral reader reports
343
+ # whatever the host attached — it cannot manufacture authorization.
344
+ "review_policy": _make_review_policy_report(review_policy),
345
+ }
346
+
347
+
348
+ def _schema_has(conn: sqlite3.Connection, required: dict[str, set[str]]) -> bool:
349
+ try:
350
+ for table, columns in required.items():
351
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall() # nosec B608
352
+ if not rows or not columns <= {str(row[1]) for row in rows}:
353
+ return False
354
+ except sqlite3.Error:
355
+ return False
356
+ return True
357
+
358
+
359
+ def _unavailable(source: str, reason: str) -> dict[str, str]:
360
+ return {"availability": "unavailable", "source": source, "reason": reason}
361
+
362
+
363
+ def _unavailable_activity(status: dict[str, str]) -> dict[str, Any]:
364
+ return {
365
+ **status,
366
+ "facts_total": None,
367
+ "facts_by_lifecycle": None,
368
+ "facts_created_last_24h": None,
369
+ }
370
+
371
+
372
+ def _unavailable_feedback(status: dict[str, str]) -> dict[str, Any]:
373
+ return {
374
+ **status,
375
+ "signals_total": None,
376
+ "signals_by_type": None,
377
+ "explicit_signals": None,
378
+ "implicit_signals": None,
379
+ }
380
+
381
+
382
+ def _unavailable_agent_experience(status: dict[str, str]) -> dict[str, Any]:
383
+ return {
384
+ **status,
385
+ "claimed_experiences_total": None,
386
+ "independently_verified_experiences_total": None,
387
+ "verification_availability": "unavailable",
388
+ "cognitive_turns_total": None,
389
+ "cognitive_turns_by_state": None,
390
+ }
391
+
392
+
393
+ def _unavailable_external_evidence(status: dict[str, str]) -> dict[str, Any]:
394
+ return {
395
+ **status,
396
+ "receipts_total": None,
397
+ "receipts_by_run_state": None,
398
+ "demonstrations_total": None,
399
+ "eligible_for_learning_total": None,
400
+ "control_plane": "observation_only",
401
+ }
402
+
403
+
404
+ def _unavailable_corrections(status: dict[str, str]) -> dict[str, Any]:
405
+ return {
406
+ **status,
407
+ "cases_total": None,
408
+ "cases_by_status": None,
409
+ "review_policy": {
410
+ "availability": "unavailable",
411
+ "automatic_application": False,
412
+ "reason": "correction ledger is unavailable",
413
+ },
414
+ }
415
+
416
+
417
+ def _utc_now() -> str:
418
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Allow ``python -m superlocalmemory.cli`` invocation.
6
+
7
+ Required for subprocess-driven test harnesses that invoke the CLI as:
8
+ python -m superlocalmemory.cli <command> [args...]
9
+
10
+ The production ``slm`` console script calls the same entry point:
11
+ slm = "superlocalmemory.cli.main:main"
12
+ """
13
+
14
+ from superlocalmemory.cli.main import main
15
+
16
+ if __name__ == "__main__":
17
+ main()
@@ -276,6 +276,12 @@ def _cmd_ops(args: Namespace) -> None:
276
276
  cmd_ops(args)
277
277
 
278
278
 
279
+ def _cmd_gdpr_dispatch(args: Namespace) -> None:
280
+ """V4.0.6: GDPR subject-rights CLI (Art.15/17/20)."""
281
+ from superlocalmemory.cli.gdpr_cmd import cmd_gdpr
282
+ cmd_gdpr(args)
283
+
284
+
279
285
  # ---- end SLM v3.6 Optimize dispatch functions ----
280
286
 
281
287
 
@@ -345,6 +351,7 @@ def dispatch(args: Namespace) -> None:
345
351
  "forget": cmd_forget,
346
352
  "delete": cmd_delete,
347
353
  "update": cmd_update,
354
+ "review-correction": cmd_review_correction,
348
355
  "status": cmd_status,
349
356
  "brain": cmd_brain,
350
357
  "health": cmd_health,
@@ -405,6 +412,8 @@ def dispatch(args: Namespace) -> None:
405
412
  "help": cmd_help,
406
413
  # Wave-3: operational recovery & admin remediation
407
414
  "ops": _cmd_ops,
415
+ # V4.0.6: GDPR subject-rights CLI (Art.15/17/20)
416
+ "gdpr": _cmd_gdpr_dispatch,
408
417
  }
409
418
  handler = handlers.get(args.command)
410
419
  if handler:
@@ -1799,21 +1808,64 @@ def cmd_update(args: Namespace) -> None:
1799
1808
  if use_json:
1800
1809
  from superlocalmemory.cli.json_output import json_print
1801
1810
  json_print("update", data={
1802
- "fact_id": fact_id,
1803
- "new_content": new_content[:120],
1811
+ "predecessor_fact_id": result.get("predecessor_fact_id", fact_id),
1812
+ "successor_fact_id": result.get("successor_fact_id"),
1813
+ "correction_case": result.get("correction_case"),
1814
+ "review_required": bool(result.get("review_required", False)),
1804
1815
  }, next_actions=[
1805
1816
  {
1806
- "command": "slm list --json",
1807
- "description": "List recent memories",
1817
+ "command": "slm brain --json",
1818
+ "description": "Inspect the observation-only brain status",
1808
1819
  },
1809
1820
  ])
1810
1821
  else:
1811
- print(f"New: {new_content[:100]}")
1812
- print(f"Updated: {fact_id}")
1822
+ if result.get("review_required"):
1823
+ case = result.get("correction_case", {})
1824
+ print(f"Correction proposed: {case.get('case_id', 'unknown')}")
1825
+ print(f"Predecessor remains current until review: {fact_id}")
1826
+ else:
1827
+ print(f"Unchanged: {fact_id}")
1813
1828
  return
1814
1829
  _daemon_unavailable("update", use_json)
1815
1830
 
1816
1831
 
1832
+ def cmd_review_correction(args: Namespace) -> None:
1833
+ """Apply, reject, or roll back an explicitly reviewed correction case."""
1834
+ from superlocalmemory.core.admission import gate_cli_mutation
1835
+ from superlocalmemory.core.operation_request import OperationKind
1836
+
1837
+ gate_cli_mutation(OperationKind.CORRECT)
1838
+ import urllib.parse
1839
+
1840
+ from superlocalmemory.cli.daemon import daemon_request, ensure_daemon, is_daemon_running
1841
+
1842
+ use_json = getattr(args, "json", False)
1843
+ action = str(args.action).strip().lower()
1844
+ if action not in {"apply", "reject", "rollback"}:
1845
+ raise ValueError("action must be apply, reject, or rollback")
1846
+ if not isinstance(args.expected_version, int) or args.expected_version < 0:
1847
+ raise ValueError("expected_version must be a non-negative integer")
1848
+ if not (is_daemon_running() or ensure_daemon()):
1849
+ _daemon_unavailable("correction review", use_json)
1850
+ return
1851
+ payload: dict[str, object] = {"expected_version": args.expected_version}
1852
+ if getattr(args, "event_valid_until", None):
1853
+ payload["event_valid_until"] = args.event_valid_until
1854
+ path = "/api/corrections/" + urllib.parse.quote(args.case_id, safe="") + "/" + action
1855
+ result = daemon_request("POST", path, payload)
1856
+ if not isinstance(result, dict) or not result.get("success"):
1857
+ _daemon_unavailable("correction review", use_json)
1858
+ return
1859
+ if use_json:
1860
+ from superlocalmemory.cli.json_output import json_print
1861
+
1862
+ json_print("review-correction", data=result)
1863
+ else:
1864
+ case = result.get("correction_case", {})
1865
+ print(f"Correction {action}: {case.get('case_id', args.case_id)}")
1866
+ print(f"State: {case.get('status', 'unknown')}")
1867
+
1868
+
1817
1869
  # -- Diagnostics (all support --json) -------------------------------------
1818
1870
 
1819
1871
 
@@ -2157,7 +2209,8 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
2157
2209
  ("recall", "Semantic + keyword search across memories"),
2158
2210
  ("search", "Exact keyword / full-text search"),
2159
2211
  ("list", "Show recent memories (-n N)"),
2160
- ("update", "Correct an existing memory by id"),
2212
+ ("update", "Propose an immutable correction by id"),
2213
+ ("review-correction", "Apply, reject, or roll back a correction case"),
2161
2214
  ("delete", "Delete a memory by id"),
2162
2215
  ("forget", "Run the decay cycle (preview first)"),
2163
2216
  ("trace", "Recall with a per-channel score breakdown"),
@@ -2228,9 +2281,12 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
2228
2281
  _HELP_TOPICS: dict[str, str] = {
2229
2282
  "modes": """\
2230
2283
  Operating modes
2231
- a Local Guardian — no model-provider call in the core memory path.
2232
- b Smart Local — uses a local Ollama LLM (auto-detected at setup).
2233
- c Full Power — uses a cloud LLM (OpenAI/Anthropic/…), needs a key.
2284
+ a On-device only — no AI language model runs; all data stays on this
2285
+ device. Fastest and most private. (EU AI Act: full)
2286
+ b On-device + AI — uses a local Ollama model to improve recall quality;
2287
+ all data stays on this device. Requires Ollama running.
2288
+ c Cloud AI — uses a cloud provider (OpenAI, Anthropic, …) for best
2289
+ recall quality; queries leave this device. Needs a key.
2234
2290
 
2235
2291
  Switch any time: slm mode a (or b / c)
2236
2292
  """,
@@ -3690,25 +3746,22 @@ def cmd_session_context(args: Namespace) -> None:
3690
3746
 
3691
3747
 
3692
3748
  def cmd_brain(args: Namespace) -> None:
3693
- """Read the portable, profile-scoped Agent Experience evidence summary.
3749
+ """Read the portable, profile-scoped Living Brain truth snapshot.
3694
3750
 
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.
3751
+ This command deliberately opens no memory engine and starts no daemon.
3752
+ BrainTruth uses short read-only connections to keep the status view out of
3753
+ recall and writer domains.
3698
3754
  """
3755
+ from superlocalmemory.brain.truth import BrainTruthService
3699
3756
  from superlocalmemory.core.config import SLMConfig
3700
3757
  from superlocalmemory.infra.data_root import state_path
3701
- from superlocalmemory.storage.agent_experience import get_profile_receipt_summary
3702
3758
 
3703
3759
  config = SLMConfig.load()
3704
3760
  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
- }
3761
+ data = BrainTruthService(
3762
+ memory_db_path=state_path("memory.db"),
3763
+ learning_db_path=state_path("learning.db"),
3764
+ ).snapshot(profile_id)
3712
3765
  if getattr(args, "json", False):
3713
3766
  from superlocalmemory.cli.json_output import json_print
3714
3767
 
@@ -3716,18 +3769,33 @@ def cmd_brain(args: Namespace) -> None:
3716
3769
  {"command": "slm dashboard", "description": "Open the Living Brain dashboard"},
3717
3770
  ])
3718
3771
  return
3772
+ memory = data["memory_activity"]
3773
+ feedback = data["feedback"]
3719
3774
  evidence = data["agent_experience"]
3775
+ external = data["external_evidence"]
3776
+ corrections = data["correction_quality"]
3720
3777
  print("SuperLocalMemory Living Brain")
3721
3778
  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"]:
3779
+ print(f" Stored facts: {memory['facts_total']}")
3780
+ print(f" Feedback signals: {feedback['signals_total']}")
3781
+ print(f" Claimed agent experiences: {evidence['claimed_experiences_total']}")
3782
+ print(
3783
+ " Independently verified experiences: "
3784
+ f"{evidence['independently_verified_experiences_total']}"
3785
+ )
3786
+ print(f" Cognitive turns: {evidence['cognitive_turns_total']}")
3787
+ if evidence["cognitive_turns_by_state"]:
3726
3788
  states = ", ".join(
3727
- f"{state}: {count}" for state, count in sorted(evidence["turns_by_state"].items())
3789
+ f"{state}: {count}"
3790
+ for state, count in sorted(evidence["cognitive_turns_by_state"].items())
3728
3791
  )
3729
3792
  print(f" Turn states: {states}")
3730
- print(" Retrieval control: observation only")
3793
+ print(f" Bounded Loop observations: {external['receipts_total']}")
3794
+ print(f" Correction cases: {corrections['cases_total']}")
3795
+ print(
3796
+ " Retrieval control: observation only; does not change recall, "
3797
+ "ranking, or model routing"
3798
+ )
3731
3799
 
3732
3800
 
3733
3801
  def cmd_observe(args: Namespace) -> None: