superlocalmemory 4.0.3 → 4.0.4

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 (51) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +11 -10
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +4 -4
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +3 -3
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-profile/SKILL.md +4 -4
  31. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  36. package/pyproject.toml +1 -1
  37. package/src/superlocalmemory/__init__.py +1 -1
  38. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +184 -0
  39. package/src/superlocalmemory/learning/database.py +2 -1
  40. package/src/superlocalmemory/mcp/profiles.py +11 -5
  41. package/src/superlocalmemory/mcp/server.py +5 -2
  42. package/src/superlocalmemory/mcp/tools_brain.py +89 -4
  43. package/src/superlocalmemory/server/routes/brain.py +6 -1
  44. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  45. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  46. package/src/superlocalmemory/storage/agent_experience.py +26 -4
  47. package/src/superlocalmemory/storage/external_evidence.py +359 -0
  48. package/src/superlocalmemory/storage/migration_runner.py +5 -0
  49. package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
  50. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  51. package/src/superlocalmemory/ui/js/od-brain.js +9 -0
@@ -28,9 +28,10 @@ _PROFILE_CORE: frozenset[str] = frozenset({ # 14
28
28
  _PROFILE_BRAIN: frozenset[str] = frozenset({
29
29
  "get_brain_evidence_status", "record_agent_experience",
30
30
  "record_cognitive_turn", "finalize_cognitive_turn",
31
+ "observe_bounded_loop_evidence",
31
32
  })
32
33
 
33
- _PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ # 28
34
+ _PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ # 29
34
35
  "build_code_graph", "get_blast_radius", "query_graph",
35
36
  "semantic_search_code", "get_review_context", "detect_changes",
36
37
  # switch_profile lets a plugin/IDE session change the active workspace over
@@ -47,21 +48,22 @@ _PROFILE_FULL_MESH: frozenset[str] = frozenset({ # 8
47
48
  "mesh_state", "mesh_lock", "mesh_events", "mesh_status",
48
49
  })
49
50
 
50
- _PROFILE_FULL: frozenset[str] = frozenset({ # 38 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
51
+ _PROFILE_FULL: frozenset[str] = frozenset({ # 39 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
51
52
  "remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
52
53
  "get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
53
54
  "run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
54
55
  "log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
55
56
  "get_brain_evidence_status", "record_agent_experience",
56
57
  "record_cognitive_turn", "finalize_cognitive_turn",
58
+ "observe_bounded_loop_evidence",
57
59
  "evolve_skill", "skill_health", "skill_lineage", "switch_profile",
58
60
  "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
59
61
  # v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
60
62
  "slm_loop_run", "slm_loop_history", "slm_loop_show",
61
63
  # prestage_context remains registered but deliberately raw-server-only.
62
- }) | _PROFILE_FULL_MESH # 46
64
+ }) | _PROFILE_FULL_MESH # 47
63
65
 
64
- _PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 58
66
+ _PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 59
65
67
  "get_version", "get_mode", "health", "consistency_check", "recall_trace",
66
68
  "get_lifecycle_status", "set_retention_policy", "compact_memories",
67
69
  "get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
@@ -84,25 +86,29 @@ _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
84
86
  # at server startup. Any other value is a configuration error (fail closed).
85
87
  _PROFILE_ALIASES: dict[str, str] = {
86
88
  "core14": "core",
87
- # 3.8.0: switch_profile (+1) then bounded-loop tools (+3) grew code/full/
89
+ # 3.8.0 and later additions grew code/full/power; every historical count
88
90
  # power. Every historical count-suffixed name is kept so a v3.6/3.7/early-
89
91
  # 3.8 config still resolves (back-compat); new 3.8.0 counts added alongside.
90
92
  "code20": "code",
91
93
  "code21": "code",
92
94
  "code24": "code",
93
95
  "code28": "code",
96
+ "code29": "code",
94
97
  "full38": "full",
95
98
  "full39": "full",
96
99
  "full42": "full",
97
100
  "full46": "full",
101
+ "full47": "full",
98
102
  "power50": "power",
99
103
  "power51": "power",
100
104
  "power54": "power",
101
105
  "power58": "power",
106
+ "power59": "power",
102
107
  "mesh8": "mesh",
103
108
  "whole81": "whole",
104
109
  "whole84": "whole",
105
110
  "whole91": "whole",
111
+ "whole92": "whole",
106
112
  }
107
113
 
108
114
  # Plain-English descriptions for UI display.
@@ -77,13 +77,13 @@ def reset_engine():
77
77
 
78
78
  # Register tools and resources -------------------------------------------------
79
79
  #
80
- # Essential-only default: 38 base tools + 8 mesh tools = 46 registered
80
+ # Essential-only default: 39 base tools + 8 mesh tools = 47 registered.
81
81
  # when mesh is enabled. Set ``SLM_MCP_ALL_TOOLS=1`` to expose the full
82
82
  # toolset. Rationale: IDEs cap at 50-100 tools total (Cursor,
83
83
  # Antigravity, Windsurf) and a maximal SLM registration crowds out
84
84
  # other MCP servers the user may have installed.
85
85
  # Admin/diagnostics tools remain available via CLI (`slm <command>`).
86
- # Set SLM_MCP_ALL_TOOLS=1 to enable all 91 tools (power users).
86
+ # Set SLM_MCP_ALL_TOOLS=1 to enable all 92 tools (power users).
87
87
 
88
88
  import os as _os_reg
89
89
 
@@ -100,6 +100,9 @@ _ESSENTIAL_TOOLS: set[str] = {
100
100
  # v4.0.2 portable Brain evidence: profile-scoped receipt reads/writes.
101
101
  "get_brain_evidence_status", "record_agent_experience",
102
102
  "record_cognitive_turn", "finalize_cognitive_turn",
103
+ # v4.0.4: explicit, optional observation from the separately installed
104
+ # Bounded Loops MCP producer. It never participates in recall/ranking.
105
+ "observe_bounded_loop_evidence",
103
106
  # Memory management (2)
104
107
  "forget", "run_maintenance",
105
108
  # NOTE: prestage_context IS registered (see register_prestage_tool below)
@@ -9,6 +9,8 @@ or changes a memory answer.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import asyncio
13
+ import sqlite3
12
14
  from pathlib import Path
13
15
  from typing import Any, Callable
14
16
 
@@ -17,6 +19,7 @@ from mcp.types import ToolAnnotations
17
19
  from superlocalmemory.core.admission import admits
18
20
  from superlocalmemory.core.operation_request import OperationKind
19
21
  from superlocalmemory.infra.data_root import state_path
22
+ from superlocalmemory.integrations.bounded_loops_mcp import BridgeUnavailable, observe_installed
20
23
  from superlocalmemory.storage.agent_experience import (
21
24
  AgentExperienceConflictError,
22
25
  AgentExperienceStore,
@@ -25,6 +28,12 @@ from superlocalmemory.storage.agent_experience import (
25
28
  ProfileAdmissionError,
26
29
  get_profile_receipt_summary,
27
30
  )
31
+ from superlocalmemory.storage.external_evidence import (
32
+ ExternalEvidenceConflictError,
33
+ ExternalEvidenceStore,
34
+ ExternalEvidenceValidationError,
35
+ get_profile_external_evidence_summary,
36
+ )
28
37
 
29
38
 
30
39
  def _store_for(engine: Any) -> AgentExperienceStore:
@@ -35,6 +44,14 @@ def _store_for(engine: Any) -> AgentExperienceStore:
35
44
  )
36
45
 
37
46
 
47
+ def _external_store_for(engine: Any) -> ExternalEvidenceStore:
48
+ active_profile = engine.profile_id
49
+ return ExternalEvidenceStore(
50
+ Path(state_path("learning.db")),
51
+ is_profile_active=lambda profile_id: profile_id == active_profile,
52
+ )
53
+
54
+
38
55
  def _require_active_profile(engine: Any, payload: dict[str, Any]) -> str | None:
39
56
  supplied = payload.get("profile_id")
40
57
  if supplied != engine.profile_id:
@@ -42,6 +59,27 @@ def _require_active_profile(engine: Any, payload: dict[str, Any]) -> str | None:
42
59
  return None
43
60
 
44
61
 
62
+ class _ExternalEvidenceWriteError(Exception):
63
+ """Preserve committed receipt count when a later snapshot item fails."""
64
+
65
+ def __init__(self, created: int, cause: Exception) -> None:
66
+ super().__init__(str(cause))
67
+ self.created = created
68
+ self.cause = cause
69
+
70
+
71
+ def _record_external_evidence(store: ExternalEvidenceStore, observed: list[dict[str, Any]]) -> int:
72
+ """Write bounded evidence off the async MCP loop; return durable inserts."""
73
+ created = 0
74
+ for payload in observed:
75
+ try:
76
+ if store.record(payload):
77
+ created += 1
78
+ except Exception as exc:
79
+ raise _ExternalEvidenceWriteError(created, exc) from exc
80
+ return created
81
+
82
+
45
83
  def register_brain_tools(server: Any, get_engine: Callable[[], Any]) -> None:
46
84
  """Register transport-neutral receipt reads and writes.
47
85
 
@@ -52,7 +90,7 @@ def register_brain_tools(server: Any, get_engine: Callable[[], Any]) -> None:
52
90
 
53
91
  @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
54
92
  async def get_brain_evidence_status() -> dict[str, Any]:
55
- """Get profile-scoped Agent Experience and Cognitive Turn totals."""
93
+ """Get profile-scoped, observation-only Brain evidence totals."""
56
94
  engine = get_engine()
57
95
  return {
58
96
  "success": True,
@@ -60,6 +98,9 @@ def register_brain_tools(server: Any, get_engine: Callable[[], Any]) -> None:
60
98
  "agent_experience": get_profile_receipt_summary(
61
99
  state_path("learning.db"), engine.profile_id
62
100
  ),
101
+ "external_graph_evidence": get_profile_external_evidence_summary(
102
+ state_path("learning.db"), engine.profile_id
103
+ ),
63
104
  "control_plane": "observation_only",
64
105
  }
65
106
 
@@ -110,9 +151,7 @@ def register_brain_tools(server: Any, get_engine: Callable[[], Any]) -> None:
110
151
 
111
152
  @server.tool()
112
153
  @admits(OperationKind.REMEMBER)
113
- async def finalize_cognitive_turn(
114
- receipt_id: str, outcome: dict[str, Any]
115
- ) -> dict[str, Any]:
154
+ async def finalize_cognitive_turn(receipt_id: str, outcome: dict[str, Any]) -> dict[str, Any]:
116
155
  """Finalize an active-profile cognitive turn with outcome evidence."""
117
156
  engine = get_engine()
118
157
  try:
@@ -130,3 +169,49 @@ def register_brain_tools(server: Any, get_engine: Callable[[], Any]) -> None:
130
169
  except (TypeError, ValueError) as exc:
131
170
  return {"success": False, "durable": False, "error": str(exc)}
132
171
  return {"success": True, "durable": True, "finalized": finalized}
172
+
173
+ @server.tool()
174
+ @admits(OperationKind.REMEMBER)
175
+ async def observe_bounded_loop_evidence(workspace: str) -> dict[str, Any]:
176
+ """Import one explicit, read-only snapshot from installed Bounded Loops.
177
+
178
+ Bounded Loops remains optional. This tool negotiates its public MCP
179
+ contract at runtime, records only compatible terminal evidence, and
180
+ never changes recall, ranking, routing, or learned behaviour.
181
+ """
182
+ engine = get_engine()
183
+ created = 0
184
+ try:
185
+ observed = await observe_installed(workspace=workspace, profile_id=engine.profile_id)
186
+ store = _external_store_for(engine)
187
+ created = await asyncio.to_thread(_record_external_evidence, store, observed)
188
+ except _ExternalEvidenceWriteError as exc:
189
+ return {
190
+ "success": False,
191
+ "durable": exc.created > 0,
192
+ "created": exc.created,
193
+ "retryable": isinstance(exc.cause, LearningWriteBusyError),
194
+ "error": str(exc.cause),
195
+ }
196
+ except (
197
+ BridgeUnavailable,
198
+ ExternalEvidenceConflictError,
199
+ ExternalEvidenceValidationError,
200
+ ProfileAdmissionError,
201
+ sqlite3.Error,
202
+ ) as exc:
203
+ return {
204
+ "success": False,
205
+ "durable": created > 0,
206
+ "created": created,
207
+ "error": str(exc),
208
+ }
209
+ except LearningWriteBusyError as exc:
210
+ return {"success": False, "durable": False, "retryable": True, "error": str(exc)}
211
+ return {
212
+ "success": True,
213
+ "durable": True,
214
+ "observed": len(observed),
215
+ "created": created,
216
+ "control_plane": "observation_only",
217
+ }
@@ -327,7 +327,12 @@ def _compute_agent_experience(profile_id: str) -> dict:
327
327
  Receipt rows record observation and verification evidence only; this read
328
328
  model never alters retrieval, ranking, or model routing.
329
329
  """
330
- return get_profile_receipt_summary(_learning_db_path(), profile_id)
330
+ from superlocalmemory.storage.external_evidence import get_profile_external_evidence_summary
331
+ path = _learning_db_path()
332
+ return {
333
+ **get_profile_receipt_summary(path, profile_id),
334
+ "external_graph_evidence": get_profile_external_evidence_summary(path, profile_id),
335
+ }
331
336
 
332
337
 
333
338
  def _resolve_phase(signals: int, model_active: bool,
@@ -147,6 +147,9 @@ 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
+ )
150
153
 
151
154
  # Emit under the runner's logger name so operational log filters that key on
152
155
  # "superlocalmemory.storage.migration_runner" keep matching after this split.
@@ -195,6 +198,7 @@ _MODULES = {
195
198
  _M038.NAME: _M038,
196
199
  _M039.NAME: _M039,
197
200
  _M040.NAME: _M040,
201
+ _M041.NAME: _M041,
198
202
  }
199
203
 
200
204
  # 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 (M041). Increment when adding new migrations or
25
25
  #: table-level breaking changes.
26
- SUPPORTED_SCHEMA_VERSION: int = 40
26
+ SUPPORTED_SCHEMA_VERSION: int = 41
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,)