claude-smart 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +47 -10
  3. package/bin/claude-smart.js +535 -23
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +27 -6
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/layout.tsx +20 -0
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  17. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  18. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  19. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  20. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  21. package/plugin/dashboard/lib/status.ts +10 -3
  22. package/plugin/dashboard/lib/types.ts +18 -6
  23. package/plugin/hooks/codex-hooks.json +0 -1
  24. package/plugin/opencode/assistant-buffer.ts +108 -0
  25. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  26. package/plugin/opencode/dist/internal.js +12 -0
  27. package/plugin/opencode/dist/payload.js +85 -0
  28. package/plugin/opencode/dist/server.mjs +232 -0
  29. package/plugin/opencode/internal.ts +18 -0
  30. package/plugin/opencode/package.json +3 -0
  31. package/plugin/opencode/payload.ts +90 -0
  32. package/plugin/opencode/server.mts +251 -0
  33. package/plugin/opencode/tsconfig.json +13 -0
  34. package/plugin/pyproject.toml +25 -4
  35. package/plugin/scripts/_lib.sh +5 -1
  36. package/plugin/scripts/backend-service.sh +22 -2
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +1 -1
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +17 -9
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +498 -12
  45. package/plugin/src/claude_smart/context_format.py +9 -9
  46. package/plugin/src/claude_smart/cs_cite.py +66 -19
  47. package/plugin/src/claude_smart/env_config.py +12 -4
  48. package/plugin/src/claude_smart/events/session_start.py +26 -7
  49. package/plugin/src/claude_smart/events/stop.py +1 -1
  50. package/plugin/src/claude_smart/ids.py +1 -1
  51. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  52. package/plugin/src/claude_smart/runtime.py +7 -1
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +54 -30
  55. package/plugin/vendor/reflexio/README.md +17 -11
  56. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  57. package/plugin/vendor/reflexio/reflexio/README.md +4 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  59. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  60. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  64. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
  65. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  66. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  67. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  71. package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
  146. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  147. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  148. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
  149. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
  151. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  152. package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
  153. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  158. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  159. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  160. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  161. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
  162. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  164. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
  165. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  166. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  167. package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
  168. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  169. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  170. package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
  171. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  172. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
  173. package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
  174. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  175. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  176. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  177. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  178. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  179. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  180. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  181. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  182. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  183. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  185. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  186. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  187. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  188. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  189. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  198. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
  199. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  200. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  201. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  202. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  203. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  204. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  205. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  206. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  207. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  208. package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
  209. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  212. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  215. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  216. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  217. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  218. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  219. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  220. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  221. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  222. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  225. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  226. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  227. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  229. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  230. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  231. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
  232. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  233. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  234. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
  238. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
  241. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  242. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  243. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
  244. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  245. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  246. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  248. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  249. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  250. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  252. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  255. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
  256. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  257. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  258. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  259. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  261. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  262. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  263. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
  264. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  265. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  266. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  277. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
  278. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
  279. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
  280. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  281. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  282. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
  289. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  290. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
  292. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
  296. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
  297. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
  311. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  312. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  313. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  314. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  315. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  316. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  317. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  318. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  319. package/scripts/setup-claude-smart.sh +8 -3
  320. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  321. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  322. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  323. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  324. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  325. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  326. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  327. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  328. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  329. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  330. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  331. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  332. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  333. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  334. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  335. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  336. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  337. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  338. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  339. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  340. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  341. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  342. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  343. package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
  344. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  345. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  346. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  347. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  348. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  349. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  350. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  351. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  352. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  353. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -0,0 +1,630 @@
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
+ "governance_subject_ref=NULL "
100
+ "WHERE profile_id=?"
101
+ )
102
+ _USER_PLAYBOOK_PURGE_SQL = (
103
+ "UPDATE user_playbooks SET "
104
+ "content='', user_id=NULL, request_id='', source=NULL, "
105
+ "trigger=NULL, rationale=NULL, blocking_issue=NULL, "
106
+ "source_interaction_ids=NULL, embedding=NULL, expanded_terms=NULL, "
107
+ "tags=NULL, source_span=NULL, notes=NULL, reader_angle=NULL, "
108
+ "governance_subject_ref=NULL "
109
+ "WHERE user_playbook_id=?"
110
+ )
111
+ # agent_playbook purge not yet required; added when Task 3/4 needs it.
112
+ _PURGE_SQL: dict[str, str] = {
113
+ "profile": _PROFILE_PURGE_SQL,
114
+ "user_playbook": _USER_PLAYBOOK_PURGE_SQL,
115
+ }
116
+
117
+
118
+ class SQLiteLineageMixin:
119
+ """SQLite implementation of the append-only, content-free lineage event log."""
120
+
121
+ # Type hints for instance attributes provided by SQLiteStorageBase via MRO.
122
+ conn: sqlite3.Connection
123
+ _lock: threading.RLock
124
+ org_id: str
125
+
126
+ def append_lineage_event(self, event: LineageEvent) -> int:
127
+ """Append an event; idempotent on (org_id, entity_type, entity_id, op, request_id).
128
+
129
+ Args:
130
+ event (LineageEvent): The fully-formed event to persist. ``event_id``
131
+ may be 0; the storage layer assigns a real id on insert. On a
132
+ duplicate ``(org_id, entity_type, entity_id, op, request_id)`` the
133
+ existing row is returned unchanged.
134
+
135
+ Returns:
136
+ int: The assigned or existing ``event_id``.
137
+ """
138
+ created = event.created_at or int(time.time())
139
+ with self._lock:
140
+ cur = _append_event_stmt(
141
+ self.conn,
142
+ org_id=event.org_id,
143
+ entity_type=event.entity_type,
144
+ entity_id=event.entity_id,
145
+ op=event.op,
146
+ prov=event.prov_relation,
147
+ source_ids=event.source_ids,
148
+ actor=event.actor,
149
+ request_id=event.request_id,
150
+ reason=event.reason,
151
+ created_at=created,
152
+ from_status=event.from_status,
153
+ to_status=event.to_status,
154
+ status_namespace=event.status_namespace,
155
+ )
156
+ if (
157
+ cur.rowcount == 0
158
+ ): # duplicate (org_id, entity_type, entity_id, op, request_id)
159
+ row = self.conn.execute(
160
+ "SELECT event_id FROM lineage_event WHERE org_id=? AND entity_type=? "
161
+ "AND entity_id=? AND op=? AND request_id=?",
162
+ (
163
+ event.org_id,
164
+ event.entity_type,
165
+ event.entity_id,
166
+ event.op,
167
+ event.request_id,
168
+ ),
169
+ ).fetchone()
170
+ eid = row[0] if row else None
171
+ self.conn.commit()
172
+ return int(eid) if eid is not None else 0
173
+ last = cur.lastrowid
174
+ self.conn.commit()
175
+ return int(last) if last is not None else 0
176
+
177
+ def get_lineage_events(
178
+ self,
179
+ *,
180
+ entity_type: str | None = None,
181
+ entity_id: str | None = None,
182
+ org_id: str | None = None,
183
+ request_id: str | None = None,
184
+ ) -> list[LineageEvent]:
185
+ """Retrieve lineage events, optionally filtered.
186
+
187
+ Args:
188
+ entity_type (str | None): Filter to events for this entity type.
189
+ entity_id (str | None): Filter to events for this entity id.
190
+ org_id (str | None): Filter to events for this org.
191
+ request_id (str | None): Filter to events for this request id.
192
+
193
+ Returns:
194
+ list[LineageEvent]: Matching events ordered by ``event_id`` ascending.
195
+ """
196
+ clauses: list[str] = []
197
+ params: list[Any] = []
198
+ for col, val in (
199
+ ("entity_type", entity_type),
200
+ ("entity_id", entity_id),
201
+ ("org_id", org_id),
202
+ ("request_id", request_id),
203
+ ):
204
+ if val is not None:
205
+ clauses.append(f"{col}=?")
206
+ params.append(val)
207
+ where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
208
+ with self._lock:
209
+ rows = self.conn.execute(
210
+ f"SELECT * FROM lineage_event{where} ORDER BY event_id", # noqa: S608
211
+ params,
212
+ ).fetchall()
213
+ return [
214
+ LineageEvent(
215
+ event_id=r["event_id"],
216
+ org_id=r["org_id"],
217
+ entity_type=r["entity_type"],
218
+ entity_id=r["entity_id"],
219
+ op=r["op"],
220
+ prov_relation=r["prov_relation"],
221
+ source_ids=json.loads(r["source_ids"]),
222
+ actor=r["actor"],
223
+ request_id=r["request_id"],
224
+ reason=r["reason"],
225
+ created_at=r["created_at"],
226
+ from_status=r["from_status"],
227
+ to_status=r["to_status"],
228
+ status_namespace=r["status_namespace"],
229
+ )
230
+ for r in rows
231
+ ]
232
+
233
+ def merge_records(
234
+ self,
235
+ *,
236
+ entity_type: EntityType,
237
+ survivor_id: str,
238
+ source_ids: list[str],
239
+ context: LineageContext,
240
+ ) -> None:
241
+ """Soft-delete each source into the survivor in one atomic transaction.
242
+
243
+ Sets ``status=MERGED`` and ``merged_into=survivor_id`` on each source
244
+ whose status is not already a tombstone. Appends a single ``merge``
245
+ lineage event keyed on ``survivor_id``. Idempotent — re-running on
246
+ already-tombstoned sources is a no-op.
247
+
248
+ Args:
249
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
250
+ or ``"profile"``.
251
+ survivor_id (str): The id of the record that survives the merge.
252
+ source_ids (list[str]): Ids of records to tombstone as merged.
253
+ context (LineageContext): Caller-supplied intent (actor, reason, etc.).
254
+
255
+ Raises:
256
+ ValueError: If ``entity_type`` is not a recognized entity type.
257
+ ValueError: If ``context.request_id`` is empty or whitespace-only.
258
+ """
259
+ if not (context.request_id and context.request_id.strip()):
260
+ raise ValueError(f"lineage merge: {_EMPTY_REQUEST_ID_MSG}")
261
+ table, pk = _resolve_table(entity_type)
262
+ now = _epoch_now()
263
+ eligible_ph = ",".join("?" * len(_GC_ELIGIBLE_STATUSES))
264
+ eligible_vals = list(_GC_ELIGIBLE_STATUSES)
265
+ with self._lock:
266
+ for sid in source_ids:
267
+ if sid == survivor_id:
268
+ # Never tombstone the survivor itself, even if it is
269
+ # accidentally listed among the source ids.
270
+ continue
271
+ # Skip sources that already carry any eligible/tombstone status
272
+ # (MERGED, SUPERSEDED, or ARCHIVED) — avoids re-tombstoning an
273
+ # already-archived source and resetting its retired_at clock.
274
+ self.conn.execute(
275
+ f"UPDATE {table} SET status=?, merged_into=?, retired_at=? " # noqa: S608
276
+ f"WHERE {pk}=? AND {pk}!=? "
277
+ f"AND (status IS NULL OR status NOT IN ({eligible_ph}))",
278
+ (
279
+ Status.MERGED.value,
280
+ survivor_id,
281
+ now,
282
+ sid,
283
+ survivor_id,
284
+ *eligible_vals,
285
+ ),
286
+ )
287
+ _append_event_stmt(
288
+ self.conn,
289
+ org_id=self.org_id,
290
+ entity_type=entity_type,
291
+ entity_id=survivor_id,
292
+ op="merge",
293
+ prov="wasDerivedFrom",
294
+ source_ids=source_ids,
295
+ actor=context.actor,
296
+ request_id=context.request_id,
297
+ reason=context.reason,
298
+ )
299
+ self.conn.commit()
300
+
301
+ def supersede_record(
302
+ self,
303
+ *,
304
+ entity_type: EntityType,
305
+ incumbent_id: str,
306
+ successor_id: str,
307
+ context: LineageContext,
308
+ ) -> bool:
309
+ """Atomically replace the incumbent with the successor if incumbent is CURRENT.
310
+
311
+ Sets ``status=SUPERSEDED`` and ``superseded_by=successor_id`` on the
312
+ incumbent **only** when its ``status IS NULL`` (CURRENT). Appends a
313
+ ``revise`` lineage event when the guard succeeds. Returns ``False``
314
+ without mutating anything when the incumbent is not CURRENT.
315
+
316
+ Args:
317
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
318
+ or ``"profile"``.
319
+ incumbent_id (str): The id of the record to supersede.
320
+ successor_id (str): The id of the record that replaces the incumbent.
321
+ context (LineageContext): Caller-supplied intent (actor, reason, etc.).
322
+
323
+ Returns:
324
+ bool: ``True`` if the incumbent was CURRENT and was superseded;
325
+ ``False`` if the incumbent was not CURRENT and no mutation occurred.
326
+
327
+ Raises:
328
+ ValueError: If ``entity_type`` is not a recognized entity type.
329
+ ValueError: If ``context.request_id`` is empty or whitespace-only.
330
+ """
331
+ if not (context.request_id and context.request_id.strip()):
332
+ raise ValueError(f"lineage supersede: {_EMPTY_REQUEST_ID_MSG}")
333
+ table, pk = _resolve_table(entity_type)
334
+ with self._lock:
335
+ cur = self.conn.execute(
336
+ f"UPDATE {table} SET status=?, superseded_by=?, retired_at=? " # noqa: S608
337
+ f"WHERE {pk}=? AND status IS NULL",
338
+ (Status.SUPERSEDED.value, successor_id, _epoch_now(), incumbent_id),
339
+ )
340
+ if cur.rowcount == 0:
341
+ self.conn.commit()
342
+ return False
343
+ _append_event_stmt(
344
+ self.conn,
345
+ org_id=self.org_id,
346
+ entity_type=entity_type,
347
+ entity_id=successor_id,
348
+ op="revise",
349
+ prov="wasRevisionOf",
350
+ source_ids=[incumbent_id],
351
+ actor=context.actor,
352
+ request_id=context.request_id,
353
+ reason=context.reason,
354
+ )
355
+ self.conn.commit()
356
+ return True
357
+
358
+ def purge_content(self, *, entity_type: EntityType, entity_id: str) -> bool:
359
+ """Blank a record's PII body, keep its lineage skeleton, emit op=purge.
360
+
361
+ Blanks every non-skeleton column for the row identified by ``entity_id``,
362
+ emits one ``op=purge`` lineage event in the same commit, then runs FTS/vec
363
+ index cleanup after the commit. The ``request_id`` is deterministic
364
+ (``"purge_" + entity_id``) so re-runs on an already-blank row do not
365
+ produce a duplicate event (``INSERT OR IGNORE`` on the unique key).
366
+
367
+ Re-running on a row whose content is already blank is safe: the UPDATE
368
+ still matches the row and sets the same values (harmless idempotent write),
369
+ while the INSERT OR IGNORE deduplicates the event on
370
+ ``(org_id, entity_type, entity_id, op, request_id)``.
371
+
372
+ Args:
373
+ entity_type (EntityType): One of ``"user_playbook"`` or ``"profile"``.
374
+ ``"agent_playbook"`` raises ``ValueError`` — agent playbooks have no
375
+ ``user_id`` and are out of scope for content purge.
376
+ entity_id (str): The entity's primary key as a string.
377
+
378
+ Returns:
379
+ bool: ``True`` if the row exists; ``False`` if the id had no matching row.
380
+
381
+ Raises:
382
+ ValueError: If ``entity_type`` is not a recognized entity type or
383
+ if ``entity_type`` is ``"agent_playbook"`` (not supported).
384
+ """
385
+ sql = _PURGE_SQL.get(entity_type)
386
+ if sql is None:
387
+ raise ValueError(f"purge_content: unsupported entity_type {entity_type!r}")
388
+ table, pk = _resolve_table(entity_type)
389
+ with self._lock:
390
+ row = self.conn.execute(
391
+ f"SELECT rowid AS _rowid FROM {table} WHERE {pk}=?", # noqa: S608
392
+ (entity_id,),
393
+ ).fetchone()
394
+ if row is None:
395
+ return False
396
+ rowid = row["_rowid"]
397
+ cur = self.conn.execute(sql, (entity_id,))
398
+ if cur.rowcount > 0:
399
+ # Row matched (rowcount==1 whenever it exists); attempt the event —
400
+ # INSERT OR IGNORE dedups on the deterministic request_id.
401
+ _append_event_stmt(
402
+ self.conn,
403
+ org_id=self.org_id,
404
+ entity_type=entity_type,
405
+ entity_id=entity_id,
406
+ op="purge",
407
+ prov="wasPurged",
408
+ source_ids=[],
409
+ actor="erasure", # no user identifier
410
+ request_id=f"purge_{entity_id}", # deterministic → idempotent
411
+ reason="content_purge",
412
+ )
413
+ self.conn.commit()
414
+ # Post-commit index cleanup (self-committing helpers; idempotent/replayable).
415
+ self._purge_search_indexes(entity_type, entity_id, rowid)
416
+ return True
417
+
418
+ def _purge_search_indexes(
419
+ self, entity_type: EntityType, entity_id: str, rowid: int
420
+ ) -> None:
421
+ """Remove FTS and vec index entries for a purged entity.
422
+
423
+ Called after the blanking commit so index cleanup never interleaves with
424
+ the atomic mutation+event transaction.
425
+
426
+ Args:
427
+ entity_type (EntityType): One of ``"user_playbook"`` or ``"profile"``.
428
+ entity_id (str): The entity's primary key (used for profiles FTS).
429
+ rowid (int): The sqlite rowid (used for playbooks FTS and all vec tables).
430
+ """
431
+ if entity_type == "profile":
432
+ self._fts_delete_profile(entity_id) # type: ignore[attr-defined]
433
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
434
+ self._vec_delete("profiles_vec", rowid) # type: ignore[attr-defined]
435
+ elif entity_type == "user_playbook":
436
+ self._fts_delete("user_playbooks_fts", rowid) # type: ignore[attr-defined]
437
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
438
+ self._vec_delete("user_playbooks_vec", rowid) # type: ignore[attr-defined]
439
+
440
+ def has_inbound_lineage_refs(
441
+ self, *, entity_type: EntityType, entity_id: str
442
+ ) -> bool:
443
+ """Return True if any row points at ``entity_id`` via merged_into/superseded_by.
444
+
445
+ Org-scoped but deliberately NOT user_id-scoped: a cross-user chain
446
+ (one user's tombstone pointing at another user's survivor) must be
447
+ detected so the survivor is purged, not hard-deleted, on erasure.
448
+
449
+ Args:
450
+ entity_type (EntityType): One of ``"user_playbook"``, ``"agent_playbook"``,
451
+ or ``"profile"``.
452
+ entity_id (str): The entity's primary key to check for inbound refs.
453
+
454
+ Returns:
455
+ bool: True if any row has ``merged_into == entity_id``
456
+ OR ``superseded_by == entity_id``; False otherwise.
457
+ """
458
+ table, _pk = _resolve_table(entity_type)
459
+ with self._lock:
460
+ row = self.conn.execute(
461
+ f"SELECT 1 FROM {table} " # noqa: S608
462
+ f"WHERE merged_into = ? OR superseded_by = ? LIMIT 1",
463
+ (entity_id, entity_id),
464
+ ).fetchone()
465
+ return row is not None
466
+
467
+ def _is_on_legal_hold(
468
+ self,
469
+ org_id: str, # noqa: ARG002
470
+ entity_type: str, # noqa: ARG002
471
+ entity_id: str, # noqa: ARG002
472
+ ) -> bool:
473
+ """Return True if this entity is under a legal hold and must not be GC'd.
474
+
475
+ Deferred seam — always returns False until a hold store exists.
476
+
477
+ Args:
478
+ org_id (str): The organisation that owns the entity.
479
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
480
+ or ``"profile"``.
481
+ entity_id (str): The entity's primary key as a string.
482
+
483
+ Returns:
484
+ bool: False (no hold store implemented yet).
485
+ """
486
+ return False
487
+
488
+ def list_org_ids(self) -> list[str]:
489
+ """Return the single org_id for this SQLite storage instance.
490
+
491
+ SQLite storage is single-tenant: each instance is scoped to exactly one
492
+ org. Returns ``[self.org_id]``.
493
+
494
+ Returns:
495
+ list[str]: A one-element list containing this instance's org_id.
496
+ """
497
+ return [self.org_id]
498
+
499
+ def gc_expired_tombstones(
500
+ self, *, entity_type: str, older_than_epoch: int, limit: int = 1000
501
+ ) -> int:
502
+ """Hard-delete tombstone rows whose retirement instant is older than the cutoff.
503
+
504
+ Ages on the uniform INTEGER ``retired_at`` column set at every tombstone
505
+ write-path (T1). Rows with ``retired_at = NULL`` (pre-T1 tombstones) are
506
+ never selected — they have no retirement clock and must be retained.
507
+
508
+ Emits one ``hard_delete`` lineage event per deleted row, atomically, before
509
+ the DELETE commits. Rows on legal hold are skipped without emitting an event.
510
+
511
+ Args:
512
+ entity_type (str): One of ``"user_playbook"``, ``"agent_playbook"``,
513
+ or ``"profile"``.
514
+ older_than_epoch (int): Unix timestamp cutoff (exclusive). Rows whose
515
+ ``retired_at`` is strictly less than this value are eligible.
516
+ limit (int): Maximum rows to delete per call. Defaults to 1000.
517
+
518
+ Returns:
519
+ int: The number of rows physically deleted.
520
+
521
+ Raises:
522
+ ValueError: If ``entity_type`` is not a recognised entity type.
523
+ """
524
+ if limit <= 0:
525
+ return 0
526
+ table, pk = _resolve_table(entity_type)
527
+
528
+ eligible_ph = ",".join("?" * len(_GC_ELIGIBLE_STATUSES))
529
+ eligible_vals = list(_GC_ELIGIBLE_STATUSES)
530
+
531
+ # ORDER BY retired_at ASC for deterministic forward progress.
532
+ # No SQL LIMIT here — the limit is applied after the legal-hold filter
533
+ # below so held rows at the front of the batch don't starve eligible rows.
534
+ select_sql = (
535
+ f"SELECT {pk} FROM {table} " # noqa: S608
536
+ f"WHERE status IN ({eligible_ph}) "
537
+ f"AND retired_at IS NOT NULL AND retired_at < ? "
538
+ f"ORDER BY retired_at ASC"
539
+ )
540
+ select_params: list[Any] = [*eligible_vals, older_than_epoch]
541
+
542
+ with self._lock:
543
+ rows = self.conn.execute(select_sql, select_params).fetchall()
544
+ if not rows:
545
+ return 0
546
+
547
+ candidate_ids: list[str] = [str(r[0]) for r in rows]
548
+ ids_to_delete: list[str] = []
549
+ for eid in candidate_ids:
550
+ if self._is_on_legal_hold(self.org_id, entity_type, eid):
551
+ # NOTE: any real hold-check implementation must run inside
552
+ # the same transaction as the DELETE to remain atomic.
553
+ capture_anomaly(
554
+ "lineage.gc.legal_hold_skip",
555
+ level="info",
556
+ org_id=self.org_id,
557
+ entity_type=entity_type,
558
+ entity_id=eid,
559
+ )
560
+ continue
561
+ ids_to_delete.append(eid)
562
+ if len(ids_to_delete) >= limit:
563
+ break
564
+
565
+ if not ids_to_delete:
566
+ return 0
567
+
568
+ batch_request_id = uuid.uuid4().hex
569
+ ph = ",".join("?" * len(ids_to_delete))
570
+
571
+ try:
572
+ # Emit hard_delete events BEFORE the DELETE, in the same transaction.
573
+ for eid in ids_to_delete:
574
+ _append_event_stmt(
575
+ self.conn,
576
+ org_id=self.org_id,
577
+ entity_type=entity_type,
578
+ entity_id=eid,
579
+ op="hard_delete",
580
+ prov="wasInvalidatedBy",
581
+ source_ids=[],
582
+ actor="system",
583
+ request_id=batch_request_id,
584
+ reason="ttl-gc",
585
+ )
586
+
587
+ # Inline FTS/vec cleanup — raw DELETE to preserve atomicity.
588
+ # Do NOT call self._fts_delete/_vec_delete: they self-commit.
589
+ if entity_type in ("user_playbook", "agent_playbook"):
590
+ kind = "user" if entity_type == "user_playbook" else "agent"
591
+ int_ids = [int(eid) for eid in ids_to_delete]
592
+ int_ph = ",".join("?" * len(int_ids))
593
+ self.conn.execute(
594
+ f"DELETE FROM {kind}_playbooks_fts WHERE rowid IN ({int_ph})",
595
+ int_ids,
596
+ )
597
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
598
+ self.conn.execute(
599
+ f"DELETE FROM {kind}_playbooks_vec WHERE rowid IN ({int_ph})",
600
+ int_ids,
601
+ )
602
+ else:
603
+ # profiles: FTS keyed on TEXT profile_id; vec keyed on implicit rowid.
604
+ self.conn.execute(
605
+ f"DELETE FROM profiles_fts WHERE profile_id IN ({ph})",
606
+ ids_to_delete,
607
+ )
608
+ if self._has_sqlite_vec: # type: ignore[attr-defined]
609
+ rowid_rows = self.conn.execute(
610
+ f"SELECT rowid FROM profiles WHERE profile_id IN ({ph})", # noqa: S608
611
+ ids_to_delete,
612
+ ).fetchall()
613
+ if rowid_rows:
614
+ rowids = [r[0] for r in rowid_rows]
615
+ rowid_ph = ",".join("?" * len(rowids))
616
+ self.conn.execute(
617
+ f"DELETE FROM profiles_vec WHERE rowid IN ({rowid_ph})",
618
+ rowids,
619
+ )
620
+
621
+ cur = self.conn.execute(
622
+ f"DELETE FROM {table} WHERE {pk} IN ({ph})", # noqa: S608
623
+ ids_to_delete,
624
+ )
625
+ self.conn.commit()
626
+ except Exception:
627
+ self.conn.rollback()
628
+ raise
629
+
630
+ return cur.rowcount