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
@@ -98,6 +98,19 @@ class RemoteSyncClient:
98
98
  self._discovery_enabled: bool = (
99
99
  os.environ.get("SLM_MESH_DISCOVERY", "on") != "off"
100
100
  )
101
+ # M05: the shared secret is a bearer token — whoever receives it can
102
+ # replay it. mDNS discovery is unauthenticated (anyone on the LAN can
103
+ # advertise _slm-mesh._tcp.local.), so we must NOT push the secret to a
104
+ # discovered peer unless the operator explicitly trusts LAN discovery.
105
+ # An explicitly-configured peer (SLM_MESH_PEER_URL) is trusted; a peer
106
+ # set programmatically stays trusted; only the discovery path can
107
+ # downgrade trust.
108
+ self._peer_url_from_config: bool = self._peer_url is not None
109
+ self._trust_discovered: bool = (
110
+ os.environ.get("SLM_MESH_TRUST_DISCOVERED", "off").strip().lower()
111
+ in ("1", "on", "true", "yes")
112
+ )
113
+ self._peer_url_trusted: bool = True
101
114
  self._sync_thread: threading.Thread | None = None
102
115
  self._discovery_thread: threading.Thread | None = None
103
116
  self._stop_event = threading.Event()
@@ -157,6 +170,14 @@ class RemoteSyncClient:
157
170
  if self._stop_event.wait(30):
158
171
  break
159
172
 
173
+ def _auth_headers(self) -> dict[str, str]:
174
+ """Bearer header for the current peer — but ONLY if that peer is
175
+ trusted. Prevents leaking the shared secret to a spoofed mDNS peer
176
+ (M05)."""
177
+ if self._shared_secret and self._peer_url_trusted:
178
+ return {"Authorization": f"Bearer {self._shared_secret}"}
179
+ return {}
180
+
160
181
  def _sync_peers_from_remote(self) -> None:
161
182
  """Fetch peers from remote /mesh/peers and update broker."""
162
183
  if not self._peer_url:
@@ -164,9 +185,7 @@ class RemoteSyncClient:
164
185
 
165
186
  try:
166
187
  with httpx.Client(timeout=5) as client:
167
- headers = {}
168
- if self._shared_secret:
169
- headers["Authorization"] = f"Bearer {self._shared_secret}"
188
+ headers = self._auth_headers()
170
189
 
