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
@@ -0,0 +1,323 @@
1
+ """Per-session JSONL buffer for interactions awaiting publish to reflexio.
2
+
3
+ Each openClaw session gets one file at
4
+ ``~/.openclaw-smart/sessions/{session_id}.jsonl``. Lines are one of:
5
+
6
+ - ``{"role": "User", ...}`` — a user turn (see InteractionData fields)
7
+ - ``{"role": "Assistant", ...}`` — a finalized assistant turn
8
+ - ``{"role": "Assistant_tool", ...}`` — a single tool invocation, attached
9
+ to the next assistant turn at ``agent_end`` time
10
+ - ``{"published_up_to": N}`` — high-water mark so ``agent_end`` /
11
+ ``session_end`` don't re-publish rows already sent to reflexio
12
+
13
+ The buffer exists for offline resilience: when reflexio is unreachable,
14
+ ``agent_end`` appends without publishing and the next successful hook drains.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ import os
22
+ import re
23
+ from collections.abc import Iterable
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ try:
28
+ import fcntl # POSIX only — Windows hooks fall back to append-without-lock.
29
+ except ImportError: # pragma: no cover — non-POSIX platforms
30
+ fcntl = None # type: ignore[assignment]
31
+
32
+ _LOGGER = logging.getLogger(__name__)
33
+
34
+ _ENV_STATE_DIR = "OPENCLAW_SMART_STATE_DIR"
35
+ _DEFAULT_STATE_DIR = Path.home() / ".openclaw-smart" / "sessions"
36
+
37
+ _TOOL_DATA_FIELD_MAX_LEN = 256
38
+
39
+ _VALID_CITATION_KINDS = frozenset({"playbook", "profile"})
40
+
41
+
42
+ def _truncate_tool_data_field(value: Any) -> Any:
43
+ """Truncate a single tool_data field value to ``_TOOL_DATA_FIELD_MAX_LEN``.
44
+
45
+ Only *top-level string* values are shortened. Nested containers
46
+ (dicts, lists) and non-string scalars pass through unchanged, even if
47
+ the container holds overlong strings — extractor prompts built from
48
+ this payload are bounded upstream by reflexio, and truncating a mid-
49
+ structure string risks producing invalid JSON when the caller later
50
+ serializes. The cap keeps long fields (``Edit.old_string`` /
51
+ ``new_string`` diffs, multi-line ``Bash`` scripts) from inflating the
52
+ extractor's input; short fields like file paths, URLs, and typical
53
+ commands stay intact.
54
+ """
55
+ if isinstance(value, str) and len(value) > _TOOL_DATA_FIELD_MAX_LEN:
56
+ return value[:_TOOL_DATA_FIELD_MAX_LEN]
57
+ return value
58
+
59
+
60
+ def state_dir() -> Path:
61
+ """Root directory for session JSONL files. Honours ``OPENCLAW_SMART_STATE_DIR``."""
62
+ override = os.environ.get(_ENV_STATE_DIR)
63
+ return Path(override) if override else _DEFAULT_STATE_DIR
64
+
65
+
66
+ # openClaw session ids are opaque tokens. The real shape we see in
67
+ # production is ``agent:<id>:<run>`` (e.g. ``agent:main:main``) — colons
68
+ # are part of the protocol, not a path separator on POSIX, so they're
69
+ # safe in filesystem paths. Restrict to a conservative
70
+ # alphanumeric + dot/underscore/hyphen/colon set so a crafted value
71
+ # with `/` or `\` path separators or shell metacharacters can't write or
72
+ # read outside ``state_dir()``. Any session id outside this charset is
73
+ # treated as malformed and the resulting filesystem op becomes a no-op
74
+ # upstream (see ``append`` / ``read_all``).
75
+ #
76
+ # We additionally reject ids that are pure-dot tokens (``.``, ``..``) or
77
+ # contain a ``..`` substring even though the underlying regex would
78
+ # accept them — those are not valid filenames and signal an escape
79
+ # attempt.
80
+ _SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$")
81
+
82
+
83
+ def _safe_session_id(session_id: str) -> str | None:
84
+ """Return ``session_id`` iff it matches the safe-id charset, else ``None``.
85
+
86
+ Args:
87
+ session_id (str): Caller-supplied session key.
88
+
89
+ Returns:
90
+ str | None: The session id unchanged when safe, ``None`` otherwise.
91
+ """
92
+ if not isinstance(session_id, str):
93
+ return None
94
+ if not _SESSION_ID_RE.fullmatch(session_id):
95
+ return None
96
+ if ".." in session_id:
97
+ return None
98
+ if set(session_id) == {"."}:
99
+ return None
100
+ return session_id
101
+
102
+
103
+ def session_path(session_id: str) -> Path | None:
104
+ """Return the JSONL path for a given session id, or ``None`` if unsafe."""
105
+ sid = _safe_session_id(session_id)
106
+ if sid is None:
107
+ return None
108
+ return state_dir() / f"{sid}.jsonl"
109
+
110
+
111
+ def injected_path(session_id: str) -> Path | None:
112
+ """Return the per-session oc-cite registry path, or ``None`` if unsafe."""
113
+ sid = _safe_session_id(session_id)
114
+ if sid is None:
115
+ return None
116
+ return state_dir() / f"{sid}.injected.jsonl"
117
+
118
+
119
+ def publish_lock_path(session_id: str) -> Path | None:
120
+ """Return the per-session publish lock path, or ``None`` if unsafe."""
121
+ sid = _safe_session_id(session_id)
122
+ if sid is None:
123
+ return None
124
+ return state_dir() / f"{sid}.publish.lock"
125
+
126
+
127
+ def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None:
128
+ """Append citation-registry entries to the per-session injected-items file.
129
+
130
+ Each entry maps a short ``id`` (4-hex-char) back to the skill or
131
+ preference it came from so the ``agent_end`` hook can resolve ids cited
132
+ by openClaw via ``oc-cite`` into human-readable titles for the
133
+ dashboard. Silently no-ops when ``entries`` is empty.
134
+ """
135
+ records = list(entries)
136
+ if not records:
137
+ return
138
+ path = injected_path(session_id)
139
+ if path is None:
140
+ _LOGGER.warning(
141
+ "rejected unsafe session_id for append_injected: %r", session_id
142
+ )
143
+ return
144
+ path.parent.mkdir(parents=True, exist_ok=True)
145
+ with path.open("a", encoding="utf-8") as fh:
146
+ if fcntl is not None:
147
+ try:
148
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
149
+ except OSError as exc:
150
+ _LOGGER.debug("flock failed on %s: %s", path, exc)
151
+ for rec in records:
152
+ fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
153
+
154
+
155
+ def read_injected(session_id: str) -> dict[str, dict[str, Any]]:
156
+ """Return the per-session citation registry keyed by id.
157
+
158
+ Later entries win when the same id was injected multiple times
159
+ (identical content produces the same hash-derived id, so the extra
160
+ record only refreshes metadata).
161
+ """
162
+ path = injected_path(session_id)
163
+ if path is None or not path.exists():
164
+ return {}
165
+ registry: dict[str, dict[str, Any]] = {}
166
+ with path.open("r", encoding="utf-8") as fh:
167
+ for line in fh:
168
+ line = line.strip()
169
+ if not line:
170
+ continue
171
+ try:
172
+ entry = json.loads(line)
173
+ except json.JSONDecodeError as exc:
174
+ _LOGGER.warning("Skipping malformed injected line in %s: %s", path, exc)
175
+ continue
176
+ item_id = entry.get("id")
177
+ if isinstance(item_id, str) and item_id:
178
+ registry[item_id] = entry
179
+ return registry
180
+
181
+
182
+ def append(session_id: str, record: dict[str, Any]) -> None:
183
+ """Append one JSON record to the session buffer. Creates the dir if needed.
184
+
185
+ Holds an exclusive ``flock`` on the buffer file across the write so
186
+ concurrent hooks (e.g. parallel ``after_tool_call`` fires) cannot
187
+ interleave JSON lines when a payload exceeds the buffered-writer's
188
+ flush size.
189
+ """
190
+ path = session_path(session_id)
191
+ if path is None:
192
+ _LOGGER.warning("rejected unsafe session_id for append: %r", session_id)
193
+ return
194
+ path.parent.mkdir(parents=True, exist_ok=True)
195
+ line = json.dumps(record, ensure_ascii=False) + "\n"
196
+ with path.open("a", encoding="utf-8") as fh:
197
+ if fcntl is not None:
198
+ try:
199
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
200
+ except OSError as exc:
201
+ _LOGGER.debug("flock failed on %s: %s", path, exc)
202
+ fh.write(line)
203
+
204
+
205
+ def read_all(session_id: str) -> list[dict[str, Any]]:
206
+ """Return every record in the buffer as a list of dicts. Missing file → []."""
207
+ path = session_path(session_id)
208
+ if path is None or not path.exists():
209
+ return []
210
+ records: list[dict[str, Any]] = []
211
+ with path.open("r", encoding="utf-8") as fh:
212
+ for line in fh:
213
+ line = line.strip()
214
+ if not line:
215
+ continue
216
+ try:
217
+ records.append(json.loads(line))
218
+ except json.JSONDecodeError as exc:
219
+ _LOGGER.warning("Skipping malformed buffer line in %s: %s", path, exc)
220
+ return records
221
+
222
+
223
+ def _to_wire_citations(cited_items: Any) -> list[dict[str, str]]:
224
+ """Map local ``cited_items`` to the wire ``Citation`` shape.
225
+
226
+ Local entries (from ``events.agent_end._resolve_cited_items``) carry
227
+ ``{id, kind, title, real_id}``; reflexio's ``InteractionData.citations``
228
+ wants ``{kind, real_id, tag, title}`` where ``tag`` is the rank id
229
+ (``s1-301``-style) we already keep under ``id``. Entries without a
230
+ ``real_id`` (unresolved injections) are dropped — the server can't
231
+ join them back to a stored row.
232
+ """
233
+ if not isinstance(cited_items, list):
234
+ return []
235
+ out: list[dict[str, str]] = []
236
+ for item in cited_items:
237
+ if not isinstance(item, dict):
238
+ continue
239
+ real_id = item.get("real_id")
240
+ kind = item.get("kind")
241
+ if not isinstance(real_id, str) or not real_id:
242
+ continue
243
+ if kind not in _VALID_CITATION_KINDS:
244
+ continue
245
+ tag = item.get("id")
246
+ title = item.get("title")
247
+ out.append(
248
+ {
249
+ "kind": kind,
250
+ "real_id": real_id,
251
+ "tag": tag if isinstance(tag, str) else "",
252
+ "title": title if isinstance(title, str) else "",
253
+ }
254
+ )
255
+ return out
256
+
257
+
258
+ def unpublished_slice(
259
+ records: Iterable[dict[str, Any]],
260
+ ) -> tuple[int, list[dict[str, Any]]]:
261
+ """Split records into (last-published index, unpublished turn records).
262
+
263
+ Walks the records in order, tracking the most recent ``published_up_to``
264
+ marker and collecting turn records (anything with a ``role``) that come
265
+ after it. Tool records are folded into the closest following Assistant
266
+ turn's ``tools_used``.
267
+
268
+ Returns:
269
+ tuple[int, list[dict]]: ``(published_up_to, interactions)``. The
270
+ integer is the watermark after which all turns are unpublished;
271
+ the list is formatted for ``InteractionData`` construction.
272
+ """
273
+ published = 0
274
+ pending_tools: list[dict[str, Any]] = []
275
+ turns: list[dict[str, Any]] = []
276
+ for idx, rec in enumerate(records):
277
+ if "published_up_to" in rec:
278
+ marker = rec.get("published_up_to")
279
+ # Clamp to the number of records seen before this marker (``idx``).
280
+ # An over-range marker (e.g. from a tampered/corrupt buffer) would
281
+ # otherwise skip every subsequent turn via the ``idx < published``
282
+ # gate below, silently dropping valid unpublished records.
283
+ if isinstance(marker, int) and 0 <= marker <= idx:
284
+ published = marker
285
+ pending_tools = []
286
+ turns = []
287
+ continue
288
+ if idx < published:
289
+ continue
290
+ role = rec.get("role")
291
+ if role == "Assistant_tool":
292
+ raw_tool_input = rec.get("tool_input")
293
+ tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
294
+ tool_output = rec.get("tool_output") or ""
295
+ tool_entry: dict[str, Any] = {
296
+ "tool_name": rec.get("tool_name", ""),
297
+ "status": rec.get("status", "success"),
298
+ }
299
+ tool_data: dict[str, Any] = {}
300
+ if tool_input:
301
+ tool_data["input"] = {
302
+ k: _truncate_tool_data_field(v) for k, v in tool_input.items()
303
+ }
304
+ if tool_output:
305
+ tool_data["output"] = _truncate_tool_data_field(tool_output)
306
+ if tool_data:
307
+ tool_entry["tool_data"] = tool_data
308
+ pending_tools.append(tool_entry)
309
+ continue
310
+ if role in {"User", "Assistant"}:
311
+ turn = {
312
+ k: v for k, v in rec.items() if k not in {"role", "ts", "cited_items"}
313
+ }
314
+ turn["role"] = role
315
+ if role == "Assistant":
316
+ citations = _to_wire_citations(rec.get("cited_items"))
317
+ if citations:
318
+ turn["citations"] = citations
319
+ if pending_tools:
320
+ turn["tools_used"] = pending_tools
321
+ pending_tools = []
322
+ turns.append(turn)
323
+ return published, turns
@@ -0,0 +1,190 @@
1
+ """End-to-end: drive all 6 hooks with synthetic payloads, assert buffer state.
2
+
3
+ Exercises the hook dispatcher → event-handler → state buffer pipeline in
4
+ order, the same sequence openClaw would trigger during a real session. The
5
+ reflexio adapter is patched to a no-op so the test never depends on a live
6
+ backend; failures inside ``publish`` would otherwise be tolerated silently
7
+ (per ``reflexio_adapter.Adapter.publish``) but explicit patching keeps the
8
+ test deterministic across CI runs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import json
15
+ from pathlib import Path
16
+ from unittest.mock import MagicMock
17
+
18
+ import pytest
19
+
20
+ pytestmark = pytest.mark.e2e
21
+
22
+
23
+ @pytest.fixture
24
+ def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
25
+ """Force state.py to use a tmp directory so the test never touches ~/.openclaw-smart/."""
26
+ state_dir = tmp_path / "sessions"
27
+ monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir))
28
+ return state_dir
29
+
30
+
31
+ @pytest.fixture
32
+ def stub_adapter(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
33
+ """Replace every imported ``Adapter`` symbol with a MagicMock that returns success.
34
+
35
+ Each consumer module does ``from openclaw_smart.reflexio_adapter import Adapter``
36
+ at import time, so patching the source module alone leaves stale bindings in
37
+ consumers. We patch the consumer-side aliases as well so the test is
38
+ isolated regardless of import order.
39
+ """
40
+ from openclaw_smart import ( # noqa: F401 — force-import the consumers
41
+ context_inject,
42
+ publish,
43
+ reflexio_adapter,
44
+ )
45
+ from openclaw_smart.events import session_start
46
+
47
+ fake = MagicMock()
48
+ fake.publish.return_value = True
49
+ fake.apply_extraction_defaults.return_value = True
50
+ fake.apply_optimizer_defaults.return_value = True
51
+ fake.fetch_stall_state.return_value = None
52
+ fake.search_all.return_value = ([], [], [])
53
+
54
+ factory = lambda *a, **kw: fake # noqa: E731 — single-line lambda is clearer here
55
+ monkeypatch.setattr(reflexio_adapter, "Adapter", factory)
56
+ monkeypatch.setattr(publish, "Adapter", factory)
57
+ monkeypatch.setattr(context_inject, "Adapter", factory)
58
+ monkeypatch.setattr(session_start, "Adapter", factory)
59
+ return fake
60
+
61
+
62
+ def _drive(monkeypatch: pytest.MonkeyPatch, event: str, payload: dict) -> int:
63
+ from openclaw_smart import hook
64
+
65
+ monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload)))
66
+ return hook.main(["openclaw", event])
67
+
68
+
69
+ def test_full_session_loop_records_all_roles(
70
+ monkeypatch: pytest.MonkeyPatch,
71
+ isolated_state: Path,
72
+ stub_adapter: MagicMock,
73
+ capsys: pytest.CaptureFixture[str],
74
+ ) -> None:
75
+ """Walk the 6 hooks; assert User, Assistant_tool, Assistant records land."""
76
+ from openclaw_smart import state
77
+
78
+ session = "e2e-1"
79
+ project = "test-project"
80
+
81
+ # session-start: pushes defaults + emits a stall banner (or empty stdout).
82
+ assert (
83
+ _drive(
84
+ monkeypatch, "session-start", {"sessionKey": session, "agentId": project}
85
+ )
86
+ == 0
87
+ )
88
+ capsys.readouterr() # drain
89
+
90
+ # before-prompt-build: appends a "User" record.
91
+ assert (
92
+ _drive(
93
+ monkeypatch,
94
+ "before-prompt-build",
95
+ {
96
+ "sessionKey": session,
97
+ "prompt": "implement OAuth flow",
98
+ "agentId": project,
99
+ },
100
+ )
101
+ == 0
102
+ )
103
+ capsys.readouterr()
104
+
105
+ # before-tool-call: observe-only stub; no record, no stdout.
106
+ assert (
107
+ _drive(
108
+ monkeypatch,
109
+ "before-tool-call",
110
+ {
111
+ "sessionKey": session,
112
+ "toolName": "Edit",
113
+ "params": {"file_path": "x.py"},
114
+ "agentId": project,
115
+ },
116
+ )
117
+ == 0
118
+ )
119
+ assert capsys.readouterr().out.strip() == ""
120
+
121
+ # after-tool-call: appends an "Assistant_tool" record (camelCase translated).
122
+ assert (
123
+ _drive(
124
+ monkeypatch,
125
+ "after-tool-call",
126
+ {
127
+ "sessionKey": session,
128
+ "toolName": "Edit",
129
+ "params": {"file_path": "x.py", "new_string": "..."},
130
+ "result": "wrote 10 lines",
131
+ "agentId": project,
132
+ },
133
+ )
134
+ == 0
135
+ )
136
+ capsys.readouterr()
137
+
138
+ # agent-end: extracts the assistant text and appends an "Assistant" record.
139
+ assert (
140
+ _drive(
141
+ monkeypatch,
142
+ "agent-end",
143
+ {
144
+ "sessionKey": session,
145
+ "agentId": project,
146
+ "messages": [
147
+ {"role": "user", "content": "implement OAuth flow"},
148
+ {"role": "assistant", "content": "Done."},
149
+ ],
150
+ },
151
+ )
152
+ == 0
153
+ )
154
+ capsys.readouterr()
155
+
156
+ # session-end: force-publish; should not crash.
157
+ assert (
158
+ _drive(monkeypatch, "session-end", {"sessionKey": session, "agentId": project})
159
+ == 0
160
+ )
161
+ capsys.readouterr()
162
+
163
+ records = state.read_all(session)
164
+ roles = [r.get("role") for r in records if "role" in r]
165
+ assert "User" in roles
166
+ assert "Assistant_tool" in roles
167
+ assert "Assistant" in roles
168
+
169
+ # session_end publishes; we asserted publish was called via the stub.
170
+ assert stub_adapter.publish.called
171
+
172
+ # The agent_end / session_end path writes a published_up_to watermark
173
+ # once a publish succeeds. The stub returns True, so we should see it.
174
+ assert any("published_up_to" in r for r in records), (
175
+ "expected published_up_to watermark after session_end"
176
+ )
177
+
178
+
179
+ def test_unknown_event_is_silent(
180
+ monkeypatch: pytest.MonkeyPatch,
181
+ isolated_state: Path,
182
+ capsys: pytest.CaptureFixture[str],
183
+ ) -> None:
184
+ """An unrecognized hook name short-circuits with empty stdout, never crashes."""
185
+ monkeypatch.setattr("sys.stdin", io.StringIO("{}"))
186
+ from openclaw_smart import hook
187
+
188
+ rc = hook.main(["openclaw", "this-does-not-exist"])
189
+ assert rc == 0
190
+ assert capsys.readouterr().out.strip() == ""
@@ -0,0 +1,112 @@
1
+ """Integration: publish buffered turns to a local SQLite-backed reflexio.
2
+
3
+ Requires a reflexio backend reachable at ``REFLEXIO_URL`` (default
4
+ ``http://localhost:8061/``). Skipped automatically when the backend is
5
+ unreachable so the suite is portable across machines.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ pytestmark = pytest.mark.integration
16
+
17
+
18
+ def _reflexio_url() -> str:
19
+ return os.environ.get("REFLEXIO_URL", "http://localhost:8061/")
20
+
21
+
22
+ def _backend_alive(url: str) -> bool:
23
+ """True iff ``GET <url>health`` returns 200 within a short window."""
24
+ import urllib.error
25
+ import urllib.request
26
+
27
+ health = url.rstrip("/") + "/health"
28
+ try:
29
+ with urllib.request.urlopen(health, timeout=1.5) as resp: # nosec — local only
30
+ return resp.status == 200
31
+ except (urllib.error.URLError, TimeoutError, OSError):
32
+ return False
33
+
34
+
35
+ @pytest.fixture
36
+ def require_backend() -> None:
37
+ if not _backend_alive(_reflexio_url()):
38
+ pytest.skip(f"reflexio backend not reachable at {_reflexio_url()}")
39
+
40
+
41
+ @pytest.fixture
42
+ def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
43
+ """Force state.py to write under tmp_path so the test doesn't leak fixtures."""
44
+ state_dir = tmp_path / "sessions"
45
+ monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir))
46
+ return state_dir
47
+
48
+
49
+ def test_publish_lands_in_reflexio_storage(
50
+ require_backend: None,
51
+ monkeypatch: pytest.MonkeyPatch,
52
+ isolated_state: Path,
53
+ worker_id: str = "test",
54
+ ) -> None:
55
+ """Append two turns to the JSONL buffer → publish → assert success status + count."""
56
+ from openclaw_smart import state
57
+ from openclaw_smart.publish import publish_unpublished
58
+
59
+ session_id = f"oc-int-{worker_id}"
60
+ project_id = f"oc-test-proj-{worker_id}"
61
+
62
+ state.append(session_id, {"role": "User", "content": "implement auth"})
63
+ state.append(
64
+ session_id,
65
+ {"role": "Assistant", "content": "Wrote login/logout endpoints."},
66
+ )
67
+
68
+ status, count = publish_unpublished(
69
+ session_id=session_id,
70
+ project_id=project_id,
71
+ force_extraction=False,
72
+ skip_aggregation=False,
73
+ )
74
+
75
+ assert status == "ok", f"publish returned {status!r} — check backend logs"
76
+ assert count == 2
77
+
78
+ # The watermark is now stamped — a second publish with no new turns is a no-op.
79
+ status2, count2 = publish_unpublished(
80
+ session_id=session_id,
81
+ project_id=project_id,
82
+ force_extraction=False,
83
+ skip_aggregation=False,
84
+ )
85
+ assert (status2, count2) == ("nothing", 0)
86
+
87
+
88
+ def test_publish_failed_when_backend_url_invalid(
89
+ monkeypatch: pytest.MonkeyPatch, isolated_state: Path
90
+ ) -> None:
91
+ """Pointing the adapter at a dead port surfaces 'failed' without stamping."""
92
+ from openclaw_smart import state
93
+ from openclaw_smart.publish import publish_unpublished
94
+ from openclaw_smart.reflexio_adapter import Adapter
95
+
96
+ # Pick a port that's almost certainly closed.
97
+ bogus = Adapter(url="http://127.0.0.1:9/")
98
+ session_id = "oc-int-bogus"
99
+ state.append(session_id, {"role": "User", "content": "hi"})
100
+
101
+ status, count = publish_unpublished(
102
+ session_id=session_id,
103
+ project_id="proj-bogus",
104
+ force_extraction=False,
105
+ skip_aggregation=False,
106
+ adapter=bogus,
107
+ )
108
+ assert status == "failed"
109
+ assert count == 1
110
+ # No watermark stamped — a retry would re-publish the same record.
111
+ records = state.read_all(session_id)
112
+ assert not any("published_up_to" in r for r in records)