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,628 @@
1
+ import json
2
+ import sqlite3
3
+ import threading
4
+ import time
5
+ import uuid
6
+ from typing import Any, Literal
7
+
8
+ from reflexio.models.api_schema.domain.entities import LineageContext, LineageEvent
9
+ from reflexio.models.api_schema.domain.enums import Status
10
+ from reflexio.server.tracing import capture_anomaly
11
+
12
+ from ._base import _epoch_now
13
+
14
+ EntityType = Literal["user_playbook", "agent_playbook", "profile"]
15
+
16
+ # GC-eligible statuses — rows with these statuses may be hard-deleted by TTL GC.
17
+ # Also used as the merge guard: a source that already carries any of these
18
+ # statuses is skipped (no re-tombstone, no clock reset).
19
+ _GC_ELIGIBLE_STATUSES: frozenset[str] = frozenset(
20
+ {Status.MERGED.value, Status.SUPERSEDED.value, Status.ARCHIVED.value}
21
+ )
22
+
23
+ # Mapping from entity_type string to (table_name, primary_key_column).
24
+ _TABLE: dict[str, tuple[str, str]] = {
25
+ "user_playbook": ("user_playbooks", "user_playbook_id"),
26
+ "agent_playbook": ("agent_playbooks", "agent_playbook_id"),
27
+ "profile": ("profiles", "profile_id"),
28
+ }
29
+
30
+ # Error message used by merge_records and supersede_record guards.
31
+ # Shared here so tests can reference this exact string without hardcoding.
32
+ _EMPTY_REQUEST_ID_MSG = "request_id must be non-empty"
33
+
34
+
35
+ def _resolve_table(entity_type: str) -> tuple[str, str]:
36
+ """Map an entity_type to its (table, primary_key), raising on bad input."""
37
+ table = _TABLE.get(entity_type)
38
+ if table is None:
39
+ raise ValueError(f"unknown entity_type: {entity_type!r}")
40
+ return table
41
+
42
+
43
+ def _append_event_stmt(
44
+ conn: sqlite3.Connection,
45
+ *,
46
+ org_id: str,
47
+ entity_type: str,
48
+ entity_id: str,
49
+ op: str,
50
+ prov: str,
51
+ source_ids: list[str],
52
+ actor: str,
53
+ request_id: str,
54
+ reason: str,
55
+ created_at: int | None = None,
56
+ from_status: str | None = None,
57
+ to_status: str | None = None,
58
+ status_namespace: str | None = None,
59
+ ) -> sqlite3.Cursor:
60
+ """Insert a lineage event row; no-ops on (org_id, entity_type, entity_id, op, request_id) duplicate.
61
+
62
+ Returns the cursor so callers can inspect ``rowcount``/``lastrowid``.
63
+ """
64
+ return conn.execute(
65
+ "INSERT OR IGNORE INTO lineage_event "
66
+ "(org_id, entity_type, entity_id, op, prov_relation, source_ids, "
67
+ "actor, request_id, reason, created_at, "
68
+ "from_status, to_status, status_namespace) "
69
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
70
+ (
71
+ org_id,
72
+ entity_type,
73
+ entity_id,
74
+ op,
75
+ prov,
76
+ json.dumps(source_ids),
77
+ actor,
78
+ request_id,
79
+ reason,
80
+ created_at if created_at is not None else int(time.time()),
81
+ from_status,
82
+ to_status,
83
+ status_namespace,
84
+ ),
85
+ )
86
+
87
+
88
+ # Per-entity purge SQL: blank every PII/content column.
89
+ # No ``content != ''`` guard — a row with content='' but other PII still populated
90
+ # (user_id, embedding, tags, …) would be skipped by that guard, leaving its PII live.
91
+ # Blanking already-blank columns is an idempotent no-op in SQLite (rowcount=1 either
92
+ # way because the row matched); event idempotency is guaranteed by the deterministic
93
+ # request_id + INSERT OR IGNORE on (org,entity_type,entity_id,op,request_id).
94
+ _PROFILE_PURGE_SQL = (
95
+ "UPDATE profiles SET "
96
+ "content='', user_id='', generated_from_request_id='', source='', "
97
+ "embedding=NULL, extractor_names=NULL, expanded_terms=NULL, tags=NULL, "
98
+ "custom_features=NULL, notes=NULL, source_span=NULL, reader_angle=NULL "
99
+ "WHERE profile_id=?"
100
+ )
101
+ _USER_PLAYBOOK_PURGE_SQL = (
102
+ "UPDATE user_playbooks SET "
103
+ "content='', user_id=NULL, source=NULL, "
104
+ "trigger=NULL, rationale=NULL, blocking_issue=NULL, "
105
+ "source_interaction_ids=NULL, embedding=NULL, expanded_terms=NULL, "
106
+ "tags=NULL, source_span=NULL, notes=NULL, reader_angle=NULL "
107
+ "WHERE user_playbook_id=?"
108
+ )
109
+ # agent_playbook purge not yet required; added when Task 3/4 needs it.
110
+ _PURGE_SQL: dict[str, str] = {
111
+ "profile": _PROFILE_PURGE_SQL,
112
+ "user_playbook": _USER_PLAYBOOK_PURGE_SQL,
113
+ }
114
+
115
+
116
+ class SQLiteLineageMixin:
117
+ """SQLite implementation of the append-only, content-free lineage event log."""
118
+
119
+ # Type hints for instance attributes provided by SQLiteStorageBase via MRO.
120
+ conn: sqlite3.Connection
121
+ _lock: threading.RLock
122
+ org_id: str
123
+
124
+ def append_lineage_event(self, event: LineageEvent) -> int:
125
+ """Append an event; idempotent on (org_id, entity_type, entity_id, op, request_id).
126
+
127
+ Args:
128
+ event (LineageEvent): The fully-formed event to persist. ``event_id``
129
+ may be 0; the storage layer assigns a real id on insert. On a
130
+ duplicate ``(org_id, entity_type, entity_id, op, request_id)`` the
131
+ existing row is returned unchanged.
132
+
133
+ Returns:
134
+ int: The assigned or existing ``event_id``.
135
+ """
136
+ created = event.created_at or int(time.time())
137
+ with self._lock:
138
+ cur = _append_event_stmt(
139
+ self.conn,
140
+ org_id=event.org_id,
141
+ entity_type=event.entity_type,
142
+ entity_id=event.entity_id,
143
+ op=event.op,
144
+ prov=event.prov_relation,
145
+ source_ids=event.source_ids,
146
+ actor=event.actor,
147
+ request_id=event.request_id,
148
+ reason=event.reason,
149
+ created_at=created,
150
+ from_status=event.from_status,
151
+ to_status=event.to_status,
152
+ status_namespace=event.status_namespace,
153
+ )
154
+ if (
155
+ cur.rowcount == 0
156
+ ): # duplicate (org_id, entity_type, entity_id, op, request_id)
157
+ row = self.conn.execute(
158
+ "SELECT event_id FROM lineage_event WHERE org_id=? AND entity_type=? "
159
+ "AND entity_id=? AND op=? AND request_id=?",
160
+ (
161
+ event.org_id,
162
+ event.entity_type,
163
+ event.entity_id,
164
+ event.op,
165
+ event.request_id,
166
+ ),
167
+ ).fetchone()
168
+ eid = row[0] if row else None
169
+ self.conn.commit()
170
+ return int(eid) if eid is not None else 0
171
+ last = cur.lastrowid
172
+ self.conn.commit()
173
+ return int(last) if last is not None else 0
174
+
175
+ def get_lineage_events(
176
+ self,
177
+ *,
178
+ entity_type: str | None = None,
179
+ entity_id: str | None = None,
180
+ org_id: str | None = None,
181
+ request_id: str | None = None,
182
+ ) -> list[LineageEvent]:
183
+ """Retrieve lineage events, optionally filtered.
184
+
185
+ Args:
186
+ entity_type (str | None): Filter to events for this entity type.
187
+ entity_id (str | None): Filter to events for this entity id.
188
+ org_id (str | None): Filter to events for this org.
189
+ request_id (str | None): Filter to events for this request id.
190
+
191
+ Returns:
192
+ list[LineageEvent]: Matching events ordered by ``event_id`` ascending.
193
+ """
194
+ clauses: list[str] = []
195
+ params: list[Any] = []
196
+ for col, val in (
197
+ ("entity_type", entity_type),
198
+ ("entity_id", entity_id),
199
+ ("org_id", org_id),
200
+ ("request_id", request_id),
201
+ ):
202
+ if val is not None:
203
+ clauses.append(f"{col}=?")
204
+ params.append(val)
205
+ where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
206
+ with self._lock:
207
+ rows = self.conn.execute(
208
+ f"SELECT * FROM lineage_event{where} ORDER BY event_id", # noqa: S608
209
+ params,
210
+ ).fetchall()
211
+ return [
212
+ LineageEvent(
213
+ event_id=r["event_id"],
214
+ org_id=r["org_id"],
215
+ entity_type=r["entity_type"],
216
+ entity_id=r["entity_id"],
217
+ op=r["op"],
218
+ prov_relation=r["prov_relation"],
219
+ source_ids=json.loads(r["source_ids"]),
220
+ actor=r["actor"],
221
+ request_id=r["request_id"],
222
+ reason=r["reason"],
223
+ created_at=r["created_at"],
224
+ from_status=r["from_status"],
225
+ to_status=r["to_status"],
226
+ status_namespace=r["status_namespace"],
227
+ )
228
+ for r in rows
229
+ ]
230
+
231
+ def merge_records(
232
+ self,
233
+ *,
234
+ entity_type: EntityType,
235
+ survivor_id: str,
236
+ source_ids: list[str],
237
+ context: LineageContext,
238
+ ) -> None:
239
+ """Soft-delete each source into the survivor in one atomic transaction.
240
+
241
+ Sets ``status=MERGED`` and ``merged_into=survivor_id`` on each source
242
+ whose status is not already a tombstone. Appends a single ``merge``
243
+ lineage event keyed on ``survivor_id``. Idempotent — re-running on
244
+ already-tombstoned sources is a no-op.
245
+
246
+ Args:
247
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
248
+ or ``"profile"``.
249
+ survivor_id (str): The id of the record that survives the merge.
250
+ source_ids (list[str]): Ids of records to tombstone as merged.
251
+ context (LineageContext): Caller-supplied intent (actor, reason, etc.).
252
+
253
+ Raises:
254
+ ValueError: If ``entity_type`` is not a recognized entity type.
255
+ ValueError: If ``context.request_id`` is empty or whitespace-only.
256
+ """
257
+ if not (context.request_id and context.request_id.strip()):
258
+ raise ValueError(f"lineage merge: {_EMPTY_REQUEST_ID_MSG}")
259
+ table, pk = _resolve_table(entity_type)
260
+ now = _epoch_now()
261
+ eligible_ph = ",".join("?" * len(_GC_ELIGIBLE_STATUSES))
262
+ eligible_vals = list(_GC_ELIGIBLE_STATUSES)
263
+ with self._lock:
264
+ for sid in source_ids:
265
+ if sid == survivor_id:
266
+ # Never tombstone the survivor itself, even if it is
267
+ # accidentally listed among the source ids.
268
+ continue
269
+ # Skip sources that already carry any eligible/tombstone status
270
+ # (MERGED, SUPERSEDED, or ARCHIVED) — avoids re-tombstoning an
271
+ # already-archived source and resetting its retired_at clock.
272
+ self.conn.execute(
273
+ f"UPDATE {table} SET status=?, merged_into=?, retired_at=? " # noqa: S608
274
+ f"WHERE {pk}=? AND {pk}!=? "
275
+ f"AND (status IS NULL OR status NOT IN ({eligible_ph}))",
276
+ (
277
+ Status.MERGED.value,
278
+ survivor_id,
279
+ now,
280
+ sid,
281
+ survivor_id,
282
+ *eligible_vals,
283
+ ),
284
+ )
285
+ _append_event_stmt(
286
+ self.conn,
287
+ org_id=self.org_id,
288
+ entity_type=entity_type,
289
+ entity_id=survivor_id,
290
+ op="merge",
291
+ prov="wasDerivedFrom",
292
+ source_ids=source_ids,
293
+ actor=context.actor,
294
+ request_id=context.request_id,
295
+ reason=context.reason,
296
+ )
297
+ self.conn.commit()
298
+
299
+ def supersede_record(
300
+ self,
301
+ *,
302
+ entity_type: EntityType,
303
+ incumbent_id: str,
304
+ successor_id: str,
305
+ context: LineageContext,
306
+ ) -> bool:
307
+ """Atomically replace the incumbent with the successor if incumbent is CURRENT.
308
+
309
+ Sets ``status=SUPERSEDED`` and ``superseded_by=successor_id`` on the
310
+ incumbent **only** when its ``status IS NULL`` (CURRENT). Appends a
311
+ ``revise`` lineage event when the guard succeeds. Returns ``False``
312
+ without mutating anything when the incumbent is not CURRENT.
313
+
314
+ Args:
315
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
316
+ or ``"profile"``.
317
+ incumbent_id (str): The id of the record to supersede.
318
+ successor_id (str): The id of the record that replaces the incumbent.
319
+ context (LineageContext): Caller-supplied intent (actor, reason, etc.).
320
+
321
+ Returns:
322
+ bool: ``True`` if the incumbent was CURRENT and was superseded;
323
+ ``False`` if the incumbent was not CURRENT and no mutation occurred.
324
+
325
+ Raises:
326
+ ValueError: If ``entity_type`` is not a recognized entity type.
327
+ ValueError: If ``context.request_id`` is empty or whitespace-only.
328
+ """
329
+ if not (context.request_id and context.request_id.strip()):
330
+ raise ValueError(f"lineage supersede: {_EMPTY_REQUEST_ID_MSG}")
331
+ table, pk = _resolve_table(entity_type)
332
+ with self._lock:
333
+ cur = self.conn.execute(
334
+ f"UPDATE {table} SET status=?, superseded_by=?, retired_at=? " # noqa: S608
335
+ f"WHERE {pk}=? AND status IS NULL",
336
+ (Status.SUPERSEDED.value, successor_id, _epoch_now(), incumbent_id),
337
+ )
338
+ if cur.rowcount == 0:
339
+ self.conn.commit()
340
+ return False
341
+ _append_event_stmt(
342
+ self.conn,
343
+ org_id=self.org_id,
344
+ entity_type=entity_type,
345
+ entity_id=successor_id,
346
+ op="revise",
347
+ prov="wasRevisionOf",
348
+ source_ids=[incumbent_id],
349
+ actor=context.actor,
350
+ request_id=context.request_id,
351
+ reason=context.reason,
352
+ )
353
+ self.conn.commit()
354
+ return True
355
+
356
+ def purge_content(self, *, entity_type: EntityType, entity_id: str) -> bool:
357
+ """Blank a record's PII body, keep its lineage skeleton, emit op=purge.
358
+
359
+ Blanks every non-skeleton column for the row identified by ``entity_id``,
360
+ emits one ``op=purge`` lineage event in the same commit, then runs FTS/vec
361
+ index cleanup after the commit. The ``request_id`` is deterministic
362
+ (``"purge_" + entity_id``) so re-runs on an already-blank row do not
363
+ produce a duplicate event (``INSERT OR IGNORE`` on the unique key).
364
+
365
+ Re-running on a row whose content is already blank is safe: the UPDATE
366
+ still matches the row and sets the same values (harmless idempotent write),
367
+ while the INSERT OR IGNORE deduplicates the event on
368
+ ``(org_id, entity_type, entity_id, op, request_id)``.
369
+
370
+ Args:
371
+ entity_type (EntityType): One of ``"user_playbook"`` or ``"profile"``.
372
+ ``"agent_playbook"`` raises ``ValueError`` — agent playbooks have no
373
+ ``user_id`` and are out of scope for content purge.
374
+ entity_id (str): The entity's primary key as a string.
375
+
376
+ Returns:
377
+ bool: ``True`` if the row exists; ``False`` if the id had no matching row.
378
+
379
+ Raises:
380
+ ValueError: If ``entity_type`` is not a recognized entity type or
381
+ if ``entity_type`` is ``"agent_playbook"`` (not supported).
382
+ """
383
+ sql = _PURGE_SQL.get(entity_type)
384
+ if sql is None:
385
+ raise ValueError(f"purge_content: unsupported entity_type {entity_type!r}")
386
+ table, pk = _resolve_table(entity_type)
387
+ with self._lock:
388
+ row = self.conn.execute(
389
+ f"SELECT rowid AS _rowid FROM {table} WHERE {pk}=?", # noqa: S608
390
+ (entity_id,),
391
+ ).fetchone()
392
+ if row is None:
393
+ return False
394
+ rowid = row["_rowid"]
395
+ cur = self.conn.execute(sql, (entity_id,))
396
+ if cur.rowcount > 0:
397
+ # Row matched (rowcount==1 whenever it exists); attempt the event —
398
+ # INSERT OR IGNORE dedups on the deterministic request_id.
399
+ _append_event_stmt(
400
+ self.conn,
401
+ org_id=self.org_id,
402
+ entity_type=entity_type,
403
+ entity_id=entity_id,
404
+ op="purge",
405
+ prov="wasPurged",
406
+ source_ids=[],
407
+ actor="erasure", # no user identifier
408
+ request_id=f"purge_{entity_id}", # deterministic → idempotent
409
+ reason="content_purge",
410
+ )
411
+ self.conn.commit()
412
+ # Post-commit index cleanup (self-committing helpers; idempotent/replayable).
413
+ self._purge_search_indexes(entity_type, entity_id, rowid)
414
+ return True
415
+
416
+ def _purge_search_indexes(
417
+ self, entity_type: EntityType, entity_id: str, rowid: int
418
+ ) -> None:
419
+ """Remove FTS and vec index entries for a purged entity.
420
+
421
+ Called after the blanking commit so index cleanup never interleaves with
422
+ the atomic mutation+event transaction.
423
+
424
+ Args:
425
+ entity_type (EntityType): One of ``"user_playbook"`` or ``"profile"``.
426
+ entity_id (str): The entity's primary key (used for profiles FTS).
427
+ rowid (int): The sqlite rowid (used for playbooks FTS and all vec tables).
428
+ """
429
+ if entity_type == "profile":
430
+ self._fts_delete_profile(entity_id) # type: ignore[attr-defined]
431
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
432
+ self._vec_delete("profiles_vec", rowid) # type: ignore[attr-defined]
433
+ elif entity_type == "user_playbook":
434
+ self._fts_delete("user_playbooks_fts", rowid) # type: ignore[attr-defined]
435
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
436
+ self._vec_delete("user_playbooks_vec", rowid) # type: ignore[attr-defined]
437
+
438
+ def has_inbound_lineage_refs(
439
+ self, *, entity_type: EntityType, entity_id: str
440
+ ) -> bool:
441
+ """Return True if any row points at ``entity_id`` via merged_into/superseded_by.
442
+
443
+ Org-scoped but deliberately NOT user_id-scoped: a cross-user chain
444
+ (one user's tombstone pointing at another user's survivor) must be
445
+ detected so the survivor is purged, not hard-deleted, on erasure.
446
+
447
+ Args:
448
+ entity_type (EntityType): One of ``"user_playbook"``, ``"agent_playbook"``,
449
+ or ``"profile"``.
450
+ entity_id (str): The entity's primary key to check for inbound refs.
451
+
452
+ Returns:
453
+ bool: True if any row has ``merged_into == entity_id``
454
+ OR ``superseded_by == entity_id``; False otherwise.
455
+ """
456
+ table, _pk = _resolve_table(entity_type)
457
+ with self._lock:
458
+ row = self.conn.execute(
459
+ f"SELECT 1 FROM {table} " # noqa: S608
460
+ f"WHERE merged_into = ? OR superseded_by = ? LIMIT 1",
461
+ (entity_id, entity_id),
462
+ ).fetchone()
463
+ return row is not None
464
+
465
+ def _is_on_legal_hold(
466
+ self,
467
+ org_id: str, # noqa: ARG002
468
+ entity_type: str, # noqa: ARG002
469
+ entity_id: str, # noqa: ARG002
470
+ ) -> bool:
471
+ """Return True if this entity is under a legal hold and must not be GC'd.
472
+
473
+ Deferred seam — always returns False until a hold store exists.
474
+
475
+ Args:
476
+ org_id (str): The organisation that owns the entity.
477
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
478
+ or ``"profile"``.
479
+ entity_id (str): The entity's primary key as a string.
480
+
481
+ Returns:
482
+ bool: False (no hold store implemented yet).
483
+ """
484
+ return False
485
+
486
+ def list_org_ids(self) -> list[str]:
487
+ """Return the single org_id for this SQLite storage instance.
488
+
489
+ SQLite storage is single-tenant: each instance is scoped to exactly one
490
+ org. Returns ``[self.org_id]``.
491
+
492
+ Returns:
493
+ list[str]: A one-element list containing this instance's org_id.
494
+ """
495
+ return [self.org_id]
496
+
497
+ def gc_expired_tombstones(
498
+ self, *, entity_type: str, older_than_epoch: int, limit: int = 1000
499
+ ) -> int:
500
+ """Hard-delete tombstone rows whose retirement instant is older than the cutoff.
501
+
502
+ Ages on the uniform INTEGER ``retired_at`` column set at every tombstone
503
+ write-path (T1). Rows with ``retired_at = NULL`` (pre-T1 tombstones) are
504
+ never selected — they have no retirement clock and must be retained.
505
+
506
+ Emits one ``hard_delete`` lineage event per deleted row, atomically, before
507
+ the DELETE commits. Rows on legal hold are skipped without emitting an event.
508
+
509
+ Args:
510
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
511
+ or ``"profile"``.
512
+ older_than_epoch (int): Unix timestamp cutoff (exclusive). Rows whose
513
+ ``retired_at`` is strictly less than this value are eligible.
514
+ limit (int): Maximum rows to delete per call. Defaults to 1000.
515
+
516
+ Returns:
517
+ int: The number of rows physically deleted.
518
+
519
+ Raises:
520
+ ValueError: If ``entity_type`` is not a recognised entity type.
521
+ """
522
+ if limit <= 0:
523
+ return 0
524
+ table, pk = _resolve_table(entity_type)
525
+
526
+ eligible_ph = ",".join("?" * len(_GC_ELIGIBLE_STATUSES))
527
+ eligible_vals = list(_GC_ELIGIBLE_STATUSES)
528
+
529
+ # ORDER BY retired_at ASC for deterministic forward progress.
530
+ # No SQL LIMIT here — the limit is applied after the legal-hold filter
531
+ # below so held rows at the front of the batch don't starve eligible rows.
532
+ select_sql = (
533
+ f"SELECT {pk} FROM {table} " # noqa: S608
534
+ f"WHERE status IN ({eligible_ph}) "
535
+ f"AND retired_at IS NOT NULL AND retired_at < ? "
536
+ f"ORDER BY retired_at ASC"
537
+ )
538
+ select_params: list[Any] = [*eligible_vals, older_than_epoch]
539
+
540
+ with self._lock:
541
+ rows = self.conn.execute(select_sql, select_params).fetchall()
542
+ if not rows:
543
+ return 0
544
+
545
+ candidate_ids: list[str] = [str(r[0]) for r in rows]
546
+ ids_to_delete: list[str] = []
547
+ for eid in candidate_ids:
548
+ if self._is_on_legal_hold(self.org_id, entity_type, eid):
549
+ # NOTE: any real hold-check implementation must run inside
550
+ # the same transaction as the DELETE to remain atomic.
551
+ capture_anomaly(
552
+ "lineage.gc.legal_hold_skip",
553
+ level="info",
554
+ org_id=self.org_id,
555
+ entity_type=entity_type,
556
+ entity_id=eid,
557
+ )
558
+ continue
559
+ ids_to_delete.append(eid)
560
+ if len(ids_to_delete) >= limit:
561
+ break
562
+
563
+ if not ids_to_delete:
564
+ return 0
565
+
566
+ batch_request_id = uuid.uuid4().hex
567
+ ph = ",".join("?" * len(ids_to_delete))
568
+
569
+ try:
570
+ # Emit hard_delete events BEFORE the DELETE, in the same transaction.
571
+ for eid in ids_to_delete:
572
+ _append_event_stmt(
573
+ self.conn,
574
+ org_id=self.org_id,
575
+ entity_type=entity_type,
576
+ entity_id=eid,
577
+ op="hard_delete",
578
+ prov="wasInvalidatedBy",
579
+ source_ids=[],
580
+ actor="system",
581
+ request_id=batch_request_id,
582
+ reason="ttl-gc",
583
+ )
584
+
585
+ # Inline FTS/vec cleanup — raw DELETE to preserve atomicity.
586
+ # Do NOT call self._fts_delete/_vec_delete: they self-commit.
587
+ if entity_type in ("user_playbook", "agent_playbook"):
588
+ kind = "user" if entity_type == "user_playbook" else "agent"
589
+ int_ids = [int(eid) for eid in ids_to_delete]
590
+ int_ph = ",".join("?" * len(int_ids))
591
+ self.conn.execute(
592
+ f"DELETE FROM {kind}_playbooks_fts WHERE rowid IN ({int_ph})",
593
+ int_ids,
594
+ )
595
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
596
+ self.conn.execute(
597
+ f"DELETE FROM {kind}_playbooks_vec WHERE rowid IN ({int_ph})",
598
+ int_ids,
599
+ )
600
+ else:
601
+ # profiles: FTS keyed on TEXT profile_id; vec keyed on implicit rowid.
602
+ self.conn.execute(
603
+ f"DELETE FROM profiles_fts WHERE profile_id IN ({ph})",
604
+ ids_to_delete,
605
+ )
606
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
607
+ rowid_rows = self.conn.execute(
608
+ f"SELECT rowid FROM profiles WHERE profile_id IN ({ph})", # noqa: S608
609
+ ids_to_delete,
610
+ ).fetchall()
611
+ if rowid_rows:
612
+ rowids = [r[0] for r in rowid_rows]
613
+ rowid_ph = ",".join("?" * len(rowids))
614
+ self.conn.execute(
615
+ f"DELETE FROM profiles_vec WHERE rowid IN ({rowid_ph})",
616
+ rowids,
617
+ )
618
+
619
+ cur = self.conn.execute(
620
+ f"DELETE FROM {table} WHERE {pk} IN ({ph})", # noqa: S608
621
+ ids_to_delete,
622
+ )
623
+ self.conn.commit()
624
+ except Exception:
625
+ self.conn.rollback()
626
+ raise
627
+
628
+ return cur.rowcount