171
190
  resp = client.get(
172
191
  f"{self._peer_url}/mesh/peers", headers=headers, timeout=5
@@ -214,9 +233,7 @@ class RemoteSyncClient:
214
233
 
215
234
  try:
216
235
  with httpx.Client(timeout=10) as client:
217
- headers = {}
218
- if self._shared_secret:
219
- headers["Authorization"] = f"Bearer {self._shared_secret}"
236
+ headers = self._auth_headers()
220
237
 
221
238
  payload = {
222
239
  "from_peer": message_data.get("from_peer", ""),
@@ -297,8 +314,33 @@ class RemoteSyncClient:
297
314
  self.add_service(zeroconf, service_type, name)
298
315
 
299
316
  def _update_peer_url(self, host: str, port: int) -> None:
300
- """Update peer URL from discovery."""
317
+ """Update peer URL from mDNS discovery.
318
+
319
+ Never overrides an explicitly-configured SLM_MESH_PEER_URL — explicit
320
+ config is the source of truth and must not be hijacked by a spoofed
321
+ mDNS announcement. A discovered peer is marked UNTRUSTED (the shared
322
+ secret is withheld) unless SLM_MESH_TRUST_DISCOVERED is enabled (M05).
323
+ """
324
+ if self._peer_url_from_config:
325
+ logger.debug(
326
+ "RemoteSyncClient: ignoring mDNS-discovered peer %s:%s — "
327
+ "SLM_MESH_PEER_URL is explicitly configured",
328
+ host, port,
329
+ )
330
+ return
301
331
  new_url = _peer_url(host, port)
302
332
  if self._peer_url != new_url:
303
333
  self._peer_url = new_url
304
- logger.info("RemoteSyncClient: updated peer URL to %s", new_url)
334
+ self._peer_url_trusted = self._trust_discovered
335
+ logger.info(
336
+ "RemoteSyncClient: updated peer URL to %s (mDNS-discovered, "
337
+ "trusted=%s)", new_url, self._peer_url_trusted,
338
+ )
339
+ if self._shared_secret and not self._trust_discovered:
340
+ logger.warning(
341
+ "RemoteSyncClient: a shared secret is set but "
342
+ "SLM_MESH_TRUST_DISCOVERED is off — the secret will NOT be "
343
+ "sent to mDNS-discovered peer %s. Set "
344
+ "SLM_MESH_TRUST_DISCOVERED=on to trust LAN-discovered peers.",
345
+ new_url,
346
+ )
@@ -1,11 +1,6 @@
1
1
  Third-party components used by SLM v3.6 Optimize module:
2
2
 
3
- 1. Headroom (Apache-2.0)
4
- Copyright (c) 2024 Headroom contributors
5
- Source: https://github.com/headroom/headroom
6
- License: Apache-2.0
7
-
8
- 2. LLMLingua-2 (MIT)
3
+ 1. LLMLingua-2 (MIT)
9
4
  Copyright (c) 2024 Microsoft Corporation
10
5
  Source: https://github.com/microsoft/LLMLingua
11
6
  License: MIT
@@ -1,7 +1,4 @@
1
- """SLM Anthropic SDK adapter.
2
-
3
- Adapted from OmniCache (MIT). See ATTRIBUTION.md.
4
- """
1
+ """SLM Anthropic SDK adapter."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
@@ -1,7 +1,4 @@
1
- """SLM OpenAI SDK adapter.
2
-
3
- Adapted from OmniCache (MIT). See ATTRIBUTION.md.
4
- """
1
+ """SLM OpenAI SDK adapter."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
@@ -298,14 +298,17 @@ class VCacheSemantic(SemanticTier):
298
298
  max_turns = int(getattr(cfg, "semantic_max_turns_for_semantic", _DEFAULT_MAX_TURNS))
299
299
  messages = _extract_messages(req)
300
300
 
301
- # Step 1: Multi-turn guard
302
- turn_count = self._context_key_builder.turn_count(messages)
303
- if turn_count > max_turns:
304
- logger.debug(
305
- "VCacheSemantic: skip (turn_count=%d > max=%d)",
306
- turn_count, max_turns,
307
- )
308
- return None
301
+ # Step 1: Multi-turn guard — on long conversations nearest-neighbour
302
+ # reuse is riskiest, so skip semantic there. Honours the
303
+ # semantic_multiturn_guard kill-switch (default on).
304
+ if bool(getattr(cfg, "semantic_multiturn_guard", True)):
305
+ turn_count = self._context_key_builder.turn_count(messages)
306
+ if turn_count > max_turns:
307
+ logger.debug(
308
+ "VCacheSemantic: skip (turn_count=%d > max=%d)",
309
+ turn_count, max_turns,
310
+ )
311
+ return None
309
312
 
310
313
  # Step 2: SAFE-CACHE centroid defense
311
314
  if bool(getattr(cfg, "semantic_centroid_defense", True)):
@@ -531,20 +534,25 @@ class VCacheSemantic(SemanticTier):
531
534
  cached_response: dict[str, Any],
532
535
  verifier_model: str,
533
536
  ) -> tuple[bool, dict[str, Any] | None]:
534
- """Phase 3.0 stub: return (True, None) treat as verified, no rewrite.
535
-
536
- Full implementation requires a sub-agent call to a cheap model.
537
- Stub is conservative: boundary learning still fires, so over time
538
- the boundary will tighten if errors accumulate.
539
-
540
- A-03 fix: callers do NOT call record_outcome() on this stub path
541
- (the True signal is fake would poison the MLE model).
537
+ """Verify a verify-band candidate before it is served. Fails CLOSED.
538
+
539
+ Real verification (a cheap sub-agent check that the cached response
540
+ actually answers *this* prompt) is not yet implemented. Until it is,
541
+ this returns (False, None) so the caller treats every verify-band
542
+ candidate as a MISS rather than serving an UNVERIFIED cached response —
543
+ which, for a semantically-similar-but-distinct prompt (common in
544
+ coding), could be a wrong answer. Only high-confidence hits
545
+ (score >= return_threshold) are ever served.
546
+
547
+ A prior stub returned (True, None), silently serving unverified hits;
548
+ that was a correctness hazard and is fixed here by failing closed.
542
549
  """
543
550
  logger.debug(
544
- "VCacheSemantic._verify_and_rewrite: stub returning True "
545
- "(verifier=%s entry=%s)", verifier_model, entry_id,
551
+ "VCacheSemantic._verify_and_rewrite: verifier not implemented "
552
+ "failing closed to a miss (verifier=%s entry=%s)",
553
+ verifier_model, entry_id,
546
554
  )
547
- return True, None
555
+ return False, None
548
556
 
549
557
  @staticmethod
550
558
  def _build_query_text(messages: list[dict[str, Any]], system: str) -> str:
@@ -1,17 +1,13 @@
1
1
  # compress/align.py
2
2
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
3
  # Licensed under AGPL-3.0-or-later
4
- #
5
- # Volatile-detection algorithm adapted from:
6
- # headroom/transforms/cache_aligner.py (Apache-2.0, Headroom contributors)
7
- # Specifically: _is_uuid(), _is_iso8601(), _is_jwt_shape(), _is_hex_hash(),
8
- # _classify_token(), _split_tokens(), detect_volatile_content()
9
- # Lines: cache_aligner.py:76-200
10
- # Attribution: See ATTRIBUTION.md.
11
4
 
12
- """CacheAligner — volatile-token detector for system prompt prefix stability.
5
+ """CacheAligner — flags volatile tokens in a system prompt.
13
6
 
14
- Phase 2: Detection only. No mutation of the system prompt.
7
+ A "volatile" token is one that differs between otherwise-identical requests —
8
+ a UUID, an ISO-8601 timestamp, a JWT, or a hex digest. When such tokens sit in
9
+ the stable prefix of a prompt they break provider prefix caching, so the router
10
+ surfaces them. This module only *detects*; it never rewrites the prompt.
15
11
  """
16
12
 
17
13
  from __future__ import annotations
@@ -25,10 +21,13 @@ from datetime import datetime
25
21
 
26
22
  logger = logging.getLogger("slm.optimize.compress.align")
27
23
 
28
- _HEX_HASH_LENGTHS = frozenset({32, 40, 64})
29
- _UUID_CANONICAL_LEN = 36 # L-02: 36 chars INCLUDING 4 dashes (RFC 4122 canonical form: 8-4-4-4-12)
30
- _JWT_SEGMENT_COUNT = 3
24
+ _HEX_HASH_LENGTHS = frozenset({32, 40, 64}) # md5 / sha1 / sha256 hex widths
25
+ _UUID_CANONICAL_LEN = 36 # 8-4-4-4-12, dashes included
26
+ _JWT_SEGMENTS = 3 # header.payload.signature
31
27
  _JWT_MIN_SEGMENT_BYTES = 4
28
+ _MAX_FINDINGS = 20 # cap the sample list we retain
29
+ _SAMPLE_LEN = 20 # chars kept per finding sample
30
+ _TOKEN_STRIP = ".,;:!?\"'()[]{}<>`|\\" # trimmed off each raw token
32
31
 
33
32
  _LABEL_UUID = "uuid"
34
33
  _LABEL_ISO8601 = "iso8601"
@@ -64,38 +63,44 @@ class CacheAligner:
64
63
  def _detect(text: str) -> AlignResult:
65
64
  tokens = _split_tokens(text)
66
65
  findings: list[VolatileFinding] = []
67
- volatile_count = 0
68
-
66
+ volatile = 0
69
67
  for token in tokens:
70
68
  label = _classify_token(token)
71
- if label is not None:
72
- volatile_count += 1
73
- if len(findings) < 20:
74
- findings.append(VolatileFinding(label=label, sample=token[:20]))
69
+ if label is None:
70
+ continue
71
+ volatile += 1
72
+ if len(findings) < _MAX_FINDINGS:
73
+ findings.append(VolatileFinding(label=label, sample=token[:_SAMPLE_LEN]))
75
74
 
76
75
  total = len(tokens)
77
- score = 1.0 - (volatile_count / total) if total > 0 else 1.0
78
-
76
+ score = round(1.0 - volatile / total, 4) if total else 1.0
79
77
  return AlignResult(
80
- prefix_stable=(volatile_count == 0),
81
- stability_score=round(score, 4),
78
+ prefix_stable=(volatile == 0),
79
+ stability_score=score,
82
80
  findings=findings,
83
81
  total_tokens_scanned=total,
84
82
  )
85
83
 
86
84
 
87
85
  def _split_tokens(content: str) -> list[str]:
86
+ """Whitespace-split, then strip surrounding punctuation/quoting from each
87
+ token so a value wrapped in backticks or brackets is still recognised."""
88
88
  if not content:
89
89
  return []
90
90
  tokens: list[str] = []
91
91
  for raw in content.split():
92
- cleaned = raw.strip(".,;:!?\"'()[]{}<>`|\\")
92
+ cleaned = raw.strip(_TOKEN_STRIP)
93
93
  if cleaned:
94
94
  tokens.append(cleaned)
95
95
  return tokens
96
96
 
97
97
 
98
98
  def _classify_token(token: str) -> str | None:
99
+ """Return the volatile-kind label for a token, or None when it is stable.
100
+
101
+ Ordered most-specific first: a canonical-length UUID before a dotted JWT,
102
+ then timestamps, then bare hex digests.
103
+ """
99
104
  if _is_uuid(token):
100
105
  return _LABEL_UUID
101
106
  if "." in token and _is_jwt_shape(token):
@@ -120,6 +125,7 @@ def _is_uuid(token: str) -> bool:
120
125
  def _is_iso8601(token: str) -> bool:
121
126
  if len(token) < 8 or ("T" not in token and "-" not in token):
122
127
  return False
128
+ # datetime.fromisoformat pre-3.11 rejects a trailing 'Z'; normalise it.
123
129
  candidate = token[:-1] + "+00:00" if token.endswith("Z") else token
124
130
  try:
125
131
  datetime.fromisoformat(candidate)
@@ -129,13 +135,13 @@ def _is_iso8601(token: str) -> bool:
129
135
 
130
136
 
131
137
  def _is_jwt_shape(token: str) -> bool:
132
- if token.count(".") != _JWT_SEGMENT_COUNT - 1:
133
- return False
134
138
  segments = token.split(".")
139
+ if len(segments) != _JWT_SEGMENTS:
140
+ return False
135
141
  for seg in segments:
136
142
  if len(seg) < _JWT_MIN_SEGMENT_BYTES:
137
143
  return False
138
- padded = seg + "=" * (-len(seg) % 4)
144
+ padded = seg + "=" * (-len(seg) % 4) # restore base64url padding
139
145
  try:
140
146
  base64.urlsafe_b64decode(padded.encode("ascii"))
141
147
  except (binascii.Error, ValueError, UnicodeEncodeError):
@@ -2,11 +2,6 @@
2
2
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
3
  # Licensed under AGPL-3.0-or-later
4
4
  #
5
- # CCR (Compressed Context Retrieval) concept and pattern:
6
- # headroom/ccr/ package (Apache-2.0, Headroom contributors)
7
- # Specifically: batch_store.py (BatchContext dataclass, TTL pattern),
8
- # tool_injection.py (MCP tool injection pattern)
9
- # Attribution: See ATTRIBUTION.md.
10
5
  # Storage: llmcache_ccr table defined in INTERFACE-CONTRACT §1.
11
6
  # Database access: CacheDB.ccr_put() and CacheDB.ccr_get() per INTERFACE-CONTRACT §1.
12
7
 
@@ -72,8 +67,8 @@ class CCRStore:
72
67
  if ttl_seconds is not None
73
68
  else None
74
69
  )
