claude-smart 0.2.45 → 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 (322) 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 +5 -1
  35. package/plugin/scripts/backend-service.sh +22 -2
  36. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  37. package/plugin/scripts/hook_entry.sh +1 -1
  38. package/plugin/scripts/opencode-claude-compat +4 -0
  39. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  40. package/plugin/scripts/opencode-claude-compat.js +225 -0
  41. package/plugin/scripts/smart-install.sh +10 -8
  42. package/plugin/src/README.md +1 -1
  43. package/plugin/src/claude_smart/cli.py +304 -6
  44. package/plugin/src/claude_smart/env_config.py +12 -4
  45. package/plugin/src/claude_smart/events/session_start.py +26 -7
  46. package/plugin/src/claude_smart/events/stop.py +1 -1
  47. package/plugin/src/claude_smart/ids.py +1 -1
  48. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  49. package/plugin/src/claude_smart/runtime.py +7 -1
  50. package/plugin/uv.lock +1 -1
  51. package/plugin/vendor/reflexio/.env.example +54 -30
  52. package/plugin/vendor/reflexio/README.md +14 -8
  53. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  54. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  56. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  57. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  65. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  66. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  67. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  68. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  141. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  142. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  143. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  144. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  146. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  147. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  148. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  149. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  158. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  159. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  160. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  161. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  162. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  163. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  164. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  165. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  167. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  168. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  169. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  170. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  171. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  172. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  173. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  174. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  176. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  178. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  179. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  197. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  198. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  208. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  209. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  211. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  216. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  221. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  222. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  238. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  244. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  248. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  249. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  250. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  252. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  281. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  285. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  286. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  287. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  289. package/scripts/setup-claude-smart.sh +8 -3
  290. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  317. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  318. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import logging
4
4
  import sqlite3
5
+ import uuid
5
6
  from concurrent.futures import ThreadPoolExecutor
6
7
  from typing import Any
7
8
 
@@ -24,6 +25,7 @@ from reflexio.server.llm.providers.embedding_service_provider import (
24
25
  )
25
26
 
26
27
  from ._base import (
28
+ _TOMBSTONE_STATUS_VALUES,
27
29
  SQLiteStorageBase,
28
30
  _build_status_sql,
29
31
  _effective_search_mode,
@@ -37,6 +39,40 @@ from ._base import (
37
39
  _true_rrf_merge,
38
40
  _vector_rank_rows,
39
41
  )
42
+ from ._lineage import _GC_ELIGIBLE_STATUSES, _append_event_stmt
43
+
44
+
45
+ def _emit_hard_delete_profile(
46
+ conn: sqlite3.Connection,
47
+ *,
48
+ org_id: str,
49
+ entity_id: str,
50
+ request_id: str,
51
+ actor: str = "api",
52
+ ) -> None:
53
+ """Emit a single hard_delete lineage event for a profile entity."""
54
+ _append_event_stmt(
55
+ conn,
56
+ org_id=org_id,
57
+ entity_type="profile",
58
+ entity_id=entity_id,
59
+ op="hard_delete",
60
+ prov="wasInvalidatedBy",
61
+ source_ids=[],
62
+ actor=actor,
63
+ request_id=request_id,
64
+ reason="erasure",
65
+ )
66
+
67
+
68
+ def _build_tags_sql(alias: str, tags: list[str] | None) -> tuple[str, list[Any]]:
69
+ if not tags:
70
+ return "", []
71
+ placeholders = ",".join("?" for _ in tags)
72
+ return (
73
+ f"EXISTS (SELECT 1 FROM json_each({alias}.tags) WHERE value IN ({placeholders}))",
74
+ list(tags),
75
+ )
40
76
 
41
77
 
42
78
  class ProfileMixin:
@@ -45,6 +81,7 @@ class ProfileMixin:
45
81
  # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
46
82
  _lock: Any
47
83
  conn: sqlite3.Connection
84
+ org_id: str
48
85
  _execute: Any
49
86
  _fetchone: Any
50
87
  _fetchall: Any
@@ -57,6 +94,8 @@ class ProfileMixin:
57
94
  _fts_delete_profile: Any
58
95
  _vec_upsert: Any
59
96
  _vec_delete: Any
97
+ _delete_profile_search_rows: Any
98
+ _delete_in_chunks: Any
60
99
  _has_sqlite_vec: bool
61
100
  llm_client: Any
62
101
  embedding_model_name: str
@@ -84,13 +123,19 @@ class ProfileMixin:
84
123
  self,
85
124
  user_id: str,
86
125
  status_filter: list[Status | None] | None = None,
126
+ tags: list[str] | None = None,
87
127
  ) -> list[UserProfile]:
88
128
  if status_filter is None:
89
129
  status_filter = [None]
90
130
  current_ts = _epoch_now()
