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.
- package/CHANGELOG.md +55 -0
- package/README.md +19 -13
- package/ide/configs/codex-mcp.toml +2 -2
- package/package.json +1 -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 +5 -4
- 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 +7 -6
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +3 -2
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -4
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/brain/__init__.py +5 -0
- package/src/superlocalmemory/brain/truth.py +348 -0
- package/src/superlocalmemory/cli/commands.py +82 -25
- package/src/superlocalmemory/cli/main.py +12 -0
- package/src/superlocalmemory/core/context_cache.py +58 -1
- package/src/superlocalmemory/core/mutations.py +155 -25
- package/src/superlocalmemory/core/recall_pipeline.py +6 -10
- 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/integrations/bounded_loops_mcp.py +185 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/profiles.py +25 -7
- package/src/superlocalmemory/mcp/server.py +7 -2
- package/src/superlocalmemory/mcp/tools_brain.py +138 -9
- package/src/superlocalmemory/mcp/tools_core.py +88 -3
- package/src/superlocalmemory/retrieval/engine.py +7 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
- package/src/superlocalmemory/server/routes/brain.py +21 -1
- package/src/superlocalmemory/server/routes/memories.py +129 -3
- package/src/superlocalmemory/storage/_migration_internals.py +8 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +26 -4
- package/src/superlocalmemory/storage/correction_cases.py +670 -0
- package/src/superlocalmemory/storage/database.py +194 -24
- package/src/superlocalmemory/storage/external_evidence.py +359 -0
- package/src/superlocalmemory/storage/migration_runner.py +12 -0
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/write_coordinator.py +4 -0
- package/src/superlocalmemory/ui/js/brain.js +43 -7
- 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
|
-
"
|
|
1803
|
-
"
|
|
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
|
|
1807
|
-
"description": "
|
|
1809
|
+
"command": "slm brain --json",
|
|
1810
|
+
"description": "Inspect the observation-only brain status",
|
|
1808
1811
|
},
|
|
1809
1812
|
])
|
|
1810
1813
|
else:
|
|
1811
|
-
|
|
1812
|
-
|
|
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", "
|
|
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
|
|
3738
|
+
"""Read the portable, profile-scoped Living Brain truth snapshot.
|
|
3694
3739
|
|
|
3695
|
-
This command deliberately opens no memory engine and starts no daemon.
|
|
3696
|
-
|
|
3697
|
-
|
|
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
|
-
"
|
|
3707
|
-
"
|
|
3708
|
-
|
|
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"
|
|
3723
|
-
print(f"
|
|
3724
|
-
print(f"
|
|
3725
|
-
|
|
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}"
|
|
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("
|
|
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
|
-
|
|
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
|
|