claude-smart 0.2.44 → 0.2.46

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 (323) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +22 -2
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +77 -29
  35. package/plugin/scripts/backend-service.sh +27 -5
  36. package/plugin/scripts/dashboard-service.sh +7 -5
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +3 -3
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +10 -8
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +304 -6
  45. package/plugin/src/claude_smart/env_config.py +12 -4
  46. package/plugin/src/claude_smart/events/session_start.py +26 -7
  47. package/plugin/src/claude_smart/events/stop.py +1 -1
  48. package/plugin/src/claude_smart/ids.py +1 -1
  49. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  50. package/plugin/src/claude_smart/runtime.py +7 -1
  51. package/plugin/uv.lock +1 -1
  52. package/plugin/vendor/reflexio/.env.example +54 -30
  53. package/plugin/vendor/reflexio/README.md +14 -8
  54. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  55. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  57. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  64. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  66. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  67. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  68. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  142. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  143. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  144. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  145. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  146. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  147. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  148. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  149. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  158. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  159. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  160. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  161. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  162. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  163. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  164. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  165. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  167. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  168. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  169. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  170. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  171. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  172. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  173. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  174. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  176. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  178. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  179. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  198. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  208. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  209. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  216. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  221. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  222. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  238. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  239. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  244. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  248. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  249. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  250. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  252. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  285. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  286. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  287. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  289. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  290. package/scripts/setup-claude-smart.sh +8 -3
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  313. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  314. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  318. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  323. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -2,11 +2,13 @@ import asyncio
2
2
  import inspect
3
3
  import logging
4
4
  import os
5
+ import time
5
6
  from collections.abc import Callable
6
7
  from concurrent.futures import ThreadPoolExecutor
7
8
  from datetime import UTC, datetime
8
9
  from typing import Any
9
10
 