91
131
  frag, params = _build_status_sql(status_filter)
92
- sql = f"SELECT * FROM profiles WHERE user_id = ? AND expiration_timestamp >= ? AND {frag}"
132
+ conditions = ["user_id = ?", "expiration_timestamp >= ?", frag]
93
133
  all_params: list[Any] = [user_id, current_ts, *params]
134
+ tag_frag, tag_params = _build_tags_sql("profiles", tags)
135
+ if tag_frag:
136
+ conditions.append(tag_frag)
137
+ all_params.extend(tag_params)
138
+ sql = f"SELECT * FROM profiles WHERE {' AND '.join(conditions)}"
94
139
  return [_row_to_profile(r) for r in self._fetchall(sql, all_params)]
95
140
 
96
141
  @SQLiteStorageBase.handle_exceptions
@@ -111,9 +156,10 @@ class ProfileMixin:
111
156
  (profile_id, user_id, content, last_modified_timestamp,
112
157
  generated_from_request_id, profile_time_to_live,
113
158
  expiration_timestamp, custom_features, embedding, source,
114
- status, extractor_names, expanded_terms,
115
- source_span, notes, reader_angle, created_at)
116
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
159
+ status, extractor_names, expanded_terms,
160
+ source_span, notes, reader_angle, tags, source_interaction_ids, created_at,
161
+ merged_into, superseded_by)
162
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
117
163
  (
118
164
  profile.profile_id,
119
165
  profile.user_id,
@@ -131,7 +177,11 @@ class ProfileMixin:
131
177
  profile.source_span,
132
178
  profile.notes,
133
179
  profile.reader_angle,
180
+ _json_dumps(profile.tags),
181
+ _json_dumps(profile.source_interaction_ids),
134
182
  _iso_now(),
183
+ profile.merged_into,
184
+ profile.superseded_by,
135
185
  ),
136
186
  )
137
187
  fts_parts = [profile.content or ""]
@@ -152,88 +202,171 @@ class ProfileMixin:
152
202
  def update_user_profile_by_id(
153
203
  self, user_id: str, profile_id: str, new_profile: UserProfile
154
204
  ) -> None:
205
+ """Replace a profile's content in-place and emit a revise lineage event.
206
+
207
+ Each call generates a fresh request_id so every edit is a distinct audit
208
+ event (not collapsed by the idempotency key). The UPDATE, lineage event,
209
+ FTS sync, and vec sync are all executed inside a single lock acquisition;
210
+ self._lock is an RLock so the inner _fts_upsert_profile/_vec_upsert calls
211
+ that re-acquire it are safe.
212
+ """
155
213
  current_ts = _epoch_now()
156
214
  row = self._fetchone(
157
215
  "SELECT profile_id FROM profiles WHERE user_id = ? AND profile_id = ? AND expiration_timestamp >= ?",
158
216
  (user_id, profile_id, current_ts),
159
217
  )
160
218
  if not row:
161
- import logging
162
-
163
- logger = logging.getLogger(__name__)
164
219
  logger.warning("User profile not found for user id: %s", user_id)
165
220
  return
166
221
  embedding = self._get_embedding(
167
222
  "\n".join([new_profile.content, str(new_profile.custom_features)])
168
223
  )
169
224
  new_profile.embedding = embedding
