superlocalmemory 3.6.22 → 3.7.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 (303) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +275 -72
  3. package/bin/slm-npm +43 -89
  4. package/docs/pi-dev-integration.md +43 -0
  5. package/ide/configs/antigravity-mcp.json +2 -2
  6. package/ide/configs/chatgpt-desktop-mcp.json +1 -1
  7. package/ide/configs/claude-desktop-mcp.json +2 -2
  8. package/ide/configs/windsurf-mcp.json +2 -2
  9. package/ide/hooks/context-hook.js +6 -2
  10. package/ide/hooks/post-recall-hook.js +7 -3
  11. package/ide/hooks/tool-event-hook.sh +2 -1
  12. package/package.json +19 -10
  13. package/plugin/.claude-plugin/plugin.json +1 -1
  14. package/plugin/_GENERATED.md +1 -1
  15. package/plugin/agents/slm-memory-advisor.md +1 -1
  16. package/plugin/requirements.txt +1 -1
  17. package/plugin/skills/slm-session/SKILL.md +1 -1
  18. package/plugin-src/rules/AGENTS.md +1 -1
  19. package/pyproject.toml +40 -8
  20. package/scripts/postinstall-interactive.js +17 -94
  21. package/scripts/postinstall.js +185 -258
  22. package/scripts/preuninstall.js +9 -50
  23. package/src/superlocalmemory/__init__.py +2 -2
  24. package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
  25. package/src/superlocalmemory/attribution/signer.py +34 -19
  26. package/src/superlocalmemory/attribution/watermark.py +1 -1
  27. package/src/superlocalmemory/cli/_lazy_init.py +3 -5
  28. package/src/superlocalmemory/cli/commands.py +490 -195
  29. package/src/superlocalmemory/cli/context_commands.py +5 -4
  30. package/src/superlocalmemory/cli/daemon.py +282 -187
  31. package/src/superlocalmemory/cli/db_migrate.py +3 -1
  32. package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
  33. package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
  34. package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
  35. package/src/superlocalmemory/cli/main.py +128 -31
  36. package/src/superlocalmemory/cli/pending_store.py +54 -38
  37. package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
  38. package/src/superlocalmemory/cli/service_installer.py +57 -52
  39. package/src/superlocalmemory/cli/setup_wizard.py +142 -88
  40. package/src/superlocalmemory/cli/version_banner.py +2 -1
  41. package/src/superlocalmemory/code_graph/config.py +3 -1
  42. package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
  43. package/src/superlocalmemory/core/config.py +65 -20
  44. package/src/superlocalmemory/core/consolidation_engine.py +9 -7
  45. package/src/superlocalmemory/core/context_cache.py +56 -8
  46. package/src/superlocalmemory/core/derivation_lineage.py +246 -0
  47. package/src/superlocalmemory/core/embedding_worker.py +32 -20
  48. package/src/superlocalmemory/core/embeddings.py +54 -18
  49. package/src/superlocalmemory/core/engine.py +150 -104
  50. package/src/superlocalmemory/core/engine_ingestion.py +513 -0
  51. package/src/superlocalmemory/core/engine_wiring.py +2 -0
  52. package/src/superlocalmemory/core/evidence_bundle.py +526 -0
  53. package/src/superlocalmemory/core/fact_consolidator.py +5 -11
  54. package/src/superlocalmemory/core/graph_analyzer.py +2 -2
  55. package/src/superlocalmemory/core/health_monitor.py +4 -2
  56. package/src/superlocalmemory/core/ingestion_command.py +636 -0
  57. package/src/superlocalmemory/core/injection.py +69 -18
  58. package/src/superlocalmemory/core/lifecycle_state.py +153 -0
  59. package/src/superlocalmemory/core/maintenance.py +23 -22
  60. package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
  61. package/src/superlocalmemory/core/mutations.py +143 -0
  62. package/src/superlocalmemory/core/platform_utils.py +7 -4
  63. package/src/superlocalmemory/core/ram_lock.py +16 -5
  64. package/src/superlocalmemory/core/rate_limit.py +1 -1
  65. package/src/superlocalmemory/core/recall_pipeline.py +60 -101
  66. package/src/superlocalmemory/core/recall_worker.py +76 -59
  67. package/src/superlocalmemory/core/registry.py +1 -1
  68. package/src/superlocalmemory/core/scale_engine.py +293 -0
  69. package/src/superlocalmemory/core/score_contract.py +62 -0
  70. package/src/superlocalmemory/core/security_primitives.py +3 -1
  71. package/src/superlocalmemory/core/slm_disabled.py +3 -5
  72. package/src/superlocalmemory/core/store_pipeline.py +172 -40
  73. package/src/superlocalmemory/core/tier_manager.py +32 -20
  74. package/src/superlocalmemory/core/worker_pool.py +13 -4
  75. package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
  76. package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
  77. package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
  78. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
  79. package/src/superlocalmemory/encoding/auto_linker.py +1 -1
  80. package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
  81. package/src/superlocalmemory/encoding/consolidator.py +22 -5
  82. package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
  83. package/src/superlocalmemory/encoding/foresight.py +2 -0
  84. package/src/superlocalmemory/encoding/graph_builder.py +1 -1
  85. package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
  86. package/src/superlocalmemory/evaluation/__init__.py +13 -0
  87. package/src/superlocalmemory/evaluation/calibration.py +308 -0
  88. package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
  89. package/src/superlocalmemory/graph/cozo_backend.py +256 -23
  90. package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
  91. package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
  92. package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
  93. package/src/superlocalmemory/hooks/auto_recall.py +31 -6
  94. package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
  95. package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
  96. package/src/superlocalmemory/hooks/claude_code_hooks.py +126 -39
  97. package/src/superlocalmemory/hooks/codex_assets.py +59 -0
  98. package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
  99. package/src/superlocalmemory/hooks/context_payload.py +1 -1
  100. package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
  101. package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
  102. package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
  103. package/src/superlocalmemory/hooks/hook_handlers.py +241 -55
  104. package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
  105. package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
  106. package/src/superlocalmemory/hooks/session_registry.py +15 -8
  107. package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
  108. package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
  109. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
  110. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
  111. package/src/superlocalmemory/infra/auth_middleware.py +38 -5
  112. package/src/superlocalmemory/infra/backup.py +7 -5
  113. package/src/superlocalmemory/infra/cloud_backup.py +18 -8
  114. package/src/superlocalmemory/infra/daemon_identity.py +248 -0
  115. package/src/superlocalmemory/infra/data_root.py +199 -0
  116. package/src/superlocalmemory/infra/event_bus.py +3 -1
  117. package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
  118. package/src/superlocalmemory/infra/process_reaper.py +23 -0
  119. package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
  120. package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
  121. package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
  122. package/src/superlocalmemory/ingestion/credentials.py +14 -7
  123. package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
  124. package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
  125. package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
  126. package/src/superlocalmemory/learning/ensemble.py +11 -0
  127. package/src/superlocalmemory/learning/entity_compiler.py +1 -1
  128. package/src/superlocalmemory/learning/feedback.py +1 -1
  129. package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
  130. package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
  131. package/src/superlocalmemory/learning/ranker.py +4 -1
  132. package/src/superlocalmemory/learning/source_quality.py +1 -1
  133. package/src/superlocalmemory/learning/trigram_index.py +3 -2
  134. package/src/superlocalmemory/llm/backbone.py +13 -8
  135. package/src/superlocalmemory/math/ebbinghaus.py +1 -1
  136. package/src/superlocalmemory/math/fisher.py +1 -1
  137. package/src/superlocalmemory/math/fisher_quantized.py +1 -1
  138. package/src/superlocalmemory/math/hopfield.py +1 -1
  139. package/src/superlocalmemory/math/langevin.py +1 -1
  140. package/src/superlocalmemory/math/polar_quant.py +3 -4
  141. package/src/superlocalmemory/math/qjl.py +1 -1
  142. package/src/superlocalmemory/math/sheaf.py +1 -1
  143. package/src/superlocalmemory/math/turbo_quant.py +3 -2
  144. package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
  145. package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
  146. package/src/superlocalmemory/mcp/http_transport.py +53 -0
  147. package/src/superlocalmemory/mcp/server.py +39 -13
  148. package/src/superlocalmemory/mcp/shared.py +69 -3
  149. package/src/superlocalmemory/mcp/tools_active.py +141 -31
  150. package/src/superlocalmemory/mcp/tools_core.py +128 -29
  151. package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
  152. package/src/superlocalmemory/mcp/tools_learning.py +42 -2
  153. package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
  154. package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
  155. package/src/superlocalmemory/mcp/tools_v28.py +23 -2
  156. package/src/superlocalmemory/mcp/tools_v3.py +26 -1
  157. package/src/superlocalmemory/mcp/tools_v33.py +56 -17
  158. package/src/superlocalmemory/mesh/broker.py +2 -0
  159. package/src/superlocalmemory/mesh/remote_sync.py +50 -12
  160. package/src/superlocalmemory/optimize/cache/manager.py +77 -1
  161. package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
  162. package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
  163. package/src/superlocalmemory/optimize/compress/router.py +6 -1
  164. package/src/superlocalmemory/optimize/config/__init__.py +5 -0
  165. package/src/superlocalmemory/optimize/config/store.py +6 -4
  166. package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
  167. package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
  168. package/src/superlocalmemory/optimize/proxy/server.py +2 -2
  169. package/src/superlocalmemory/optimize/storage/db.py +14 -13
  170. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  171. package/src/superlocalmemory/retrieval/ann_index.py +1 -1
  172. package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
  173. package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
  174. package/src/superlocalmemory/retrieval/engine.py +169 -79
  175. package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
  176. package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
  177. package/src/superlocalmemory/retrieval/fusion.py +1 -1
  178. package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
  179. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  180. package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
  181. package/src/superlocalmemory/retrieval/reranker.py +56 -20
  182. package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
  183. package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
  184. package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
  185. package/src/superlocalmemory/retrieval/strategy.py +1 -1
  186. package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
  187. package/src/superlocalmemory/retrieval/vector_store.py +1 -1
  188. package/src/superlocalmemory/server/api.py +10 -7
  189. package/src/superlocalmemory/server/bandit_loops.py +4 -2
  190. package/src/superlocalmemory/server/recall_serializer.py +24 -0
  191. package/src/superlocalmemory/server/route_mutations.py +84 -0
  192. package/src/superlocalmemory/server/routes/agents.py +8 -6
  193. package/src/superlocalmemory/server/routes/brain.py +14 -12
  194. package/src/superlocalmemory/server/routes/chat.py +29 -12
  195. package/src/superlocalmemory/server/routes/data_io.py +55 -24
  196. package/src/superlocalmemory/server/routes/helpers.py +29 -4
  197. package/src/superlocalmemory/server/routes/ingest.py +53 -36
  198. package/src/superlocalmemory/server/routes/memories.py +104 -43
  199. package/src/superlocalmemory/server/routes/mesh.py +31 -0
  200. package/src/superlocalmemory/server/routes/profiles.py +26 -4
  201. package/src/superlocalmemory/server/routes/tiers.py +43 -11
  202. package/src/superlocalmemory/server/routes/timeline.py +5 -1
  203. package/src/superlocalmemory/server/routes/v3_api.py +76 -21
  204. package/src/superlocalmemory/server/security_middleware.py +1 -1
  205. package/src/superlocalmemory/server/ui.py +6 -3
  206. package/src/superlocalmemory/server/unified_daemon.py +680 -293
  207. package/src/superlocalmemory/server/write_identity.py +147 -0
  208. package/src/superlocalmemory/storage/access_log.py +4 -3
  209. package/src/superlocalmemory/storage/database.py +118 -25
  210. package/src/superlocalmemory/storage/migration_runner.py +84 -1
  211. package/src/superlocalmemory/storage/migration_v33.py +1 -1
  212. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
  213. package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
  214. package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
  215. package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
  216. package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
  217. package/src/superlocalmemory/storage/models.py +16 -0
  218. package/src/superlocalmemory/storage/quantized_store.py +20 -3
  219. package/src/superlocalmemory/storage/v2_migrator.py +5 -3
  220. package/src/superlocalmemory/ui/favicon.svg +5 -0
  221. package/src/superlocalmemory/ui/index.html +1 -0
  222. package/src/superlocalmemory/ui/js/compliance.js +1 -1
  223. package/src/superlocalmemory/ui/js/core.js +49 -8
  224. package/src/superlocalmemory/ui/js/dashboard.js +23 -2
  225. package/src/superlocalmemory/ui/js/feedback.js +1 -1
  226. package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
  227. package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
  228. package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
  229. package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
  230. package/src/superlocalmemory/ui/js/settings.js +4 -2
  231. package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
  232. package/bin/slm +0 -59
  233. package/bin/slm.bat +0 -77
  234. package/bin/slm.cmd +0 -5
  235. package/ide/integrations/langchain/README.md +0 -106
  236. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
  237. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
  238. package/ide/integrations/langchain/pyproject.toml +0 -38
  239. package/ide/integrations/langchain/tests/__init__.py +0 -3
  240. package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
  241. package/ide/integrations/langchain/tests/test_security.py +0 -117
  242. package/ide/integrations/llamaindex/README.md +0 -81
  243. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
  244. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
  245. package/ide/integrations/llamaindex/pyproject.toml +0 -43
  246. package/ide/integrations/llamaindex/tests/__init__.py +0 -3
  247. package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
  248. package/ide/integrations/llamaindex/tests/test_security.py +0 -241
  249. package/plugin-src/.mcp.json +0 -12
  250. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  251. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  252. package/plugin-src/hooks/.gitkeep +0 -0
  253. package/plugin-src/hooks/hooks.json +0 -23
  254. package/plugin-src/manifest.json +0 -25
  255. package/plugin-src/requirements.txt +0 -1
  256. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  257. package/plugin-src/scripts/ensure-venv.bat +0 -122
  258. package/plugin-src/scripts/ensure-venv.sh +0 -105
  259. package/plugin-src/scripts/slm-launch +0 -15
  260. package/plugin-src/scripts/slm-launch.bat +0 -17
  261. package/plugin-src/settings.json +0 -16
  262. package/plugin-src/skills/slm-cache/SKILL.md +0 -140
  263. package/plugin-src/skills/slm-compress/SKILL.md +0 -143
  264. package/plugin-src/skills/slm-graph/SKILL.md +0 -300
  265. package/plugin-src/skills/slm-recall/SKILL.md +0 -204
  266. package/plugin-src/skills/slm-remember/SKILL.md +0 -194
  267. package/plugin-src/skills/slm-session/SKILL.md +0 -207
  268. package/plugin-src/skills/slm-status/SKILL.md +0 -149
  269. package/scripts/__tests__/build-plugin.test.mjs +0 -613
  270. package/scripts/_savings_math.py +0 -270
  271. package/scripts/build-dmg.sh +0 -417
  272. package/scripts/build-plugin.js +0 -742
  273. package/scripts/build-slm-hook.ps1 +0 -40
  274. package/scripts/build-slm-hook.sh +0 -45
  275. package/scripts/build_entry.py +0 -452
  276. package/scripts/ci/stage5b_gate.sh +0 -50
  277. package/scripts/dogfood_savings.py +0 -490
  278. package/scripts/generate-thumbnails.py +0 -218
  279. package/scripts/install-skills.ps1 +0 -4
  280. package/scripts/install-skills.sh +0 -5
  281. package/scripts/install.ps1 +0 -701
  282. package/scripts/install.sh +0 -1015
  283. package/scripts/postinstall_binary.js +0 -287
  284. package/scripts/prepack.js +0 -33
  285. package/scripts/release_manifest.py +0 -273
  286. package/scripts/slm-hook.spec +0 -56
  287. package/scripts/start-dashboard.ps1 +0 -52
  288. package/scripts/start-dashboard.sh +0 -41
  289. package/scripts/sync-wiki.ps1 +0 -127
  290. package/scripts/sync-wiki.sh +0 -82
  291. package/scripts/test-dmg.sh +0 -161
  292. package/scripts/test-npm-package.ps1 +0 -252
  293. package/scripts/test-npm-package.sh +0 -207
  294. package/scripts/verify-install.ps1 +0 -294
  295. package/scripts/verify-install.sh +0 -266
  296. package/scripts/verify-v27.ps1 +0 -301
  297. package/scripts/verify-v27.sh +0 -233
  298. package/src/superlocalmemory.egg-info/PKG-INFO +0 -513
  299. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
  300. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  301. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  302. package/src/superlocalmemory.egg-info/requires.txt +0 -71
  303. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -1,490 +0,0 @@
