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.
- package/CHANGELOG.md +90 -0
- package/README.md +23 -14
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -0
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +3 -2
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +2 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +6 -5
- package/plugin-src/skills/slm-graph/SKILL.md +2 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -0
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +418 -0
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +96 -28
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +88 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/core/remember_runtime.py +271 -2
- package/src/superlocalmemory/core/store_pipeline.py +100 -38
- package/src/superlocalmemory/encoding/consolidator.py +17 -47
- package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/profiles.py +19 -7
- package/src/superlocalmemory/mcp/server.py +4 -2
- package/src/superlocalmemory/mcp/tools_brain.py +54 -10
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +28 -10
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +297 -14
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +230 -24
- package/src/superlocalmemory/storage/migration_runner.py +7 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +280 -84
- 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
|
-
"
|
|
1803
|
-
"
|
|
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
|
|
1807
|
-
"description": "
|
|
1817
|
+
"command": "slm brain --json",
|
|
1818
|
+
"description": "Inspect the observation-only brain status",
|
|
1808
1819
|
},
|
|
1809
1820
|
])
|
|
1810
1821
|
else:
|
|
1811
|
-
|
|
1812
|
-
|
|
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", "
|
|
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
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
|
3749
|
+
"""Read the portable, profile-scoped Living Brain truth snapshot.
|
|
3694
3750
|
|
|
3695
|
-
This command deliberately opens no memory engine and starts no daemon.
|
|
3696
|
-
|
|
3697
|
-
|
|
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
|
-
"
|
|
3707
|
-
"
|
|
3708
|
-
|
|
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"
|
|
3723
|
-
print(f"
|
|
3724
|
-
print(f"
|
|
3725
|
-
|
|
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}"
|
|
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("
|
|
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:
|