claude-smart 0.2.45 → 0.2.47

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 (353) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +47 -10
  3. package/bin/claude-smart.js +535 -23
  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 +27 -6
  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/layout.tsx +20 -0
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  17. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  18. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  19. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  20. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  21. package/plugin/dashboard/lib/status.ts +10 -3
  22. package/plugin/dashboard/lib/types.ts +18 -6
  23. package/plugin/hooks/codex-hooks.json +0 -1
  24. package/plugin/opencode/assistant-buffer.ts +108 -0
  25. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  26. package/plugin/opencode/dist/internal.js +12 -0
  27. package/plugin/opencode/dist/payload.js +85 -0
  28. package/plugin/opencode/dist/server.mjs +232 -0
  29. package/plugin/opencode/internal.ts +18 -0
  30. package/plugin/opencode/package.json +3 -0
  31. package/plugin/opencode/payload.ts +90 -0
  32. package/plugin/opencode/server.mts +251 -0
  33. package/plugin/opencode/tsconfig.json +13 -0
  34. package/plugin/pyproject.toml +25 -4
  35. package/plugin/scripts/_lib.sh +5 -1
  36. package/plugin/scripts/backend-service.sh +22 -2
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +1 -1
  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 +17 -9
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +498 -12
  45. package/plugin/src/claude_smart/context_format.py +9 -9
  46. package/plugin/src/claude_smart/cs_cite.py +66 -19
  47. package/plugin/src/claude_smart/env_config.py +12 -4
  48. package/plugin/src/claude_smart/events/session_start.py +26 -7
  49. package/plugin/src/claude_smart/events/stop.py +1 -1
  50. package/plugin/src/claude_smart/ids.py +1 -1
  51. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  52. package/plugin/src/claude_smart/runtime.py +7 -1
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +54 -30
  55. package/plugin/vendor/reflexio/README.md +17 -11
  56. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  57. package/plugin/vendor/reflexio/reflexio/README.md +4 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  59. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  60. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  64. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
  65. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  66. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  67. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  71. package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
  146. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  147. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  148. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
  149. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
  151. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  152. package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
  153. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  158. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  159. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  160. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  161. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
  162. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  164. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
  165. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  166. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  167. package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
  168. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  169. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  170. package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
  171. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  172. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
  173. package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
  174. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  175. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  176. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  177. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  178. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  179. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  180. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  181. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  182. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  183. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  185. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  186. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  187. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  188. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  189. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  198. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
  199. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  200. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  201. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  202. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  203. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  204. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  205. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  206. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  207. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  208. package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
  209. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  212. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  215. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  216. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  217. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  218. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  219. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  220. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  221. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  222. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  225. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  226. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  227. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  229. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  230. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  231. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
  232. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  233. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  234. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
  238. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
  241. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  242. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  243. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
  244. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  245. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  246. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  248. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  249. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  250. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  252. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  255. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
  256. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  257. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  258. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  259. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  261. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  262. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  263. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
  264. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  265. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  266. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  277. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
  278. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
  279. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
  280. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  281. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  282. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
  289. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  290. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
  292. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
  296. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
  297. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
  311. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  312. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  313. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  314. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  315. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  316. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  317. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  318. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  319. package/scripts/setup-claude-smart.sh +8 -3
  320. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  321. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  322. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  323. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  324. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  325. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  326. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  327. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  328. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  329. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  330. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  331. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  332. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  333. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  334. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  335. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  336. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  337. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  338. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  339. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  340. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  341. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  342. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  343. package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
  344. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  345. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  346. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  347. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  348. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  349. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  350. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  351. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  352. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  353. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -17,7 +17,8 @@ from collections import OrderedDict
17
17
  from collections.abc import Callable
18
18
  from concurrent.futures import Future, ThreadPoolExecutor
19
19
  from concurrent.futures import TimeoutError as FuturesTimeoutError
20
- from typing import TYPE_CHECKING, Any
20
+ from datetime import UTC, datetime
21
+ from typing import TYPE_CHECKING, Any, cast
21
22
 
22
23
  from reflexio.models.api_schema.retriever_schema import (
23
24
  ConversationTurn,
@@ -41,7 +42,14 @@ from reflexio.models.config_schema import (
41
42
  from reflexio.server.llm.litellm_client import LiteLLMClient
42
43
  from reflexio.server.prompt.prompt_manager import PromptManager
43
44
  from reflexio.server.services.pre_retrieval import QueryReformulator
44
- from reflexio.server.services.retrieval.relevance_floor import apply_relevance_floor
45
+ from reflexio.server.services.retrieval.recency import (
46
+ RecencyConfig,
47
+ ScoredItem,
48
+ additive_penalty,
49
+ decay_for_item,
50
+ multiplicative_factor,
51
+ )
52
+ from reflexio.server.services.retrieval.relevance_floor import apply_relevance_floors
45
53
  from reflexio.server.services.storage.storage_base import BaseStorage
46
54
  from reflexio.server.tracing import profile_step, set_span_data
47
55
 
@@ -50,6 +58,7 @@ if TYPE_CHECKING:
50
58
 
51
59
  logger = logging.getLogger(__name__)
52
60
  _DEFAULT_ENTITY_TYPES = frozenset({"profiles", "agent_playbooks", "user_playbooks"})
61
+ _SOURCE_USER_PLAYBOOK_IDS_KEY = "_source_user_playbook_ids"
53
62
  # Statuses returned for agent_playbooks when the caller does not pass an
54
63
  # explicit ``agent_playbook_status_filter``. Excludes REJECTED so that a
55
64
  # rejection in the dashboard immediately suppresses the playbook from search
@@ -66,6 +75,7 @@ _SEARCH_FANOUT_EXECUTOR = ThreadPoolExecutor(
66
75
  max_workers=_SEARCH_FANOUT_MAX_WORKERS,
67
76
  thread_name_prefix="reflexio-search",
68
77
  )
78
+ _ENV_SINGLE_RPC = "REFLEXIO_UNIFIED_SEARCH_SINGLE_RPC"
69
79
  _EMBEDDING_CACHE_TTL_SECONDS = max(
70
80
  0, int(os.getenv("REFLEXIO_QUERY_EMBEDDING_CACHE_TTL_SECONDS", "300") or "300")
71
81
  )
@@ -76,6 +86,20 @@ _embedding_cache_lock = threading.Lock()
76
86
  _embedding_cache: OrderedDict[tuple[str, int, str, str], tuple[float, list[float]]] = (
77
87
  OrderedDict()
78
88
  )
89
+ RetrievalCaptureHook = Callable[
90
+ [UnifiedSearchRequest, UnifiedSearchResponse, BaseStorage, str], None
91
+ ]
92
+ _retrieval_capture_hook: RetrievalCaptureHook | None = None
93
+
94
+
95
+ def configure_retrieval_capture_hook(hook: RetrievalCaptureHook | None) -> None:
96
+ """Register an optional final-response retrieval capture hook.
97
+
98
+ Deployments that capture retrieval logs install the hook; OSS leaves it
99
+ unset so unified search behavior is unchanged by default.
100
+ """
101
+ global _retrieval_capture_hook
102
+ _retrieval_capture_hook = hook
79
103
 
80
104
 
81
105
  def run_unified_search(
@@ -86,6 +110,7 @@ def run_unified_search(
86
110
  prompt_manager: PromptManager,
87
111
  pre_retrieval_model_name: str | None = None,
88
112
  retrieval_floor: RetrievalFloorConfig | None = None,
113
+ recency: RecencyConfig | None = None,
89
114
  ) -> UnifiedSearchResponse:
90
115
  """
91
116
  Search across all entity types (profiles, agent playbooks, user playbooks) in parallel.
@@ -113,7 +138,12 @@ def run_unified_search(
113
138
 
114
139
  floor_cfg = retrieval_floor or RetrievalFloorConfig()
115
140
  floor_on = floor_cfg.enabled
116
- fetch_k = max(top_k, floor_cfg.pool_size) if floor_on else top_k
141
+ recency_on = bool(recency and recency.enabled)
142
+ fetch_k = max(
143
+ top_k,
144
+ floor_cfg.pool_size if floor_on else 0,
145
+ recency.pool_size if recency_on and recency is not None else 0,
146
+ )
117
147
 
118
148
  # --- Phase A: query reformulation + embedding generation ---
119
149
  reformulated_query, embedding = _run_phase_a(
@@ -137,6 +167,7 @@ def run_unified_search(
137
167
  query=reformulated_query,
138
168
  top_k=fetch_k,
139
169
  threshold=threshold,
170
+ recency_on=recency_on,
140
171
  )
141
172
 
142
173
  if profiles is None:
@@ -150,9 +181,36 @@ def run_unified_search(
150
181
  user_playbooks=user_playbooks, # type: ignore[arg-type]
151
182
  top_k=top_k,
152
183
  cfg=floor_cfg,
184
+ recency=recency if recency_on else None,
185
+ )
186
+ elif recency_on and recency is not None:
187
+ profiles = _apply_combined_score_recency(
188
+ profiles or [], entity_type="profiles", top_k=top_k, cfg=recency
189
+ )
190
+ agent_playbooks = _apply_combined_score_recency(
191
+ agent_playbooks or [],
192
+ entity_type="agent_playbooks",
193
+ top_k=top_k,
194
+ cfg=recency,
195
+ )
196
+ user_playbooks = _apply_combined_score_recency(
197
+ user_playbooks or [],
198
+ entity_type="user_playbooks",
199
+ top_k=top_k,
200
+ cfg=recency,
153
201
  )
202
+ else:
203
+ profiles = _unwrap_items(profiles or [])[:top_k]
204
+ agent_playbooks = _unwrap_items(agent_playbooks or [])[:top_k]
205
+ user_playbooks = _unwrap_items(user_playbooks or [])[:top_k]
154
206
 
155
- return UnifiedSearchResponse(
207
+ user_playbooks = _suppress_source_user_playbooks(
208
+ storage=storage,
209
+ agent_playbooks=agent_playbooks or [],
210
+ user_playbooks=user_playbooks or [],
211
+ )
212
+
213
+ response = UnifiedSearchResponse(
156
214
  success=True,
157
215
  profiles=profiles,
158
216
  agent_playbooks=agent_playbooks, # type: ignore[reportArgumentType]
@@ -161,6 +219,32 @@ def run_unified_search(
161
219
  if reformulated_query != request.query
162
220
  else None,
163
221
  )
222
+ _maybe_capture_final_response(
223
+ request=request,
224
+ response=response,
225
+ storage=storage,
226
+ org_id=org_id,
227
+ )
228
+ return response
229
+
230
+
231
+ def _maybe_capture_final_response(
232
+ *,
233
+ request: UnifiedSearchRequest,
234
+ response: UnifiedSearchResponse,
235
+ storage: BaseStorage,
236
+ org_id: str,
237
+ ) -> None:
238
+ hook = _retrieval_capture_hook
239
+ if hook is None:
240
+ return
241
+ try:
242
+ hook(request, response, storage, org_id)
243
+ except Exception:
244
+ logger.warning(
245
+ "Unified search retrieval capture hook failed",
246
+ exc_info=True,
247
+ )
164
248
 
165
249
 
166
250
  def _run_phase_a(
@@ -236,10 +320,11 @@ def _run_phase_b(
236
320
  query: str,
237
321
  top_k: int,
238
322
  threshold: float,
323
+ recency_on: bool = False,
239
324
  ) -> tuple[
240
- list[UserProfile] | None,
241
- list[AgentPlaybook] | None,
242
- list[UserPlaybook] | None,
325
+ list[Any] | None,
326
+ list[Any] | None,
327
+ list[Any] | None,
243
328
  ]:
244
329
  """Run parallel searches across all entity types by delegating to storage methods.
245
330
 
@@ -266,6 +351,43 @@ def _run_phase_b(
266
351
  entity_types=sorted(entity_types),
267
352
  top_k=top_k,
268
353
  ) as span:
354
+ # Recency needs the per-row ``combined_score``, which only the scored
355
+ # single-RPC method threads back. Backends that don't advertise
356
+ # ``supports_unified_hybrid_search`` (e.g. native Postgres, which still
357
+ # inherits ``unified_hybrid_search_scored`` and runs it via the same
358
+ # ``_rpc`` it already uses for ``hybrid_match_*``) opt into the scored
359
+ # path only when recency is on, so non-recency routing is unchanged.
360
+ wants_scored_single_rpc = recency_on and callable(
361
+ getattr(storage, "unified_hybrid_search_scored", None)
362
+ )
363
+ if _unified_single_rpc_enabled() and (
364
+ getattr(storage, "supports_unified_hybrid_search", False)
365
+ or wants_scored_single_rpc
366
+ ):
367
+ combined = _run_phase_b_single_rpc(
368
+ request=request,
369
+ storage=storage,
370
+ embedding=embedding,
371
+ query=query,
372
+ top_k=top_k,
373
+ threshold=threshold,
374
+ entity_types=entity_types,
375
+ allowed_agent_statuses=allowed_agent_statuses,
376
+ recency_on=recency_on,
377
+ )
378
+ if combined is not None:
379
+ profiles, agent_playbooks, user_playbooks = combined
380
+ set_span_data(
381
+ span,
382
+ {
383
+ "single_rpc": True,
384
+ "profiles_count": len(profiles),
385
+ "agent_playbooks_count": len(agent_playbooks),
386
+ "user_playbooks_count": len(user_playbooks),
387
+ },
388
+ )
389
+ return profiles, agent_playbooks, user_playbooks
390
+ span.set_data("single_rpc_fallback", True)
269
391
  profiles_future = (
270
392
  _submit_with_current_context(
271
393
  _SEARCH_FANOUT_EXECUTOR,
@@ -347,6 +469,96 @@ def _run_phase_b(
347
469
  return profiles, agent_playbooks, user_playbooks
348
470
 
349
471
 
472
+ def _unified_single_rpc_enabled() -> bool:
473
+ """Kill switch for the combined Phase B RPC (default on)."""
474
+ return os.getenv(_ENV_SINGLE_RPC, "1").strip().lower() not in {"0", "false", "off"}
475
+
476
+
477
+ def _run_phase_b_single_rpc(
478
+ *,
479
+ request: UnifiedSearchRequest,
480
+ storage: BaseStorage,
481
+ embedding: list[float] | None,
482
+ query: str,
483
+ top_k: int,
484
+ threshold: float,
485
+ entity_types: set[str],
486
+ allowed_agent_statuses: list[PlaybookStatus] | None,
487
+ recency_on: bool = False,
488
+ ) -> tuple[list[Any], list[Any], list[Any]] | None:
489
+ """Run all Phase B arms through one combined storage round trip.
490
+
491
+ Trades the per-arm round-trip overhead for serialized execution of the
492
+ three queries inside one database session — a win when round-trip
493
+ overhead dominates per-arm query time (toggle via
494
+ ``REFLEXIO_UNIFIED_SEARCH_SINGLE_RPC`` to compare).
495
+
496
+ Returns:
497
+ The three result lists, or None when the combined call fails so the
498
+ caller can fall back to the per-arm fan-out (e.g. the SQL function
499
+ is not yet migrated on this deployment). Timeouts propagate like the
500
+ fan-out path so a hung database is not retried.
501
+ """
502
+ statuses = (
503
+ list(allowed_agent_statuses)
504
+ if allowed_agent_statuses
505
+ else list(_DEFAULT_AGENT_PLAYBOOK_STATUSES)
506
+ )
507
+ # Resolve storage.unified_hybrid_search before submit so missing or stale
508
+ # capability flags can fall back to the fan-out path.
509
+ method_name = (
510
+ "unified_hybrid_search_scored" if recency_on else "unified_hybrid_search"
511
+ )
512
+ unified_hybrid_search = getattr(storage, method_name, None)
513
+ if not callable(unified_hybrid_search):
514
+ if recency_on:
515
+ logger.warning(
516
+ "event=search_recency_missing_scores source=single_rpc method=%s",
517
+ method_name,
518
+ )
519
+ return None
520
+
521
+ future = _submit_with_current_context(
522
+ _SEARCH_FANOUT_EXECUTOR,
523
+ unified_hybrid_search,
524
+ query=query,
525
+ query_embedding=embedding,
526
+ top_k=top_k,
527
+ threshold=threshold,
528
+ user_id=request.user_id,
529
+ agent_version=request.agent_version,
530
+ playbook_name=request.playbook_name,
531
+ agent_playbook_statuses=statuses,
532
+ search_mode=request.search_mode,
533
+ include_profiles="profiles" in entity_types and bool(request.user_id),
534
+ include_agent_playbooks="agent_playbooks" in entity_types,
535
+ include_user_playbooks="user_playbooks" in entity_types,
536
+ )
537
+ try:
538
+ profiles, agent_playbooks, user_playbooks = future.result(timeout=30)
539
+ except FuturesTimeoutError:
540
+ raise
541
+ except Exception:
542
+ logger.warning(
543
+ "Unified single-RPC search failed; falling back to per-arm fan-out",
544
+ exc_info=True,
545
+ )
546
+ return None
547
+
548
+ # Mirror _search_agent_playbooks_via_storage: dedupe by id, cap at top_k.
549
+ deduped: list[Any] = []
550
+ seen_ids: set[str] = set()
551
+ for candidate in agent_playbooks:
552
+ playbook = _unwrap_item(candidate)
553
+ playbook_id = str(getattr(playbook, "agent_playbook_id", ""))
554
+ if playbook_id and playbook_id not in seen_ids:
555
+ seen_ids.add(playbook_id)
556
+ deduped.append(candidate)
557
+ if len(deduped) >= top_k:
558
+ break
559
+ return profiles, deduped, user_playbooks
560
+
561
+
350
562
  def _apply_floors(
351
563
  query: str,
352
564
  profiles: list[UserProfile],
@@ -354,37 +566,163 @@ def _apply_floors(
354
566
  user_playbooks: list[UserPlaybook],
355
567
  top_k: int,
356
568
  cfg: RetrievalFloorConfig,
569
+ recency: RecencyConfig | None = None,
357
570
  ) -> tuple[list[UserProfile], list[AgentPlaybook], list[UserPlaybook]]:
358
- """Apply the per-arm relevance floor to each entity arm in parallel."""
359
- with ThreadPoolExecutor(max_workers=3) as ex:
360
- f_profiles = _submit_with_current_context(
361
- ex,
362
- apply_relevance_floor,
363
- query,
364
- profiles,
365
- cfg.profile_floor,
366
- top_k,
367
- arm="profiles",
571
+ """Apply the per-arm relevance floor with one batched cross-encoder call."""
572
+ floored_profiles, floored_agent, floored_user = apply_relevance_floors(
573
+ query,
574
+ [
575
+ ("profiles", profiles, cfg.profile_floor),
576
+ ("agent_playbooks", agent_playbooks, cfg.agent_playbook_floor),
577
+ ("user_playbooks", user_playbooks, cfg.user_playbook_floor),
578
+ ],
579
+ top_k,
580
+ content_of=lambda item: _unwrap_item(item).content,
581
+ )
582
+ return (
583
+ _finalize_floor_arm(
584
+ floored_profiles, entity_type="profiles", top_k=top_k, recency=recency
585
+ ),
586
+ _finalize_floor_arm(
587
+ floored_agent,
588
+ entity_type="agent_playbooks",
589
+ top_k=top_k,
590
+ recency=recency,
591
+ ),
592
+ _finalize_floor_arm(
593
+ floored_user,
594
+ entity_type="user_playbooks",
595
+ top_k=top_k,
596
+ recency=recency,
597
+ ),
598
+ )
599
+
600
+
601
+ def _finalize_floor_arm(
602
+ result: Any,
603
+ *,
604
+ entity_type: str,
605
+ top_k: int,
606
+ recency: RecencyConfig | None,
607
+ ) -> list[Any]:
608
+ if not recency or not recency.enabled:
609
+ return _unwrap_items(result.items)[:top_k]
610
+ if result.scores is None:
611
+ return _apply_combined_score_recency(
612
+ result.items, entity_type=entity_type, top_k=top_k, cfg=recency
613
+ )
614
+ now = int(datetime.now(UTC).timestamp())
615
+ rescored = []
616
+ for item, score in zip(result.items, result.scores, strict=True):
617
+ unwrapped = _unwrap_item(item)
618
+ freshness = decay_for_item(unwrapped, entity_type=entity_type, now=now)
619
+ rescored.append(
620
+ (unwrapped, score - additive_penalty(freshness, recency.max_penalty_logit))
368
621
  )
369
- f_agent = _submit_with_current_context(
370
- ex,
371
- apply_relevance_floor,
372
- query,
373
- agent_playbooks,
374
- cfg.agent_playbook_floor,
375
- top_k,
376
- arm="agent_playbooks",
622
+ rescored.sort(key=lambda pair: pair[1], reverse=True)
623
+ return [item for item, _score in rescored[:top_k]]
624
+
625
+
626
+ def _apply_combined_score_recency(
627
+ items: list[Any],
628
+ *,
629
+ entity_type: str,
630
+ top_k: int,
631
+ cfg: RecencyConfig,
632
+ ) -> list[Any]:
633
+ if not items:
634
+ return []
635
+ scored_items: list[tuple[Any, float]] = []
636
+ for item in items:
637
+ if not isinstance(item, ScoredItem) or item.score is None:
638
+ logger.warning(
639
+ "event=search_recency_missing_scores entity_type=%s items=%d",
640
+ entity_type,
641
+ len(items),
642
+ )
643
+ return _unwrap_items(items)[:top_k]
644
+ scored_items.append((item.item, item.score))
645
+ now = int(datetime.now(UTC).timestamp())
646
+ rescored = []
647
+ for item, score in scored_items:
648
+ freshness = decay_for_item(item, entity_type=entity_type, now=now)
649
+ rescored.append(
650
+ (
651
+ item,
652
+ score * multiplicative_factor(freshness, cfg.max_penalty_frac),
653
+ )
377
654
  )
378
- f_user = _submit_with_current_context(
379
- ex,
380
- apply_relevance_floor,
381
- query,
382
- user_playbooks,
383
- cfg.user_playbook_floor,
384
- top_k,
385
- arm="user_playbooks",
655
+ rescored.sort(key=lambda pair: pair[1], reverse=True)
656
+ return [item for item, _score in rescored[:top_k]]
657
+
658
+
659
+ def _unwrap_item(item: Any) -> Any:
660
+ return item.item if isinstance(item, ScoredItem) else item
661
+
662
+
663
+ def _unwrap_items(items: list[Any]) -> list[Any]:
664
+ return [_unwrap_item(item) for item in items]
665
+
666
+
667
+ def _suppress_source_user_playbooks(
668
+ *,
669
+ storage: BaseStorage,
670
+ agent_playbooks: list[AgentPlaybook],
671
+ user_playbooks: list[UserPlaybook],
672
+ ) -> list[UserPlaybook]:
673
+ """Drop user playbooks already represented by returned agent playbooks."""
674
+ if not agent_playbooks or not user_playbooks:
675
+ return user_playbooks
676
+
677
+ source_user_playbook_ids: set[int] = set()
678
+ agent_ids_needing_lookup: list[int] = []
679
+ for playbook in agent_playbooks:
680
+ source_ids = getattr(playbook, _SOURCE_USER_PLAYBOOK_IDS_KEY, None)
681
+ if source_ids is None:
682
+ agent_playbook_id = int(getattr(playbook, "agent_playbook_id", 0) or 0)
683
+ if agent_playbook_id:
684
+ agent_ids_needing_lookup.append(agent_playbook_id)
685
+ continue
686
+ source_user_playbook_ids.update(int(source_id) for source_id in source_ids)
687
+
688
+ if agent_ids_needing_lookup:
689
+ lookup = getattr(
690
+ storage, "get_source_user_playbook_ids_for_agent_playbooks", None
386
691
  )
387
- return f_profiles.result(), f_agent.result(), f_user.result()
692
+ if callable(lookup):
693
+ try:
694
+ source_ids_by_agent = cast(
695
+ dict[int, list[int]], lookup(agent_ids_needing_lookup)
696
+ )
697
+ except Exception:
698
+ logger.warning(
699
+ "Failed to resolve source user playbooks for unified search suppression",
700
+ exc_info=True,
701
+ )
702
+ else:
703
+ for source_ids in source_ids_by_agent.values():
704
+ source_user_playbook_ids.update(
705
+ int(source_id) for source_id in source_ids
706
+ )
707
+
708
+ if not source_user_playbook_ids:
709
+ return user_playbooks
710
+
711
+ filtered = [
712
+ playbook
713
+ for playbook in user_playbooks
714
+ if int(getattr(playbook, "user_playbook_id", 0) or 0)
715
+ not in source_user_playbook_ids
716
+ ]
717
+ suppressed_count = len(user_playbooks) - len(filtered)
718
+ if suppressed_count:
719
+ with profile_step(
720
+ "search.suppress_source_user_playbooks",
721
+ suppressed_count=suppressed_count,
722
+ source_user_playbook_count=len(source_user_playbook_ids),
723
+ ):
724
+ pass
725
+ return filtered
388
726
 
389
727
 
390
728
  def _get_cached_query_embedding(
@@ -11,7 +11,7 @@ Description: Global configuration manager for site-wide variables and per-org fe
11
11
 
12
12
  ## Purpose
13
13
 
14
- 1. **Global settings** - Model names, embedding models
14
+ 1. **Global settings** - Model names, embedding models, and retrieval defaults
15
15
  2. **Feature flags** - Per-org feature gating (global enable or per-org allowlist)
16
16
  3. **Dual storage** - File-based with optional Redis caching
17
17
  4. **Auto-fallback** - Redis → File system graceful degradation
@@ -62,6 +62,7 @@ site_var/
62
62
  └── site_var_sources/
63
63
  ├── app_config.json # JSON → parsed dict
64
64
  ├── model_config.json
65
+ ├── search_settings.json # Default retrieval mode and hybrid search weights
65
66
  └── feature_flags.json # Feature flag config (per-flag enable + org allowlist)
66
67
  ```
67
68
 
@@ -55,7 +55,7 @@ def is_feature_enabled(org_id: str, feature_name: str) -> bool:
55
55
  if feature_config.get("enabled", False):
56
56
  return True
57
57
 
58
- enabled_org_ids = feature_config.get("enabled_org_ids", [])
58
+ enabled_org_ids = feature_config.get("enabled_org_ids", []) or []
59
59
  return org_id in enabled_org_ids
60
60
 
61
61
 
@@ -103,6 +103,103 @@ def is_deduplicator_enabled(org_id: str) -> bool:
103
103
  return is_feature_enabled(org_id, "deduplicator")
104
104
 
105
105
 
106
+ def _is_fail_closed_flag_enabled(org_id: str, feature_key: str) -> bool:
107
+ """
108
+ Shared fail-closed helper: returns False if key absent or config malformed.
109
+
110
+ Unlike is_feature_enabled (fail-open), this function returns False when the
111
+ feature key is absent or when its value is not a dict. This is the safety
112
+ invariant for soft-delete flags — unconfigured or malformed entries must
113
+ never accidentally activate tombstone-producing paths.
114
+
115
+ A feature is enabled if:
116
+ - The feature's "enabled" field is True (globally enabled), OR
117
+ - The org_id is in the feature's "enabled_org_ids" list.
118
+
119
+ Args:
120
+ org_id (str): The organization ID to check
121
+ feature_key (str): The feature flag key in the config dict
122
+
123
+ Returns:
124
+ bool: True only if the feature is explicitly enabled for this org
125
+ """
126
+ config = _get_feature_flags_config()
127
+ feature_config = config.get(feature_key)
128
+
129
+ if feature_config is None:
130
+ # Key absent — fail-CLOSED
131
+ return False
132
+
133
+ if not isinstance(feature_config, dict):
134
+ logger.warning(
135
+ "feature_flags[%s] is not a dict (got %s), defaulting to OFF",
136
+ feature_key,
137
+ type(feature_config).__name__,
138
+ )
139
+ return False
140
+
141
+ # Strict bool identity — truthy strings like "false" must not enable (#195).
142
+ enabled = feature_config.get("enabled", False)
143
+ if enabled is True:
144
+ return True
145
+
146
+ # Reject non-list values — a string does substring `in` match, not membership (#195).
147
+ org_ids = feature_config.get("enabled_org_ids", [])
148
+ if not isinstance(org_ids, list):
149
+ return False
150
+ return org_id in org_ids
151
+
152
+
153
+ def _is_default_open_flag_enabled(org_id: str, feature_key: str) -> bool:
154
+ """
155
+ Shared default-open helper for soft-delete flags.
156
+
157
+ Returns True when the feature key is absent from config (default ON), but
158
+ preserves all explicit-disable and per-org override semantics:
159
+ - Key absent or None → True (default ON — GC is also ON by default).
160
+ - Key present but malformed (not a dict) → False with a warning (safe fallback).
161
+ - Key present, enabled=True → True for all orgs.
162
+ - Key present, enabled=False, org in enabled_org_ids → True.
163
+ - Key present, enabled=False, org NOT in enabled_org_ids → False (explicit disable).
164
+
165
+ Strict-bool and strict-list guards from _is_fail_closed_flag_enabled are
166
+ preserved: truthy strings and non-bool ints do NOT enable, and a string
167
+ enabled_org_ids does NOT match via substring (anti-#195).
168
+
169
+ Args:
170
+ org_id (str): The organization ID to check
171
+ feature_key (str): The feature flag key in the config dict
172
+
173
+ Returns:
174
+ bool: True when the flag is on (including when absent/unconfigured)
175
+ """
176
+ config = _get_feature_flags_config()
177
+ feature_config = config.get(feature_key)
178
+
179
+ if feature_config is None:
180
+ # Key absent — default OPEN (soft-delete is on by default; GC runs too).
181
+ return True
182
+
183
+ if not isinstance(feature_config, dict):
184
+ logger.warning(
185
+ "feature_flags[%s] is not a dict (got %s), defaulting to OFF",
186
+ feature_key,
187
+ type(feature_config).__name__,
188
+ )
189
+ return False
190
+
191
+ # Strict bool identity — truthy strings like "false" must not enable (#195).
192
+ enabled = feature_config.get("enabled", False)
193
+ if enabled is True:
194
+ return True
195
+
196
+ # Reject non-list values — a string does substring `in` match, not membership (#195).
197
+ org_ids = feature_config.get("enabled_org_ids", [])
198
+ if not isinstance(org_ids, list):
199
+ return False
200
+ return org_id in org_ids
201
+
202
+
106
203
  def is_resumable_extraction_agent_enabled(org_id: str) -> bool:
107
204
  """
108
205
  Convenience check for whether classic extraction should use the resumable agent.
@@ -114,3 +211,25 @@ def is_resumable_extraction_agent_enabled(org_id: str) -> bool:
114
211
  bool: True if the resumable extraction agent is enabled
115
212
  """
116
213
  return is_feature_enabled(org_id, "resumable_extraction_agent")
214
+
215
+
216
+ def is_lineage_dual_read_diff_enabled(org_id: str) -> bool:
217
+ """
218
+ Check if the lineage dual-read diff path is enabled for a given organization.
219
+
220
+ Defaults to DISABLED when the key is absent from config (default-CLOSED). This
221
+ flag gates an experimental read path that compares new and old lineage query
222
+ results side-by-side; it must never activate accidentally on unconfigured
223
+ deployments.
224
+
225
+ A feature is enabled only if:
226
+ - The feature's "enabled" field is True (globally enabled), OR
227
+ - The org_id is in the feature's "enabled_org_ids" list.
228
+
229
+ Args:
230
+ org_id (str): The organization ID to check
231
+
232
+ Returns:
233
+ bool: True only if the feature is explicitly enabled for this org
234
+ """
235
+ return _is_fail_closed_flag_enabled(org_id, "lineage_dual_read_diff")
@@ -9,5 +9,9 @@
9
9
  "resumable_extraction_agent": {
10
10
  "enabled": true,
11
11
  "enabled_org_ids": []
12
+ },
13
+ "lineage_dual_read_diff": {
14
+ "enabled": true,
15
+ "enabled_org_ids": []
12
16
  }
13
17
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "search_mode": "hybrid",
3
+ "vector_weight": 1.0,
4
+ "fts_weight": 1.0
5
+ }