225
+ with self._lock:
226
+ cur = self.conn.execute(
227
+ """UPDATE profiles SET content=?, last_modified_timestamp=?,
228
+ generated_from_request_id=?, profile_time_to_live=?,
229
+ expiration_timestamp=?, custom_features=?, embedding=?,
230
+ source=?, status=?, extractor_names=?, expanded_terms=?,
231
+ source_span=?, notes=?, reader_angle=?, tags=?, source_interaction_ids=?
232
+ WHERE profile_id=?""",
233
+ (
234
+ new_profile.content,
235
+ new_profile.last_modified_timestamp,
236
+ new_profile.generated_from_request_id,
237
+ new_profile.profile_time_to_live.value,
238
+ new_profile.expiration_timestamp,
239
+ _json_dumps(new_profile.custom_features),
240
+ _json_dumps(new_profile.embedding),
241
+ new_profile.source,
242
+ new_profile.status.value if new_profile.status else None,
243
+ _json_dumps(new_profile.extractor_names),
244
+ new_profile.expanded_terms,
245
+ new_profile.source_span,
246
+ new_profile.notes,
247
+ new_profile.reader_angle,
248
+ _json_dumps(new_profile.tags),
249
+ _json_dumps(new_profile.source_interaction_ids),
250
+ profile_id,
251
+ ),
252
+ )
253
+ if cur.rowcount > 0:
254
+ _append_event_stmt(
255
+ self.conn,
256
+ org_id=self.org_id,
257
+ entity_type="profile",
258
+ entity_id=str(profile_id),
259
+ op="revise",
260
+ prov="wasRevisionOf",
261
+ source_ids=[],
262
+ actor="api",
263
+ request_id=uuid.uuid4().hex,
264
+ reason="in-place update",
265
+ )
266
+ self.conn.commit()
267
+ fts_parts = [new_profile.content or ""]
268
+ if new_profile.custom_features:
269
+ fts_parts.extend(
270
+ str(v) for v in new_profile.custom_features.values() if v
271
+ )
272
+ if new_profile.expanded_terms:
273
+ fts_parts.append(new_profile.expanded_terms)
274
+ self._fts_upsert_profile(profile_id, " ".join(fts_parts))
275
+ rowid_row = self._fetchone(
276
+ "SELECT rowid FROM profiles WHERE profile_id = ?", (profile_id,)
277
+ )
278
+ if rowid_row and embedding:
279
+ self._vec_upsert("profiles_vec", rowid_row["rowid"], embedding)
280
+
281
+ @SQLiteStorageBase.handle_exceptions
282
+ def update_user_profile_tags(
283
+ self, user_id: str, profile_id: str, tags: list[str]
284
+ ) -> None:
170
285
  self._execute(
171
- """UPDATE profiles SET content=?, last_modified_timestamp=?,
172
- generated_from_request_id=?, profile_time_to_live=?,
173
- expiration_timestamp=?, custom_features=?, embedding=?,
174
- source=?, status=?, extractor_names=?, expanded_terms=?,
175
- source_span=?, notes=?, reader_angle=?
176
- WHERE profile_id=?""",
177
- (
178
- new_profile.content,
179
- new_profile.last_modified_timestamp,
180
- new_profile.generated_from_request_id,
181
- new_profile.profile_time_to_live.value,
182
- new_profile.expiration_timestamp,
183
- _json_dumps(new_profile.custom_features),
184
- _json_dumps(new_profile.embedding),
185
- new_profile.source,
186
- new_profile.status.value if new_profile.status else None,
187
- _json_dumps(new_profile.extractor_names),
188
- new_profile.expanded_terms,
189
- new_profile.source_span,
190
- new_profile.notes,
191
- new_profile.reader_angle,
192
- profile_id,
193
- ),
286
+ "UPDATE profiles SET tags=? WHERE user_id=? AND profile_id=?",
287
+ (_json_dumps(tags), user_id, profile_id),
194
288
  )
195
- fts_parts = [new_profile.content or ""]
196
- if new_profile.custom_features:
197
- fts_parts.extend(str(v) for v in new_profile.custom_features.values() if v)
198
- if new_profile.expanded_terms:
199
- fts_parts.append(new_profile.expanded_terms)
200
- self._fts_upsert_profile(profile_id, " ".join(fts_parts))
201
- rowid_row = self._fetchone(
202
- "SELECT rowid FROM profiles WHERE profile_id = ?", (profile_id,)
203
- )
204
- if rowid_row and embedding:
205
- self._vec_upsert("profiles_vec", rowid_row["rowid"], embedding)
206
289
 
207
290
  @SQLiteStorageBase.handle_exceptions
208
291
  def delete_user_profile(self, request: DeleteUserProfileRequest) -> None:
209
- rowid_row = self._fetchone(
210
- "SELECT rowid FROM profiles WHERE profile_id = ?",
211
- (request.profile_id,),
212
- )
213
- self._fts_delete_profile(request.profile_id)
214
- if rowid_row:
215
- self._vec_delete("profiles_vec", rowid_row["rowid"])
216
- self._execute(
217
- "DELETE FROM profiles WHERE user_id = ? AND profile_id = ?",
218
- (request.user_id, request.profile_id),
219
- )
292
+ # Atomic: fts + vec + row + lineage in ONE lock/commit to prevent rowid reuse
293
+ # race. profiles uses implicit (reusable) rowid keyed by TEXT PK a cleanup
294
+ # running after commit could race with a concurrent INSERT reusing the freed
295
+ # rowid and delete the NEW profile's vec row. (#196)
296
+ with self._lock:
297
+ rowid_row = self.conn.execute(
298
+ "SELECT rowid FROM profiles WHERE user_id = ? AND profile_id = ?",
299
+ (request.user_id, request.profile_id),
300
+ ).fetchone()
301
+ if rowid_row is None:
302
+ return
303
+ self.conn.execute(
304
+ "DELETE FROM profiles_fts WHERE profile_id = ?",
305
+ (request.profile_id,),
306
+ )
307
+ if self._has_sqlite_vec and rowid_row:
308
+ self.conn.execute(
309
+ "DELETE FROM profiles_vec WHERE rowid = ?",
310
+ (rowid_row["rowid"],),
311
+ )
312
+ cur = self.conn.execute(
313
+ "DELETE FROM profiles WHERE user_id = ? AND profile_id = ?",
314
+ (request.user_id, request.profile_id),
315
+ )
316
+ if cur.rowcount > 0:
317
+ _emit_hard_delete_profile(
318
+ self.conn,
319
+ org_id=self.org_id,
320
+ entity_id=str(request.profile_id),
321
+ request_id=uuid.uuid4().hex,
322
+ )
323
+ self.conn.commit()
220
324
 