11
+ from anyio.to_thread import current_default_thread_limiter
10
12
  from fastapi import (
11
13
  APIRouter,
12
14
  BackgroundTasks,
@@ -52,6 +54,7 @@ from reflexio.models.api_schema.retriever_schema import (
52
54
  GetEvaluationResultsViewResponse,
53
55
  GetInteractionsRequest,
54
56
  GetInteractionsViewResponse,
57
+ GetLearningProvenanceRequest,
55
58
  GetPlaybookApplicationStatsRequest,
56
59
  GetPlaybookApplicationStatsResponse,
57
60
  GetProfileStatisticsResponse,
@@ -61,6 +64,7 @@ from reflexio.models.api_schema.retriever_schema import (
61
64
  GetUserPlaybooksRequest,
62
65
  GetUserPlaybooksViewResponse,
63
66
  GetUserProfilesRequest,
67
+ LearningProvenanceViewResponse,
64
68
  ProfileChangeLogViewResponse,
65
69
  RequestDataView,
66
70
  RerankUserProfilesRequest,
@@ -74,6 +78,7 @@ from reflexio.models.api_schema.retriever_schema import (
74
78
  SearchUserProfileRequest,
75
79
  SessionView,
76
80
  SetConfigResponse,
81
+ SourceUserPlaybookProvenanceView,
77
82
  StorageStatsRequest,
78
83
  StorageStatsResponse,
79
84
  UnifiedSearchRequest,
@@ -153,10 +158,16 @@ from reflexio.models.api_schema.ui.converters import (
153
158
  to_user_playbook_view,
154
159
  )
155
160
  from reflexio.models.config_schema import (
161
+ DEFAULT_WINDOW_SIZE,
156
162
  SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
157
163
  Config,
158
164
  )
159
- from reflexio.server._auth import DEFAULT_ORG_ID, default_get_org_id
165
+ from reflexio.server._auth import (
166
+ DEFAULT_ORG_ID,
167
+ default_billing_gate,
168
+ default_get_caller_type,
169
+ default_get_org_id,
170
+ )
160
171
  from reflexio.server.api_endpoints import (
161
172
  account_api,
162
173
  health_api,
@@ -172,22 +183,34 @@ from reflexio.server.correlation import correlation_id_var, generate_correlation
172
183
  from reflexio.server.operation_limiter import (
173
184
  OperationName,
174
185
  limiter_http_exception,
186
+ log_publish_hardware_capacity,
175
187
  run_with_operation_limit,
176
188
  )
177
- from reflexio.server.services.agent_success_evaluation.group_evaluation_runner import (
178
- run_group_evaluation,
179
- )
180
189
  from reflexio.server.services.agent_success_evaluation.regen_jobs import (
181
190
  REGEN_JOBS,
182
191
  run_regen,
183
192
  )
193
+ from reflexio.server.services.agent_success_evaluation.runner import (
194
+ run_group_evaluation,
195
+ )
196
+ from reflexio.server.services.extractor_interaction_utils import (
197
+ get_effective_source_filter,
198
+ get_extractor_window_params,
199
+ )
200
+ from reflexio.server.tracing import profile_step
184
201
 
185
202
  logger = logging.getLogger(__name__)
186
203
 
187
204
  # Re-exported for backwards compatibility — callers that did
188
205
  # ``from reflexio.server.api import default_get_org_id`` or ``DEFAULT_ORG_ID``
189
206
  # continue to work.
190
- __all__ = ["DEFAULT_ORG_ID", "create_app", "default_get_org_id"]
207
+ __all__ = [
208
+ "DEFAULT_ORG_ID",
209
+ "create_app",
210
+ "default_billing_gate",
211
+ "default_get_caller_type",
212
+ "default_get_org_id",
213
+ ]
191
214
 
192
215
  # Bot protection configuration
193
216
  REQUEST_TIMEOUT_SECONDS = 60
@@ -255,8 +278,41 @@ def get_rate_limit_key(request: Request) -> str:
255
278
  return get_remote_address(request)
256
279
 
257
280
 
281
+ def _storage_backend_name(limiter_obj: Limiter) -> str:
282
+ storage = getattr(limiter_obj, "_storage", None)
283
+ if storage is None:
284
+ return "unknown"
285
+ return storage.__class__.__name__
286
+
287
+
288
+ def _trace_external_rate_limit_backend(limiter_obj: Limiter) -> None:
289
+ """Trace rate-limit storage hits when the backend is an external service."""
290
+ backend = _storage_backend_name(limiter_obj)
291
+ if backend == "MemoryStorage":
292
+ return
293
+
294
+ strategy = getattr(limiter_obj, "limiter", None)
295
+ if strategy is None or getattr(strategy, "_reflexio_traced", False):
296
+ return
297
+
298
+ original_hit = strategy.hit
299
+
300
+ def traced_hit(item: Any, *identifiers: str, cost: int = 1) -> bool:
301
+ with profile_step(
302
+ "rate_limit.backend_hit",
303
+ storage_backend=backend,
304
+ strategy=strategy.__class__.__name__,
305
+ cost=cost,
306
+ ):
307
+ return original_hit(item, *identifiers, cost=cost)
308
+
309
+ strategy.hit = traced_hit
310
+ strategy._reflexio_traced = True
311
+
312
+
258
313
  # Initialize rate limiter
259
314
  limiter = Limiter(key_func=get_rate_limit_key)
315
+ _trace_external_rate_limit_backend(limiter)
260
316
 
261
317
 
262
318
  def _run_limited_api[T](
@@ -448,6 +504,16 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
448
504
  ) -> Response:
449
505
  cid = generate_correlation_id()
450
506
  correlation_id_var.set(cid)
507
+ try:
508
+ stats = current_default_thread_limiter().statistics()
509
+ request.state.tp_borrowed = stats.borrowed_tokens
510
+ request.state.tp_total = stats.total_tokens
511
+ request.state.tp_waiting = stats.tasks_waiting
512
+ except Exception as exc: # noqa: BLE001
513
+ logger.debug("Failed to snapshot threadpool limiter stats: %s", exc)
514
+ request.state.tp_borrowed = None
515
+ request.state.tp_total = None
516
+ request.state.tp_waiting = None
451
517
  response = await call_next(request)
452
518
  response.headers["X-Correlation-ID"] = cid
453
519
  return response
@@ -456,6 +522,86 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
456
522
  core_router = APIRouter()
457
523
 
458
524
 
525
+ def _stamp_search_dependencies_done(request: Request) -> None:
526
+ """Stamp when dependency resolution reaches its final search dependency."""
527
+ request.state.search_deps_done_monotonic = time.monotonic()
528
+
529
+
530
+ def _meter_applied_learnings(
531
+ *,
532
+ org_id: str,
533
+ caller_type: str,
534
+ surfaced_count: int,
535
+ request_id: str | None = None,
536
+ session_id: str | None = None,
537
+ ) -> None:
538
+ """Emit the ③ Application event via the OSS emission helper.
539
+
540
+ No-op unless a production-agent caller surfaced >= 1 result. The cheap
541
+ caller-type and count guard runs first so the get_reflexio / config lookup
542
+ is skipped on the free paths (dashboard / empty result).
543
+
544
+ Args:
545
+ org_id: Organization ID for the requesting caller.
546
+ caller_type: Resolved caller classification (e.g. ``"production_agent"``).
547
+ surfaced_count: Total number of learnings returned to the caller.
548
+ request_id: Optional request correlation ID from the payload.
549
+ session_id: Optional session ID from the payload.
550
+ """
551
+ if caller_type != "production_agent" or surfaced_count <= 0:
552
+ return
553
+ try:
554
+ from reflexio.server.billing_meter import record_applied_learnings
555
+ from reflexio.server.billing_signals import platform_llm_from_config
556
+
557
+ config = get_reflexio(org_id=org_id).request_context.configurator.get_config()
558
+ record_applied_learnings(
559
+ org_id=org_id,
560
+ surfaced_count=surfaced_count,
561
+ caller_type=caller_type,
562
+ platform_llm=platform_llm_from_config(config),
563
+ platform_storage=None, # resolved enterprise-side at rollup (Phase 1)
564
+ request_id=request_id,
565
+ session_id=session_id,
566
+ )
567
+ except Exception:
568
+ logger.warning(
569
+ "applied-learnings metering failed for org %s", org_id, exc_info=True
570
+ )
571
+
572
+
573
+ def _meter_search_request(
574
+ *,
575
+ org_id: str,
576
+ caller_type: str,
577
+ request_id: str | None = None,
578
+ session_id: str | None = None,
579
+ ) -> None:
580
+ """Emit one production-agent search request metric.
581
+
582
+ Args:
583
+ org_id: Organization ID for the requesting caller.
584
+ caller_type: Resolved caller classification.
585
+ request_id: Optional request correlation ID from the payload.
586
+ session_id: Optional session ID from the payload.
587
+ """
588
+ if caller_type != "production_agent":
589
+ return
590
+ try:
591
+ from reflexio.server.billing_meter import record_search_request
592
+
593
+ record_search_request(
594
+ org_id=org_id,
595
+ caller_type=caller_type,
596
+ request_id=request_id,
597
+ session_id=session_id,
598
+ )
599
+ except Exception:
600
+ logger.warning(
601
+ "search-request metering failed for org %s", org_id, exc_info=True
602
+ )
603
+
604
+
459
605
  @core_router.get("/")
460
606
  def root() -> dict[str, str]:
461
607
  return {
@@ -466,7 +612,7 @@ def root() -> dict[str, str]:
466
612
 
467
613
 
468
614
  @core_router.get("/health")
469
- def health_check() -> dict[str, str]:
615
+ async def health_check() -> dict[str, str]:
470
616
  """Health check endpoint for ECS/container orchestration."""
471
617
  return {"status": "healthy"}
472
618
 
@@ -534,6 +680,7 @@ def publish_user_interaction(
534
680
  background_tasks: BackgroundTasks,
535
681
  org_id: str = Depends(default_get_org_id),
536
682
  wait_for_response: bool = False,
683
+ _gate: None = Depends(default_billing_gate("learnings_generated")), # noqa: B008
537
684
  ) -> PublishUserInteractionResponse:
538
685
  if wait_for_response:
539
686
  # Process synchronously so the caller gets the real result
@@ -551,6 +698,7 @@ def publish_user_interaction(
551
698
  fn=lambda: publisher_api.add_user_interaction(
552
699
  org_id=org_id, request=payload
553
700
  ),
701
+ wait_forever=False,
554
702
  )
555
703
  except TimeoutError:
556
704
  logger.warning(
@@ -647,17 +795,33 @@ def search_user_profiles(
647
795
  request: Request,
648
796
  payload: SearchUserProfileRequest,
649
797
  org_id: str = Depends(default_get_org_id),
798
+ caller_type: str = Depends(default_get_caller_type),
799
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
650
800
  ) -> SearchProfilesViewResponse:
651
801
  response = _run_limited_api(
652
802
  org_id,
653
803
  "search",
654
804
  lambda: get_reflexio(org_id=org_id).search_user_profiles(payload),
655
805
  )
656
- return SearchProfilesViewResponse(
806
+ resp = SearchProfilesViewResponse(
657
807
  success=response.success,
658
808
  user_profiles=[to_profile_view(p) for p in response.user_profiles],
659
809
  msg=response.msg,
660
810
  )
811
+ _meter_search_request(
812
+ org_id=org_id,
813
+ caller_type=caller_type,
814
+ request_id=getattr(payload, "request_id", None),
815
+ session_id=getattr(payload, "session_id", None),
816
+ )
817
+ _meter_applied_learnings(
818
+ org_id=org_id,
819
+ caller_type=caller_type,
820
+ surfaced_count=len(resp.user_profiles),
821
+ request_id=getattr(payload, "request_id", None),
822
+ session_id=getattr(payload, "session_id", None),
823
+ )
824
+ return resp
661
825
 
662
826
 
663
827
  @core_router.post(
@@ -757,6 +921,8 @@ def search_user_playbooks_endpoint(
757
921
  request: Request,
758
922
  payload: SearchUserPlaybookRequest,
759
923
  org_id: str = Depends(default_get_org_id),
924
+ caller_type: str = Depends(default_get_caller_type),
925
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
760
926
  ) -> SearchUserPlaybooksViewResponse:
761
927
  """Search user playbooks with semantic search and advanced filtering.
762
928
 
@@ -767,6 +933,7 @@ def search_user_playbooks_endpoint(
767
933
  request (Request): The HTTP request object (for rate limiting)
768
934
  payload (SearchUserPlaybookRequest): The search request
769
935
  org_id (str): Organization ID
936
+ caller_type (str): Billing caller classification (injected via dependency).
770
937
 
771
938
  Returns:
772
939
  SearchUserPlaybooksViewResponse: Response containing matching user playbooks
@@ -776,11 +943,25 @@ def search_user_playbooks_endpoint(
776
943
  "search",
777
944
  lambda: get_reflexio(org_id=org_id).search_user_playbooks(payload),
778
945
  )
779
- return SearchUserPlaybooksViewResponse(
946
+ resp = SearchUserPlaybooksViewResponse(
780
947
  success=response.success,
781
948
  user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
782
949
  msg=response.msg,
783
950
  )
951
+ _meter_search_request(
952
+ org_id=org_id,
953
+ caller_type=caller_type,
954
+ request_id=getattr(payload, "request_id", None),
955
+ session_id=getattr(payload, "session_id", None),
956
+ )
957
+ _meter_applied_learnings(
958
+ org_id=org_id,
959
+ caller_type=caller_type,
960
+ surfaced_count=len(resp.user_playbooks),
961
+ request_id=getattr(payload, "request_id", None),
962
+ session_id=getattr(payload, "session_id", None),
963
+ )
964
+ return resp
784
965
 
785
966
 
786
967
  @core_router.post(
@@ -793,6 +974,8 @@ def search_agent_playbooks_endpoint(
793
974
  request: Request,
794
975
  payload: SearchAgentPlaybookRequest,
795
976
  org_id: str = Depends(default_get_org_id),
977
+ caller_type: str = Depends(default_get_caller_type),
978
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
796
979
  ) -> SearchAgentPlaybooksViewResponse:
797
980
  """Search agent playbooks with semantic search and advanced filtering.
798
981
 
@@ -803,6 +986,7 @@ def search_agent_playbooks_endpoint(
803
986
  request (Request): The HTTP request object (for rate limiting)
804
987
  payload (SearchAgentPlaybookRequest): The search request
805
988
  org_id (str): Organization ID
989
+ caller_type (str): Billing caller classification (injected via dependency).
806
990
 
807
991
  Returns:
808
992
  SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
@@ -812,11 +996,25 @@ def search_agent_playbooks_endpoint(
812
996
  "search",
813
997
  lambda: get_reflexio(org_id=org_id).search_agent_playbooks(payload),
814
998
  )
815
- return SearchAgentPlaybooksViewResponse(
999
+ resp = SearchAgentPlaybooksViewResponse(
816
1000
  success=response.success,
817
1001
  agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
818
1002
  msg=response.msg,
819
1003
  )
1004
+ _meter_search_request(
1005
+ org_id=org_id,
1006
+ caller_type=caller_type,
1007
+ request_id=getattr(payload, "request_id", None),
1008
+ session_id=getattr(payload, "session_id", None),
1009
+ )
1010
+ _meter_applied_learnings(
1011
+ org_id=org_id,
1012
+ caller_type=caller_type,
1013
+ surfaced_count=len(resp.agent_playbooks),
1014
+ request_id=getattr(payload, "request_id", None),
1015
+ session_id=getattr(payload, "session_id", None),
1016
+ )
1017
+ return resp
820
1018
 
821
1019
 
822
1020
  @core_router.post(
@@ -828,7 +1026,11 @@ def search_agent_playbooks_endpoint(
828
1026
  def unified_search_endpoint(
829
1027
  request: Request,
830
1028
  payload: UnifiedSearchRequest,
1029
+ background_tasks: BackgroundTasks,
831
1030
  org_id: str = Depends(default_get_org_id),
1031
+ caller_type: str = Depends(default_get_caller_type),
1032
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
1033
+ _deps_done: None = Depends(_stamp_search_dependencies_done),
832
1034
  ) -> UnifiedSearchViewResponse:
833
1035
  """Search across all entity types (profiles, agent playbooks, user playbooks).
834
1036
 
@@ -840,31 +1042,76 @@ def unified_search_endpoint(
840
1042
  request (Request): The HTTP request object (for rate limiting)
841
1043
  payload (UnifiedSearchRequest): The unified search request
842
1044
  org_id (str): Organization ID
1045
+ caller_type (str): Billing caller classification (injected via dependency).
843
1046
 
844
1047
  Returns:
845
1048
  UnifiedSearchViewResponse: Combined search results
846
1049
  """
847
- response = _run_limited_api(
848
- org_id,
849
- "search",
850
- lambda: get_reflexio(org_id=org_id).unified_search(payload, org_id=org_id),
851
- )
852
- return UnifiedSearchViewResponse(
853
- success=response.success,
854
- profiles=[to_profile_view(p) for p in response.profiles],
855
- agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
856
- user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
857
- reformulated_query=response.reformulated_query,
858
- msg=response.msg,
859
- agent_trace=response.agent_trace,
860
- rehydrated_text=response.rehydrated_text,
1050
+ deps_done = getattr(request.state, "search_deps_done_monotonic", None)
1051
+ deps_to_body_ms = (
1052
+ int((time.monotonic() - deps_done) * 1000) if deps_done is not None else None
861
1053
  )
1054
+ with profile_step(
1055
+ "search.endpoint",
1056
+ enabled=bool(payload.enable_reformulation),
1057
+ has_conversation_history=bool(payload.conversation_history),
1058
+ search_mode=payload.search_mode,
1059
+ ) as endpoint_span:
1060
+ endpoint_span.set_data("deps_to_body_ms", deps_to_body_ms)
1061
+ endpoint_span.set_data(
1062
+ "tp_borrowed", getattr(request.state, "tp_borrowed", None)
1063
+ )
1064
+ endpoint_span.set_data("tp_total", getattr(request.state, "tp_total", None))
1065
+ endpoint_span.set_data("tp_waiting", getattr(request.state, "tp_waiting", None))
1066
+
1067
+ def run_search() -> Any:
1068
+ with profile_step("search.reflexio_cache"):
1069
+ reflexio = get_reflexio(org_id=org_id)
1070
+ return reflexio.unified_search(payload, org_id=org_id)
1071
+
1072
+ response = _run_limited_api(org_id, "search", run_search)
1073
+ with profile_step("search.response_view"):
1074
+ resp = UnifiedSearchViewResponse(
1075
+ success=response.success,
1076
+ profiles=[to_profile_view(p) for p in response.profiles],
1077
+ agent_playbooks=[
1078
+ to_agent_playbook_view(fb) for fb in response.agent_playbooks
1079
+ ],
1080
+ user_playbooks=[
1081
+ to_user_playbook_view(rf) for rf in response.user_playbooks
1082
+ ],
1083
+ reformulated_query=response.reformulated_query,
1084
+ msg=response.msg,
1085
+ agent_trace=response.agent_trace,
1086
+ rehydrated_text=response.rehydrated_text,
1087
+ )
1088
+ background_tasks.add_task(
1089
+ _meter_search_request,
1090
+ org_id=org_id,
1091
+ caller_type=caller_type,
1092
+ request_id=getattr(payload, "request_id", None),
1093
+ session_id=getattr(payload, "session_id", None),
1094
+ )
1095
+ background_tasks.add_task(
1096
+ _meter_applied_learnings,
1097
+ org_id=org_id,
1098
+ caller_type=caller_type,
1099
+ surfaced_count=len(resp.profiles)
1100
+ + len(resp.agent_playbooks)
1101
+ + len(resp.user_playbooks),
1102
+ request_id=getattr(payload, "request_id", None),
1103
+ session_id=getattr(payload, "session_id", None),
1104
+ )
1105
+ return resp
862
1106
 
863
1107
 
864
1108
  @core_router.get("/api/profile_change_log", response_model=ProfileChangeLogViewResponse)
865
1109
  def get_profile_change_log(
866
1110
  org_id: str = Depends(default_get_org_id),
867
1111
  ) -> ProfileChangeLogViewResponse:
1112
+ # Serves the reconstructed profile change log (rebuilt from lineage events). The
1113
+ # legacy `profile_change_logs` table is no longer written; see
1114
+ # reconstruct_profile_change_log in lib/_profiles.py.
868
1115
  response = get_reflexio(org_id=org_id).get_profile_change_logs()
869
1116
  return ProfileChangeLogViewResponse(
870
1117
  success=response.success,
@@ -1269,6 +1516,307 @@ def get_requests_endpoint(
1269
1516
  )
1270
1517
 
1271
1518
 
1519
+ def _learning_provenance_error(
1520
+ payload: GetLearningProvenanceRequest,
1521
+ msg: str,
1522
+ ) -> LearningProvenanceViewResponse:
1523
+ return LearningProvenanceViewResponse(
1524
+ success=False,
1525
+ target_kind=payload.kind,
1526
+ target_id=payload.id,
1527
+ provenance_status="unavailable",
1528
+ msg=msg,
1529
+ )
1530
+
1531
+
1532
+ def _parse_learning_target_int(
1533
+ payload: GetLearningProvenanceRequest,
1534
+ ) -> int | LearningProvenanceViewResponse:
1535
+ try:
1536
+ return int(payload.id)
1537
+ except ValueError:
1538
+ return _learning_provenance_error(
1539
+ payload,
1540
+ f"{payload.kind} id must be an integer",
1541
+ )
1542
+
1543
+
1544
+ def _sort_interactions_by_time(interactions: list[Any]) -> list[Any]:
1545
+ return sorted(
1546
+ interactions,
1547
+ key=lambda i: (
1548
+ getattr(i, "created_at", 0) or 0,
1549
+ getattr(i, "interaction_id", 0),
1550
+ ),
1551
+ )
1552
+
1553
+
1554
+ def _interaction_views(interactions: list[Any]) -> list[Any]:
1555
+ return [to_interaction_view(i) for i in _sort_interactions_by_time(interactions)]
1556
+
1557
+
1558
+ def _effective_profile_provenance_window(
1559
+ reflexio: Any, trigger_source: str | None
1560
+ ) -> tuple[int, list[str] | None]:
1561
+ config = reflexio.request_context.configurator.get_config()
1562
+ profile_config = getattr(config, "profile_extractor_config", None)
1563
+ if profile_config is None:
1564
+ return getattr(config, "window_size", DEFAULT_WINDOW_SIZE), None
1565
+
1566
+ window_size, _ = get_extractor_window_params(
1567
+ profile_config,
1568
+ getattr(config, "window_size", None),
1569
+ getattr(config, "stride_size", None),
1570
+ )
1571
+ should_skip, source_filter = get_effective_source_filter(
1572
+ profile_config,
1573
+ trigger_source,
1574
+ )
1575
+ if should_skip:
1576
+ return window_size, None
1577
+ return window_size, source_filter
1578
+
1579
+
1580
+ def _get_profile_learning_provenance(
1581
+ payload: GetLearningProvenanceRequest,
1582
+ reflexio: Any,
1583
+ ) -> LearningProvenanceViewResponse:
1584
+ storage = reflexio.request_context.storage
1585
+ profile = storage.get_profile_by_id(payload.id)
1586
+ if profile is None:
1587
+ return _learning_provenance_error(payload, "Profile not found")
1588
+
1589
+ if profile.source_interaction_ids:
1590
+ interactions = storage.get_interactions_by_ids(profile.source_interaction_ids)
1591
+ return LearningProvenanceViewResponse(
1592
+ success=True,
1593
+ target_kind=payload.kind,
1594
+ target_id=payload.id,
1595
+ provenance_status="exact",
1596
+ trigger_request_id=profile.generated_from_request_id,
1597
+ interactions=_interaction_views(interactions),
1598
+ )
1599
+
1600
+ if not profile.generated_from_request_id:
1601
+ return _learning_provenance_error(
1602
+ payload,
1603
+ "Profile has no generation request for provenance reconstruction",
1604
+ )
1605
+
1606
+ trigger_request = storage.get_request(profile.generated_from_request_id)
1607
+ if trigger_request is None:
1608
+ return _learning_provenance_error(
1609
+ payload,
1610
+ "Profile generation request was not found",
1611
+ )
1612
+
1613
+ trigger_interactions = storage.get_interactions_by_request_ids(
1614
+ [profile.generated_from_request_id]
1615
+ )
1616
+ anchor_time = (
1617
+ max(i.created_at for i in trigger_interactions)
1618
+ if trigger_interactions
1619
+ else trigger_request.created_at
1620
+ )
1621
+ window_size, source_filter = _effective_profile_provenance_window(
1622
+ reflexio,
1623
+ trigger_request.source,
1624
+ )
1625
+ _, interactions = storage.get_last_k_interactions_grouped(
1626
+ user_id=profile.user_id,
1627
+ k=window_size,
1628
+ sources=source_filter,
1629
+ end_time=anchor_time,
1630
+ )
1631
+ return LearningProvenanceViewResponse(
1632
+ success=True,
1633
+ target_kind=payload.kind,
1634
+ target_id=payload.id,
1635
+ provenance_status="best_effort" if interactions else "unavailable",
1636
+ trigger_request_id=profile.generated_from_request_id,
1637
+ interactions=_interaction_views(interactions),
1638
+ msg=None if interactions else "No interactions found for reconstructed window",
1639
+ )
1640
+
1641
+
1642
+ def _get_user_playbook_learning_provenance(
1643
+ payload: GetLearningProvenanceRequest,
1644
+ reflexio: Any,
1645
+ ) -> LearningProvenanceViewResponse:
1646
+ parsed_id = _parse_learning_target_int(payload)
1647
+ if isinstance(parsed_id, LearningProvenanceViewResponse):
1648
+ return parsed_id
1649
+
1650
+ storage = reflexio.request_context.storage
1651
+ playbook = storage.get_user_playbook_by_id(parsed_id)
1652
+ if playbook is None:
1653
+ return _learning_provenance_error(payload, "User playbook not found")
1654
+
1655
+ status = "exact"
1656
+ if playbook.source_interaction_ids:
1657
+ interactions = storage.get_interactions_by_ids(playbook.source_interaction_ids)
1658
+ elif playbook.request_id:
1659
+ status = "best_effort"
1660
+ interactions = storage.get_interactions_by_request_ids([playbook.request_id])
1661
+ else:
1662
+ status = "unavailable"
1663
+ interactions = []
1664
+
1665
+ return LearningProvenanceViewResponse(
1666
+ success=True,
1667
+ target_kind=payload.kind,
1668
+ target_id=payload.id,
1669
+ provenance_status=status if interactions else "unavailable",
1670
+ trigger_request_id=playbook.request_id,
1671
+ interactions=_interaction_views(interactions),
1672
+ msg=None if interactions else "No interactions found for this user playbook",
1673
+ )
1674
+
1675
+
1676
+ def _get_agent_playbook_learning_provenance(
1677
+ payload: GetLearningProvenanceRequest,
1678
+ reflexio: Any,
1679
+ ) -> LearningProvenanceViewResponse:
1680
+ parsed_id = _parse_learning_target_int(payload)
1681
+ if isinstance(parsed_id, LearningProvenanceViewResponse):
1682
+ return parsed_id
1683
+
1684
+ storage = reflexio.request_context.storage
1685
+ agent_playbook = storage.get_agent_playbook_by_id(parsed_id)
1686
+ if agent_playbook is None:
1687
+ return _learning_provenance_error(payload, "Agent playbook not found")
1688
+
1689
+ windows = storage.get_source_windows_for_agent_playbook(parsed_id)
1690
+ if not windows:
1691
+ return LearningProvenanceViewResponse(
1692
+ success=True,
1693
+ target_kind=payload.kind,
1694
+ target_id=payload.id,
1695
+ provenance_status="unavailable",
1696
+ msg="No source user playbooks are recorded for this agent playbook",
1697
+ )
1698
+
1699
+ user_playbook_ids = [window.user_playbook_id for window in windows]
1700
+ user_playbooks = storage.get_user_playbooks_by_ids_any_user(
1701
+ user_playbook_ids,
1702
+ status_filter=None,
1703
+ )
1704
+ user_playbooks_by_id = {
1705
+ playbook.user_playbook_id: playbook for playbook in user_playbooks
1706
+ }
1707
+
1708
+ interaction_ids_to_fetch: set[int] = set()
1709
+ request_ids_to_fetch: set[str] = set()
1710
+ group_sources: dict[int, tuple[list[int], str | None, bool]] = {}
1711
+ for window in windows:
1712
+ source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
1713
+ if source_user_playbook is None:
1714
+ continue
1715
+
1716
+ source_ids = list(window.source_interaction_ids)
1717
+ request_id: str | None = None
1718
+ uses_fallback = False
1719
+ if not source_ids and source_user_playbook.source_interaction_ids:
1720
+ source_ids = list(source_user_playbook.source_interaction_ids)
1721
+ elif not source_ids and source_user_playbook.request_id:
1722
+ request_id = source_user_playbook.request_id
1723
+ uses_fallback = True
1724
+ elif not source_ids:
1725
+ uses_fallback = True
1726
+
1727
+ interaction_ids_to_fetch.update(source_ids)
1728
+ if request_id:
1729
+ request_ids_to_fetch.add(request_id)
1730
+ group_sources[window.user_playbook_id] = (source_ids, request_id, uses_fallback)
1731
+
1732
+ interactions_by_id = (
1733
+ {
1734
+ interaction.interaction_id: interaction
1735
+ for interaction in storage.get_interactions_by_ids(
1736
+ sorted(interaction_ids_to_fetch)
1737
+ )
1738
+ }
1739
+ if interaction_ids_to_fetch
1740
+ else {}
1741
+ )
1742
+ request_interactions: dict[str, list[Any]] = {}
1743
+ if request_ids_to_fetch:
1744
+ for interaction in storage.get_interactions_by_request_ids(
1745
+ sorted(request_ids_to_fetch)
1746
+ ):
1747
+ request_interactions.setdefault(interaction.request_id, []).append(
1748
+ interaction
1749
+ )
1750
+
1751
+ groups: list[SourceUserPlaybookProvenanceView] = []
1752
+ used_fallback = False
1753
+ found_interactions = False
1754
+ for window in windows:
1755
+ source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
1756
+ if source_user_playbook is None:
1757
+ used_fallback = True
1758
+ continue
1759
+
1760
+ source_ids, request_id, uses_fallback = group_sources.get(
1761
+ window.user_playbook_id,
1762
+ ([], None, True),
1763
+ )
1764
+ used_fallback = used_fallback or uses_fallback
1765
+ interactions = (
1766
+ [interactions_by_id[i] for i in source_ids if i in interactions_by_id]
1767
+ if source_ids
1768
+ else request_interactions.get(request_id or "", [])
1769
+ )
1770
+
1771
+ found_interactions = found_interactions or bool(interactions)
1772
+ groups.append(
1773
+ SourceUserPlaybookProvenanceView(
1774
+ user_playbook=to_user_playbook_view(source_user_playbook),
1775
+ interactions=_interaction_views(interactions),
1776
+ source_interaction_ids=source_ids,
1777
+ )
1778
+ )
1779
+
1780
+ if not groups:
1781
+ return _learning_provenance_error(
1782
+ payload,
1783
+ "Recorded source user playbooks were not found",
1784
+ )
1785
+
1786
+ return LearningProvenanceViewResponse(
1787
+ success=True,
1788
+ target_kind=payload.kind,
1789
+ target_id=payload.id,
1790
+ provenance_status=(
1791
+ "exact"
1792
+ if found_interactions and not used_fallback
1793
+ else "best_effort"
1794
+ if found_interactions
1795
+ else "unavailable"
1796
+ ),
1797
+ source_user_playbooks=groups,
1798
+ msg=None if found_interactions else "No source interactions were found",
1799
+ )
1800
+
1801
+
1802
+ @core_router.post(
1803
+ "/api/get_learning_provenance",
1804
+ response_model=LearningProvenanceViewResponse,
1805
+ response_model_exclude_none=True,
1806
+ )
1807
+ def get_learning_provenance(
1808
+ payload: GetLearningProvenanceRequest,
1809
+ org_id: str = Depends(default_get_org_id),
1810
+ ) -> LearningProvenanceViewResponse:
1811
+ """Return read-only learning provenance for a generated learning row."""
1812
+ reflexio = get_reflexio(org_id=org_id)
1813
+ if payload.kind == "profile":
1814
+ return _get_profile_learning_provenance(payload, reflexio)
1815
+ if payload.kind == "user_playbook":
1816
+ return _get_user_playbook_learning_provenance(payload, reflexio)
1817
+ return _get_agent_playbook_learning_provenance(payload, reflexio)
1818
+
1819
+
1272
1820
  @core_router.post(
1273
1821
  "/api/get_profiles",
1274
1822
  response_model=GetProfilesViewResponse,
@@ -1296,6 +1844,8 @@ def get_all_profiles(
1296
1844
  request: Request,
1297
1845
  limit: int = 100,
1298
1846
  status_filter: str | None = None,
1847
+ profile_id: str | None = None,
1848
+ include_tombstones: bool = False,
1299
1849
  org_id: str = Depends(default_get_org_id),
1300
1850
  ) -> GetProfilesViewResponse:
1301
1851
  """Get all user profiles across all users.
@@ -1303,6 +1853,9 @@ def get_all_profiles(
1303
1853
  Args:
1304
1854
  limit (int, optional): Maximum number of profiles to return. Defaults to 100.
1305
1855
  status_filter (str, optional): Filter by profile status. Can be "current", "pending", or "archived".
1856
+ profile_id (str, optional): Exact profile ID to retrieve.
1857
+ include_tombstones (bool, optional): Include merged/superseded rows when
1858
+ looking up a specific profile_id.
1306
1859
  org_id (str): Organization ID
1307
1860
 
1308
1861
  Returns:
@@ -1310,6 +1863,24 @@ def get_all_profiles(
1310
1863
  """
1311
1864
  reflexio = get_reflexio(org_id=org_id)
1312
1865
 
1866
+ if profile_id:
1867
+ storage = reflexio.request_context.storage
1868
+ if storage is None:
1869
+ return GetProfilesViewResponse(
1870
+ success=True,
1871
+ user_profiles=[],
1872
+ msg="Storage is not configured",
1873
+ )
1874
+ profile = storage.get_profile_by_id(
1875
+ profile_id, include_tombstones=include_tombstones
1876
+ )
1877
+ profiles = [profile] if profile else []
1878
+ return GetProfilesViewResponse(
1879
+ success=True,
1880
+ user_profiles=[to_profile_view(p) for p in profiles],
1881
+ msg=f"Found {len(profiles)} profile(s)",
1882
+ )
1883
+
1313
1884
  # Map status_filter string to Status list
1314
1885
  status_filter_list = None
1315
1886
  if status_filter == "current":
@@ -1378,17 +1949,30 @@ def set_config(
1378
1949
  """Set configuration for the organization.
1379
1950
 
1380
1951
  Args:
1381
- config (Config): The configuration to set
1952
+ config (dict[str, Any]): The configuration payload to set
1382
1953
  org_id (str): Organization ID
1383
1954
 
1384
1955
  Returns:
1385
1956
  dict: Response containing success status and message
1386
1957
  """
1958
+ from pydantic import ValidationError
1959
+
1387
1960
  # Create Reflexio instance to access the configurator through request_context
1388
1961
  reflexio = get_reflexio(org_id=org_id)
1962
+ configurator = reflexio.request_context.configurator
1963
+ normalized_config = configurator.normalize_config_payload(config)
1964
+ if not isinstance(normalized_config, dict):
1965
+ normalized_config = config
1966
+ try:
1967
+ Config.model_validate(normalized_config)
1968
+ except ValidationError as exc:
1969
+ raise HTTPException(
1970
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
1971
+ detail=exc.errors(),
1972
+ ) from exc
1389
1973
 
1390
1974
  # Set the config using Reflexio's set_config method
1391
- response = reflexio.set_config(config)
1975
+ response = reflexio.set_config(normalized_config)
1392
1976
 
1393
1977
  # Invalidate cache on successful config change to ensure fresh instance next request
1394
1978
  if response.success:
@@ -1568,26 +2152,40 @@ def get_user_playbooks(
1568
2152
  response_model=GetAgentPlaybooksViewResponse,
1569
2153
  response_model_exclude_none=True,
1570
2154
  )
2155
+ @limiter.limit("120/minute") # Rate limit for read operations
1571
2156
  def get_agent_playbooks(
1572
- request: GetAgentPlaybooksRequest,
2157
+ request: Request,
2158
+ payload: GetAgentPlaybooksRequest,
1573
2159
  org_id: str = Depends(default_get_org_id),
2160
+ caller_type: str = Depends(default_get_caller_type),
2161
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
1574
2162
  ) -> GetAgentPlaybooksViewResponse:
1575
2163
  """Get agent playbooks with internal fields filtered out.
1576
2164
 
1577
2165
  Args:
1578
- request (GetAgentPlaybooksRequest): The get request
2166
+ request (Request): The HTTP request object (for rate limiting)
2167
+ payload (GetAgentPlaybooksRequest): The get request
1579
2168
  org_id (str): Organization ID
2169
+ caller_type (str): Billing caller classification (injected via dependency).
1580
2170
 
1581
2171
  Returns:
1582
2172
  GetAgentPlaybooksViewResponse: Response containing agent playbooks without internal fields
1583
2173
  """
1584
2174
  reflexio = get_reflexio(org_id=org_id)
1585
- response = reflexio.get_agent_playbooks(request)
1586
- return GetAgentPlaybooksViewResponse(
2175
+ response = reflexio.get_agent_playbooks(payload)
2176
+ resp = GetAgentPlaybooksViewResponse(
1587
2177
  success=response.success,
1588
2178
  agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
1589
2179
  msg=response.msg,
1590
2180
  )
2181
+ _meter_applied_learnings(
2182
+ org_id=org_id,
2183
+ caller_type=caller_type,
2184
+ surfaced_count=len(resp.agent_playbooks),
2185
+ request_id=getattr(payload, "request_id", None),
2186
+ session_id=getattr(payload, "session_id", None),
2187
+ )
2188
+ return resp
1591
2189
 
1592
2190
 
1593
2191
  @core_router.post(
@@ -2094,10 +2692,8 @@ def _read_grade_on_demand_cache(
2094
2692
  def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
2095
2693
  """Look up the user_id that owns a session_id without requiring it as input.
2096
2694
 
2097
- Uses ``get_sessions(session_id=...)`` because it's the only storage
2098
- method on ``BaseStorage`` that accepts session_id alone every other
2099
- request-fetcher requires a user_id paired with it. Returns None when
2100
- the session has no requests, signalling NO_REQUESTS to the caller.
2695
+ Uses the first-request bulk helper even for this single-session path so the
2696
+ lookup can use the same indexed query shape as evaluation overview.
2101
2697
 
2102
2698
  Args:
2103
2699
  storage: The request's storage backend.
@@ -2107,17 +2703,17 @@ def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
2107
2703
  str | None: The user_id of the earliest request in the session,
2108
2704
  or None when no requests exist.
2109
2705
  """
2110
- sessions = storage.get_sessions(session_id=session_id, top_k=100)
2111
- rows = sessions.get(session_id) or []
2112
- if not rows:
2706
+ first_requests = storage.get_first_requests_by_session_ids([session_id])
2707
+ first = first_requests.get(session_id)
2708
+ if first is None:
2113
2709
  return None
2114
- earliest = min(rows, key=lambda r: r.request.created_at)
2115
- return earliest.request.user_id
2710
+ return first.user_id
2116
2711
 
2117
2712
 
2118
2713
  def _find_fresh_result_id(
2119
2714
  storage: Any,
2120
2715
  *,
2716
+ user_id: str,
2121
2717
  session_id: str,
2122
2718
  agent_version: str,
2123
2719
  evaluation_name: str,
@@ -2125,12 +2721,12 @@ def _find_fresh_result_id(
2125
2721
  ) -> int | None:
2126
2722
  """Locate the result_id written by the most-recent grade for this session.
2127
2723
 
2128
- The runner writes rows but doesn't return the id. Reading back through
2129
- ``get_agent_success_evaluation_results`` matches the pattern used by
2130
- the regen worker's prior-row capture (group_evaluation_runner step 5).
2724
+ The runner writes rows but doesn't return the id. Use the targeted result-id
2725
+ lookup so this path does not scan broad evaluation windows.
2131
2726
 
2132
2727
  Args:
2133
2728
  storage: The request's storage backend.
2729
+ user_id (str): The user whose session slice was graded.
2134
2730
  session_id (str): The graded session.
2135
2731
  agent_version (str): The version dimension.
2136
2732
  evaluation_name (str): Evaluator/result namespace to isolate readback.
@@ -2140,20 +2736,16 @@ def _find_fresh_result_id(
2140
2736
  int | None: result_id of the latest matching row, or None if the
2141
2737
  runner wrote nothing.
2142
2738
  """
2143
- rows = storage.get_agent_success_evaluation_results(
2144
- limit=1000, agent_version=agent_version
2145
- )
2146
- matched = [
2147
- r
2148
- for r in rows
2149
- if r.session_id == session_id
2150
- and r.evaluation_name == evaluation_name
2151
- and r.result_id not in previous_result_ids
2152
- ]
2153
- if not matched:
2739
+ result_ids = storage.get_agent_success_evaluation_result_ids(
2740
+ user_id=user_id,
2741
+ session_id=session_id,
2742
+ evaluation_name=evaluation_name,
2743
+ agent_version=agent_version,
2744
+ )
2745
+ fresh_result_ids = [rid for rid in result_ids if rid not in previous_result_ids]
2746
+ if not fresh_result_ids:
2154
2747
  return None
2155
- latest = max(matched, key=lambda r: r.created_at)
2156
- return latest.result_id
2748
+ return max(fresh_result_ids)
2157
2749
 
2158
2750
 
2159
2751
  @core_router.post(
@@ -2224,13 +2816,14 @@ def grade_on_demand(
2224
2816
  skipped_reason="NO_REQUESTS",
2225
2817
  )
2226
2818
 
2227
- previous_result_ids = {
2228
- r.result_id
2229
- for r in storage.get_agent_success_evaluation_results(
2230
- limit=1000, agent_version=payload.agent_version
2819
+ previous_result_ids = set(
2820
+ storage.get_agent_success_evaluation_result_ids(
2821
+ user_id=user_id,
2822
+ session_id=payload.session_id,
2823
+ evaluation_name=evaluation_name,
2824
+ agent_version=payload.agent_version,
2231
2825
  )
2232
- if r.session_id == payload.session_id and r.evaluation_name == evaluation_name
2233
- }
2826
+ )
2234
2827
 
2235
2828
  # Two operation_state rows are intentionally written for this session:
2236
2829
  # 1) `grade_on_demand::{org_id}::{session_id}::{agent_version}::{evaluation_name}`
@@ -2255,6 +2848,7 @@ def grade_on_demand(
2255
2848
 
2256
2849
  result_id = _find_fresh_result_id(
2257
2850
  storage,
2851
+ user_id=user_id,
2258
2852
  session_id=payload.session_id,
2259
2853
  agent_version=payload.agent_version,
2260
2854
  evaluation_name=evaluation_name,
@@ -2307,8 +2901,8 @@ def get_recent_shadow_comparisons(
2307
2901
  Filters to the org's currently pinned
2308
2902
  ``Config.shadow_comparison_judge_prompt_version`` so verdicts produced
2309
2903
  under an older rubric do not mix into the drawer or the Top 10
2310
- disagreements widget. Storage returns verdicts in ascending ``created_at``
2311
- order; we reverse to "newest first" and cap at ``limit``.
2904
+ disagreements widget. Storage returns verdicts newest-first and caps the
2905
+ read at ``limit``.
2312
2906
 
2313
2907
  Args:
2314
2908
  limit (int): Max verdicts to return. Clamped to ``[1, 100]``.
@@ -2340,10 +2934,11 @@ def get_recent_shadow_comparisons(
2340
2934
 
2341
2935
  now = int(datetime.now(UTC).timestamp())
2342
2936
  try:
2343
- verdicts = storage.get_shadow_comparison_verdicts(
2937
+ verdicts = storage.get_recent_shadow_comparison_verdicts(
2344
2938
  from_ts=now - _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS,
2345
2939
  to_ts=now,
2346
2940
  judge_prompt_version=pinned_version,
2941
+ limit=clamped_limit,
2347
2942
  )
2348
2943
  except NotImplementedError:
2349
2944
  # Backends that don't support shadow verdicts (e.g., Disk) should
@@ -2351,9 +2946,7 @@ def get_recent_shadow_comparisons(
2351
2946
  # "no data yet" in the UI.
2352
2947
  return GetRecentShadowComparisonsResponse(verdicts=[])
2353
2948
 
2354
- # Storage contract returns ascending — flip to "newest first" and cap.
2355
- newest_first = list(reversed(verdicts))[:clamped_limit]
2356
- return GetRecentShadowComparisonsResponse(verdicts=newest_first)
2949
+ return GetRecentShadowComparisonsResponse(verdicts=verdicts)
2357
2950
 
2358
2951
 
2359
2952
  @core_router.post(
@@ -2714,6 +3307,9 @@ def create_app(
2714
3307
  additional_routers: list[APIRouter] | None = None,
2715
3308
  middleware_config: dict | None = None,
2716
3309
  require_auth: bool = False,
3310
+ get_caller_type: Callable[..., str] | None = None,
3311
+ get_billing_gate: Callable[[str], Callable[..., None]] | None = None,
3312
+ mount_data_plane: bool = True,
2717
3313
  ) -> FastAPI:
2718
3314
  """Factory to create a FastAPI app.
2719
3315
 
@@ -2724,6 +3320,23 @@ def create_app(
2724
3320
  middleware_config: Optional middleware overrides (not used yet, reserved for future).
2725
3321
  require_auth: When True, declares a Bearer security scheme in the OpenAPI spec
2726
3322
  so Swagger UI shows lock icons and the Authorize button works.
3323
+ get_caller_type: Custom dependency for classifying the caller (e.g., production
3324
+ agent vs dashboard). When provided, overrides the default_get_caller_type
3325
+ dependency globally, exactly mirroring the get_org_id override.
3326
+ get_billing_gate: Optional factory ``(line: str) -> FastAPI dependency`` that
3327
+ replaces the default no-op gate for each billable billing line. When
3328
+ provided, for every line used in the app (``"application"`` and
3329
+ ``"learnings_generated"``) the returned dependency overrides the
3330
+ ``default_billing_gate(line)`` sentinel in ``dependency_overrides``,
3331
+ exactly mirroring the ``get_caller_type`` override pattern.
3332
+ mount_data_plane: When True (default), include the data-plane routers
3333
+ (core, stall-state, pending-tool-call) and run the data-plane
3334
+ lifespan work (LLM availability check, cross-encoder prewarm,
3335
+ resume scheduler). When False, skip both so a control-plane host
3336
+ can build an app without requiring LLM/storage or starting the
3337
+ scheduler, while keeping all other scaffolding (middleware, CORS,
3338
+ auth overrides, OpenAPI security, health, ``/meta/version``,
3339
+ ``additional_routers``).
2727
3340
 
2728
3341
  Returns:
2729
3342
  Configured FastAPI application.
@@ -2731,12 +3344,19 @@ def create_app(
2731
3344
  from collections.abc import AsyncIterator
2732
3345
  from contextlib import asynccontextmanager
2733
3346
 
2734
- from reflexio.server._auth import default_get_org_id
3347
+ from reflexio.server._auth import (
3348
+ default_billing_gate,
3349
+ default_get_caller_type,
3350
+ default_get_org_id,
3351
+ )
2735
3352
  from reflexio.server.api_endpoints.request_context import RequestContext
2736
3353
  from reflexio.server.llm.model_defaults import validate_llm_availability
2737
3354
  from reflexio.server.services.extraction.resume_scheduler import (
2738
3355
  maybe_start_resume_scheduler,
2739
3356
  )
3357
+ from reflexio.server.services.lineage.gc_scheduler import (
3358
+ maybe_start_lineage_gc,
3359
+ )
2740
3360
 
2741
3361
  def _lifespan_org_id() -> str:
2742
3362
  if get_org_id is None:
@@ -2755,23 +3375,33 @@ def create_app(
2755
3375
 
2756
3376
  @asynccontextmanager
2757
3377
  async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
2758
- validate_llm_availability()
2759
- from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
2760
-
2761
- _prewarm_cross_encoder()
2762
- # The scheduler discovers every org with resumable work each tick and
2763
- # drives a per-org worker with org-scoped claims, so it is not limited
2764
- # to the bootstrap org. The bootstrap org is only used to read config
2765
- # and to seed cross-org discovery.
2766
- scheduler = maybe_start_resume_scheduler(
2767
- lambda org_id: RequestContext(org_id=org_id),
2768
- bootstrap_org_id=_lifespan_org_id(),
2769
- )
3378
+ scheduler = None
3379
+ gc_scheduler = None
3380
+ if mount_data_plane:
3381
+ log_publish_hardware_capacity()
3382
+ validate_llm_availability()
3383
+ from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
3384
+
3385
+ _prewarm_cross_encoder()
3386
+ # The scheduler discovers every org with resumable work each tick and
3387
+ # drives a per-org worker with org-scoped claims, so it is not limited
3388
+ # to the bootstrap org. The bootstrap org is only used to read config
3389
+ # and to seed cross-org discovery.
3390
+ scheduler = maybe_start_resume_scheduler(
3391
+ lambda org_id: RequestContext(org_id=org_id),
3392
+ bootstrap_org_id=_lifespan_org_id(),
3393
+ )
3394
+ gc_scheduler = maybe_start_lineage_gc(
3395
+ lambda org_id: RequestContext(org_id=org_id),
3396
+ bootstrap_org_id=_lifespan_org_id(),
3397
+ )
2770
3398
  try:
2771
3399
  yield
2772
3400
  finally:
2773
3401
  if scheduler is not None:
2774
3402
  scheduler.stop()
3403
+ if gc_scheduler is not None:
3404
+ gc_scheduler.stop()
2775
3405
 
2776
3406
  app = FastAPI(docs_url="/docs", lifespan=lifespan)
2777
3407
 
@@ -2837,6 +3467,21 @@ def create_app(
2837
3467
  if get_org_id is not None:
2838
3468
  app.dependency_overrides[default_get_org_id] = get_org_id
2839
3469
 
3470
+ # Override get_caller_type dependency if custom one provided
3471
+ if get_caller_type is not None:
3472
+ app.dependency_overrides[default_get_caller_type] = get_caller_type
3473
+
3474
+ # Override billing gate dependencies if a custom gate factory is provided.
3475
+ # Each billing line needs its own override because dependency_overrides is
3476
+ # keyed by callable identity. ``default_billing_gate`` uses lru_cache so
3477
+ # the same sentinel object is returned for the same line — which is why
3478
+ # the overrides reliably fire at request time.
3479
+ if get_billing_gate is not None:
3480
+ for line in ("application", "learnings_generated"):
3481
+ app.dependency_overrides[default_billing_gate(line)] = get_billing_gate(
3482
+ line
3483
+ )
3484
+
2840
3485
  # When a custom get_org_id is provided together with require_auth,
2841
3486
  # auth is enforced on every route — mark this app instance so the
2842
3487
  # token-gated my_config endpoint becomes reachable. Using
@@ -2845,14 +3490,18 @@ def create_app(
2845
3490
  # multi-tenant embeddings) can coexist without leaking state.
2846
3491
  app.state.my_config_enabled = bool(get_org_id is not None and require_auth)
2847
3492
 
2848
- # Include core routes
2849
- app.include_router(core_router)
3493
+ # Include data-plane routes (core, stall-state, pending-tool-call). A
3494
+ # control-plane host sets mount_data_plane=False to skip these while
3495
+ # keeping every other piece of scaffolding below.
3496
+ if mount_data_plane:
3497
+ # Include core routes
3498
+ app.include_router(core_router)
2850
3499
 
2851
- # Include stall_state routes
2852
- app.include_router(stall_state_api.router)
3500
+ # Include stall_state routes
3501
+ app.include_router(stall_state_api.router)
2853
3502
 
2854
- # Include pending tool call routes
2855
- app.include_router(pending_tool_call_api.router)
3503
+ # Include pending tool call routes
3504
+ app.include_router(pending_tool_call_api.router)
2856
3505
 
2857
3506
  # Include additional routers
2858
3507
  for router in additional_routers or []: