clsplusplus 7.3.2__tar.gz → 7.4.1__tar.gz

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 (188) hide show
  1. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/PKG-INFO +1 -1
  2. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/pyproject.toml +1 -1
  3. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/__init__.py +9 -1
  4. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/api.py +380 -0
  5. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/config.py +13 -0
  6. clsplusplus-7.4.1/src/clsplusplus/durable_log/__init__.py +21 -0
  7. clsplusplus-7.4.1/src/clsplusplus/durable_log/file_wal.py +181 -0
  8. clsplusplus-7.4.1/src/clsplusplus/durable_log/manager.py +149 -0
  9. clsplusplus-7.4.1/src/clsplusplus/durable_log/record.py +88 -0
  10. clsplusplus-7.4.1/src/clsplusplus/durable_log/replicas.py +77 -0
  11. clsplusplus-7.4.1/src/clsplusplus/login_activity.py +97 -0
  12. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/memory_service.py +67 -4
  13. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/models.py +3 -0
  14. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/integration_store.py +28 -0
  15. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/l1_indexing_store.py +64 -0
  16. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/user_store.py +32 -0
  17. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/PKG-INFO +1 -1
  18. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/SOURCES.txt +8 -0
  19. clsplusplus-7.4.1/tests/test_admin_integrations_activity.py +289 -0
  20. clsplusplus-7.4.1/tests/test_durable_log.py +317 -0
  21. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/LICENSE +0 -0
  22. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/README.md +0 -0
  23. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/setup.cfg +0 -0
  24. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/abuse_guard.py +0 -0
  25. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/api_usage_analytics.py +0 -0
  26. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/auth.py +0 -0
  27. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/cli.py +0 -0
  28. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/client.py +0 -0
  29. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/cost_forecast.py +0 -0
  30. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/cost_model.py +0 -0
  31. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/debug_console.py +0 -0
  32. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/demo_llm.py +0 -0
  33. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/demo_llm_calls.py +0 -0
  34. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/demo_local.py +0 -0
  35. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/email_service.py +0 -0
  36. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/embeddings.py +0 -0
  37. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/funnel_routes.py +0 -0
  38. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/geo.py +0 -0
  39. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/health_metrics.py +0 -0
  40. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/homepage_autopromote.py +0 -0
  41. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/idempotency.py +0 -0
  42. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integration_service.py +0 -0
  43. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integrations/__init__.py +0 -0
  44. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integrations/autogen.py +0 -0
  45. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integrations/crewai.py +0 -0
  46. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integrations/langgraph.py +0 -0
  47. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/integrations/llamaindex.py +0 -0
  48. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/jwt_utils.py +0 -0
  49. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/local_routes.py +0 -0
  50. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/main.py +0 -0
  51. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/mcp_http.py +0 -0
  52. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/mcp_server.py +0 -0
  53. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/memory_cycle.py +0 -0
  54. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/memory_phase.py +0 -0
  55. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/__init__.py +0 -0
  56. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/__main__.py +0 -0
  57. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/billing.py +0 -0
  58. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
  59. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/notifier.py +0 -0
  60. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/pricing.py +0 -0
  61. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/reconciler.py +0 -0
  62. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/schema.py +0 -0
  63. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metering_v2/writer.py +0 -0
  64. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/metrics.py +0 -0
  65. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/middleware.py +0 -0
  66. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/namespace_resolver.py +0 -0
  67. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/oauth_server.py +0 -0
  68. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/permissions.py +0 -0
  69. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/plasticity.py +0 -0
  70. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/pricing.py +0 -0
  71. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/pricing_models.py +0 -0
  72. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/pricing_store.py +0 -0
  73. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/prompt_log.py +0 -0
  74. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/rate_limit.py +0 -0
  75. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/razorpay_service.py +0 -0
  76. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/rbac_service.py +0 -0
  77. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/reconsolidation.py +0 -0
  78. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/resilience.py +0 -0
  79. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/sleep_cycle.py +0 -0
  80. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/__init__.py +0 -0
  81. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/base.py +0 -0
  82. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/chat_session_store.py +0 -0
  83. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
  84. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
  85. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
  86. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/l3_postgres.py +0 -0
  87. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/oauth_store.py +0 -0
  88. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/rbac_store.py +0 -0
  89. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/waitlist_store.py +0 -0
  90. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stores/web_events_store.py +0 -0
  91. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/stripe_service.py +0 -0
  92. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/subscription_watchdog.py +0 -0
  93. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/temporal.py +0 -0
  94. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/test_suite.py +0 -0
  95. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/tier_resolver.py +0 -0
  96. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/tiers.py +0 -0
  97. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/topical_resonance.py +0 -0
  98. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/tracer.py +0 -0
  99. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/usage.py +0 -0
  100. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/user_embeddings.py +0 -0
  101. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/user_pulse.py +0 -0
  102. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/user_service.py +0 -0
  103. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/waitlist_service.py +0 -0
  104. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/webhook_dispatcher.py +0 -0
  105. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/weblab.py +0 -0
  106. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/weblab_watcher.py +0 -0
  107. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/window_limits.py +0 -0
  108. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus/write_metrics.py +0 -0
  109. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
  110. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/entry_points.txt +0 -0
  111. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/requires.txt +0 -0
  112. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/src/clsplusplus.egg-info/top_level.txt +0 -0
  113. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_abuse_guard.py +0 -0
  114. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_admin.py +0 -0
  115. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_admin_seed.py +0 -0
  116. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_api.py +0 -0
  117. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_api_comprehensive.py +0 -0
  118. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_api_endpoints.py +0 -0
  119. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_api_usage_analytics.py +0 -0
  120. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_auth.py +0 -0
  121. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_auth_me_api_key.py +0 -0
  122. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_autogen_memory.py +0 -0
  123. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_billing_e2e.py +0 -0
  124. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_burst_hardening.py +0 -0
  125. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_client_sdk.py +0 -0
  126. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_config.py +0 -0
  127. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_cost_forecast.py +0 -0
  128. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_crewai_provider.py +0 -0
  129. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_cross_llm_memory.py +0 -0
  130. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_debug_console.py +0 -0
  131. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_demo_llm.py +0 -0
  132. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_embeddings.py +0 -0
  133. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_extension_cross_model_runtime.py +0 -0
  134. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_extension_integration.py +0 -0
  135. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_extension_ui.py +0 -0
  136. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_feedback_github.py +0 -0
  137. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_free_tier_lifecycle.py +0 -0
  138. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_full_api_coverage.py +0 -0
  139. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_funnel_metrics.py +0 -0
  140. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_geo_gating.py +0 -0
  141. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_health_metrics.py +0 -0
  142. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_idempotency.py +0 -0
  143. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_integration_service.py +0 -0
  144. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_integration_sync_cli_contract.py +0 -0
  145. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_langgraph_store.py +0 -0
  146. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_llamaindex_block.py +0 -0
  147. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_mcp_connect.py +0 -0
  148. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_mcp_oauth.py +0 -0
  149. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_mem0_failure_modes.py +0 -0
  150. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_memory_cycle.py +0 -0
  151. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_memory_phase.py +0 -0
  152. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_memory_service.py +0 -0
  153. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_memory_write_no_schema_garbage.py +0 -0
  154. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_healthcheck.py +0 -0
  155. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_notifier.py +0 -0
  156. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_pricing.py +0 -0
  157. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_reconciler.py +0 -0
  158. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_schema.py +0 -0
  159. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_metering_v2_writer.py +0 -0
  160. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_middleware.py +0 -0
  161. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_models.py +0 -0
  162. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_overage_billing.py +0 -0
  163. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_performance.py +0 -0
  164. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_plasticity.py +0 -0
  165. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_pricing.py +0 -0
  166. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_prototype_e2e.py +0 -0
  167. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_rate_limit.py +0 -0
  168. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_razorpay_billing.py +0 -0
  169. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_razorpay_subscription_webhooks.py +0 -0
  170. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_reconsolidation.py +0 -0
  171. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_regression.py +0 -0
  172. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_resilience.py +0 -0
  173. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_security.py +0 -0
  174. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_sleep_cycle.py +0 -0
  175. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_stores.py +0 -0
  176. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_subscription_watchdog.py +0 -0
  177. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_tier_resolver.py +0 -0
  178. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_tiers.py +0 -0
  179. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_usage.py +0 -0
  180. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_user_auth.py +0 -0
  181. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_user_embeddings.py +0 -0
  182. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_user_pulse.py +0 -0
  183. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_user_stories.py +0 -0
  184. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_waitlist.py +0 -0
  185. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_webhook_dispatcher.py +0 -0
  186. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_weblab.py +0 -0
  187. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_window_limits.py +0 -0
  188. {clsplusplus-7.3.2 → clsplusplus-7.4.1}/tests/test_write_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clsplusplus