221
325
  @SQLiteStorageBase.handle_exceptions
222
326
  def delete_all_profiles_for_user(self, user_id: str) -> None:
223
- pids = [
224
- r["profile_id"]
225
- for r in self._fetchall(
226
- "SELECT profile_id FROM profiles WHERE user_id = ?", (user_id,)
227
- )
228
- ]
229
- for pid in pids:
230
- self._fts_delete_profile(pid)
231
- self._execute("DELETE FROM profiles WHERE user_id = ?", (user_id,))
327
+ # Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
328
+ # prevention (see delete_user_profile comment, #196).
329
+ batch_request_id = uuid.uuid4().hex
330
+ with self._lock:
331
+ rows = self.conn.execute(
332
+ "SELECT rowid, profile_id FROM profiles WHERE user_id = ?", (user_id,)
333
+ ).fetchall()
334
+ if not rows:
335
+ return
336
+ pids = [r["profile_id"] for r in rows]
337
+ rowids = [r["rowid"] for r in rows]
338
+ self._delete_in_chunks("profiles_fts", "profile_id", pids)
339
+ if self._has_sqlite_vec and rowids:
340
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
341
+ self.conn.execute("DELETE FROM profiles WHERE user_id = ?", (user_id,))
342
+ for pid in pids:
343
+ _emit_hard_delete_profile(
344
+ self.conn,
345
+ org_id=self.org_id,
346
+ entity_id=str(pid),
347
+ request_id=batch_request_id,
348
+ )
349
+ self.conn.commit()
232
350
 
233
351
  @SQLiteStorageBase.handle_exceptions
234
352
  def delete_all_profiles(self) -> None:
353
+ # Also wipe profiles_vec (full-wipe variant of the rowid-race fix, #196).
354
+ batch_request_id = uuid.uuid4().hex
235
355
  with self._lock:
356
+ pids = [
357
+ r["profile_id"]
358
+ for r in self.conn.execute("SELECT profile_id FROM profiles").fetchall()
359
+ ]
360
+ for pid in pids:
361
+ _emit_hard_delete_profile(
362
+ self.conn,
363
+ org_id=self.org_id,
364
+ entity_id=str(pid),
365
+ request_id=batch_request_id,
366
+ )
236
367
  self.conn.execute("DELETE FROM profiles_fts")
368
+ if self._has_sqlite_vec:
369
+ self.conn.execute("DELETE FROM profiles_vec")
237
370
  self.conn.execute("DELETE FROM profiles")
238
371
  self.conn.commit()
239
372
 
@@ -251,25 +384,60 @@ class ProfileMixin:
251
384
  ) -> int:
252
385
  new_val = new_status.value if new_status else None
253
386
  now_ts = _epoch_now()
254
- params: list[Any] = [new_val, now_ts]
387
+ old_val_str = old_status.value if old_status else "None"
388
+ new_val_str = new_status.value if new_status else "None"
389
+ reason = f"{old_val_str}->{new_val_str}"
255
390
 
256
391
  if old_status is None or (
257
392
  hasattr(old_status, "value") and old_status.value is None
258
393
  ):
259
394
  where = "status IS NULL"
395
+ select_params: list[Any] = []
260
396
  else:
261
397
  where = "status = ?"
262
- params.append(old_status.value)
398
+ select_params = [old_status.value]
263
399
 
400
+ extra_params: list[Any] = []
264
401
  if user_ids is not None:
265
402
  placeholders = ",".join("?" for _ in user_ids)
266
403
  where += f" AND user_id IN ({placeholders})"
267
- params.extend(user_ids)
404
+ extra_params.extend(user_ids)
268
405
 
269
- cur = self._execute(
270
- f"UPDATE profiles SET status = ?, last_modified_timestamp = ? WHERE {where}",
271
- params,
272
- )
406
+ # Set retired_at = now when transitioning to a GC-eligible status; clear to NULL otherwise.
407
+ retired_at_val = now_ts if new_val in _GC_ELIGIBLE_STATUSES else None
408
+
409
+ batch_request_id = uuid.uuid4().hex
410
+ with self._lock:
411
+ affected = [
412
+ r["profile_id"]
413
+ for r in self.conn.execute(
414
+ f"SELECT profile_id FROM profiles WHERE {where}",
415
+ select_params + extra_params,
416
+ ).fetchall()
417
+ ]
418
+ cur = self.conn.execute(
419
+ f"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? WHERE {where}",
420
+ [new_val, now_ts, retired_at_val] + select_params + extra_params,
421
+ )
422
+ from_val = old_status.value if old_status else None
423
+ to_val = new_status.value if new_status else None
424
+ for pid in affected:
425
+ _append_event_stmt(
426
+ self.conn,
427
+ org_id=self.org_id,
428
+ entity_type="profile",
429
+ entity_id=str(pid),
430
+ op="status_change",
431
+ prov="wasInvalidatedBy",
432
+ source_ids=[],
433
+ actor="api",
434
+ request_id=batch_request_id,
435
+ reason=reason,
436
+ from_status=from_val,
437
+ to_status=to_val,
438
+ status_namespace="lifecycle_status",
439
+ )
440
+ self.conn.commit()
273
441
  return cur.rowcount
274
442
 
275
443
  @SQLiteStorageBase.handle_exceptions
@@ -295,26 +463,206 @@ class ProfileMixin:
295
463
  return [_row_to_profile(r) for r in self._fetchall(sql, params)]
296
464
 
297
465
  @SQLiteStorageBase.handle_exceptions
298
- def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
299
- cur = self._execute(
300
- "UPDATE profiles SET status = ?, last_modified_timestamp = ? "
301
- "WHERE profile_id = ? AND user_id = ? AND status IS NULL",
302
- (Status.ARCHIVED.value, _epoch_now(), profile_id, user_id),
466
+ def get_profile_by_id(
467
+ self, profile_id: str, *, include_tombstones: bool = False
468
+ ) -> UserProfile | None:
469
+ """Fetch a single profile by primary key.
470
+
471
+ Args:
472
+ profile_id: The profile's primary key.
473
+ include_tombstones: When False (default), MERGED/SUPERSEDED profiles
474
+ return None. Set to True for lineage resolution (resolve_current).
475
+
476
+ Returns:
477
+ The UserProfile if found and not filtered, otherwise None.
478
+ """
479
+ sql = "SELECT * FROM profiles WHERE profile_id = ?"
480
+ if not include_tombstones:
481
+ sql += " AND (status IS NULL OR status NOT IN (?, ?))"
482
+ row = self._fetchone(sql, (profile_id, *_TOMBSTONE_STATUS_VALUES))
483
+ else:
484
+ row = self._fetchone(sql, (profile_id,))
485
+ return _row_to_profile(row) if row else None
486
+
487
+ @SQLiteStorageBase.handle_exceptions
488
+ def get_distinct_generated_from_request_ids(self) -> list[str]:
489
+ """Return DISTINCT non-empty generated_from_request_id values, including tombstones.
490
+
491
+ Returns:
492
+ list[str]: Distinct non-empty ``generated_from_request_id`` values.
493
+ """
494
+ rows = self._fetchall(
495
+ "SELECT DISTINCT generated_from_request_id FROM profiles"
496
+ " WHERE generated_from_request_id IS NOT NULL"
497
+ " AND generated_from_request_id != ''",
498
+ (),
499
+ )
500
+ return [row[0] for row in rows]
501
+
502
+ @SQLiteStorageBase.handle_exceptions
503
+ def get_profiles_by_generated_from_request_id(
504
+ self,
505
+ request_id: str,
506
+ ) -> list[UserProfile]:
507
+ """Return all profiles for a generated_from_request_id, including tombstones.
508
+
509
+ Args:
510
+ request_id (str): The generated_from_request_id to filter on.
511
+
512
+ Returns:
513
+ list[UserProfile]: All matching profiles (any status).
514
+ """
515
+ rows = self._fetchall(
516
+ "SELECT * FROM profiles WHERE generated_from_request_id = ?",
517
+ (request_id,),
518
+ )
519
+ return [_row_to_profile(r) for r in rows]
520
+
521
+ def get_all_generated_profiles(self) -> list[UserProfile]:
522
+ """All profiles (any status) with a non-empty generated_from_request_id."""
523
+ rows = self._fetchall(
524
+ "SELECT * FROM profiles "
525
+ "WHERE generated_from_request_id IS NOT NULL "
526
+ "AND generated_from_request_id <> ''",
303
527
  )
528
+ return [_row_to_profile(r) for r in rows]
529
+
530
+ @SQLiteStorageBase.handle_exceptions
531
+ def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
532
+ with self._lock:
533
+ now_ts = _epoch_now()
534
+ cur = self.conn.execute(
535
+ "UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? "
536
+ "WHERE profile_id = ? AND user_id = ? AND status IS NULL",
537
+ (Status.ARCHIVED.value, now_ts, now_ts, profile_id, user_id),
538
+ )
539
+ if cur.rowcount > 0:
540
+ _append_event_stmt(
541
+ self.conn,
542
+ org_id=self.org_id,
543
+ entity_type="profile",
544
+ entity_id=str(profile_id),
545
+ op="status_change",
546
+ prov="wasInvalidatedBy",
547
+ source_ids=[],
548
+ actor="api",
549
+ request_id=uuid.uuid4().hex,
550
+ reason="None->archived",
551
+ from_status=None,
552
+ to_status="archived",
553
+ status_namespace="lifecycle_status",
554
+ )
555
+ self.conn.commit()
304
556
  return cur.rowcount > 0
305
557
 
558
+ @SQLiteStorageBase.handle_exceptions
559
+ def supersede_profiles_by_ids(
560
+ self,
561
+ user_id: str,
562
+ profile_ids: list[str],
563
+ request_id: str,
564
+ ) -> list[str]:
565
+ """Soft-delete profiles by setting status to SUPERSEDED, emitting set-based lineage.
566
+
567
+ For each matching id (user_id scoped, currently CURRENT), updates status to
568
+ SUPERSEDED and emits one ``status_change`` event under the shared ``request_id``.
569
+ Atomic: one ``conn.commit()`` at the end, guarded on rowcount per id.
570
+ FTS/vec rows are NOT removed — reads exclude tombstones by status filter.
571
+
572
+ Args:
573
+ user_id (str): Owning user id.
574
+ profile_ids (list[str]): Profile ids to supersede.
575
+ request_id (str): Shared request id for all emitted lineage events.
576
+
577
+ Returns:
578
+ list[str]: The profile ids actually superseded by this call, in input order
579
+ (already-superseded or absent ids are omitted).
580
+ """
581
+ if not profile_ids:
582
+ return []
583
+ if not request_id:
584
+ raise ValueError("request_id must be non-empty for supersede")
585
+ now_ts = _epoch_now()
586
+ # Eligibility: CURRENT (NULL) or PENDING — the two live statuses dedup can target.
587
+ eligible = (None, Status.PENDING.value)
588
+ committed_ids: list[str] = []
589
+ with self._lock:
590
+ for pid in profile_ids:
591
+ # Read current status for from_status derivation (user_id scoped)
592
+ row = self.conn.execute(
593
+ "SELECT status FROM profiles WHERE profile_id = ? AND user_id = ?",
594
+ (pid, user_id),
595
+ ).fetchone()
596
+ if row is None:
597
+ continue
598
+ old_status_val = (
599
+ row[0] if isinstance(row, (tuple, list)) else row["status"]
600
+ )
601
+ if old_status_val not in eligible:
602
+ continue
603
+ cur = self.conn.execute(
604
+ "UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? "
605
+ "WHERE profile_id = ? AND user_id = ? "
606
+ "AND (status IS NULL OR status = ?)",
607
+ (
608
+ Status.SUPERSEDED.value,
609
+ now_ts,
610
+ now_ts,
611
+ pid,
612
+ user_id,
613
+ Status.PENDING.value,
614
+ ),
615
+ )
616
+ if cur.rowcount > 0:
617
+ _append_event_stmt(
618
+ self.conn,
619
+ org_id=self.org_id,
620
+ entity_type="profile",
621
+ entity_id=str(pid),
622
+ op="status_change",
623
+ prov="wasInvalidatedBy",
624
+ source_ids=[],
625
+ actor="dedup",
626
+ request_id=request_id,
627
+ reason=f"{old_status_val}->superseded",
628
+ from_status=old_status_val,
629
+ to_status=Status.SUPERSEDED.value,
630
+ status_namespace="lifecycle_status",
631
+ )
632
+ committed_ids.append(pid)
633
+ self.conn.commit()
634
+ return committed_ids
635
+
306
636
  @SQLiteStorageBase.handle_exceptions
307
637
  def delete_all_profiles_by_status(self, status: Status) -> int:
308
- # Clean up FTS for profiles being deleted
309
- pids = [
310
- r["profile_id"]
311
- for r in self._fetchall(
312
- "SELECT profile_id FROM profiles WHERE status = ?", (status.value,)
638
+ # Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
639
+ # prevention (see delete_user_profile comment, #196).
640
+ batch_request_id = uuid.uuid4().hex
641
+ with self._lock:
642
+ rows = self.conn.execute(
643
+ "SELECT rowid, profile_id FROM profiles WHERE status = ?",
644
+ (status.value,),
645
+ ).fetchall()
646
+ if not rows:
647
+ return 0
648
+ pids = [r["profile_id"] for r in rows]
649
+ rowids = [r["rowid"] for r in rows]
650
+ self._delete_in_chunks("profiles_fts", "profile_id", pids)
651
+ if self._has_sqlite_vec and rowids:
652
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
653
+ ph = ",".join("?" for _ in pids)
654
+ cur = self.conn.execute(
655
+ f"DELETE FROM profiles WHERE profile_id IN ({ph})",
656
+ pids, # noqa: S608
313
657
  )
314
- ]
315
- for pid in pids:
316
- self._fts_delete_profile(pid)
317
- cur = self._execute("DELETE FROM profiles WHERE status = ?", (status.value,))
658
+ for pid in pids:
659
+ _emit_hard_delete_profile(
660
+ self.conn,
661
+ org_id=self.org_id,
662
+ entity_id=str(pid),
663
+ request_id=batch_request_id,
664
+ )
665
+ self.conn.commit()
318
666
  return cur.rowcount
319
667
 
320
668
  @SQLiteStorageBase.handle_exceptions
@@ -331,15 +679,41 @@ class ProfileMixin:
331
679
  return [r["user_id"] for r in rows]
332
680
 
333
681
  @SQLiteStorageBase.handle_exceptions
334
- def delete_profiles_by_ids(self, profile_ids: list[str]) -> int:
682
+ def delete_profiles_by_ids(
683
+ self, profile_ids: list[str], *, emit_hard_delete: bool = True
684
+ ) -> int:
335
685
  if not profile_ids:
336
686
  return 0
337
- for pid in profile_ids:
338
- self._fts_delete_profile(pid)
687
+ # Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
688
+ # prevention (see delete_user_profile comment, #196).
339
689
  ph = ",".join("?" for _ in profile_ids)
340
- cur = self._execute(
341
- f"DELETE FROM profiles WHERE profile_id IN ({ph})", profile_ids
342
- )
690
+ batch_request_id = uuid.uuid4().hex
691
+ with self._lock:
692
+ pre_rows = self.conn.execute(
693
+ f"SELECT rowid, profile_id FROM profiles WHERE profile_id IN ({ph})",
694
+ profile_ids,
695
+ ).fetchall()
696
+ if not pre_rows:
697
+ return 0
698
+ existing = [r["profile_id"] for r in pre_rows]
699
+ rowids = [r["rowid"] for r in pre_rows]
700
+ self._delete_in_chunks("profiles_fts", "profile_id", existing)
701
+ if self._has_sqlite_vec and rowids:
702
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
703
+ cur = self.conn.execute(
704
+ f"DELETE FROM profiles WHERE profile_id IN ({ph})",
705
+ profile_ids, # noqa: S608
706
+ )
707
+ if emit_hard_delete:
708
+ for pid in existing:
709
+ _emit_hard_delete_profile(
710
+ self.conn,
711
+ org_id=self.org_id,
712
+ entity_id=str(pid),
713
+ request_id=batch_request_id,
714
+ actor="system",
715
+ )
716
+ self.conn.commit()
343
717
  return cur.rowcount
344
718
 
345
719
  # ------------------------------------------------------------------
@@ -376,9 +750,9 @@ class ProfileMixin:
376
750
  """INSERT OR REPLACE INTO interactions
377
751
  (interaction_id, user_id, content, request_id, created_at,
378
752
  role, user_action, user_action_description,
379
- interacted_image_url, shadow_content, expert_content,
380
- tools_used, citations, embedding)
381
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
753
+ interacted_image_url, image_encoding, shadow_content,
754
+ expert_content, tools_used, citations, embedding)
755
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
382
756
  (
383
757
  interaction.interaction_id,
384
758
  interaction.user_id,
@@ -389,6 +763,7 @@ class ProfileMixin:
389
763
  interaction.user_action.value,
390
764
  interaction.user_action_description,
391
765
  interaction.interacted_image_url,
766
+ interaction.image_encoding,
392
767
  interaction.shadow_content,
393
768
  interaction.expert_content,
394
769
  _json_dumps([t.model_dump() for t in interaction.tools_used]),
@@ -402,9 +777,9 @@ class ProfileMixin:
402
777
  """INSERT INTO interactions
403
778
  (user_id, content, request_id, created_at,
404
779
  role, user_action, user_action_description,
405
- interacted_image_url, shadow_content, expert_content,
406
- tools_used, citations, embedding)
407
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
780
+ interacted_image_url, image_encoding, shadow_content,
781
+ expert_content, tools_used, citations, embedding)
782
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
408
783
  (
409
784
  interaction.user_id,
410
785
  interaction.content,
@@ -414,6 +789,7 @@ class ProfileMixin:
414
789
  interaction.user_action.value,
415
790
  interaction.user_action_description,
416
791
  interaction.interacted_image_url,
792
+ interaction.image_encoding,
417
793
  interaction.shadow_content,
418
794
  interaction.expert_content,
419
795
  _json_dumps([t.model_dump() for t in interaction.tools_used]),
@@ -464,34 +840,49 @@ class ProfileMixin:
464
840
 
465
841
  @SQLiteStorageBase.handle_exceptions
466
842
  def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
467
- self._fts_delete("interactions_fts", request.interaction_id)
468
- self._execute(
469
- "DELETE FROM interactions WHERE user_id = ? AND interaction_id = ?",
470
- (request.user_id, request.interaction_id),
471
- )
843
+ with self._lock:
844
+ row = self.conn.execute(
845
+ "SELECT interaction_id FROM interactions WHERE user_id = ? AND interaction_id = ?",
846
+ (request.user_id, request.interaction_id),
847
+ ).fetchone()
848
+ if row is None:
849
+ return
850
+ self.conn.execute(
851
+ "DELETE FROM interactions_fts WHERE rowid = ?",
852
+ (request.interaction_id,),
853
+ )
854
+ if self._has_sqlite_vec:
855
+ self.conn.execute(
856
+ "DELETE FROM interactions_vec WHERE rowid = ?",
857
+ (request.interaction_id,),
858
+ )
859
+ self.conn.execute(
860
+ "DELETE FROM interactions WHERE user_id = ? AND interaction_id = ?",
861
+ (request.user_id, request.interaction_id),
862
+ )
863
+ self.conn.commit()
472
864
 
473
865
  @SQLiteStorageBase.handle_exceptions
474
866
  def delete_all_interactions_for_user(self, user_id: str) -> None:
475
- # Delete FTS entries for this user's interactions
476
- ids = [
477
- r["interaction_id"]
478
- for r in self._fetchall(
867
+ with self._lock:
868
+ rows = self.conn.execute(
479
869
  "SELECT interaction_id FROM interactions WHERE user_id = ?", (user_id,)
480
- )
481
- ]
482
- if ids:
483
- placeholders = ",".join("?" for _ in ids)
484
- with self._lock:
485
- self.conn.execute(
486
- f"DELETE FROM interactions_fts WHERE rowid IN ({placeholders})", ids
487
- )
488
- self.conn.commit()
489
- self._execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
870
+ ).fetchall()
871
+ if not rows:
872
+ return
873
+ ids = [r["interaction_id"] for r in rows]
874
+ self._delete_in_chunks("interactions_fts", "rowid", ids)
875
+ if self._has_sqlite_vec:
876
+ self._delete_in_chunks("interactions_vec", "rowid", ids)
877
+ self.conn.execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
878
+ self.conn.commit()
490
879
 
491
880
  @SQLiteStorageBase.handle_exceptions
492
881
  def delete_all_interactions(self) -> None:
493
882
  with self._lock:
494
883
  self.conn.execute("DELETE FROM interactions_fts")
884
+ if self._has_sqlite_vec:
885
+ self.conn.execute("DELETE FROM interactions_vec")
495
886
  self.conn.execute("DELETE FROM interactions")
496
887
  self.conn.commit()
497
888
 
@@ -504,22 +895,18 @@ class ProfileMixin:
504
895
  def delete_oldest_interactions(self, count: int) -> int:
505
896
  if count <= 0:
506
897
  return 0
507
- rows = self._fetchall(
508
- "SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
509
- (count,),
510
- )
511
- if not rows:
512
- return 0
513
- ids = [r["interaction_id"] for r in rows]
514
- placeholders = ",".join("?" for _ in ids)
515
898
  with self._lock:
516
- self.conn.execute(
517
- f"DELETE FROM interactions_fts WHERE rowid IN ({placeholders})", ids
518
- )
519
- self.conn.execute(
520
- f"DELETE FROM interactions WHERE interaction_id IN ({placeholders})",
521
- ids,
522
- )
899
+ rows = self.conn.execute(
900
+ "SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
901
+ (count,),
902
+ ).fetchall()
903
+ if not rows:
904
+ return 0
905
+ ids = [r["interaction_id"] for r in rows]
906
+ self._delete_in_chunks("interactions_fts", "rowid", ids)
907
+ if self._has_sqlite_vec:
908
+ self._delete_in_chunks("interactions_vec", "rowid", ids)
909
+ self._delete_in_chunks("interactions", "interaction_id", ids)
523
910
  self.conn.commit()
524
911
  return len(ids)
525
912
 
@@ -657,6 +1044,10 @@ class ProfileMixin:
657
1044
  frag, sparams = _build_status_sql(status_filter)
658
1045
  conditions.append(frag)
659
1046
  params.extend(sparams)
1047
+ tag_frag, tag_params = _build_tags_sql("p", req.tags)
1048
+ if tag_frag:
1049
+ conditions.append(tag_frag)
1050
+ params.extend(tag_params)
660
1051
 
661
1052
  where_clause = " AND ".join(conditions)
662
1053
  overfetch = match_count * 5 if mode != SearchMode.FTS else match_count