superlocalmemory 3.7.7 → 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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -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 -2
  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 +96 -12
  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/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -46,27 +46,34 @@ from superlocalmemory.evolution.budget import (
46
46
  EvolutionBudget,
47
47
  )
48
48
  from superlocalmemory.evolution.llm_dispatch import _dispatch_llm
49
+ from superlocalmemory.evolution.model_selection import (
50
+ _MODEL_ALIASES,
51
+ _resolve_model_alias,
52
+ resolve_evolution_models,
53
+ )
49
54
 
50
55
  logger = logging.getLogger(__name__)
51
56
 
52
57
  EVOLVED_SKILLS_DIR = Path.home() / ".claude" / "skills" / "evolved"
53
58
 
54
- # Short-name allow-listed model id. Kept narrow so an unknown alias
55
- # falls through to haiku rather than silently dispatching sonnet.
56
- _MODEL_ALIASES: dict[str, str] = {
57
- "haiku": "claude-haiku-4-5",
58
- "sonnet": "claude-sonnet-4-6",
59
- "ollama": "ollama:llama3",
60
- "ollama:llama3": "ollama:llama3",
61
- "ollama:qwen2.5": "ollama:qwen2.5",
62
- "claude-haiku-4-5": "claude-haiku-4-5",
63
- "claude-sonnet-4-6": "claude-sonnet-4-6",
64
- }
59
+ # Model aliasing + per-step selection live in ``model_selection`` a single
60
+ # source of truth shared with the config layer. ``_MODEL_ALIASES`` and
61
+ # ``_resolve_model_alias`` are imported above and re-exported here for
62
+ # callers/tests that still reference them via ``skill_evolver``.
63
+ __all__ = ["SkillEvolver", "detect_backend", "_resolve_model_alias"]
65
64
 
66
65
 
67
- def _resolve_model_alias(alias: str) -> str:
68
- """Translate a short alias (``haiku``/``sonnet``) to an allow-listed id."""
69
- return _MODEL_ALIASES.get(alias, "claude-haiku-4-5")
66
+ def _ollama_running() -> bool:
67
+ """Return True if a local Ollama daemon answers on the default port."""
68
+ try:
69
+ import urllib.request
70
+ req = urllib.request.Request(
71
+ "http://127.0.0.1:11434/api/tags", method="GET",
72
+ )
73
+ with urllib.request.urlopen(req, timeout=2): # noqa: S310
74
+ return True
75
+ except Exception:
76
+ return False
70
77
 
71
78
 
72
79
  def detect_backend() -> str:
@@ -81,13 +88,8 @@ def detect_backend() -> str:
81
88
  return "claude"
82
89
 
83
90
  # 2. Ollama running?
84
- try:
85
- import urllib.request
86
- req = urllib.request.Request("http://127.0.0.1:11434/api/tags", method="GET")
87
- with urllib.request.urlopen(req, timeout=2):
88
- return "ollama"
89
- except Exception:
90
- pass
91
+ if _ollama_running():
92
+ return "ollama"
91
93
 
92
94
  # 3. API key set?
93
95
  if os.environ.get("ANTHROPIC_API_KEY"):
@@ -119,9 +121,11 @@ class SkillEvolver:
119
121
  self._db_path = str(db_path)
120
122
  self._store = EvolutionStore(db_path)
121
123
  self._degradation = DegradationTrigger(db_path)
122
- self._health = HealthCheckTrigger(db_path)
124
+ self._health = HealthCheckTrigger(db_path, profile_id=profile_id)
123
125
  self._config = config
124
126
  self._backend: str | None = None
127
+ # ResolvedModels, resolved lazily & cached via _get_models().
128
+ self._models = None
125
129
  self._profile_id = profile_id
126
130
  self._current_cycle_id: str | None = None
127
131
 
@@ -160,6 +164,26 @@ class SkillEvolver:
160
164
  logger.info("Evolution backend: %s", self._backend)
161
165
  return self._backend
162
166
 