3
- Version: 7.3.2
3
+ Version: 7.4.1
4
4
  Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
5
5
  Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
6
6
  Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clsplusplus"
7
- version = "7.3.2"
7
+ version = "7.4.1"
8
8
  description = "Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -15,7 +15,15 @@ Or as module-level functions:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- __version__ = "7.0.0"
18
+ # Single source of truth is pyproject.toml: read the installed package version so
19
+ # clsplusplus.__version__ never drifts from what was published. The literal is a
20
+ # fallback for a raw source checkout that was never pip-installed.
21
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
22
+
23
+ try:
24
+ __version__ = _pkg_version("clsplusplus")
25
+ except PackageNotFoundError: # uninstalled source tree
26
+ __version__ = "7.4.1"
19
27
 
20
28
  __all__ = ["Brain", "learn", "ask", "context", "forget", "CLS", "CLSClient", "__version__"]
21
29
 
@@ -1248,12 +1248,21 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
1248
1248
  "error": "Add CLS_REDIS_URL and CLS_DATABASE_URL in Render Dashboard.",
1249
1249
  }
1250
1250
  wm = write_metrics.snapshot()
1251
+ wal = None
1252
+ _dl = getattr(memory_service, "_durable_log", None)
1253
+ if _dl is not None:
1254
+ wal = {
1255
+ "pending_reconcile": _dl.pending_reconcile(),
1256
+ "appended": _dl.appended,
1257
+ "sinks": {s.name: {"healthy": s.healthy, "lag": s.lag} for s in _dl.states()},
1258
+ }
1251
1259
  return HealthResponse(
1252
1260
  status=h["status"],
1253
1261
  stores=stores,
1254
1262
  writes=wm["total_writes"],
1255
1263
  degraded_writes=wm["degraded_writes"],
1256
1264
  degraded_write_rate=wm["degraded_write_rate"],
1265
+ wal=wal,
1257
1266
  )
