clsplusplus 7.3.0__tar.gz → 7.3.2__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 (180) hide show
  1. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/PKG-INFO +1 -1
  2. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/pyproject.toml +1 -1
  3. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/api.py +14 -0
  4. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_phase.py +4 -0
  5. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_service.py +28 -1
  6. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/PKG-INFO +1 -1
  7. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/SOURCES.txt +2 -0
  8. clsplusplus-7.3.2/tests/test_extension_cross_model_runtime.py +280 -0
  9. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_extension_integration.py +26 -2
  10. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_extension_ui.py +1 -1
  11. clsplusplus-7.3.2/tests/test_mem0_failure_modes.py +706 -0
  12. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/LICENSE +0 -0
  13. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/README.md +0 -0
  14. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/setup.cfg +0 -0
  15. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/__init__.py +0 -0
  16. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/abuse_guard.py +0 -0
  17. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/api_usage_analytics.py +0 -0
  18. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/auth.py +0 -0
  19. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cli.py +0 -0
  20. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/client.py +0 -0
  21. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/config.py +0 -0
  22. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cost_forecast.py +0 -0
  23. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/cost_model.py +0 -0
  24. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/debug_console.py +0 -0
  25. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_llm.py +0 -0
  26. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_llm_calls.py +0 -0
  27. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/demo_local.py +0 -0
  28. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/email_service.py +0 -0
  29. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/embeddings.py +0 -0
  30. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/funnel_routes.py +0 -0
  31. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/geo.py +0 -0
  32. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/health_metrics.py +0 -0
  33. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/homepage_autopromote.py +0 -0
  34. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/idempotency.py +0 -0
  35. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integration_service.py +0 -0
  36. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/__init__.py +0 -0
  37. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/autogen.py +0 -0
  38. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/crewai.py +0 -0
  39. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/langgraph.py +0 -0
  40. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/integrations/llamaindex.py +0 -0
  41. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/jwt_utils.py +0 -0
  42. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/local_routes.py +0 -0
  43. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/main.py +0 -0
  44. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/mcp_http.py +0 -0
  45. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/mcp_server.py +0 -0
  46. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/memory_cycle.py +0 -0
  47. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/__init__.py +0 -0
  48. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/__main__.py +0 -0
  49. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/billing.py +0 -0
  50. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
  51. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/notifier.py +0 -0
  52. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/pricing.py +0 -0
  53. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/reconciler.py +0 -0
  54. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/schema.py +0 -0
  55. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metering_v2/writer.py +0 -0
  56. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/metrics.py +0 -0
  57. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/middleware.py +0 -0
  58. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/models.py +0 -0
  59. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/namespace_resolver.py +0 -0
  60. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/oauth_server.py +0 -0
  61. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/permissions.py +0 -0
  62. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/plasticity.py +0 -0
  63. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing.py +0 -0
  64. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing_models.py +0 -0
  65. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/pricing_store.py +0 -0
  66. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/prompt_log.py +0 -0
  67. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/rate_limit.py +0 -0
  68. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/razorpay_service.py +0 -0
  69. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/rbac_service.py +0 -0
  70. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/reconsolidation.py +0 -0
  71. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/resilience.py +0 -0
  72. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/sleep_cycle.py +0 -0
  73. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/__init__.py +0 -0
  74. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/base.py +0 -0
  75. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/chat_session_store.py +0 -0
  76. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/integration_store.py +0 -0
  77. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
  78. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
  79. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
  80. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
  81. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/l3_postgres.py +0 -0
  82. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/oauth_store.py +0 -0
  83. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/rbac_store.py +0 -0
  84. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/user_store.py +0 -0
  85. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/waitlist_store.py +0 -0
  86. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stores/web_events_store.py +0 -0
  87. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/stripe_service.py +0 -0
  88. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/subscription_watchdog.py +0 -0
  89. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/temporal.py +0 -0
  90. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/test_suite.py +0 -0
  91. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tier_resolver.py +0 -0
  92. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tiers.py +0 -0
  93. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/topical_resonance.py +0 -0
  94. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/tracer.py +0 -0
  95. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/usage.py +0 -0
  96. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_embeddings.py +0 -0
  97. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_pulse.py +0 -0
  98. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/user_service.py +0 -0
  99. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/waitlist_service.py +0 -0
  100. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/webhook_dispatcher.py +0 -0
  101. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/weblab.py +0 -0
  102. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/weblab_watcher.py +0 -0
  103. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/window_limits.py +0 -0
  104. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus/write_metrics.py +0 -0
  105. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
  106. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/entry_points.txt +0 -0
  107. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/requires.txt +0 -0
  108. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/src/clsplusplus.egg-info/top_level.txt +0 -0
  109. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_abuse_guard.py +0 -0
  110. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_admin.py +0 -0
  111. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_admin_seed.py +0 -0
  112. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api.py +0 -0
  113. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_comprehensive.py +0 -0
  114. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_endpoints.py +0 -0
  115. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_api_usage_analytics.py +0 -0
  116. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_auth.py +0 -0
  117. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_auth_me_api_key.py +0 -0
  118. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_autogen_memory.py +0 -0
  119. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_billing_e2e.py +0 -0
  120. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_burst_hardening.py +0 -0
  121. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_client_sdk.py +0 -0
  122. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_config.py +0 -0
  123. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_cost_forecast.py +0 -0
  124. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_crewai_provider.py +0 -0
  125. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_cross_llm_memory.py +0 -0
  126. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_debug_console.py +0 -0
  127. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_demo_llm.py +0 -0
  128. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_embeddings.py +0 -0
  129. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_feedback_github.py +0 -0
  130. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_free_tier_lifecycle.py +0 -0
  131. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_full_api_coverage.py +0 -0
  132. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_funnel_metrics.py +0 -0
  133. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_geo_gating.py +0 -0
  134. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_health_metrics.py +0 -0
  135. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_idempotency.py +0 -0
  136. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_integration_service.py +0 -0
  137. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_integration_sync_cli_contract.py +0 -0
  138. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_langgraph_store.py +0 -0
  139. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_llamaindex_block.py +0 -0
  140. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_mcp_connect.py +0 -0
  141. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_mcp_oauth.py +0 -0
  142. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_cycle.py +0 -0
  143. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_phase.py +0 -0
  144. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_service.py +0 -0
  145. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_memory_write_no_schema_garbage.py +0 -0
  146. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_healthcheck.py +0 -0
  147. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_notifier.py +0 -0
  148. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_pricing.py +0 -0
  149. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_reconciler.py +0 -0
  150. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_schema.py +0 -0
  151. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_metering_v2_writer.py +0 -0
  152. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_middleware.py +0 -0
  153. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_models.py +0 -0
  154. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_overage_billing.py +0 -0
  155. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_performance.py +0 -0
  156. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_plasticity.py +0 -0
  157. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_pricing.py +0 -0
  158. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_prototype_e2e.py +0 -0
  159. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_rate_limit.py +0 -0
  160. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_razorpay_billing.py +0 -0
  161. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_razorpay_subscription_webhooks.py +0 -0
  162. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_reconsolidation.py +0 -0
  163. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_regression.py +0 -0
  164. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_resilience.py +0 -0
  165. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_security.py +0 -0
  166. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_sleep_cycle.py +0 -0
  167. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_stores.py +0 -0
  168. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_subscription_watchdog.py +0 -0
  169. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_tier_resolver.py +0 -0
  170. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_tiers.py +0 -0
  171. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_usage.py +0 -0
  172. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_auth.py +0 -0
  173. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_embeddings.py +0 -0
  174. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_pulse.py +0 -0
  175. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_user_stories.py +0 -0
  176. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_waitlist.py +0 -0
  177. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_webhook_dispatcher.py +0 -0
  178. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_weblab.py +0 -0
  179. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/tests/test_window_limits.py +0 -0
  180. {clsplusplus-7.3.0 → clsplusplus-7.3.2}/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.0