167
+ def _get_models(self):
168
+ """Resolve per-step evolution models (cached).
169
+
170
+ Defaults to the lowest-cost model for the active backend and keeps
171
+ the blind verifier independent of the generator where possible
172
+ (see ``model_selection.resolve_evolution_models``). Falls back to
173
+ stock ``EvolutionConfig`` defaults when no config is attached.
174
+ """
175
+ if self._models is not None:
176
+ return self._models
177
+ backend = self._get_backend()
178
+ evo_cfg = getattr(self._config, "evolution", None)
179
+ if evo_cfg is None:
180
+ from superlocalmemory.core.config import EvolutionConfig
181
+ evo_cfg = EvolutionConfig()
182
+ self._models = resolve_evolution_models(
183
+ evo_cfg, backend, ollama_available=_ollama_running(),
184
+ )
185
+ return self._models
186
+
163
187
  def run_consolidation_cycle(self, profile_id: str = "default") -> dict:
164
188
  """Run during consolidation. Checks triggers 2 and 3.
165
189
 
@@ -200,7 +224,7 @@ class SkillEvolver:
200
224
  self, profile_id: str, backend: str,
201
225
  ) -> dict:
202
226
  """Inner consolidation loop — runs under an open budget cycle."""
203
- self._store.reset_cycle()
227
+ self._store.reset_cycle(profile_id)
204
228
  results = {"candidates": 0, "evolved": 0, "rejected": 0, "skipped": 0, "backend": backend}
205
229
 
206
230
  # Prune recovered skills from anti-loop tracking
@@ -215,7 +239,7 @@ class SkillEvolver:
215
239
  results["candidates"] = len(candidates)
216
240
 
217
241
  for candidate in candidates:
218
- if not self._store.can_evolve():
242
+ if not self._store.can_evolve(profile_id):
219
243
  results["skipped"] += len(candidates) - results["evolved"] - results["rejected"]
220
244
  break
221
245
 
@@ -271,7 +295,7 @@ class SkillEvolver:
271
295
  results["candidates"] = len(candidates)
272
296
 
273
297
  for candidate in candidates:
274
- if not self._store.can_evolve():
298
+ if not self._store.can_evolve(profile_id):
275
299
  break
276
300
  outcome = self._process_candidate(candidate, profile_id)
277
301
  if outcome == "evolved":
@@ -281,6 +305,30 @@ class SkillEvolver:
281
305
 
282
306
  return results
283
307
 
308
+ def evolve_candidate(
309
+ self, candidate: EvolutionCandidate, profile_id: str = "default",
310
+ ) -> str:
311
+ """Evolve a single candidate under a budget cycle (audit-10 fix).
312
+
313
+ Public entry point for the ``evolve_skill`` MCP tool. Opening the
314
+ budget cycle HERE is what makes the per-cycle LLM-call, wall-time and
315
+ per-day caps apply to manual/MCP-triggered evolution, exactly as they
316
+ do to consolidation. The MCP tool previously called
317
+ ``_process_candidate`` directly, so budget charges happened outside a
318
+ cycle, the guard raised RuntimeError, ``_llm_call`` swallowed it, and
319
+ every cap was silently bypassed.
320
+ """
321
+ try:
322
+ with self._budget.cycle():
323
+ self._current_cycle_id = None
324
+ return self._process_candidate(candidate, profile_id)
325
+ except BudgetExhausted as exc:
326
+ logger.info(
327
+ "evolve_candidate skipped: budget exhausted [%s]",
328
+ getattr(exc, "dimension", "?"),
329
+ )
330
+ return "skipped"
331
+
284
332
  def _process_candidate(
285
333
  self, candidate: EvolutionCandidate, profile_id: str,
286
334
  ) -> str:
@@ -301,7 +349,7 @@ class SkillEvolver:
301
349
  if self._store.is_addressed(candidate.skill_name, context_hash):
302
350
  return "skipped"
303
351
 
304
- if self._store.has_exceeded_attempts(candidate.skill_name):
352
+ if self._store.has_exceeded_attempts(candidate.skill_name, profile_id):
305
353
  logger.info("Skill %s exceeded max attempts, flagging for review", candidate.skill_name)
306
354
  return "skipped"
307
355
 
@@ -323,7 +371,7 @@ class SkillEvolver:
323
371
  original_content=original_content[:2000],
324
372
  created_at=now,
325
373
  )
326
- self._store.save_record(record)
374
+ self._store.save_record(record, profile_id)
327
375
 
328
376
  # Step 2: LLM confirmation gate (uses Haiku for cost)
329
377
  confirmed = self._llm_confirm(candidate, original_content)
@@ -334,7 +382,7 @@ class SkillEvolver:
334
382
  rejection_reason="LLM confirmation gate rejected",
335
383
  completed_at=datetime.now(timezone.utc).isoformat(),
336
384
  )
337
- self._store.save_record(record)
385
+ self._store.save_record(record, profile_id)
338
386
  return "rejected"
339
387
 
340
388
  # Step 3: Generate mutation (uses Sonnet for quality)
@@ -347,7 +395,7 @@ class SkillEvolver:
347
395
  rejection_reason="Mutation generation failed",
348
396
  completed_at=datetime.now(timezone.utc).isoformat(),
349
397
  )
350
- self._store.save_record(record)
398
+ self._store.save_record(record, profile_id)
351
399
  return "rejected"
352
400
 
353
401
  # Step 4: Blind verification (uses Haiku — different model from generator)
@@ -365,7 +413,7 @@ class SkillEvolver:
365
413
  blind_verified=False,
366
414
  completed_at=datetime.now(timezone.utc).isoformat(),
367
415
  )
368
- self._store.save_record(record)
416
+ self._store.save_record(record, profile_id)
369
417
  return "rejected"
370
418
 
371
419
  # Step 5: Persist evolved skill
@@ -373,7 +421,9 @@ class SkillEvolver:
373
421
  skill_path = self._write_evolved_skill(candidate, evolved_content, record_id)
374
422
 
375
423
  # M-GENERATION: Compute generation from parent's history
376
- parent_history = self._store.get_skill_history(candidate.skill_name, limit=1)
424
+ parent_history = self._store.get_skill_history(
425
+ candidate.skill_name, profile_id, limit=1,
426
+ )
377
427
  parent_gen = (
378
428
  parent_history[0].generation
379
429
  if parent_history and parent_history[0].status == EvolutionStatus.PROMOTED
@@ -390,8 +440,8 @@ class SkillEvolver:
390
440
  generation=parent_gen + 1,
391
441
  completed_at=datetime.now(timezone.utc).isoformat(),
392
442
  )
393
- self._store.save_record(record)
394
- self._store.record_evolution_attempt()
443
+ self._store.save_record(record, profile_id)
444
+ self._store.record_evolution_attempt(profile_id)
395
445
 
396
446
  logger.info(
397
447
  "Evolved skill: %s (%s via %s) → %s",
@@ -451,8 +501,20 @@ class SkillEvolver:
451
501
  cycle_id=self._current_cycle_id,
452
502
  )
453
503
  except ValueError as exc:
454
- # Gate rejected treat like "no LLM available".
455
- logger.warning("evolution dispatch rejected: %s", exc)
504
+ # A ValueError from _dispatch_llm is a CONTRACT breach, not a
505
+ # runtime/transport failure: forbidden or unlisted model,
506
+ # non-positive max_tokens, max_tokens over MAX_TOKENS_CAP, or
507
+ # an empty profile_id. Every one is a misconfiguration in our
508
+ # own wiring — e.g. a caller requesting more tokens than the
509
+ # ceiling, which is exactly how mutation generation died
510
+ # silently for releases. Log at ERROR so it surfaces in logs
511
+ # and CI instead of masquerading as a normal "no LLM"
512
+ # fail-closed. Behaviour is unchanged: we still return "" so a
513
+ # consolidation cycle degrades gracefully rather than crashing.
514
+ logger.error(
515
+ "evolution dispatch misconfigured — no evolution this "
516
+ "call (returning empty string): %s", exc,
517
+ )
456
518
  return ""
457
519
  except Exception as exc: # noqa: BLE001 — fail-closed
458
520
  logger.debug("evolution dispatch failed: %s", exc)
@@ -467,16 +529,25 @@ class SkillEvolver:
467
529
  f"Should this skill be evolved ({candidate.evolution_type.value})? "
468
530
  f"Reply YES or NO with brief reason."
469
531
  )
470
- response = self._llm_call(prompt, max_tokens=100)
532
+ response = self._llm_call(
533
+ prompt, max_tokens=100, model=self._get_models().confirm,
534
+ )
471
535
  if not response:
472
536
  logger.warning("LLM confirmation gate: empty response, skipping evolution for %s", candidate.skill_name)
473
537
  return False # Fail-closed: no LLM = no evolution
474
538
  return "yes" in response.lower()
475
539
 
476
540
  def _generate_mutation(self, prompt: str) -> Optional[str]:
477
- """Generate evolved SKILL.md (uses sonnet for quality)."""
541
+ """Generate evolved SKILL.md via the configured mutation model.
542
+
543
+ Defaults to the lowest-cost model for the backend (v3.7.9);
544
+ users can opt up (e.g. to sonnet) via ``evolution.mutation_model``.
545
+ """
546
+ mutation_model = self._get_models().mutation
478
547
  for attempt in range(mutgen.MAX_APPLY_RETRIES):
479
- response = self._llm_call(prompt, max_tokens=4000, model="sonnet")
548
+ response = self._llm_call(
549
+ prompt, max_tokens=4000, model=mutation_model,
550
+ )
480
551
  if not response:
481
552
  return None
482
553
  content = mutgen.parse_mutation_output(response)
@@ -492,8 +563,15 @@ class SkillEvolver:
492
563
  return None
493
564
 
494
565
  def _blind_verify(self, prompt: str) -> verifier.VerificationResult:
495
- """Blind verification."""
496
- response = self._llm_call(prompt, max_tokens=500)
566
+ """Blind verification — runs on a model independent of the generator.
567
+
568
+ See ``model_selection.resolve_evolution_models``: the verify model
569
+ is kept different from the mutation model where possible so the
570
+ generator can't grade its own homework.
571
+ """
572
+ response = self._llm_call(
573
+ prompt, max_tokens=500, model=self._get_models().verify,
574
+ )
497
575
  if not response:
498
576
  return verifier.VerificationResult(
499
577
  passed=False, confidence=0.0, reasoning="No LLM response",
@@ -545,8 +623,11 @@ class SkillEvolver:
545
623
  """Write evolved SKILL.md to ~/.claude/skills/evolved/."""
546
624
  EVOLVED_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
547
625
 
548
- # Build directory name
549
- base_name = candidate.skill_name.replace(":", "-")
626
+ # Build directory name. skill_name derives from a behavioral-assertion
627
+ # trigger (any non-whitespace), so sanitize for ALL evolution types —
628
+ # "../../.claude/settings" must not escape EVOLVED_SKILLS_DIR.
629
+ base_name = re.sub(r"[^a-zA-Z0-9_-]", "-",
630
+ candidate.skill_name).lower()[:50] or "skill"
550
631
  if candidate.evolution_type == EvolutionType.FIX:
551
632
  dir_name = f"{base_name}-v{record_id[:6]}"
552
633
  elif candidate.evolution_type == EvolutionType.DERIVED:
@@ -556,8 +637,12 @@ class SkillEvolver:
556
637
  dir_name = re.sub(r"[^a-zA-Z0-9_-]", "-", dir_name).lower()[:50]
557
638
  else:
558
639
  dir_name = base_name
640
+ dir_name = dir_name or "skill"
559
641
 
560
642
  skill_dir = EVOLVED_SKILLS_DIR / dir_name
643
+ # Defense in depth: never write outside the evolved-skills directory.
644
+ if not str(skill_dir.resolve()).startswith(str(EVOLVED_SKILLS_DIR.resolve())):
645
+ raise ValueError(f"evolved skill path escapes sandbox: {dir_name!r}")
561
646
  skill_dir.mkdir(parents=True, exist_ok=True)
562
647
 
563
648
  skill_path = skill_dir / "SKILL.md"
@@ -294,17 +294,19 @@ class HealthCheckTrigger:
294
294
 
295
295
  _STATE_KEY = "health_check_cycle_count"
296
296
 
297
- def __init__(self, db_path: str | Path):
297
+ def __init__(self, db_path: str | Path, profile_id: str = "default"):
298
298
  self._db_path = str(db_path)
299
+ self._profile_id = profile_id or "default"
299
300
  self._check_every_n = 3 # Every 3rd consolidation (~18h)
300
301
 
301
302
  def _read_cycle_count(self) -> int:
302
- """Read persisted cycle count from DB."""
303
+ """Read this profile's persisted cycle count."""
303
304
  conn = sqlite3.connect(self._db_path, timeout=10)