1258
1267
 
1259
1268
  @app.get("/v1/health/score", response_model=HealthResponse)
@@ -1619,6 +1628,14 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
1619
1628
  )
1620
1629
  logger.error("Login error: %s: %s", type(e).__name__, e)
1621
1630
  raise HTTPException(status_code=500, detail="Login service unavailable")
1631
+ # Real login telemetry for the admin activity dashboard (best-effort).
1632
+ from clsplusplus.login_activity import record_login
1633
+ await record_login(
1634
+ settings,
1635
+ user_id=user.get("id"),
1636
+ email=user.get("email"),
1637
+ method="password",
1638
+ )
1622
1639
  response = JSONResponse(content=user)
1623
1640
  return _set_session_cookie(response, token)
1624
1641
 
@@ -1806,6 +1823,13 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
1806
1823
  _target = (settings.frontend_url.rstrip("/") + _safe_redirect) if settings.frontend_url else _safe_redirect
1807
1824
  response = RedirectResponse(_target)
1808
1825
  _set_session_cookie(response, token)
1826
+ from clsplusplus.login_activity import record_login
1827
+ await record_login(
1828
+ settings,
1829
+ user_id=user.get("id"),
1830
+ email=user.get("email"),
1831
+ method="google",
1832
+ )
1809
1833
  return response
1810
1834
 
1811
1835
  # ------------------------------------------------------------------
@@ -1927,6 +1951,13 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
1927
1951
  _target = (settings.frontend_url.rstrip("/") + safe_redirect) if settings.frontend_url else safe_redirect
1928
1952
  response = RedirectResponse(_target)
1929
1953
  _set_session_cookie(response, token)
1954
+ from clsplusplus.login_activity import record_login
1955
+ await record_login(
1956
+ settings,
1957
+ user_id=user.get("id"),
1958
+ email=user.get("email"),
1959
+ method="github",
1960
+ )
1930
1961
  return response
1931
1962
 
1932
1963
  # =========================================================================
@@ -2687,6 +2718,344 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
2687
2718
  "error": "integration data degraded",
2688
2719
  }
2689
2720
 
2721
+ @app.get("/admin/integrations/activity")
2722
+ async def admin_integrations_activity(request: Request):
2723
+ """Real per-integration ACTIVITY for the admin dashboard.
2724
+
2725
+ Every metric is computed from a real backing store; nothing is
2726
+ fabricated. A source with no attributable data returns null fields and
2727
+ an honest telemetry classification ("partial" or "none") rather than a
2728
+ made-up number. The handler is fully resilient — each backing store is
2729
+ isolated and any failure degrades that section to null/empty instead of
2730
+ returning 500.
2731
+
2732
+ Telemetry honesty contract per integration:
2733
+ - "tracked": the integration writes a dedicated L1 `source` (the
2734
+ framework adapters: crewai/langgraph/llamaindex/autogen) or has its
2735
+ own Redis counters (chrome-extension) or its own table (api-key).
2736
+ - "partial": the integration shares the generic `source="mcp"` bucket
2737
+ so volume is real but NOT separable per-integration
2738
+ (claude-remote-mcp, codex, cursor, n8n).
2739
+ - "none": no attributable signal exists for it yet.
2740
+ """
2741
+ _require_admin(request)
2742
+
2743
+ from datetime import timezone as _tz
2744
+ now = datetime.now(_tz.utc)
2745
+ _empty_window = {
2746
+ "active_users_24h": 0, "active_users_7d": 0, "logins_24h": 0,
2747
+ "total_memories": 0, "writes_24h": 0, "reads_24h": 0,
2748
+ "api_error_rate_60m": 0.0,
2749
+ }
2750
+
2751
+ def _iso(dt) -> Optional[str]:
2752
+ if dt is None:
2753
+ return None
2754
+ try:
2755
+ return dt.isoformat()
2756
+ except Exception:
2757
+ return None
2758
+
2759
+ # --- Static catalogue: ids/names MUST match the frontend dashboard. ---
2760
+ # `bucket` says where this integration's REAL write volume comes from.
2761
+ # "adapter:<source>" -> dedicated L1 source -> telemetry tracked
2762
+ # "mcp" -> shared mcp source bucket -> telemetry partial
2763
+ # "ext" -> chrome-ext redis counters-> telemetry tracked
2764
+ # "api" -> api_credentials table -> telemetry tracked
2765
+ # None -> nothing attributable -> telemetry none
2766
+ catalogue = [
2767
+ {"id": "claude-remote-mcp", "name": "Claude (Remote MCP)", "category": "mcp", "bucket": "mcp"},
2768
+ {"id": "codex", "name": "Codex", "category": "mcp", "bucket": "mcp"},
2769
+ {"id": "cursor", "name": "Cursor", "category": "mcp", "bucket": "mcp"},
2770
+ {"id": "n8n", "name": "n8n", "category": "mcp", "bucket": "mcp"},
2771
+ {"id": "crewai", "name": "CrewAI", "category": "framework", "bucket": "adapter:crewai"},
2772
+ {"id": "langgraph", "name": "LangGraph", "category": "framework", "bucket": "adapter:langgraph"},
2773
+ {"id": "llamaindex", "name": "LlamaIndex", "category": "framework", "bucket": "adapter:llamaindex"},
2774
+ {"id": "autogen", "name": "AutoGen", "category": "framework", "bucket": "adapter:autogen"},
2775
+ {"id": "chrome-extension", "name": "Chrome Extension", "category": "browser", "bucket": "ext"},
2776
+ {"id": "api-key", "name": "API Key", "category": "api", "bucket": "api"},
2777
+ ]
2778
+
2779
+ # --- Source 1: L1 writes grouped by source (real attribution). ------
2780
+ by_source: dict[str, dict] = {}
2781
+ recent_l1_writes: list[dict] = []
2782
+ total_memories: Optional[int] = None
2783
+ try:
2784
+ rows = await memory_service.l1.writes_by_source(since_days=7)
2785
+ by_source = {r["source"]: r for r in rows}
2786
+ except Exception as e:
2787
+ logger.warning("activity: l1 writes_by_source failed: %s", e)
2788
+ try:
2789
+ recent_l1_writes = await memory_service.l1.recent_writes(limit=20)
2790
+ except Exception as e:
2791
+ logger.warning("activity: l1 recent_writes failed: %s", e)
2792
+ try:
2793
+ total_memories = await memory_service.l1.count_all()
2794
+ except Exception as e:
2795
+ logger.warning("activity: l1 count_all failed: %s", e)
2796
+
2797
+ # --- Source 2: chrome-extension Redis analytics. --------------------
2798
+ ext_analytics: dict = {}
2799
+ try:
2800
+ ext_analytics = await _metrics.get_extension_analytics()
2801
+ except Exception as e:
2802
+ logger.warning("activity: extension analytics failed: %s", e)
2803
+
2804
+ # --- Source 3: api-key usage from api_credentials. -----------------
2805
+ api_activity: dict = {}
2806
+ try:
2807
+ api_activity = await _integration_store.api_key_activity(since_days=7)
2808
+ except Exception as e:
2809
+ logger.warning("activity: api_key_activity failed: %s", e)
2810
+
2811
+ # --- Source 4: aggregate usage + active counts (Redis metrics). -----
2812
+ agg: dict = {}
2813
+ active_now = 0
2814
+ dau = 0
2815
+ try:
2816
+ agg = await _metrics.get_aggregate_metrics()
2817
+ except Exception as e:
2818
+ logger.warning("activity: aggregate metrics failed: %s", e)
2819
+ try:
2820
+ active_now = await _metrics.get_active_now(window_seconds=86400)
2821
+ except Exception:
2822
+ active_now = 0
2823
+ try:
2824
+ dau = await _metrics.get_dau()
2825
+ except Exception:
2826
+ dau = 0
2827
+ active_7d = 0
2828
+ try:
2829
+ from datetime import timedelta as _td
2830
+ from clsplusplus.metrics import _redis_client as _mredis
2831
+ client = _mredis(settings.redis_url)
2832
+ seen = set()
2833
+ for d in range(7):
2834
+ day = (now - _td(days=d)).strftime("%Y-%m-%d")
2835
+ try:
2836
+ members = await client.smembers(f"cls:dau:global:{day}")
2837
+ seen.update(members or [])
2838
+ except Exception:
2839
+ continue
2840
+ active_7d = len(seen)
2841
+ except Exception:
2842
+ active_7d = 0
2843
+
2844
+ # --- Source 5: api error rate (health metrics, last 60m). ----------
2845
+ api_error_rate_60m = 0.0
2846
+ try:
2847
+ from clsplusplus.health_metrics import aggregate_health
2848
+ health = await aggregate_health(settings.redis_url, 60)
2849
+ api_error_rate_60m = float(health.get("error_rate") or 0.0)
2850
+ except Exception as e:
2851
+ logger.warning("activity: health aggregate failed: %s", e)
2852
+
2853
+ # --- Source 6: login activity (new instrumentation). ---------------
2854
+ logins_24h = 0
2855
+ recent_logins: list[dict] = []
2856
+ try:
2857
+ from clsplusplus.login_activity import logins_since, recent_logins as _recent_logins
2858
+ logins_24h = await logins_since(settings, 86400)
2859
+ recent_logins = await _recent_logins(settings, limit=20)
2860
+ except Exception as e:
2861
+ logger.warning("activity: login activity failed: %s", e)
2862
+
2863
+ # --- Build per-integration rows honestly. --------------------------
2864
+ # The shared "mcp" bucket covers all four MCP clients at once, so its
2865
+ # write volume cannot be split between them — mark each "partial" and
2866
+ # surface the SHARED bucket totals (clearly partial, not per-client).
2867
+ mcp_bucket = by_source.get("mcp")
2868
+
2869
+ def _health_from(writes, reads, last_at) -> str:
2870
+ if writes is None and reads is None:
2871
+ return "no_data"
2872
+ if (writes or 0) > 0 or (reads or 0) > 0:
2873
+ return "healthy"
2874
+ return "degraded"
2875
+
2876
+ integrations_out = []
2877
+ for c in catalogue:
2878
+ bucket = c["bucket"]
2879
+ telemetry = "none"
2880
+ unique_users = writes = reads = None
2881
+ last_activity = None
2882
+ error_rate = None
2883
+
2884
+ if bucket and bucket.startswith("adapter:"):
2885
+ src = bucket.split(":", 1)[1]
2886
+ row = by_source.get(src)
2887
+ if row:
2888
+ telemetry = "tracked"
2889
+ writes = row["writes"]
2890
+ unique_users = row["unique_namespaces"]
2891
+ reads = None # reads are not attributed per-source in L1
2892
+ last_activity = _iso(row["last_at"])
2893
+ else:
2894
+ telemetry = "tracked" # we DO track this source; just no rows
2895
+ elif bucket == "mcp":
2896
+ # Shared bucket — real volume, but not separable per client.
2897
+ telemetry = "partial"
2898
+ if mcp_bucket:
2899
+ writes = mcp_bucket["writes"]
2900
+ unique_users = mcp_bucket["unique_namespaces"]
2901
+ last_activity = _iso(mcp_bucket["last_at"])
2902
+ elif bucket == "ext":
2903
+ telemetry = "tracked"
2904
+ wau = ext_analytics.get("wau")
2905
+ msgs = ext_analytics.get("messages_captured") or {}
2906
+ if ext_analytics:
2907
+ unique_users = int(wau) if wau is not None else None
2908
+ # Captured messages are the extension's real write signal.
2909
+ writes = sum(int(v) for v in msgs.values()) if msgs else 0
2910
+ sites = ext_analytics.get("site_usage") or {}
2911
+ reads = sum(int(v) for v in sites.values()) if sites else 0
2912
+ elif bucket == "api":
2913
+ telemetry = "tracked"
2914
+ if api_activity:
2915
+ unique_users = api_activity.get("active_integrations")
2916
+ last_activity = _iso(api_activity.get("last_used_at"))
2917
+
2918
+ health = _health_from(writes, reads, last_activity)
2919
+ if telemetry == "none":
2920
+ health = "no_data"
2921
+
2922
+ integrations_out.append({
2923
+ "id": c["id"],
2924
+ "name": c["name"],
2925
+ "category": c["category"],
2926
+ "telemetry": telemetry,
2927
+ "unique_users_7d": unique_users,
2928
+ "writes_7d": writes,
2929
+ "reads_7d": reads,
2930
+ "last_activity": last_activity,
2931
+ "health": health,
2932
+ "error_rate": error_rate,
2933
+ })
2934
+
2935
+ # --- Totals. -------------------------------------------------------
2936
+ totals = {
2937
+ "active_users_24h": int(active_now or dau or 0),
2938
+ "active_users_7d": int(active_7d or 0),
2939
+ "logins_24h": int(logins_24h or 0),
2940
+ "total_memories": int(total_memories) if total_memories is not None else 0,
2941
+ "writes_24h": int(agg.get("write", 0) or 0),
2942
+ "reads_24h": int(agg.get("read", 0) or 0),
2943
+ "api_error_rate_60m": round(float(api_error_rate_60m), 2),
2944
+ }
2945
+
2946
+ # --- Issues: feedback + frustrated pulse (+ best-effort github). ----
2947
+ issues_recent: list[dict] = []
2948
+ try:
2949
+ fb_rows = await user_service.store.get_recent_feedback(limit=15)
2950
+ for f in fb_rows:
2951
+ score = f.get("score") or 0
2952
+ sentiment = f.get("sentiment")
2953
+ if sentiment == "down" or score <= 2:
2954
+ severity = "high"
2955
+ elif score == 3:
2956
+ severity = "med"
2957
+ else:
2958
+ severity = "low"
2959
+ issues_recent.append({
2960
+ "kind": "feedback",
2961
+ "title": (f.get("comment") or f.get("context") or "Feedback").strip()[:120],
2962
+ "who": f.get("email"),
2963
+ "severity": severity,
2964
+ "at": f.get("created_at"),
2965
+ "url": None,
2966
+ })
2967
+ except Exception as e:
2968
+ logger.warning("activity: recent feedback failed: %s", e)
2969
+
2970
+ try:
2971
+ from clsplusplus.user_pulse import list_frustrated_users
2972
+ frustrated = await list_frustrated_users(settings, limit=10)
2973
+ for p in frustrated:
2974
+ issues_recent.append({
2975
+ "kind": "pulse",
2976
+ "title": "User frustration detected",
2977
+ "who": p.get("user_id"),
2978
+ "severity": "high" if p.get("score", 0) >= 75 else "med",
2979
+ "at": _iso(now),
2980
+ "url": None,
2981
+ })
2982
+ except Exception as e:
2983
+ logger.warning("activity: pulse failed: %s", e)
2984
+
2985
+ # Best-effort open GitHub bug count (skip silently when no token).
2986
+ open_issues = len([i for i in issues_recent if i["severity"] == "high"])
2987
+ try:
2988
+ gh_token = getattr(settings, "github_issue_token", "")
2989
+ gh_repo = getattr(settings, "github_issue_repo", "")
2990
+ if gh_token and gh_repo:
2991
+ import httpx as _httpx
2992
+ async with _httpx.AsyncClient(timeout=5.0) as _c:
2993
+ r = await _c.get(
2994
+ f"https://api.github.com/repos/{gh_repo}/issues",
2995
+ params={"state": "open", "labels": "bug", "per_page": 5},
2996
+ headers={
2997
+ "Authorization": f"Bearer {gh_token}",
2998
+ "Accept": "application/vnd.github+json",
2999
+ },
3000
+ )
3001
+ if r.status_code == 200:
3002
+ for gi in r.json()[:5]:
3003
+ issues_recent.append({
3004
+ "kind": "github",
3005
+ "title": (gi.get("title") or "")[:120],
3006
+ "who": (gi.get("user") or {}).get("login"),
3007
+ "severity": "high",
3008
+ "at": gi.get("created_at"),
3009
+ "url": gi.get("html_url"),
3010
+ })
3011
+ except Exception as e:
3012
+ logger.warning("activity: github issues probe failed: %s", e)
3013
+
3014
+ issues_recent = issues_recent[:15]
3015
+ issues = {"open": open_issues, "recent": issues_recent}
3016
+
3017
+ # --- Activity feed: recent logins + recent memory writes. ----------
3018
+ activity: list[tuple] = []
3019
+ for lg in recent_logins:
3020
+ activity.append((
3021
+ lg.get("ts", 0),
3022
+ {
3023
+ "at": lg.get("at"),
3024
+ "user": lg.get("email") or lg.get("user_id"),
3025
+ "integration": None,
3026
+ "action": "login",
3027
+ },
3028
+ ))
3029
+ for w in recent_l1_writes:
3030
+ at_iso = _iso(w.get("at"))
3031
+ ts = 0
3032
+ try:
3033
+ ts = int(w["at"].timestamp()) if w.get("at") else 0
3034
+ except Exception:
3035
+ ts = 0
3036
+ src = w.get("source")
3037
+ integ = src if src and src != "user" else None
3038
+ activity.append((
3039
+ ts,
3040
+ {
3041
+ "at": at_iso,
3042
+ "user": w.get("namespace"),
3043
+ "integration": integ,
3044
+ "action": "write",
3045
+ },
3046
+ ))
3047
+ activity.sort(key=lambda t: t[0], reverse=True)
3048
+ activity_out = [a[1] for a in activity[:20]]
3049
+
3050
+ return {
3051
+ "generated_at": _iso(now),
3052
+ "window": "7d",
3053
+ "totals": totals,
3054
+ "integrations": integrations_out,
3055
+ "issues": issues,
3056
+ "activity": activity_out,
3057
+ }
3058
+
2690
3059
  def _require_admin(request: Request):
2691
3060
  """Helper to enforce admin access on endpoints."""
2692
3061
  if not getattr(request.state, "is_admin", False):
@@ -4017,6 +4386,17 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
4017
4386
  # ── 1. Warm up all persisted namespaces from L1 ────────────────────
4018
4387
  await memory_service.startup_preload()
4019
4388
 
4389
+ # ── 1b. Start the durable WAL (file fsync anchor + Postgres/Redis ──
4390
+ # replicas). With it active, writes ack only after fsync and are
4391
+ # replicated off the hot path; on restart, replay re-ships any
4392
+ # records past the durable checkpoint. No-op if CLS_WAL_ENABLED
4393
+ # is false. Redis client attaches when available.
4394
+ try:
4395
+ _wal_redis = getattr(memory_service, "_redis_client", None) or getattr(app.state, "redis", None)
4396
+ memory_service.init_durable_log(redis_client=_wal_redis)
4397
+ except Exception as e:
4398
+ _api_logger.error("Durable WAL startup failed: %s", e)
4399
+
4020
4400
  # ── 2. Periodic hippocampal replay (consolidation / sleep cycle) ───
4021
4401
  async def _periodic_replay():
4022
4402
  while True:
@@ -150,6 +150,19 @@ class Settings(BaseSettings):
150
150
  # current write behavior is unchanged.
151
151
  belief_revision_on_write: bool = False # CLS_BELIEF_REVISION_ON_WRITE
152
152
 
153
+ # ---- Durable write-ahead log (WAL) --------------------------------------
154
+ # Durability contract: a write is acked ONLY after it is fsync'd to the
155
+ # local WAL. Redis + Postgres are async replicas fed from the WAL; if either
156
+ # is unreachable the record is already durable on disk and the background
157
+ # reconciler retries until both confirm, then the record is checkpointed.
158
+ wal_enabled: bool = True # CLS_WAL_ENABLED
159
+ wal_dir: str = "./cls_wal" # CLS_WAL_DIR (mount a persistent disk here in prod)
160
+ wal_fsync: str = "always" # CLS_WAL_FSYNC: "always" (zero-loss) | "everysec"
161
+ wal_segment_max_bytes: int = 64 * 1024 * 1024 # CLS_WAL_SEGMENT_MAX_BYTES (rotate at 64MB)
162
+ wal_redis_stream: str = "cls:wal" # CLS_WAL_REDIS_STREAM
163
+ wal_reconcile_interval_seconds: float = 1.0 # CLS_WAL_RECONCILE_INTERVAL_SECONDS
164
+ wal_reconcile_batch: int = 256 # CLS_WAL_RECONCILE_BATCH
165
+
153
166
  # Working buffer
