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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +69 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +94 -10
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +8 -1
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- 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
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
"""
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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:
|
|
545
|
-
"(verifier=%s entry=%s)",
|
|
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
|
|
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
|
|
5
|
+
"""CacheAligner — flags volatile tokens in a system prompt.
|
|
13
6
|
|
|
14
|
-
|
|
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
|
|
30
|
-
|
|
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
|
-
|
|
68
|
-
|
|
66
|
+
volatile = 0
|
|
69
67
|
for token in tokens:
|
|
70
68
|
label = _classify_token(token)
|
|
71
|
-
if label is
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 -
|
|
78
|
-
|
|
76
|
+
score = round(1.0 - volatile / total, 4) if total else 1.0
|
|
79
77
|
return AlignResult(
|
|
80
|
-
prefix_stable=(
|
|
81
|
-
stability_score=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
):
|
|
285
|
-
return text, tokens_before, tokens_before, "none"
|
|
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" #
|
|
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
|
|
292
|
-
|
|
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
|
-
"""
|
|
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=
|
|
26
|
+
compress_enabled=True,
|
|
27
27
|
compress_mode="safe",
|
|
28
28
|
compress_prose=False,
|
|
29
29
|
compress_protect_recent=4,
|