3
+ Version: 7.3.2
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.0"
7
+ version = "7.3.2"
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"
@@ -481,6 +481,20 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
481
481
  "distinct_id": distinct_id,
482
482
  }
483
483
 
484
+ @app.get("/v1/config/extension")
485
+ async def extension_provider_config():
486
+ """Hot-updatable provider URL rules for the Chrome extension.
487
+
488
+ When ChatGPT/Claude/Gemini change API paths, update extension/provider_rules.json
489
+ on the server — clients pick it up without a Chrome Web Store redeploy.
490
+ """
491
+ rules_path = FilePath(__file__).resolve().parents[2] / "extension" / "provider_rules.json"
492
+ try:
493
+ import json as _json
494
+ return _json.loads(rules_path.read_text(encoding="utf-8"))
495
+ except Exception:
496
+ return {"version": 0, "providers": {}}
497
+
484
498
  @app.get("/admin/weblab")
485
499
  async def admin_weblab_status(request: Request):
486
500
  """Weblab control status — watched flags + latest green/red verdict."""
@@ -496,6 +496,10 @@ class PhaseMemoryItem:
496
496
  # True = a newer fact on the same (subject, relation) dimension exists.
497
497
  # Superseded items are hidden from default reads but never deleted.
498
498
  superseded: bool = False
499
+ # Belief-revision provenance: lineage holds the ids of prior facts this item
500
+ # supersedes (oldest first); version increments on each adjudicated overwrite.
501
+ lineage: list = field(default_factory=list)
502
+ version: int = 1
499
503
 
500
504
  # --- Crystallization (Liquid → Solid phase transition) ---
501
505
  schema_meta: Optional[SchemaMeta] = None # Non-None = solid/glass phase
@@ -112,6 +112,8 @@ class MemoryService:
112
112
  object=(phase_item.fact.value[:256] if phase_item.fact.value else None) or (req.object if req else None),
113
113
  event_at=getattr(phase_item, "event_at", None),
114
114
  superseded=getattr(phase_item, "superseded", False),
115
+ lineage=list(getattr(phase_item, "lineage", []) or []),
116
+ version=getattr(phase_item, "version", 1),
115
117
  )
116
118
 
117
119
  # =========================================================================
@@ -1245,6 +1247,7 @@ class MemoryService:
1245
1247
  new_item = MemoryItem(text=new_text, namespace=namespace)
1246
1248
  new_item = self.embedding_service.embed_item(new_item)
1247
1249
 
1250
+ revised = None # the gate-updated item carrying lineage/version, if any
1248
1251
  if existing and existing.embedding:
1249
1252
  updated, archived, should_engrave = self.reconsolidation.prepare_for_reconsolidation(
1250
1253
  new_item, existing, evidence,
@@ -1252,6 +1255,7 @@ class MemoryService:
1252
1255
  if not should_engrave:
1253
1256
  # Quorum not met — return existing unchanged
1254
1257
  return existing
1258
+ revised = updated # lineage = old.lineage + [old.id], version = old.version + 1
1255
1259
 
1256
1260
  # Quorum met (or no existing) — store via PhaseMemoryEngine
1257
1261
  req = WriteRequest(
@@ -1260,7 +1264,30 @@ class MemoryService:
1260
1264
  source="adjudicate",
1261
1265
  authority=1.0, # High authority for adjudicated facts
1262
1266
  )
1263
- return await self.write(req)
1267
+ result = await self.write(req)
1268
+
1269
+ # Persist belief-revision provenance: carry the gate's lineage/version onto
1270
+ # the engraved fact and archive (never delete) the superseded one. Without
1271
+ # this the overwrite would land as a fresh v1 with no history — the gap a
1272
+ # naive "latest wins" store (e.g. Mem0 #4536) leaves behind.
1273
+ if revised is not None:
1274
+ new_phase = self.engine._item_by_id.get(result.id)
1275
+ if new_phase is not None:
1276
+ new_phase.lineage = list(revised.lineage)
1277
+ new_phase.version = revised.version
1278
+ result.lineage = list(revised.lineage)
1279
+ result.version = revised.version
1280
+ if existing is not None:
1281
+ old_phase = self.engine._item_by_id.get(existing.id)
1282
+ if old_phase is not None:
1283
+ old_phase.superseded = True
1284
+ # Best-effort durable archive of the superseded fact in L1.
1285
+ try:
1286
+ await self.l1.update_superseded(existing.id, namespace)
1287
+ except Exception as _arch_err: # pragma: no cover - archive is best-effort
1288
+ logger.debug("Adjudicate archive (L1) non-fatal: %s", _arch_err)
1289
+
1290
+ return result
1264
1291
 
1265
1292
  # =========================================================================
1266
1293
  # Multi-hop read (Issue #36)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clsplusplus
3
- Version: 7.3.0
3
+ Version: 7.3.2
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>
@@ -120,6 +120,7 @@ tests/test_cross_llm_memory.py
120
120
  tests/test_debug_console.py
121
121
  tests/test_demo_llm.py
122
122
  tests/test_embeddings.py
123
+ tests/test_extension_cross_model_runtime.py
123
124
  tests/test_extension_integration.py
124
125
  tests/test_extension_ui.py
125
126
  tests/test_feedback_github.py
@@ -135,6 +136,7 @@ tests/test_langgraph_store.py
135
136
  tests/test_llamaindex_block.py
136
137
  tests/test_mcp_connect.py
137
138
  tests/test_mcp_oauth.py
139
+ tests/test_mem0_failure_modes.py
138
140
  tests/test_memory_cycle.py
139
141
  tests/test_memory_phase.py
140
142
  tests/test_memory_service.py
@@ -0,0 +1,280 @@
1
+ """
2
+ Executable cross-model extension runtime tests.
3
+
4
+ Simulates the ChatGPT → Claude handoff without a browser:
5
+ store on GPT-shaped body, prefetch mailbox, inject on Claude-shaped bodies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import time
14
+ from copy import deepcopy
15
+
16
+ import pytest
17
+
18
+ EXT_DIR = os.path.join(os.path.dirname(__file__), "..", "extension")
19
+ CTX_MARKER = "For context, here are some things I have mentioned before"
20
+
21
+
22
+ def read_ext(name: str) -> str:
23
+ with open(os.path.join(EXT_DIR, name), "r") as f:
24
+ return f.read()
25
+
26
+
27
+ def build_ctx(facts: list[str]) -> str:
28
+ return (
29
+ "For context, here are some things I have mentioned before in other conversations:\n"
30
+ + "\n".join(f"- {f[:200]}" for f in facts)
31
+ + "\n\nNow, my actual question:\n"
32
+ )
33
+
34
+
35
+ def is_llm(url: str, host: str) -> bool:
36
+ u = url.lower()
37
+ h = host.lower()
38
+ if "chatgpt" in h or "openai" in h:
39
+ if "/backend-api/" not in u or "conversation" not in u:
40
+ return False
41
+ if "/init" in u or "/prepare" in u:
42
+ return False
43
+ return True
44
+ if "claude" in h:
45
+ if "/v1/sessions/" in u and "/events" in u:
46
+ return True
47
+ if "/api/organizations/" in u and "chat_conversations" in u and "completion" in u:
48
+ return True
49
+ return "/api/" in u and any(k in u for k in ("chat", "completion", "message"))
50
+ if "gemini" in h:
51
+ return "batchexecute" in u
52
+ return False
53
+
54
+
55
+ def extract(body: str, host: str) -> tuple[str, callable] | None:
56
+ try:
57
+ b = json.loads(body)
58
+ except json.JSONDecodeError:
59
+ return None
60
+
61
+ if "chatgpt" in host or "openai" in host:
62
+ messages = b.get("messages") or []
63
+ if messages:
64
+ m = messages[-1]
65
+ content = m.get("content")
66
+ if isinstance(content, dict) and content.get("parts"):
67
+ q = content["parts"][0]
68
+ def setter(ctx, _b=b, _m=m):
69
+ _m["content"]["parts"][0] = ctx + q
70
+ return json.dumps(_b)
71
+ return q, setter
72
+ if isinstance(content, str):
73
+ def setter(ctx, _b=b, _m=m):
74
+ _m["content"] = ctx + content
75
+ return json.dumps(_b)
76
+ return content, setter
77
+
78
+ if isinstance(b.get("prompt"), str) and len(b["prompt"]) > 3:
79
+ q = b["prompt"]
80
+ def setter(ctx, _b=b):
81
+ _b["prompt"] = ctx + q
82
+ return json.dumps(_b)
83
+ return q, setter
84
+
85
+ events = b.get("events")
86
+ if isinstance(events, list):
87
+ for ev in reversed(events):
88
+ if not ev or ev.get("type") != "user":
89
+ continue
90
+ msg = ev.get("message") or {}
91
+ content = msg.get("content")
92
+ if isinstance(content, str) and content:
93
+ def setter(ctx, _msg=msg, _q=content, _b=b):
94
+ _msg["content"] = ctx + _q
95
+ return json.dumps(_b)
96
+ return content, setter
97
+ if isinstance(content, list):
98
+ for block in content:
99
+ if block.get("type") == "text" and block.get("text"):
100
+ q = block["text"]
101
+ def setter(ctx, _block=block, _q=q, _b=b):
102
+ _block["text"] = ctx + _q
103
+ return json.dumps(_b)
104
+ return q, setter
105
+ return None
106
+
107
+
108
+ def inject(body: str, host: str, facts: list[str]) -> str | None:
109
+ parsed = extract(body, host)
110
+ if not parsed:
111
+ return None
112
+ _q, setter = parsed
113
+ return setter(build_ctx(facts))
114
+
115
+
116
+ class TestURLDetection:
117
+ def test_chatgpt_conversation(self):
118
+ assert is_llm("https://chatgpt.com/backend-api/conversation", "chatgpt.com")
119
+
120
+ def test_claude_completion_legacy(self):
121
+ url = "https://claude.ai/api/organizations/org/chat_conversations/conv/completion"
122
+ assert is_llm(url, "claude.ai")
123
+
124
+ def test_claude_sessions_events_2026(self):
125
+ url = "https://claude.ai/v1/sessions/session_01ABC/events"
126
+ assert is_llm(url, "claude.ai")
127
+
128
+ def test_claude_org_list_not_llm(self):
129
+ assert not is_llm("https://claude.ai/api/organizations", "claude.ai")
130
+
131
+
132
+ class TestCrossModelInjection:
133
+ def test_chatgpt_store_shape_injects(self):
134
+ body = json.dumps({
135
+ "messages": [{
136
+ "role": "user",
137
+ "content": {"parts": ["My favorite color is teal"]},
138
+ }]
139
+ })
140
+ out = inject(body, "chatgpt.com", ["My favorite color is teal", "I live in Austin"])
141
+ assert out
142
+ assert CTX_MARKER in out
143
+ assert "teal" in out
144
+
145
+ def test_claude_completion_injects(self):
146
+ body = json.dumps({"prompt": "What is my favorite color?"})
147
+ out = inject(body, "claude.ai", ["My favorite color is teal"])
148
+ assert out
149
+ assert CTX_MARKER in out
150
+ assert "teal" in out
151
+
152
+ def test_claude_sessions_events_injects(self):
153
+ body = json.dumps({
154
+ "events": [{
155
+ "type": "user",
156
+ "message": {"role": "user", "content": "What is my favorite color?"},
157
+ }]
158
+ })
159
+ out = inject(body, "claude.ai", ["My favorite color is teal"])
160
+ assert out
161
+ assert CTX_MARKER in out
162
+ assert "teal" in out
163
+
164
+ def test_claude_sessions_blocks_injects(self):
165
+ body = json.dumps({
166
+ "events": [{
167
+ "type": "user",
168
+ "message": {
169
+ "role": "user",
170
+ "content": [{"type": "text", "text": "Remind me where I live"}],
171
+ },
172
+ }]
173
+ })
174
+ out = inject(body, "claude.ai", ["I live in Springfield"])
175
+ assert out
176
+ assert "Springfield" in out
177
+
178
+
179
+ class TestMailboxRace:
180
+ def test_delayed_mailbox_still_injects_within_wait_window(self):
181
+ """Simulates capture.js prefetch arriving after GPT store but before Claude send."""
182
+ facts: list[str] = []
183
+ mailbox_ready_at = time.time() + 0.15
184
+
185
+ def get_ctx_now() -> str:
186
+ if time.time() < mailbox_ready_at or not facts:
187
+ return ""
188
+ return build_ctx(facts)
189
+
190
+ def wait_for_ctx(timeout_ms: int = 1200) -> str:
191
+ deadline = time.time() + timeout_ms / 1000
192
+ while time.time() < deadline:
193
+ ctx = get_ctx_now()
194
+ if ctx:
195
+ return ctx
196
+ time.sleep(0.04)
197
+ return ""
198
+
199
+ # GPT turn stores fact (async)
200
+ facts.append("My dog is named Max")
201
+
202
+ claude_body = json.dumps({"prompt": "What is my dog's name?"})
203
+ parsed = extract(claude_body, "claude.ai")
204
+ assert parsed
205
+ q, setter = parsed
206
+
207
+ ctx = get_ctx_now()
208
+ if not ctx:
209
+ ctx = wait_for_ctx()
210
+ assert ctx
211
+ out = setter(ctx)
212
+ assert "Max" in out
213
+
214
+
215
+ class TestExtensionSourceGuards:
216
+ def test_intercept_has_sessions_extractor(self):
217
+ js = read_ext("intercept.js")
218
+ assert "b.events" in js
219
+ assert "/v1/sessions/" in js
220
+
221
+ def test_intercept_async_uses_wait_path(self):
222
+ js = read_ext("intercept.js")
223
+ assert "async function injectBody" in js
224
+ assert "await injectBody(body, 'async')" in js
225
+
226
+ def test_background_bumps_mailbox_after_store(self):
227
+ js = read_ext("background.js")
228
+ assert "cls_mem_bump" in js
229
+
230
+ def test_capture_listens_for_mem_bump(self):
231
+ js = read_ext("capture.js")
232
+ assert "cls_mem_bump" in js
233
+
234
+ def test_query_aware_recall_on_send(self):
235
+ js = read_ext("capture.js")
236
+ assert "data-cls-query" in js
237
+ assert "FETCH_RELEVANT" in js
238
+ assert "refreshMemoriesForQuery" in js
239
+
240
+ def test_remote_provider_rules_sync(self):
241
+ js = read_ext("capture.js")
242
+ assert "data-cls-provider-rules" in js
243
+ assert "GET_PROVIDER_RULES" in js
244
+ js2 = read_ext("intercept.js")
245
+ assert "getProviderRules" in js2
246
+ js3 = read_ext("background.js")
247
+ assert "/v1/config/extension" in js3
248
+
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_prototype_mock_claude_sessions_endpoint():
252
+ """Server mock validates injected sessions API body."""
253
+ import sys
254
+ from pathlib import Path
255
+ from httpx import ASGITransport, AsyncClient
256
+
257
+ root = Path(__file__).resolve().parents[1]
258
+ for p in (str(root / "prototype"), str(root / "src")):
259
+ if p not in sys.path:
260
+ sys.path.insert(0, p)
261
+ import server as proto_server # noqa: PLC0415
262
+
263
+ transport = ASGITransport(app=proto_server.app)
264
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
265
+ body = {
266
+ "events": [{
267
+ "type": "user",
268
+ "message": {
269
+ "role": "user",
270
+ "content": (
271
+ "For context, here are some things I have mentioned before in other conversations:\n"
272
+ "- My favorite color is teal\n\n"
273
+ "Now, my actual question:\nWhat is my favorite color?"
274
+ ),
275
+ },
276
+ }]
277
+ }
278
+ r = await client.post("/v1/sessions/session_test/events", json=body)
279
+ assert r.status_code == 200
280
+ assert r.json().get("injection_ok") is True
@@ -90,7 +90,8 @@ class TestInterceptClaudeExtraction:
90
90
 
91
91
  def test_detects_claude_url(self):
92
92
  assert "claude" in self.js
93
- assert "/api/" in self.js
93
+ assert "/v1/sessions/" in self.js
94
+ assert "chat_conversations" in self.js
94
95
 
95
96
  def test_extracts_prompt_field(self):
96
97
  assert "b.prompt" in self.js
@@ -106,6 +107,12 @@ class TestInterceptClaudeExtraction:
106
107
  def test_extracts_text_field(self):
107
108
  assert "b.text" in self.js
108
109
 
110
+ def test_has_short_wait_fallback_for_first_claude_send(self):
111
+ """Injection waits for mailbox when facts are not ready yet."""
112
+ assert "waitForCtx" in self.js
113
+ assert "async function injectBody" in self.js
114
+ assert "await injectBody(body, 'async')" in self.js
115
+
109
116
 
110
117
  class TestInterceptGeminiExtraction:
111
118
  """Verify Gemini batchexecute URL-encoded form data extraction."""
@@ -212,6 +219,10 @@ class TestCaptureDOMMailbox:
212
219
  def test_writes_timestamp(self):
213
220
  assert "data-ts" in self.js
214
221
 
222
+ def test_uses_bridge_node_fallback_before_body_exists(self):
223
+ """Early document_start execution may run before body is available."""
224
+ assert "document.body || document.documentElement" in self.js
225
+
215
226
  def test_reads_outbox(self):
216
227
  assert "data-cls-outbox" in self.js
217
228
  assert "JSON.parse" in self.js
@@ -365,7 +376,8 @@ class TestBackgroundMessageRouting:
365
376
 
366
377
  def test_handles_all_message_types(self):
367
378
  required = [
368
- "STORE", "FETCH", "ACTIVITY", "USAGE", "SEARCH",
379
+ "STORE", "FETCH", "FETCH_RELEVANT", "GET_PROVIDER_RULES",
380
+ "ACTIVITY", "USAGE", "SEARCH",
369
381
  "DAILY_COUNTERS", "INCREMENT_INJECTED", "VERIFY_KEY", "OPEN_PANEL",
370
382
  ]
371
383
  for msg_type in required:
@@ -431,6 +443,12 @@ class TestCrossWorldArchitecture:
431
443
  assert main[0]["js"] == ["intercept.js"]
432
444
  assert isolated[0]["js"] == ["capture.js"]
433
445
 
446
+ def test_capture_runs_at_document_start(self):
447
+ """Capture must initialize mailbox before first provider send."""
448
+ manifest = json.loads(read_ext("manifest.json"))
449
+ capture = [s for s in manifest["content_scripts"] if s.get("js") == ["capture.js"]][0]
450
+ assert capture["run_at"] == "document_start"
451
+
434
452
 
435
453
  # ═══════════════════════════════════════════════════════════════════════════════
436
454
  # Memory Filter Logic (capture.js)
@@ -457,3 +475,9 @@ class TestMemoryFiltering:
457
475
 
458
476
  def test_filters_questions(self):
459
477
  assert "?" in self.js # endsWith('?') filter
478
+
479
+ def test_refreshes_on_tab_reentry(self):
480
+ """Switching models/tabs should trigger immediate prefetch refresh."""
481
+ assert "visibilitychange" in self.js
482
+ assert "pageshow" in self.js
483
+ assert "focus" in self.js
@@ -648,7 +648,7 @@ class TestCaptureJS:
648
648
  assert "data-facts" in self.js
649
649
 
650
650
  def test_refresh_interval(self):
651
- assert "setInterval(refreshMemories, 30000)" in self.js
651
+ assert "setInterval(refreshMemories, 10000)" in self.js
652
652
 
653
653
  def test_outbox_poll_interval(self):
654
654
  assert "setInterval(checkOutbox, 500)" in self.js