154
167
  l0_capacity_tokens: int = 4096
155
168
  l0_ttl_seconds: int = 300
@@ -0,0 +1,21 @@
1
+ """Durable write-ahead log for CLS++.
2
+
3
+ Durability contract: a memory write is acknowledged ONLY after its record has
4
+ been fsync'd to the local WAL. Redis and Postgres are asynchronous replicas fed
5
+ from the WAL — if either is unreachable the record is already durable on disk and
6
+ the background reconciler retries replication until both confirm, after which the
7
+ record is checkpointed and its segment can be truncated.
8
+
9
+ Components:
10
+ - WalRecord the serialized unit of durability (length-prefixed + CRC32)
11
+ - FileWal append + fsync, segmented, crash-safe replay, checkpoint
12
+ - Replica (protocol) an async sink (Redis stream, Postgres outbox) fed from the WAL
13
+ - DurableLog orchestrator: hot-path append/ack + concurrent replication
14
+ + background reconciler + startup replay
15
+ """
16
+
17
+ from clsplusplus.durable_log.record import WalRecord
18
+ from clsplusplus.durable_log.file_wal import FileWal
19
+ from clsplusplus.durable_log.manager import DurableLog, Replica, ReplicaState
20
+
21
+ __all__ = ["WalRecord", "FileWal", "DurableLog", "Replica", "ReplicaState"]