1
- #!/usr/bin/env python3
2
- # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
- # Licensed under AGPL-3.0-or-later - see LICENSE file
4
- # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
5
-
6
- """dogfood_savings.py — WP-14 metered-pipeline $-savings evidence script.
7
-
8
- Drives a repeated-prompt workload through the production CacheManager +
9
- MetricsCollector, reads the MetricsSnapshot delta, and emits a Report
10
- (JSON ledger + Markdown) with reproducible $ savings.
11
-
12
- RESEARCH INTEGRITY: Every $ number traces to a real (tokens_saved × price)
13
- computation from actual cache/compress stats. No hardcoded percentages.
14
- Prices cite R6 (2026-06-16). OpenAI prices are UNVERIFIED-OFFICIAL.
15
-
16
- Usage:
17
- # Sim (no API key, no network, CI-safe):
18
- python3 scripts/dogfood_savings.py --mode sim --provider anthropic
19
-
20
- # Live (ONE manual run by Varun, ~$0.05-0.20):
21
- SLM_DOGFOOD_API_KEY=sk-... python3 scripts/dogfood_savings.py \\
22
- --mode live --i-will-spend-money --provider anthropic
23
-
24
- AC6: proxy state is restored in try/finally — including on exception.
25
- """
26
-
27
- from __future__ import annotations
28
-
29
- import argparse
30
- import contextlib
31
- import json
32
- import os
33
- import sys
34
- import tempfile
35
- from pathlib import Path
36
- from typing import Iterator
37
-
38
- # ---------------------------------------------------------------------------
39
- # Path setup — allow running as scripts/dogfood_savings.py from repo root
40
- # ---------------------------------------------------------------------------
41
- _REPO_ROOT = Path(__file__).resolve().parent.parent
42
- _SRC = _REPO_ROOT / "src"
43
- _SCRIPTS = _REPO_ROOT / "scripts"
44
-
45
- if str(_SRC) not in sys.path:
46
- sys.path.insert(0, str(_SRC))
47
- if str(_SCRIPTS) not in sys.path:
48
- sys.path.insert(0, str(_SCRIPTS))
49
-
50
- from _savings_math import ( # noqa: E402 — import after path setup
51
- MetricsDelta,
52
- Report,
53
- compute_savings_usd,
54
- metrics_delta,
55
- resolve_price,
56
- to_inr,
57
- )
58
-
59
- # ---------------------------------------------------------------------------
60
- # Constants (from optimize_constants.py — read-only)
61
- # ---------------------------------------------------------------------------
62
- # We import here to maintain contract-twin parity with optimize_cmd.py:145-155.
63
- from superlocalmemory.cli.optimize_constants import ( # noqa: E402
64
- DEFAULT_COST_PER_MILLION_INPUT_TOKENS,
65
- _PRICING_DATE,
66
- )
67
-
68
- _DEFAULT_PROMPTS = 20
69
- _DEFAULT_REPEAT = 5
70
- _DEFAULT_PROVIDER = "anthropic"
71
- _DEFAULT_INR_RATE = 83.5
72
-
73
- # Openai prices are from R6 but UNVERIFIED-OFFICIAL.
74
- _OPENAI_UNVERIFIED_NOTE = "UNVERIFIED-OFFICIAL"
75
-
76
- # Output paths per LLD §2 / D-B LOCKED
77
- _EVIDENCE_DIR = _REPO_ROOT / ".backup" / "v3.6.14" / "evidence" / "wp14"
78
- _DOCS_EVIDENCE_MD = _REPO_ROOT / "docs" / "evidence" / "optimize-savings.md"
79
-
80
-
81
- # ---------------------------------------------------------------------------
82
- # Sim workload
83
- # ---------------------------------------------------------------------------
84
-
85
- def _build_sim_prompt(index: int) -> str:
86
- """Build a char-exact prompt so chars//4 = integer tokens exactly.
87
-
88
- Pattern: 40 chars (10 tokens) per prompt, varied by index.
89
- This ensures M-01 (chars//4) produces predictable, non-zero values.
90
- """
91
- # 40 chars = 10 tokens exactly via chars // 4 = 40 // 4 = 10
92
- base = f"Prompt{index:04d}: What is the capital of France? "
93
- # Pad/truncate to exactly 40 chars
94
- padded = base[:40].ljust(40)
95
- return padded
96
-
97
-
98
- def _build_sim_response(output_tokens: int = 8) -> dict:
99
- """Build a synthetic provider response with a usage block.
100
-
101
- The usage block is required for M-02 (output token recovery) in on_hit.
102
- """
103
- return {
104
- "id": "sim-resp-001",
105
- "type": "message",
106
- "role": "assistant",
107
- "content": [{"type": "text", "text": "Paris is the capital of France."}],
108
- "stop_reason": "end_turn",
109
- "usage": {
110
- "input_tokens": 10,
111
- "output_tokens": output_tokens,
112
- },
113
- }
114
-
115
-
116
- def run_workload_sim(
117
- n_prompts: int,
118
- n_repeat: int,
119
- tmp_db_path: Path,
120
- ) -> None:
121
- """Drive N prompts × (1 miss + R hits) through production CacheManager.
122
-
123
- Uses a temp CacheDB — never touches user's llmcache.db (AC8 / LLD §8).
124
- Synthetic responses include a `usage` block so M-02 recovers output tokens.
125
- """
126
- import superlocalmemory.optimize.storage.db as _db_mod
127
- from superlocalmemory.optimize.cache.manager import CacheManager
128
- from superlocalmemory.optimize.metrics.counters import MetricsCollector
129
- from superlocalmemory.optimize.proxy.lifecycle import ProxyRequest
130
- from superlocalmemory.optimize.storage.db import CacheDB
131
-
132
- # Isolate AES key to tmp dir (mirrors tests/optimize/conftest.py pattern)
133
- original_key_file = getattr(_db_mod, "_KEY_FILE", None)
134
- _db_mod._KEY_FILE = tmp_db_path.parent / "opt-key.bin"
135
-
136
- try:
137
- cache_db = CacheDB(tmp_db_path)
138
- cm = CacheManager(cache_db)
139
- # Reset MetricsCollector singleton for isolation
140
- MetricsCollector._instance = None
141
- collector = MetricsCollector.get_instance()
142
-
143
- for i in range(n_prompts):
144
- prompt_text = _build_sim_prompt(i)
145
- sim_response = _build_sim_response()
146
- resp_bytes = json.dumps(sim_response).encode()
147
-
148
- # Build a ProxyRequest (immutable)
149
- req = ProxyRequest(
150
- provider="anthropic",
151
- method="POST",
152
- path="/v1/messages",
153
- headers={}, # redacted per CWE-532
154
- body={
155
- "model": "claude-sonnet-4-6",
156
- "messages": [{"role": "user", "content": prompt_text}],
157
- "max_tokens": 50,
158
- },
159
- body_bytes=b"",
160
- request_id=f"sim-{i:04d}",
161
- stream=False,
162
- has_tools=False,
163
- )
164
-
165
- # First call: miss (store response)
166
- hit = cm.check(req)
167
- if hit is None or not hit.hit:
168
- from superlocalmemory.optimize.proxy.lifecycle import ProviderResponse
169
- prov_resp = ProviderResponse(
170
- modified=False,
171
- body=sim_response,
172
- body_bytes=resp_bytes,
173
- tokens_before=0,
174
- tokens_after=0,
175
- strategy="none",
176
- )
177
- cm.store(req, prov_resp)
178
-
179
- # Repeat calls: hits (tokens recovered via on_hit M-01/M-02)
180
- for _ in range(n_repeat):
181
- hit = cm.check(req)
182
- if hit is not None and hit.hit:
183
- cm.on_hit(req, hit.data or resp_bytes, tokens_saved=0)
184
- else:
185
- # Should not happen in sim; record miss for safety
186
- collector.on_miss()
187
-
188
- finally:
189
- if original_key_file is not None:
190
- _db_mod._KEY_FILE = original_key_file
191
-
192
-
193
- # ---------------------------------------------------------------------------
194
- # Proxy context manager (for live mode, AC6)
195
- # ---------------------------------------------------------------------------
196
-
197
- @contextlib.contextmanager
198
- def with_proxy_enabled() -> Iterator[None]:
199
- """Enable proxy for the duration of the block; restore in finally.
200
-
201
- Reads optimize.json, sets enable_cache=True, restores on exit (AC6).
202
- Only safe for ONE concurrent user — live docs "no other proxy traffic".
203
- """
204
- from superlocalmemory.optimize.config.store import ConfigStore
205
-
206
- store = ConfigStore()
207
- cfg = store.get()
208
- original_enable_cache = cfg.enable_cache
209
- original_enable_compress = cfg.enable_compress
210
-
211
- try:
212
- # Enable cache for the live run
213
- new_cfg = store.get()
214
- new_cfg.enable_cache = True
215
- store.save(new_cfg)
216
- yield
217
- finally:
218
- # Restore exactly (AC6 — even on exception)
219
- restore_cfg = store.get()
220
- restore_cfg.enable_cache = original_enable_cache
221
- restore_cfg.enable_compress = original_enable_compress
222
- store.save(restore_cfg)
223
-
224
-
225
- # ---------------------------------------------------------------------------
226
- # Live workload (AC4 gated)
227
- # ---------------------------------------------------------------------------
228
-
229
- def run_workload_live(
230
- n_prompts: int,
231
- n_repeat: int,
232
- api_key: str,
233
- provider: str,
234
- ) -> None:
235
- """Route real HTTP through proxy at 127.0.0.1:8765.
236
-
237
- SECURITY: api_key comes from env only, NEVER logged or printed.
238
- This function is excluded from CI coverage (live path).
239
- """
240
- import urllib.request # stdlib only — no new deps
241
-
242
- port = 8765
243
- base_url = f"http://127.0.0.1:{port}"
244
-
245
- headers = {
246
- "Content-Type": "application/json",
247
- "x-api-key": api_key, # never logged
248
- }
249
-
250
- prompt = "What is 2 + 2? Answer in one word."
251
-
252
- for i in range(n_prompts):
253
- body = json.dumps({
254
- "model": "claude-haiku-4-5", # cheapest Anthropic model
255
- "messages": [{"role": "user", "content": f"[{i}] {prompt}"}],
256
- "max_tokens": 16,
257
- }).encode()
258
-
259
- # First call: miss
260
- req = urllib.request.Request(
261
- f"{base_url}/anthropic/v1/messages",
262
- data=body,
263
- headers=headers,
264
- method="POST",
265
- )
266
- with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
267
- resp.read()
268
-
269
- # Repeat calls: hits
270
- for _ in range(n_repeat):
271
- req2 = urllib.request.Request(
272
- f"{base_url}/anthropic/v1/messages",
273
- data=body,
274
- headers=headers,
275
- method="POST",
276
- )
277
- with urllib.request.urlopen(req2, timeout=30) as resp2: # noqa: S310
278
- resp2.read()
279
-
280
-
281
- # ---------------------------------------------------------------------------
282
- # Snapshot helpers
283
- # ---------------------------------------------------------------------------
284
-
285
- def _snapshot_from_collector() -> object:
286
- """Read current MetricsCollector state as a MetricsSnapshot-compatible object."""
287
- from superlocalmemory.optimize.metrics.counters import MetricsCollector
288
- from superlocalmemory.optimize.storage.db import MetricsSnapshot
289
-
290
- c = MetricsCollector.get_instance()
291
- snap = c.snapshot() if hasattr(c, "snapshot") else None
292
- if snap is not None:
293
- return snap
294
- # Fallback: read fields directly
295
- return MetricsSnapshot(
296
- hits=c._hits,
297
- misses=c._misses,
298
- tokens_saved_input=c._tokens_saved_input,
299
- tokens_saved_output=c._tokens_saved_output,
300
- tokens_saved_compress=c._tokens_saved_compress,
301
- )
302
-
303
-
304
- def _zero_snapshot() -> object:
305
- """Return a zeroed MetricsSnapshot."""
306
- from superlocalmemory.optimize.storage.db import MetricsSnapshot
307
- return MetricsSnapshot()
308
-
309
-
310
- # ---------------------------------------------------------------------------
311
- # Main
312
- # ---------------------------------------------------------------------------
313
-
314
- def _build_price_source(provider: str, price: float, source_key: str) -> str:
315
- """Build a human-readable price source string for the report."""
316
- date = _PRICING_DATE
317
- r6 = "R6 (2026-06-16)"
318
- if provider.lower() == "openai":
319
- return (
320
- f"DEFAULT_COST_PER_MILLION_INPUT_TOKENS[{provider}]={price}, "
321
- f"{date}, {r6}, {_OPENAI_UNVERIFIED_NOTE}"
322
- )
323
- return f"{source_key}={price}, {date}, {r6}, VERIFIED (Anthropic official)"
324
-
325
-
326
- def main() -> int:
327
- parser = argparse.ArgumentParser(
328
- description="WP-14: metered-pipeline $-savings dogfood evidence script.",
329
- formatter_class=argparse.RawDescriptionHelpFormatter,
330
- epilog=(
331
- "RESEARCH INTEGRITY: Every $ number traces to tokens_saved × price.\n"
332
- "Sim mode is CI-safe (no key, no network). Live mode costs real money.\n"
333
- "OpenAI prices: UNVERIFIED-OFFICIAL (see R6 2026-06-16).\n"
334
- ),
335
- )
336
- parser.add_argument(
337
- "--mode", choices=["sim", "live"], default="sim",
338
- help="sim = offline/CI-safe; live = real HTTP via proxy (costs money)",
339
- )
340
- parser.add_argument(
341
- "--provider", default=_DEFAULT_PROVIDER,
342
- choices=["anthropic", "openai", "gemini"],
343
- help="Provider key for pricing lookup (default: anthropic)",
344
- )
345
- parser.add_argument(
346
- "--prompts", type=int, default=_DEFAULT_PROMPTS,
347
- help="Number of distinct prompts (N). Default: 20",
348
- )
349
- parser.add_argument(
350
- "--repeat", type=int, default=_DEFAULT_REPEAT,
351
- help="Number of cache-hit repeats per prompt (R). Default: 5",
352
- )
353
- parser.add_argument(
354
- "--i-will-spend-money", action="store_true",
355
- help="Required for --mode live. Confirms you accept real API spend.",
356
- )
357
- parser.add_argument(
358
- "--out-md", type=Path, default=None,
359
- help="Output path for Markdown report (default: docs/evidence/optimize-savings.md)",
360
- )
361
- parser.add_argument(
362
- "--out-json", type=Path, default=None,
363
- help="Output path for JSON ledger (default: .backup/v3.6.14/evidence/wp14/raw-metrics-<mode>-<utc>.json)",
364
- )
365
-
366
- args = parser.parse_args()
367
-
368
- # AC4: live mode guards
369
- if args.mode == "live":
370
- if not getattr(args, "i_will_spend_money", False):
371
- print(
372
- "ERROR: --mode live requires --i-will-spend-money flag.\n"
373
- "This mode routes real requests through the proxy and costs real money.",
374
- file=sys.stderr,
375
- )
376
- return 2
377
-
378
- api_key = os.environ.get("SLM_DOGFOOD_API_KEY", "")
379
- if not api_key:
380
- print(
381
- "ERROR: SLM_DOGFOOD_API_KEY environment variable not set.\n"
382
- "Set it to your Anthropic API key before running --mode live.",
383
- file=sys.stderr,
384
- )
385
- return 2
386
-
387
- # Resolve price (parity with optimize_cmd.py:146-153)
388
- price, price_source_key = resolve_price(
389
- args.provider,
390
- DEFAULT_COST_PER_MILLION_INPUT_TOKENS,
391
- {}, # no config overrides in dogfood — uses defaults for reproducibility
392
- )
393
- price_source = _build_price_source(args.provider, price, price_source_key)
394
-
395
- # ---------------------------------------------------------------------------
396
- # Run workload and collect delta
397
- # ---------------------------------------------------------------------------
398
-
399
- if args.mode == "sim":
400
- with tempfile.TemporaryDirectory() as _tmp:
401
- tmp_dir = Path(_tmp)
402
- tmp_db = tmp_dir / "llmcache.db"
403
-
404
- # Capture before (zeroed — fresh temp DB)
405
- before = _zero_snapshot()
406
-
407
- run_workload_sim(args.prompts, args.repeat, tmp_db)
408
-
409
- # Read after from the MetricsCollector (which on_hit updated)
410
- after = _snapshot_from_collector()
411
-
412
- else:
413
- # Live mode: use real CacheDB via proxy
414
- before_db = _read_live_snapshot()
415
- with with_proxy_enabled():
416
- api_key = os.environ["SLM_DOGFOOD_API_KEY"] # already validated above
417
- run_workload_live(args.prompts, args.repeat, api_key, args.provider)
418
- after_db = _read_live_snapshot()
419
- before = before_db
420
- after = after_db
421
-
422
- # ---------------------------------------------------------------------------
423
- # Compute savings
424
- # ---------------------------------------------------------------------------
425
-
426
- delta = metrics_delta(before, after)
427
- savings_usd = compute_savings_usd(
428
- delta.tokens_saved_input,
429
- delta.tokens_saved_output,
430
- delta.tokens_saved_compress,
431
- price_per_1m=price,
432
- )
433
- savings_inr = to_inr(savings_usd, _DEFAULT_INR_RATE)
434
-
435
- report = Report(
436
- mode=args.mode,
437
- provider=args.provider,
438
- prompts=args.prompts,
439
- repeat=args.repeat,
440
- delta=delta,
441
- price_per_1m=price,
442
- price_source=price_source,
443
- savings_usd=savings_usd,
444
- savings_inr=savings_inr,
445
- inr_rate=_DEFAULT_INR_RATE,
446
- )
447
-
448
- # ---------------------------------------------------------------------------
449
- # Write artifacts
450
- # ---------------------------------------------------------------------------
451
-
452
- import datetime as _dt
453
-
454
- utc_tag = _dt.datetime.now(_dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
455
-
456
- # JSON ledger path
457
- if args.out_json is not None:
458
- json_path = Path(args.out_json)
459
- else:
460
- _EVIDENCE_DIR.mkdir(parents=True, exist_ok=True)
461
- json_path = _EVIDENCE_DIR / f"raw-metrics-{args.mode}-{utc_tag}.json"
462
-
463
- # MD report path
464
- if args.out_md is not None:
465
- md_path = Path(args.out_md)
466
- else:
467
- md_path = _DOCS_EVIDENCE_MD
468
-
469
- json_path.parent.mkdir(parents=True, exist_ok=True)
470
- md_path.parent.mkdir(parents=True, exist_ok=True)
471
-
472
- json_path.write_text(report.to_json(), encoding="utf-8")
473
- md_path.write_text(report.to_markdown(), encoding="utf-8")
474
-
475
- print(report.to_markdown())
476
- print(f"\nJSON ledger: {json_path}")
477
- print(f"MD report: {md_path}")
478
-
479
- return 0
480
-
481
-
482
- def _read_live_snapshot() -> object:
483
- """Read snapshot from the live CacheDB (for --mode live)."""
484
- from superlocalmemory.optimize.storage.db import CacheDB
485
- db = CacheDB.get_default()
486
- return db.metrics_load()
487
-
488
-
489
- if __name__ == "__main__":
490
- raise SystemExit(main())