superlocalmemory 3.7.8 → 3.8.0

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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -1
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +94 -10
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +384 -56
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -28,6 +28,103 @@ from mcp.types import ToolAnnotations
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # M03: Simple circuit breaker for mesh_send
34
+ # ---------------------------------------------------------------------------
35
+ # Prevents runaway retries (~30s stall) when the daemon is dead.
36
+ # State machine: CLOSED → OPEN (after 3 consecutive failures)
37
+ # OPEN → HALF_OPEN (after 60s cooldown)
38
+ # HALF_OPEN → CLOSED (on one successful probe)
39
+ # HALF_OPEN → OPEN (on probe failure)
40
+
41
+ _CB_FAILURE_THRESHOLD = 3
42
+ _CB_COOLDOWN_SECONDS = 60
43
+
44
+ # Client-side mesh message cap (mirrors the broker's MAX_MESSAGE_SIZE).
45
+ MAX_MESSAGE_SIZE = 4096
46
+
47
+ _CB_STATE_CLOSED = "closed"
48
+ _CB_STATE_OPEN = "open"
49
+ _CB_STATE_HALF_OPEN = "half_open"
50
+
51
+
52
+ class _SendCircuitBreaker:
53
+ """Thread-safe circuit breaker scoped to mesh_send daemon calls."""
54
+
55
+ def __init__(self) -> None:
56
+ self._lock = threading.Lock()
57
+ self._state: str = _CB_STATE_CLOSED
58
+ self._failure_count: int = 0
59
+ self._opened_at: float = 0.0
60
+
61
+ def reset(self) -> None:
62
+ """Reset to CLOSED; used by tests for isolation."""
63
+ with self._lock:
64
+ self._state = _CB_STATE_CLOSED
65
+ self._failure_count = 0
66
+ self._opened_at = 0.0
67
+
68
+ def is_open(self) -> bool:
69
+ with self._lock:
70
+ if self._state == _CB_STATE_OPEN:
71
+ if time.monotonic() - self._opened_at >= _CB_COOLDOWN_SECONDS:
72
+ self._state = _CB_STATE_HALF_OPEN
73
+ return False # allow one probe
74
+ return True
75
+ return False
76
+
77
+ def allow_request(self) -> bool:
78
+ """Return True if the call should proceed; False if circuit is OPEN.
79
+
80
+ State transitions inside the lock prevent concurrent probe races:
81
+ - CLOSED → allow
82
+ - OPEN (cooldown not elapsed) → block
83
+ - OPEN (cooldown elapsed) → transition to HALF_OPEN, allow one probe,
84
+ and immediately re-enter OPEN so any concurrent call is blocked until
85
+ the probe result comes in via record_success / record_failure.
86
+ - HALF_OPEN → block (probe already dispatched and not yet resolved)
87
+ """
88
+ with self._lock:
89
+ if self._state == _CB_STATE_CLOSED:
90
+ return True
91
+ if self._state == _CB_STATE_OPEN:
92
+ if time.monotonic() - self._opened_at >= _CB_COOLDOWN_SECONDS:
93
+ # Grant exactly one probe by briefly entering HALF_OPEN then
94
+ # going back to OPEN. Subsequent callers are blocked until
95
+ # record_success or record_failure resolves the probe.
96
+ self._state = _CB_STATE_HALF_OPEN
97
+ return True
98
+ return False
99
+ # HALF_OPEN: probe is in flight — block until resolved
100
+ return False
101
+
102
+ def record_success(self) -> None:
103
+ with self._lock:
104
+ self._state = _CB_STATE_CLOSED
105
+ self._failure_count = 0
106
+
107
+ def record_failure(self) -> None:
108
+ with self._lock:
109
+ if self._state == _CB_STATE_HALF_OPEN:
110
+ # Probe failed — reopen immediately
111
+ self._state = _CB_STATE_OPEN
112
+ self._opened_at = time.monotonic()
113
+ return
114
+ self._failure_count += 1
115
+ if self._failure_count >= _CB_FAILURE_THRESHOLD:
116
+ self._state = _CB_STATE_OPEN
117
+ self._opened_at = time.monotonic()
118
+ logger.warning(
119
+ "mesh_send circuit breaker OPEN after %d consecutive failures; "
120
+ "fast-failing for %ds",
121
+ self._failure_count,
122
+ _CB_COOLDOWN_SECONDS,
123
+ )
124
+
125
+
126
+ _SEND_CIRCUIT = _SendCircuitBreaker()
127
+
31
128
  # Unique peer ID for this MCP server session