75
- db.ccr_put(ccr_id, original, ttl_expires=ttl_expires)
76
- logger.debug("CCR stored ccr_id=%s orig_bytes=%d", ccr_id, len(original))
70
+ db.ccr_put(ccr_id, original, ttl_expires=ttl_expires, tenant_id=tenant_id)
71
+ logger.debug("CCR stored ccr_id=%s orig_bytes=%d tenant=%s", ccr_id, len(original), tenant_id)
77
72
  return ccr_id
78
73
  except Exception as exc:
79
74
  logger.warning("CCRStore.store failed (fail-open): %s", exc)
@@ -88,84 +83,32 @@ class CCRStore:
88
83
  except Exception as exc:
89
84
  logger.debug("CCRStore.update_compressed failed (non-fatal): %s", exc)
90
85
 
91
- def retrieve(self, ccr_id: str) -> bytes | None:
92
- """Retrieve a CCR original by ccr_id. Returns None if not found or TTL expired."""
86
+ def retrieve(self, ccr_id: str, *, tenant_id: str = "default") -> bytes | None:
87
+ """Retrieve a CCR original by ccr_id scoped to tenant. Returns None if not found or TTL expired.
88
+
89
+ H-02: tenant_id enforces row-level isolation — a caller cannot retrieve
90
+ content stored under a different tenant's scope.
91
+ """
93
92
  try:
94
93
  db = self._get_db()
95
- return db.ccr_get(ccr_id)
94
+ return db.ccr_get(ccr_id, tenant_id=tenant_id)
96
95
  except Exception as exc:
97
96
  logger.warning("CCRStore.retrieve failed (ccr_id=%s): %s", ccr_id, exc)
98
97
  return None
99
98
 
100
- def delete(self, ccr_id: str) -> None:
101
- """Delete a CCR row by ccr_id. Idempotent — never raises.
99
+ def delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
100
+ """Delete a CCR row by ccr_id scoped to tenant. Idempotent — never raises.
102
101
 
103
102
  WP-10 D6: defensive infra. Deleting a non-existent ccr_id is a no-op.
103
+ H-02: tenant_id guard prevents cross-tenant deletion.
104
104
  """
105
105
  try:
106
106
  db = self._get_db()
107
- db.ccr_delete(ccr_id)
108
- logger.debug("CCR deleted ccr_id=%s", ccr_id)
107
+ db.ccr_delete(ccr_id, tenant_id=tenant_id)
108
+ logger.debug("CCR deleted ccr_id=%s tenant=%s", ccr_id, tenant_id)
109
109
  except Exception as exc:
110
110
  logger.warning("CCRStore.delete failed (non-fatal): %s", exc)
111
111
 
112
- def get_mcp_tool_definition(self) -> dict:
113
- return {
114
- "name": "headroom_retrieve",
115
- "description": (
116
- "Retrieve the original (pre-compression) text for a compressed content block. "
117
- "Use this when you need the full, uncompressed version of content that was "
118
- "compressed by SLM. Provide the ccr_id from the compression stub comment."
119
- ),
120
- "inputSchema": {
121
- "type": "object",
122
- "properties": {
123
- "ccr_id": {
124
- "type": "string",
125
- "description": "The ccr_id from the compression stub comment.",
126
- }
127
- },
128
- "required": ["ccr_id"],
129
- },
130
- }
131
-
132
- async def handle_mcp_call(self, arguments: dict) -> dict:
133
- ccr_id = arguments.get("ccr_id", "")
134
- if not ccr_id:
135
- return {
136
- "isError": True,
137
- "content": [{"type": "text", "text": "ccr_id is required"}],
138
- }
139
- if not _UUID4_RE.match(ccr_id):
140
- return {
141
- "isError": True,
142
- "content": [{
143
- "type": "text",
144
- "text": f"ccr_id must be a UUID4, got {ccr_id!r}",
145
- }],
146
- }
147
- original = self.retrieve(ccr_id)
148
- if original is None:
149
- return {
150
- "isError": True,
151
- "content": [{
152
- "type": "text",
153
- "text": (
154
- f"CCR original not found for ccr_id={ccr_id!r}. "
155
- "Possible causes: entry expired, never stored, or ccr_id incorrect."
156
- ),
157
- }],
158
- }
159
- try:
160
- text = original.decode("utf-8")
161
- except UnicodeDecodeError:
162
- logger.warning(
163
- "CCR ccr_id=%s: original bytes not valid UTF-8; falling back to latin-1",
164
- ccr_id,
165
- )
166
- text = original.decode("latin-1")
167
- return {"content": [{"type": "text", "text": text}]}
168
-
169
112
  def _get_db(self) -> "CacheDB":
170
113
  if self._db is None:
171
114
  from superlocalmemory.optimize.storage.db import CacheDB
@@ -1,11 +1,6 @@
1
1
  # compress/router.py
2
2
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
3
  # Licensed under AGPL-3.0-or-later
4
- #
5
- # Routing pattern adapted from:
6
- # headroom/transforms/content_router.py (Apache-2.0, Headroom contributors)
7
- # Specifically: ContentRouter._determine_strategy(), _strategy_from_detection()
8
- # Attribution: See ATTRIBUTION.md.
9
4
 
10
5
  """CompressRouter — implements CompressHook, dispatches to sub-compressors.
11
6
 
@@ -31,6 +26,10 @@ if TYPE_CHECKING:
31
26
  logger = logging.getLogger("slm.optimize.compress.router")
32
27
 
33
28
  _MIN_CHARS_FOR_COMPRESSION: int = 500
29
+ # Above this size a JSON-looking payload is passed through untouched rather than
30
+ # parsed — bounds worst-case parse cost on the hot path. Pretty-printed JSON in
31
+ # the 0.5 KB–2 MB range (the common tool-output case) is losslessly minified.
32
+ _MAX_JSON_MINIFY_CHARS: int = 2_000_000
34
33
 
35
34
 
36
35
  class CompressRouter:
@@ -143,8 +142,9 @@ class CompressRouter:
143
142
  try:
144
143
  saved = max(0, before_tokens - after_tokens)
145
144
  if self._metrics_counters is not None:
146
- # M-02: pass before/after directly (bytes_original, bytes_after contract)
147
- self._metrics_counters.on_compress(before_tokens, after_tokens)
145
+ # M-02: pass before/after directly (bytes_original, bytes_after contract).
146
+ # lossy distinguishes Layer-2 (LLMLingua) from lossless runs.
147
+ self._metrics_counters.on_compress(before_tokens, after_tokens, lossy)
148
148
  logger.debug("on_compress: saved=%d tokens lossy=%s", saved, lossy)
149
149
  except Exception as exc:
150
150
  logger.debug("on_compress metrics update failed (non-fatal): %s", exc)
@@ -271,25 +271,34 @@ class CompressRouter:
271
271
  if len(text) < _MIN_CHARS_FOR_COMPRESSION:
272
272
  return text, tokens_before, tokens_before, "none"
273
273
 
274
- # K-01/K-02/K-03: NEVER compress structured content (JSON or code)
274
+ # K-01 refined (P4a): structured JSON is never *lossily* compressed, but it
275
+ # IS losslessly minified — parse → reserialize with no insignificant
276
+ # whitespace, which preserves the exact parsed value (see _json_minify for
277
+ # the duplicate-key + round-trip guards that keep it provably lossless).
278
+ # Code is still passed through untouched below.
275
279
  stripped = text.strip()
276
280
  if stripped.startswith(("{", "[")):
277
- # PERF-02: for large content, structural bracket-match avoids O(n) json.loads().
278
- # Conservative: matching outer brackets → treat as JSON and skip compression.
279
- # K-01 mandate is safety-first: false-positive (non-JSON treated as JSON) is
280
- # safe; false-negative (JSON compressed) would be a correctness violation.
281
- _last = stripped[-1] if stripped else ""
282
- if len(stripped) > 8192 and (
283
- (stripped[0] == "{" and _last == "}") or (stripped[0] == "[" and _last == "]")
284
- ):
285
- return text, tokens_before, tokens_before, "none" # large JSON → passthrough
281
+ _last = stripped[-1]
282
+ bracket_matched = (
283
+ (stripped[0] == "{" and _last == "}")
284
+ or (stripped[0] == "[" and _last == "]")
285
+ )
286
+ # PERF-02: over the size cap, a bracket-matched payload is assumed
287
+ # structured and skipped avoids a pathological parse on the hot path.
288
+ if len(stripped) > _MAX_JSON_MINIFY_CHARS and bracket_matched:
289
+ return text, tokens_before, tokens_before, "none"
290
+ minified = _json_minify(stripped)
291
+ if minified is not None:
292
+ return minified, tokens_before, _token_estimate(minified), "json_minify"
293
+ # Not shrinkable: already-compact valid JSON passes through (K-01);
294
+ # anything that is not valid JSON falls through to prose/code handling.
286
295
  try:
287
296
  json.loads(stripped)
288
- return text, tokens_before, tokens_before, "none" # valid JSON → passthrough
289
- except json.JSONDecodeError:
297
+ return text, tokens_before, tokens_before, "none" # already-compact JSON
298
+ except (json.JSONDecodeError, ValueError):
290
299
  pass # not valid JSON — treat as prose
291
- except Exception as exc:
292
- logger.warning("compress: unexpected error probing JSON content: %s", exc)
300
+ except RecursionError:
301
+ return text, tokens_before, tokens_before, "none" # pathological nesting
293
302
 
294
303
  if _detect_language(text) is not None:
295
304
  return text, tokens_before, tokens_before, "none" # code → passthrough
@@ -417,9 +426,20 @@ class CompressRouter:
417
426
  # ── Public convenience method (M-06) ──────────────────────────────────
418
427
 
419
428
  def compress_text(self, text: str, strategy: str = "auto") -> "CompressTextResult":
420
- """Convenience method for test harness. NEVER raises."""
429
+ """Compress a single text blob (used by the slm_compress MCP tool). NEVER raises.
430
+
431
+ Honours ``compress_enabled``: when the operator has turned compression off
432
+ (``slm optimize off``) this is a pass-through, so the reported config state
433
+ and the tool's actual behaviour always agree (P4a config-honesty).
434
+ """
421
435
  try:
422
436
  cfg = self._get_config()
437
+ if not getattr(cfg, "compress_enabled", False):
438
+ t = _token_estimate(text)
439
+ return CompressTextResult(
440
+ compressed_text=text, strategy="none",
441
+ tokens_before=t, tokens_after=t, lossy=False,
442
+ )
423
443
  aggressive = cfg.compress_mode == "aggressive"
424
444
  compressed, tb, ta, strat = self._compress_text(
425
445
  text, aggressive, request_id="eval", model="", tenant_id="default"
@@ -447,6 +467,13 @@ class CompressTextResult:
447
467
  In the default install (LLMLingua optional dep not installed), lossy is
448
468
  always False — install `llmlingua>=0.2.0` and set compress_prose=True +
449
469
  compress_mode="aggressive" to activate lossy compression.
470
+
471
+ L2 note: this result is produced by the NON-proxy compress_text() path,
472
+ where Layer 2 (lossy) can apply. The LIVE proxy path
473
+ (_compress_messages(is_proxy=True)) DISABLES Layer 2 by design (D5-B) so
474
+ real LLM traffic is never lossily rewritten — so proxy-path compression
475
+ will never report strategy="llmlingua2_prose"/lossy=True regardless of
476
+ compress_prose/compress_mode.
450
477
  """
451
478
  compressed_text: str
452
479
  strategy: str # "normalize" | "llmlingua2_prose" | "none"
@@ -461,6 +488,62 @@ def _token_estimate(text: str) -> int:
461
488
  return len(text.split()) if text else 0
462
489
 
463
490
 
491
+ def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
492
+ """object_pairs_hook that refuses JSON objects with duplicate keys.
493
+
494
+ Reserializing ``{"a":1,"a":2}`` keeps only the last value, which is lossy —
495
+ so we bail on such inputs rather than silently drop data. Applied at every
496
+ nesting level by json.loads.
497
+ """
498
+ seen: set[str] = set()
499
+ for key, _ in pairs:
500
+ if key in seen:
501
+ raise ValueError("duplicate JSON object key")
502
+ seen.add(key)
503
+ return dict(pairs)
504
+
505
+
506
+ def _reject_nonfinite(_constant: str) -> None:
507
+ """parse_constant hook that refuses NaN / Infinity / -Infinity.
508
+
509
+ These are not valid standard JSON and reserializing them would emit a
510
+ non-standard token a strict downstream parser could reject — so we pass the
511
+ original through untouched rather than rewrite it.
512
+ """
513
+ raise ValueError(f"non-finite JSON constant: {_constant}")
514
+
515
+
516
+ def _json_minify(text: str) -> str | None:
517
+ """Losslessly minify a JSON document by removing insignificant whitespace.
518
+
519
+ Returns the compact form ONLY when it is strictly shorter than ``text`` AND
520
+ provably round-trips to the same parsed value; otherwise ``None`` (the caller
521
+ passes the original through untouched). Pure stdlib, zero-LLM — safe in every
522
+ mode including Mode A.
523
+
524
+ Losslessness guards:
525
+ - duplicate object keys are rejected (reserialization would drop data);
526
+ - NaN / Infinity constants are rejected (non-standard, reserialization risk);
527
+ - the compact form is re-parsed and compared to the original value as a
528
+ final belt-and-suspenders check before any rewrite is accepted.
529
+ """
530
+ stripped = text.strip()
531
+ if not stripped or stripped[0] not in "{[":
532
+ return None
533
+ try:
534
+ value = json.loads(
535
+ stripped,
536
+ object_pairs_hook=_reject_duplicate_keys,
537
+ parse_constant=_reject_nonfinite,
538
+ )
539
+ compact = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
540
+ if json.loads(compact) != value:
541
+ return None # representation not stable → do not rewrite
542
+ except (ValueError, RecursionError):
543
+ return None
544
+ return compact if len(compact) < len(text) else None
545
+
546
+
464
547
  def _msg_has_tool_result(msg: dict) -> bool:
465
548
  """B-09: Detect historical tool_result blocks in messages."""
466
549
  content = msg.get("content", "")
@@ -23,7 +23,7 @@ DEFAULT_OPTIMIZE_CONFIG = OptimizeConfig(
23
23
  semantic_boundary_floor=0.85,
24
24
  semantic_pad_latency_ms=0.0,
25
25
  semantic_centroid_min_similarity=0.85,
26
- compress_enabled=False,
26
+ compress_enabled=True,
27
27
  compress_mode="safe",
28
28
  compress_prose=False,
29
29
  compress_protect_recent=4,