304
305
  try:
305
306
  row = conn.execute(
306
- "SELECT value FROM evolution_cycle_state WHERE key = ?",
307
- (self._STATE_KEY,),
307
+ "SELECT value FROM evolution_cycle_state "
308
+ "WHERE profile_id = ? AND key = ?",
309
+ (self._profile_id, self._STATE_KEY),
308
310
  ).fetchone()
309
311
  return int(row[0]) if row else 0
310
312
  except sqlite3.OperationalError:
@@ -314,23 +316,30 @@ class HealthCheckTrigger:
314
316
  conn.close()
315
317
 
316
318
  def _write_cycle_count(self, count: int) -> None:
317
- """Persist cycle count to DB.
319
+ """Persist this profile's cycle count.
318
320
 
319
- Relies on evolution_cycle_state table created by EvolutionStore.
320
- Falls back to CREATE IF NOT EXISTS for standalone use.
321
+ The evolution_cycle_state table (profile_id, key) is created by
322
+ EvolutionStore with the correct composite-PK schema we do NOT
323
+ CREATE it here (the old single-key CREATE produced a conflicting
324
+ schema and cross-profile collisions).
321
325
  """
322
326
  conn = sqlite3.connect(self._db_path, timeout=10)
323
327
  try:
324
- # Table schema matches evolution_store.py: value INTEGER
328
+ # Correct composite-PK schema (matches EvolutionStore). Created only
329
+ # for standalone use (tests); a real deployment already has it.
325
330
  conn.execute(
326
- "CREATE TABLE IF NOT EXISTS evolution_cycle_state "
327
- "(key TEXT PRIMARY KEY, value INTEGER DEFAULT 0, updated_at TEXT)",
331
+ "CREATE TABLE IF NOT EXISTS evolution_cycle_state ("
332
+ "profile_id TEXT NOT NULL DEFAULT 'default', key TEXT NOT NULL, "
333
+ "value INTEGER DEFAULT 0, updated_at TEXT, "
334
+ "PRIMARY KEY (profile_id, key))",
328
335
  )
329
336
  now = datetime.now(timezone.utc).isoformat()
330
337
  conn.execute(
331
- "INSERT OR REPLACE INTO evolution_cycle_state (key, value, updated_at) "
332
- "VALUES (?, ?, ?)",
333
- (self._STATE_KEY, count, now),
338
+ "INSERT INTO evolution_cycle_state (profile_id, key, value, updated_at) "
339
+ "VALUES (?, ?, ?, ?) "
340
+ "ON CONFLICT(profile_id, key) DO UPDATE SET "
341
+ "value=excluded.value, updated_at=excluded.updated_at",
342
+ (self._profile_id, self._STATE_KEY, count, now),
334
343
  )
335
344
  conn.commit()
336
345
  except sqlite3.OperationalError as exc:
@@ -672,32 +672,34 @@ class CozoDBGraphBackend:
672
672
  # Tier Sync
673
673
  # ------------------------------------------------------------------
674
674
 
675
+ # Rebind the whole entity row (Cozo has no partial ``:update`` op — that
676
+ # token does not parse) while binding the id/tier/timestamp as query
677
+ # parameters so an entity id containing a quote can never become Datalog.
678
+ # The ``*entity{...}`` match means non-existent ids are a safe no-op rather
679
+ # than creating a stub row.
680
+ _TIER_SYNC_QUERY = (
681
+ "?[id, name, entity_type, tier, properties, profile_id, created_at, updated_at] := "
682
+ "*entity{id, name, entity_type, properties, profile_id, created_at}, "
683
+ "id = $id, tier = $tier, updated_at = $now "
684
+ ":put entity {id => name, entity_type, tier, properties, profile_id, created_at, updated_at}"
685
+ )
686
+
675
687
  def sync_tier_changes(
676
688
  self, added: list[str], removed: list[str]
677
689
  ) -> None:
678
- """Sync tier changes: add promoted entities, mark demoted."""
690
+ """Sync tier changes: promote added entities to active, demote removed to cold."""
679
691
  now = datetime.now().isoformat()
680
-
681
- if added:
682
- # Fetch entity data from existing CozoDB entities or set defaults
683
- for entity_id in added:
684
- try:
685
- self._db.run(f"""
686
- ?[id, tier] <- [['{entity_id}', 'active']]
687
- :update entity {{id => tier, updated_at: '{now}'}}
688
- """)
689
- except Exception:
690
- pass
691
-
692
- if removed:
693
- for entity_id in removed:
692
+ for entity_ids, tier in ((added, "active"), (removed, "cold")):
693
+ for entity_id in entity_ids:
694
694
  try:
695
- self._db.run(f"""
696
- ?[id, tier] <- [['{entity_id}', 'cold']]
697
- :update entity {{id => tier, updated_at: '{now}'}}
698
- """)
695
+ self._db.run(
696
+ self._TIER_SYNC_QUERY,
697
+ {"id": entity_id, "tier": tier, "now": now},
698
+ )
699
699
  except Exception:
700
- pass
700
+ logger.debug(
701
+ "cozo tier sync skipped for entity %s", entity_id, exc_info=True
702
+ )
701
703
 
702
704
  # ------------------------------------------------------------------
703
705
  # Health Check
@@ -712,12 +714,20 @@ class CozoDBGraphBackend:
712
714
  edge_count = self._db.run(
713
715
  "?[count(from_id)] := *edge{from_id}"
714
716
  )
717
+ # fact_entity bridge rows (fact_id, entity_id) — counted so parity
718
+ # verification can catch a silent bridge-import failure that would
719
+ # otherwise leave Cozo entity recall returning empty results.
720
+ fe_count = self._db.run(
721
+ "?[count(fact_id)] := *fact_entity{fact_id, entity_id}"
722
+ )
715
723
  ec = entity_count.values.tolist()[0][0] if len(entity_count) > 0 else 0
716
724
  edc = edge_count.values.tolist()[0][0] if len(edge_count) > 0 else 0
725
+ fec = fe_count.values.tolist()[0][0] if len(fe_count) > 0 else 0
717
726
  return {
718
727
  "status": "active",
719
728
  "entities": int(ec),
720
729
  "edges": int(edc),
730
+ "fact_entity": int(fec),
721
731
  "shadow_checks": self._shadow_checks,
722
732
  "shadow_mismatches": self._shadow_mismatches,
723
733
  "shadow_errors": self._shadow_errors,
@@ -730,6 +740,19 @@ class CozoDBGraphBackend:
730
740
  "db_path": self._db_path,
731
741
  }
732
742
 
743
+ def entity_ids(self, limit: int = 1000) -> list[str]:
744
+ """Return up to ``limit`` entity IDs, for scale-engine content parity.
745
+
746
+ Count parity alone cannot prove the import reproduced the right rows;
747
+ comparing this ID set against canonical SQLite catches an import that
748
+ landed the correct *number* of entities with wrong identities.
749
+ """
750
+ try:
751
+ res = self._db.run("?[id] := *entity{id} :limit " + str(int(limit)))
752
+ return [row[0] for row in res.values.tolist()]
753
+ except Exception:
754
+ return []
755
+
733
756
  # ------------------------------------------------------------------
734
757
  # Rebuild (from SQLite canonical)
735
758
  # ------------------------------------------------------------------
@@ -38,7 +38,11 @@ from typing import Protocol, runtime_checkable
38
38
  # ---------------------------------------------------------------------------
39
39
 
40
40
  HARD_BYTES_CAP = 4096
41
- COPILOT_SOFT_BYTES = 2048
41
+ # Soft budget for the managed instruction block. Raised 2048 -> 2560 -> 2816 as
42
+ # the block grew to carry the memory, token-optimization, and (compact)
43
+ # bounded-loop protocols; the 4 KB hard cap still bounds total size (recall
44
+ # content is truncated to stay under it).
45
+ COPILOT_SOFT_BYTES = 2816
42
46
  TRUNCATION_MARKER = b"\n<!-- truncated -->"
43
47
 
44
48
 
@@ -10,6 +10,7 @@ import logging
10
10
  from typing import Any, Callable
11
11
 
12
12
  from superlocalmemory.core.injection import InjectableMemory, render_context
13
+ from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
@@ -85,7 +86,13 @@ class AutoRecall:
85
86
  )
86
87
  for r in relevant[:self._max_memories]
87
88
  ]
88
- return render_context(memories, mode="B", cfg=None, wrap=True)
89
+ ctx = render_context(memories, mode="B", cfg=None, wrap=True)
90
+ # T-inject: anchor the injected memories to "now" so a time-blind
91
+ # model can weigh recency. Prepend a one-line temporal frame.
92
+ frame = temporal_frame(
93
+ [getattr(r.fact, "created_at", "") for r in relevant[:self._max_memories]]
94
+ )
95
+ return f"{frame}\n\n{ctx}" if ctx else ctx
89
96
  except Exception as exc:
90
97
  logger.warning("Auto-recall failed: %s", exc)
91
98
  return ""
@@ -104,12 +111,17 @@ class AutoRecall:
104
111
  response = self._recall(query, self._max_memories)
105
112
  if response is None:
106
113
  return []
114
+ from datetime import datetime as _dt, timezone as _tz
115
+ _now = _dt.now(_tz.utc)
107
116
  results = []
108
117
  for r in response.results:
109
118
  if r.score >= self._threshold:
119
+ _created = getattr(r.fact, "created_at", "") or ""
110
120
  results.append({
111
121
  "fact_id": r.fact.fact_id,
112
122
  "content": r.fact.content[:300],
123
+ "created_at": _created,
124
+ "age_label": relative_age(_created, _now),
113
125
  "score": round(r.score, 3),
114
126
  "relevance_score": round(
115
127
  getattr(r, "relevance_score", r.score) or 0.0, 3
@@ -261,6 +261,17 @@ def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
261
261
  "command": _wrap_python_cmd("stop_outcome"),
262
262
  "timeout": 10000,
263
263
  },
264
+ # Commit temporal summaries so session decisions survive beyond
265
+ # the git-state snapshot written by `slm hook stop`.
266
+ {
267
+ "type": "command",
268
+ "command": (
269
+ 'cmd /c "slm session close 2>NUL || exit /b 0"'
270
+ if sys.platform == "win32"
271
+ else "slm session close 2>/dev/null || true"
272
+ ),
273
+ "timeout": 15000,
274
+ },
264
275
  ]
265
276
  }
266
277
  ],
@@ -7,10 +7,16 @@ import sysconfig
7
7
  from pathlib import Path
8
8
 
9
9
  SKILLS = ("slm-cache", "slm-compress", "slm-graph", "slm-recall", "slm-remember", "slm-session", "slm-status")
10
- AGENTS = {
11
- "slm-memory-advisor.toml": 'name = "slm-memory-advisor"\ndescription = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\ninstructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n',
12
- "slm-optimize-advisor.toml": 'name = "slm-optimize-advisor"\ndescription = "Analyze SuperLocalMemory retrieval, ingestion, cache, compression, and optimization evidence."\ninstructions = "Inspect real SLM evidence before advising. Separate observed performance from targets and recommend measurable experiments."\n',
13
- }
10
+
11
+ # Codex subagent files written to ~/.codex/agents (content built by _agent_files()).
12
+ AGENTS = ("slm-memory-advisor.toml", "slm-optimize-advisor.toml")
13
+
14
+ _MEMORY_ADVISOR_TOML = (
15
+ 'name = "slm-memory-advisor"\n'
16
+ 'description = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\n'
17
+ 'instructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n'
18
+ )
19
+
14
20
 
15
21
  def _source_root() -> Path:
16
22
  development = Path(__file__).resolve().parents[3] / "plugin-src" / "skills"
@@ -21,6 +27,57 @@ def _source_root() -> Path:
21
27
  return installed
22
28
  raise FileNotFoundError("Bundled Codex skills were not found in this installation")
23
29
 
30
+
31
+ def _agents_source_root() -> Path | None:
32
+ development = Path(__file__).resolve().parents[3] / "plugin-src" / "agents"
33
+ if development.exists():
34
+ return development
35
+ installed = Path(sysconfig.get_path("data")) / "share" / "superlocalmemory" / "codex" / "agents"
36
+ return installed if installed.exists() else None
37
+
38
+
39
+ def _optimize_advisor_toml() -> str:
40
+ """Build the optimize-advisor TOML from the canonical advisor doc so Codex
41
+ ships the FULL decision rules (the 8-rule tree), not a one-line stub. Falls
42
+ back to a short instruction only if the source doc is unavailable.
43
+ """
44
+ description = (
45
+ "Apply SuperLocalMemory's no-proxy context-optimization rules — reversible "
46
+ "compression of large tool output and KV-caching of repeated reads/searches."
47
+ )
48
+ body = ""
49
+ root = _agents_source_root()
50
+ if root is not None:
51
+ src = root / "slm-optimize-advisor.md"
52
+ if src.exists():
53
+ text = src.read_text(encoding="utf-8")
54
+ if text.startswith("---"): # strip YAML frontmatter, keep the guidance body
55
+ end = text.find("\n---", 3)
56
+ if end != -1:
57
+ text = text[end + 4:]
58
+ body = text.strip()
59
+ if not body:
60
+ body = (
61
+ "Reduce context-window pressure with the Surface-B tools (reversible CCR "
62
+ "compression + a per-agent KV cache); fail-open — never block the task."
63
+ )
64
+ # TOML literal multi-line string ('''...'''): no escape processing, and the
65
+ # advisor body contains no ''' sequence.
66
+ return (
67
+ 'name = "slm-optimize-advisor"\n'
68
+ f'description = "{description}"\n'
69
+ f"instructions = '''\n{body}\n'''\n"
70
+ )
71
+
72
+
73
+ def _agent_files() -> dict:
74
+ """Return {filename: TOML content} for the Codex subagents."""
75
+ return {
76
+ "slm-memory-advisor.toml": _MEMORY_ADVISOR_TOML,
77
+ "slm-optimize-advisor.toml": _optimize_advisor_toml(),
78
+ }
79
+
80
+
24
81
  def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
25
82
  """Copy only named SLM assets; never rewrite user-owned assets."""
26
83
  home = home or Path.home()
@@ -37,10 +94,11 @@ def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
37
94
  target = skills_root / skill
38
95
  target.mkdir(parents=True, exist_ok=True)
39
96
  shutil.copy2(source / skill / "SKILL.md", target / "SKILL.md")
40
- for filename, content in AGENTS.items():
97
+ for filename, content in _agent_files().items():
41
98
  (agents_root / filename).write_text(content, encoding="utf-8")
42
99
  return {"success": True, "skills": list(SKILLS), "agents": list(AGENTS), "dry_run": False}
43
100
 
101
+
44
102
  def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
45
103
  """Remove only the known SLM directories and files."""
46
104
  home = home or Path.home()
@@ -52,6 +110,7 @@ def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
52
110
  shutil.rmtree(target) if target.is_dir() else target.unlink()
53
111
  return {"success": True, "removed": [str(x) for x in existing], "dry_run": dry_run}
54
112
 
113
+
55
114
  def status_assets(*, home: Path | None = None) -> dict:
56
115
  home = home or Path.home()
57
116
  skills = [x for x in SKILLS if (home / ".agents" / "skills" / x / "SKILL.md").exists()]