superlocalmemory 4.0.3 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +19 -13
  3. package/ide/configs/codex-mcp.toml +2 -2
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.mcp.json +1 -0
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +3 -2
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +5 -4
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +7 -6
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-graph/SKILL.md +3 -2
  30. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-profile/SKILL.md +5 -4
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +1 -1
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/brain/__init__.py +5 -0
  41. package/src/superlocalmemory/brain/truth.py +348 -0
  42. package/src/superlocalmemory/cli/commands.py +82 -25
  43. package/src/superlocalmemory/cli/main.py +12 -0
  44. package/src/superlocalmemory/core/context_cache.py +58 -1
  45. package/src/superlocalmemory/core/mutations.py +155 -25
  46. package/src/superlocalmemory/core/recall_pipeline.py +6 -10
  47. package/src/superlocalmemory/core/remember_runtime.py +271 -2
  48. package/src/superlocalmemory/core/store_pipeline.py +100 -38
  49. package/src/superlocalmemory/encoding/consolidator.py +17 -47
  50. package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
  51. package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
  52. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +185 -0
  53. package/src/superlocalmemory/learning/database.py +2 -1
  54. package/src/superlocalmemory/mcp/profiles.py +25 -7
  55. package/src/superlocalmemory/mcp/server.py +7 -2
  56. package/src/superlocalmemory/mcp/tools_brain.py +138 -9
  57. package/src/superlocalmemory/mcp/tools_core.py +88 -3
  58. package/src/superlocalmemory/retrieval/engine.py +7 -10
  59. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
  60. package/src/superlocalmemory/server/routes/brain.py +21 -1
  61. package/src/superlocalmemory/server/routes/memories.py +129 -3
  62. package/src/superlocalmemory/storage/_migration_internals.py +8 -0
  63. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  64. package/src/superlocalmemory/storage/agent_experience.py +26 -4
  65. package/src/superlocalmemory/storage/correction_cases.py +670 -0
  66. package/src/superlocalmemory/storage/database.py +194 -24
  67. package/src/superlocalmemory/storage/external_evidence.py +359 -0
  68. package/src/superlocalmemory/storage/migration_runner.py +12 -0
  69. package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
  70. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
  71. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  72. package/src/superlocalmemory/storage/write_coordinator.py +4 -0
  73. package/src/superlocalmemory/ui/js/brain.js +43 -7
  74. package/src/superlocalmemory/ui/js/od-brain.js +44 -19
@@ -52,6 +52,7 @@ from typing import Any
52
52
 
53
53
  from fastapi import APIRouter, Depends, HTTPException, Request
54
54
  from superlocalmemory import __version__
55
+ from superlocalmemory.brain import BrainTruthService
55
56
 
56
57
  from superlocalmemory.core.security_primitives import (
57
58
  redact_secrets,
@@ -327,7 +328,12 @@ def _compute_agent_experience(profile_id: str) -> dict:
327
328
  Receipt rows record observation and verification evidence only; this read
328
329
  model never alters retrieval, ranking, or model routing.
329
330
  """
330
- return get_profile_receipt_summary(_learning_db_path(), profile_id)
331
+ from superlocalmemory.storage.external_evidence import get_profile_external_evidence_summary
332
+ path = _learning_db_path()
333
+ return {
334
+ **get_profile_receipt_summary(path, profile_id),
335
+ "external_graph_evidence": get_profile_external_evidence_summary(path, profile_id),
336
+ }
331
337
 
332
338
 
333
339
  def _resolve_phase(signals: int, model_active: bool,
@@ -1125,11 +1131,16 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
1125
1131
  # "default" — the Brain must reflect whichever profile is active.
1126
1132
  profile_id = _authorized_profile(request, profile_id)
1127
1133
  lrn_db = LearningDatabase(_learning_db_path())
1134
+ truth_service = BrainTruthService(
1135
+ memory_db_path=_memory_dir() / "memory.db",
1136
+ learning_db_path=_learning_db_path(),
1137
+ )
1128
1138
 
1129
1139
  (
1130
1140
  preferences, learning, usage, bandit_snap, cache,
1131
1141
  cross_platform, outcomes_preview, evolution, active_clients,
1132
1142
  feedback_loop, source_quality, graph_summary, agent_experience,
1143
+ brain_truth,
1133
1144
  ) = await asyncio.gather(
1134
1145
  asyncio.to_thread(_compute_preferences, profile_id),
1135
1146
  asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
@@ -1147,6 +1158,7 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
1147
1158
  asyncio.to_thread(_compute_source_quality, profile_id),
1148
1159
  asyncio.to_thread(_compute_graph_summary, profile_id),
1149
1160
  asyncio.to_thread(_compute_agent_experience, profile_id),
1161
+ asyncio.to_thread(truth_service.snapshot, profile_id),
1150
1162
  return_exceptions=True,
1151
1163
  )
1152
1164
 
@@ -1208,6 +1220,14 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
1208
1220
  "source_quality": source_quality,
1209
1221
  "graph": graph_summary,
1210
1222
  "agent_experience": agent_experience,
1223
+ # Additive v4.0.5 contract. Keep the preceding legacy keys for
1224
+ # existing dashboard/API clients; new clients should render this
1225
+ # one shared, unavailable-aware snapshot.
1226
+ "brain_truth": _ok(brain_truth, {
1227
+ "availability": "unavailable",
1228
+ "source": "BrainTruth service unavailable",
1229
+ "control_plane": "observation_only",
1230
+ }),
1211
1231
  "source": "local durable stores + ephemeral session registry",
1212
1232
  },
1213
1233
  "evolution_preview": _ok(evolution, {
@@ -1182,9 +1182,9 @@ async def merge_memory(request: Request, fact_id: str):
1182
1182
  raise _canonical_mutation_error(exc, "Merge error")
1183
1183
 
1184
1184
 
1185
- @router.patch("/api/memories/{fact_id}")
1185
+ @router.patch("/api/memories/{fact_id}", status_code=202)
1186
1186
  async def edit_memory(request: Request, fact_id: str):
1187
- """Edit the content of a specific memory (atomic fact)."""
1187
+ """Propose an immutable, review-required correction for one memory."""
1188
1188
  try:
1189
1189
  body = await request.json()
1190
1190
  new_content = (body.get("content") or "").strip()
@@ -1210,13 +1210,139 @@ async def edit_memory(request: Request, fact_id: str):
1210
1210
  )
1211
1211
  if not result.get("ok"):
1212
1212
  raise HTTPException(status_code=404, detail="Memory not found")
1213
- return {"success": True, "fact_id": fact_id, "content": new_content}
1213
+ if result.get("unchanged"):
1214
+ return {"success": True, "fact_id": fact_id, "content": new_content, "unchanged": True}
1215
+ correction = result["correction_case"]
1216
+ return {
1217
+ "success": True,
1218
+ "fact_id": fact_id,
1219
+ "predecessor_fact_id": result["predecessor_fact_id"],
1220
+ "successor_fact_id": result["successor_fact_id"],
1221
+ "correction_case": correction,
1222
+ "review_required": True,
1223
+ "status": "proposed",
1224
+ }
1214
1225
  except HTTPException:
1215
1226
  raise
1216
1227
  except Exception as exc:
1217
1228
  raise _canonical_mutation_error(exc, "Edit error")
1218
1229
 
1219
1230
 
1231
+ @router.post("/api/corrections/{case_id}/{action}")
1232
+ async def review_correction(request: Request, case_id: str, action: str):
1233
+ """Apply, reject, or roll back an active-profile correction case.
1234
+
1235
+ The caller authenticates through the daemon boundary. It cannot select a
1236
+ profile, fact scope, or trust tier; the canonical writer rechecks all of
1237
+ those fields in its one SQLite transaction.
1238
+ """
1239
+ try:
1240
+ body = await request.json()
1241
+ if action not in {"apply", "reject", "rollback"}:
1242
+ raise HTTPException(422, detail="action must be apply, reject, or rollback")
1243
+ expected_version = body.get("expected_version") if isinstance(body, dict) else None
1244
+ if not isinstance(expected_version, int) or isinstance(expected_version, bool):
1245
+ raise HTTPException(422, detail="expected_version must be a non-negative integer")
1246
+ if expected_version < 0:
1247
+ raise HTTPException(422, detail="expected_version must be a non-negative integer")
1248
+ event_valid_until = body.get("event_valid_until") if isinstance(body, dict) else None
1249
+ if event_valid_until is not None and not isinstance(event_valid_until, str):
1250
+ raise HTTPException(422, detail="event_valid_until must be an RFC3339 timestamp")
1251
+ if event_valid_until is not None and action != "apply":
1252
+ raise HTTPException(422, detail="event_valid_until is permitted only for apply")
1253
+ engine, active_profile, hook_context = _authorize_memory_mutation(
1254
+ request, "update", case_id, run_pre_hook=False
1255
+ )
1256
+ result = _canonical_mutation_runtime(request).transition_correction(
1257
+ active_profile,
1258
+ case_id,
1259
+ action=action,
1260
+ expected_version=expected_version,
1261
+ actor_id=hook_context["agent_id"],
1262
+ event_valid_until=event_valid_until,
1263
+ idempotency_key=_mutation_idempotency_key(request),
1264
+ )
1265
+ if not result.get("ok"):
1266
+ raise HTTPException(404, detail="Correction case not found")
1267
+ if action in {"apply", "rollback"}:
1268
+ from superlocalmemory.core.mutations import purge_profile_context_cache
1269
+
1270
+ purge_profile_context_cache(engine, active_profile)
1271
+ engine._hooks.run_post("update", hook_context)
1272
+ return {"success": True, "correction_case": result}
1273
+ except HTTPException:
1274
+ raise
1275
+ except Exception as exc:
1276
+ raise _canonical_mutation_error(exc, "Correction review error")
1277
+
1278
+
1279
+ def _correction_case_response(case) -> dict[str, object]:
1280
+ """Return review metadata only; correction ledgers never contain fact text."""
1281
+ return {
1282
+ "case_id": case.case_id,
1283
+ "profile_id": case.profile_id,
1284
+ "scope": case.scope,
1285
+ "predecessor_fact_id": case.predecessor_fact_id,
1286
+ "successor_fact_id": case.successor_fact_id,
1287
+ "reason_code": case.reason_code,
1288
+ "status": case.status,
1289
+ "version": case.version,
1290
+ "created_at": case.created_at,
1291
+ "updated_at": case.updated_at,
1292
+ "reviewed_at": case.reviewed_at,
1293
+ "applied_at": case.applied_at,
1294
+ "system_effective_at": case.system_effective_at,
1295
+ "event_valid_from": case.event_valid_from,
1296
+ "event_valid_until": case.event_valid_until,
1297
+ }
1298
+
1299
+
1300
+ def _correction_store_for(engine, active_profile: str):
1301
+ from superlocalmemory.storage.correction_cases import CorrectionCaseStore
1302
+
1303
+ return CorrectionCaseStore(
1304
+ engine._db.db_path,
1305
+ is_profile_active=lambda candidate: candidate == active_profile,
1306
+ # Read operations never invoke this callback; writes use the daemon's
1307
+ # canonical runtime, which derives the authenticated actor separately.
1308
+ is_actor_trusted=lambda _actor: False,
1309
+ )
1310
+
1311
+
1312
+ @router.get("/api/corrections")
1313
+ async def list_corrections(request: Request, limit: int = 100):
1314
+ """List bounded review metadata for the active owning profile."""
1315
+ try:
1316
+ engine, active_profile, _context = _authorize_memory_mutation(
1317
+ request, "update", "correction-list", run_pre_hook=False
1318
+ )
1319
+ cases = _correction_store_for(engine, active_profile).list_cases(active_profile, limit=limit)
1320
+ return {"success": True, "corrections": [_correction_case_response(case) for case in cases]}
1321
+ except HTTPException:
1322
+ raise
1323
+ except Exception as exc:
1324
+ raise _canonical_mutation_error(exc, "Correction list error")
1325
+
1326
+
1327
+ @router.get("/api/corrections/{case_id}")
1328
+ async def get_correction(request: Request, case_id: str):
1329
+ """Get one active-profile correction case without exposing raw memory text."""
1330
+ try:
1331
+ engine, active_profile, _context = _authorize_memory_mutation(
1332
+ request, "update", case_id, run_pre_hook=False
1333
+ )
1334
+ case = _correction_store_for(engine, active_profile).get_case(case_id)
1335
+ return {"success": True, "correction": _correction_case_response(case)}
1336
+ except HTTPException:
1337
+ raise
1338
+ except Exception as exc:
1339
+ from superlocalmemory.storage.correction_cases import CorrectionNotFoundError
1340
+
1341
+ if isinstance(exc, CorrectionNotFoundError):
1342
+ raise HTTPException(404, detail="Correction case not found") from exc
1343
+ raise _canonical_mutation_error(exc, "Correction lookup error")
1344
+
1345
+
1220
1346
  _VALID_SCOPES = ("personal", "shared", "global")
1221
1347
 
1222
1348
 
@@ -147,6 +147,12 @@ from superlocalmemory.storage.migrations import (
147
147
  from superlocalmemory.storage.migrations import (
148
148
  M040_agent_experience_receipts as _M040,
149
149
  )
150
+ from superlocalmemory.storage.migrations import (
151
+ M041_external_evidence_receipts as _M041,
152
+ )
153
+ from superlocalmemory.storage.migrations import (
154
+ M042_correction_case_ledger as _M042,
155
+ )
150
156
 
151
157
  # Emit under the runner's logger name so operational log filters that key on
152
158
  # "superlocalmemory.storage.migration_runner" keep matching after this split.
@@ -195,6 +201,8 @@ _MODULES = {
195
201
  _M038.NAME: _M038,
196
202
  _M039.NAME: _M039,
197
203
  _M040.NAME: _M040,
204
+ _M041.NAME: _M041,
205
+ _M042.NAME: _M042,
198
206
  }
199
207
 
200
208
  # Exact historical DDL fingerprints whose resulting schema is intentionally
@@ -21,9 +21,9 @@ import sqlite3
21
21
  from pathlib import Path
22
22
 
23
23
  #: Highest schema_version this runner can write. Matches the trailing serial
24
- #: of the latest migration (M040). Increment when adding new migrations or
24
+ #: of the latest migration (M042). Increment when adding new migrations or
25
25
  #: table-level breaking changes.
26
- SUPPORTED_SCHEMA_VERSION: int = 40
26
+ SUPPORTED_SCHEMA_VERSION: int = 42
27
27
 
28
28
 
29
29
  class SchemaVersionError(RuntimeError):
@@ -245,17 +245,29 @@ class AgentExperienceStore:
245
245
  turn_count = conn.execute(
246
246
  "DELETE FROM cognitive_turn_receipts WHERE profile_id=?", (profile_id,)
247
247
  ).rowcount
248
+ external_count = 0
249
+ has_external = conn.execute(
250
+ "SELECT 1 FROM sqlite_master WHERE type='table' "
251
+ "AND name='external_evidence_receipts'"
252
+ ).fetchone() is not None
253
+ if has_external:
254
+ external_count = conn.execute(
255
+ "DELETE FROM external_evidence_receipts WHERE profile_id=?", (profile_id,)
256
+ ).rowcount
257
+ receipt_tables = ["agent_experiences", "cognitive_turn_receipts"]
258
+ if has_external:
259
+ receipt_tables.append("external_evidence_receipts")
248
260
  residue = sum(
249
261
  int(
250
262
  conn.execute(
251
263
  f"SELECT COUNT(*) FROM {table} WHERE profile_id=?", (profile_id,)
252
264
  ).fetchone()[0]
253
265
  )
254
- for table in ("agent_experiences", "cognitive_turn_receipts")
266
+ for table in receipt_tables
255
267
  )
256
268
  if residue:
257
269
  raise RuntimeError("learning receipt erasure left profile residue")
258
- return experience_count + turn_count
270
+ return experience_count + turn_count + external_count
259
271
 
260
272
  return self._write(erase)
261
273
 
@@ -422,7 +434,7 @@ def purge_profile_receipts(
422
434
  for row in conn.execute(
423
435
  "SELECT name FROM sqlite_master WHERE type='table' "
424
436
  "AND name IN ('agent_experiences', 'cognitive_turn_receipts', "
425
- "'agent_receipt_profile_closures')"
437
+ "'agent_receipt_profile_closures', 'external_evidence_receipts')"
426
438
  )
427
439
  }
428
440
  if not tables:
@@ -431,6 +443,16 @@ def purge_profile_receipts(
431
443
  "agent_experiences", "cognitive_turn_receipts", "agent_receipt_profile_closures"
432
444
  }
433
445
  if tables != expected:
446
+ if tables == expected | {"external_evidence_receipts"}:
447
+ from superlocalmemory.storage.migrations import M041_external_evidence_receipts as m041
448
+
449
+ with sqlite3.connect(path) as conn:
450
+ # Erasure needs a valid table, not its optional performance indexes.
451
+ # A damaged index must never strand profile-scoped evidence.
452
+ if m041._table_is_valid(conn):
453
+ return AgentExperienceStore(
454
+ path, is_profile_active=lambda _: True
455
+ ).erase_profile(profile_id, close_profile=close_profile)
434
456
  raise sqlite3.OperationalError("incomplete Agent Experience receipt schema")
435
457
  return AgentExperienceStore(
436
458
  path, is_profile_active=lambda _: True
@@ -459,7 +481,7 @@ def get_profile_receipt_summary(
459
481
  if not path.exists():
460
482
  return unavailable
461
483
  try:
462
- conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=0.5)
484
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=0.5)
463
485
  try:
464
486
  experience = conn.execute(
465
487
  "SELECT COUNT(*) FROM agent_experiences WHERE profile_id=?", (profile_id,)