32
129
  _PEER_ID = str(uuid.uuid4())[:12]
33
130
  _SESSION_SUMMARY = ""
@@ -69,7 +166,15 @@ def _ensure_registered() -> None:
69
166
  "session_id": os.environ.get("CLAUDE_SESSION_ID", _PEER_ID),
70
167
  "summary": _SESSION_SUMMARY or "SLM MCP session",
71
168
  "project_path": _PROJECT_PATH,
72
- "agent_type": os.environ.get("CLAUDE_AGENT_TYPE", "claude_code"),
169
+ # Peer identity is the canonical SLM_AGENT_ID (same var memory
170
+ # attribution uses), so Antigravity/Hermes/Cursor/etc. show as
171
+ # themselves instead of collapsing to "claude_code". Fall back to the
172
+ # legacy CLAUDE_AGENT_TYPE, then a generic default.
173
+ "agent_type": (
174
+ os.environ.get("SLM_AGENT_ID")
175
+ or os.environ.get("CLAUDE_AGENT_TYPE")
176
+ or "claude_code"
177
+ ),
73
178
  })
74
179
  if result:
75
180
  # v3.6.12 (mesh-1): the broker mints its OWN peer_id (RegisterRequest has
@@ -174,12 +279,34 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
174
279
  - "project:/path/to/dir" (all sessions in that project directory)
175
280
  message: The message content (max 4KB — use file paths for large data)
176
281
  """
282
+ # Enforce the documented 4KB notification cap client-side too (the
283
+ # broker also caps, but fail fast without a round-trip).
284
+ if len(message.encode("utf-8")) > MAX_MESSAGE_SIZE:
285
+ return {"ok": False, "error": (
286
+ f"message too large (max {MAX_MESSAGE_SIZE} bytes) — "
287
+ "reference a file path instead")}
288
+ # M03: circuit breaker — fast-fail if daemon is repeatedly unreachable
289
+ if not _SEND_CIRCUIT.allow_request():
290
+ return {
291
+ "ok": False,
292
+ "error": (
293
+ "mesh_send circuit open: daemon unreachable after repeated failures. "
294
+ f"Retrying in {_CB_COOLDOWN_SECONDS}s."
295
+ ),
296
+ }
297
+
177
298
  await asyncio.to_thread(_ensure_registered)
178
299
  result = await asyncio.to_thread(
179
300
  _mesh_request, "POST", "/send",
180
301
  {"from_peer": _PEER_ID, "to_peer": to, "content": message},
181
302
  )
182
- return result or {"ok": False, "error": "Failed to send message"}
303
+ # Circuit tracks daemon-unreachable (None) only not valid broker errors
304
+ # like "recipient not found", which are application-level, not failures.
305
+ if result is None:
306
+ _SEND_CIRCUIT.record_failure()
307
+ return {"ok": False, "error": "Failed to send message"}
308
+ _SEND_CIRCUIT.record_success()
309
+ return result
183
310
 
184
311
  @server.tool()
185
312
  async def mesh_inbox() -> dict:
@@ -190,9 +317,11 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
190
317
  Messages auto-expire after 48 hours.
191
318
  """
192
319
  await asyncio.to_thread(_ensure_registered)
320
+ from urllib.parse import quote
193
321
  project = _PROJECT_PATH or _detect_project_path()
194
322
  messages = await asyncio.to_thread(
195
- _mesh_request, "GET", f"/inbox/{_PEER_ID}?project_path={project}",
323
+ _mesh_request, "GET",
324
+ f"/inbox/{_PEER_ID}?project_path={quote(project, safe='')}",
196
325
  )
197
326
  msg_list = (messages or {}).get("messages", [])
198
327
  # Auto-mark unread messages as read. v3.6.12 (failopen-2): use .get("id")
@@ -250,9 +379,16 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
250
379
  Before editing a shared file, check if another session has it locked.
251
380
 
252
381
  Args:
253
- file_path: Path to the file
382
+ file_path: Absolute path to the file
254
383
  action: "query" (check lock), "acquire" (lock file), "release" (unlock)
255
384
  """
385
+ # Require a non-empty absolute path; a relative/blank path is ambiguous
386
+ # and lets a caller probe arbitrary strings via the coordination store.
387
+ if not file_path or not (
388
+ file_path.startswith("/")
389
+ or (len(file_path) >= 3 and file_path[1] == ":")
390
+ ):
391
+ return {"ok": False, "error": "file_path must be a non-empty absolute path"}
256
392
  await asyncio.to_thread(_ensure_registered)
257
393
  result = await asyncio.to_thread(
258
394
  _mesh_request, "POST", "/lock",
@@ -170,7 +170,7 @@ def register_optimize_tools(server) -> None:
170
170
  "ok": False, "content": None, "size_bytes": 0,
171
171
  "error": "ccr_id must be a UUID4",
172
172
  }
173
- original = CCRStore.get_instance().retrieve(ccr_id)
173
+ original = CCRStore.get_instance().retrieve(ccr_id, tenant_id=_tenant())
174
174
  if original is None:
175
175
  return {
176
176
  "ok": False, "content": None, "size_bytes": 0,
@@ -257,13 +257,16 @@ def register_optimize_tools(server) -> None:
257
257
  cache_key = hashlib.sha256(f"mcpkv:{tenant}:{key}".encode()).hexdigest()
258
258
  norm_tid = _normalize_tenant_id(tenant)
259
259
 
260
- blob = CacheDB.get_default().get_value(cache_key, norm_tid)
260
+ db = CacheDB.get_default()
261
+ blob = db.get_value(cache_key, norm_tid)
261
262
  if blob is None:
262
263
  with _kv_lock:
263
264
  _kv_misses += 1
265
+ db.kv_counter_incr("kv_misses") # M2: durable across restarts
264
266
  return {"ok": True, "hit": False, "value": None, "note": None}
265
267
  with _kv_lock:
266
268
  _kv_hits += 1
269
+ db.kv_counter_incr("kv_hits") # M2: durable across restarts
267
270
  return {"ok": True, "hit": True, "value": blob.decode("utf-8"), "note": None}
268
271
 
269
272
  except Exception as exc:
@@ -277,14 +280,18 @@ def register_optimize_tools(server) -> None:
277
280
  async def slm_optimize_stats() -> dict:
278
281
  """Return compression and cache statistics.
279
282
 
280
- Proxy/compress stats are daemon-persisted (accurate across restarts).
281
- KV stats are in-module counters for this MCP process session only.
283
+ Proxy/compress AND KV stats are daemon-persisted (accurate across
284
+ restarts). KV counters fall back to this session's in-memory tally if
285
+ the persisted counters can't be read.
282
286
  """
283
287
  try:
284
- snap = CacheDB.get_default().metrics_load()
288
+ db = CacheDB.get_default()
289
+ snap = db.metrics_load()
290
+ # M2: prefer the durable KV counters; fall back to session counters.
291
+ persisted = db.kv_counters_load()
285
292
  with _kv_lock:
286
- kv_h = _kv_hits
287
- kv_m = _kv_misses
293
+ kv_h = persisted.get("kv_hits", _kv_hits)
294
+ kv_m = persisted.get("kv_misses", _kv_misses)
288
295
  return {
289
296
  "ok": True,
290
297
  "compress_runs": snap.compress_runs,
@@ -297,7 +304,7 @@ def register_optimize_tools(server) -> None:
297
304
  "CCR entry count not tracked per-session; "
298
305
  "see daemon /api/v1/metrics"
299
306
  ),
300
- "note": "proxy stats are daemon-persisted; kv stats are this session only",
307
+ "note": "proxy and kv stats are daemon-persisted across restarts",
301
308
  }
302
309
  except Exception as exc:
303
310
  logger.error("slm_optimize_stats failed (fail-open): %s", exc)