claude-smart 0.2.45 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +22 -2
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +5 -1
  35. package/plugin/scripts/backend-service.sh +22 -2
  36. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  37. package/plugin/scripts/hook_entry.sh +1 -1
  38. package/plugin/scripts/opencode-claude-compat +4 -0
  39. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  40. package/plugin/scripts/opencode-claude-compat.js +225 -0
  41. package/plugin/scripts/smart-install.sh +10 -8
  42. package/plugin/src/README.md +1 -1
  43. package/plugin/src/claude_smart/cli.py +304 -6
  44. package/plugin/src/claude_smart/env_config.py +12 -4
  45. package/plugin/src/claude_smart/events/session_start.py +26 -7
  46. package/plugin/src/claude_smart/events/stop.py +1 -1
  47. package/plugin/src/claude_smart/ids.py +1 -1
  48. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  49. package/plugin/src/claude_smart/runtime.py +7 -1
  50. package/plugin/uv.lock +1 -1
  51. package/plugin/vendor/reflexio/.env.example +54 -30
  52. package/plugin/vendor/reflexio/README.md +14 -8
  53. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  54. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  56. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  57. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  65. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  66. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  67. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  68. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  141. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  142. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  143. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  144. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  146. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  147. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  148. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  149. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  158. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  159. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  160. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  161. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  162. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  163. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  164. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  165. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  167. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  168. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  169. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  170. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  171. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  172. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  173. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  174. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  176. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  178. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  179. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  197. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  198. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  208. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  209. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  211. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  216. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  221. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  222. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  238. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  244. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  248. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  249. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  250. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  252. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  281. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  285. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  286. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  287. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  289. package/scripts/setup-claude-smart.sh +8 -3
  290. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  317. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  318. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -14,7 +14,7 @@ import math
14
14
  import re
15
15
  import sqlite3
16
16
  import threading
17
- from collections.abc import Callable
17
+ from collections.abc import Callable, Sequence
18
18
  from datetime import UTC, datetime
19
19
  from pathlib import Path
20
20
  from typing import Any, ClassVar, Literal
@@ -22,14 +22,10 @@ from typing import Any, ClassVar, Literal
22
22
  from reflexio.models.api_schema.common import BlockingIssue
23
23
  from reflexio.models.api_schema.service_schemas import (
24
24
  AgentPlaybook,
25
- AgentPlaybookSnapshot,
26
- AgentPlaybookUpdateEntry,
27
25
  AgentSuccessEvaluationResult,
28
26
  Citation,
29
27
  Interaction,
30
- PlaybookAggregationChangeLog,
31
28
  PlaybookStatus,
32
- ProfileChangeLog,
33
29
  ProfileTimeToLive,
34
30
  RegularVsShadow,
35
31
  Request,
@@ -53,7 +49,10 @@ from reflexio.server.llm.model_defaults import (
53
49
  from reflexio.server.llm.providers.embedding_service_provider import (
54
50
  EmbeddingUnavailableError,
55
51
  )
56
- from reflexio.server.services.storage.error import StorageError
52
+ from reflexio.server.services.storage.error import (
53
+ StorageError,
54
+ require_non_empty_session_id,
55
+ )
57
56
  from reflexio.server.services.storage.retention import RetentionTarget
58
57
  from reflexio.server.services.storage.retention_mixin import (
59
58
  RETENTION_DELETE_CHUNK,
@@ -182,10 +181,10 @@ def _effective_search_mode(
182
181
 
183
182
 
184
183
  def _vector_rank_rows(
185
- rows: list[sqlite3.Row],
184
+ rows: Sequence[Any],
186
185
  query_embedding: list[float],
187
186
  match_count: int,
188
- ) -> list[sqlite3.Row]:
187
+ ) -> list[Any]:
189
188
  """Rank rows by cosine similarity to the query embedding.
190
189
 
191
190
  Args:
@@ -196,7 +195,7 @@ def _vector_rank_rows(
196
195
  Returns:
197
196
  Top ``match_count`` rows sorted by cosine similarity descending.
198
197
  """
199
- scored: list[tuple[sqlite3.Row, float]] = []
198
+ scored: list[tuple[Any, float]] = []
200
199
  for row in rows:
201
200
  raw_emb = row["embedding"] if "embedding" in row.keys() else None # noqa: SIM118
202
201
  emb = _json_loads(raw_emb) if raw_emb else None
@@ -225,14 +224,14 @@ def _vector_rank_rows(
225
224
 
226
225
 
227
226
  def _true_rrf_merge(
228
- fts_rows: list[sqlite3.Row],
229
- vec_rows: list[sqlite3.Row],
227
+ fts_rows: Sequence[Any],
228
+ vec_rows: Sequence[Any],
230
229
  id_column: str,
231
230
  match_count: int,
232
231
  rrf_k: int = 60,
233
232
  vector_weight: float = 1.0,
234
233
  fts_weight: float = 1.0,
235
- ) -> list[sqlite3.Row]:
234
+ ) -> list[Any]:
236
235
  """Merge independent FTS and vector result sets via Reciprocal Rank Fusion.
237
236
 
238
237
  Unlike ``_rrf_rerank`` (which re-ranks FTS results only), this function
@@ -255,7 +254,7 @@ def _true_rrf_merge(
255
254
  return []
256
255
 
257
256
  # Collect unique rows by ID (first-seen wins for the Row object)
258
- row_by_id: dict[str | int, sqlite3.Row] = {}
257
+ row_by_id: dict[str | int, Any] = {}
259
258
  for row in (*fts_rows, *vec_rows):
260
259
  rid = row[id_column]
261
260
  if rid not in row_by_id:
@@ -271,7 +270,7 @@ def _true_rrf_merge(
271
270
  fts_penalty = len(fts_rows) + 1
272
271
  vec_penalty = len(vec_rows) + 1
273
272
 
274
- scored: list[tuple[sqlite3.Row, float]] = []
273
+ scored: list[tuple[Any, float]] = []
275
274
  for rid, row in row_by_id.items():
276
275
  f_rank = fts_rank.get(rid, fts_penalty)
277
276
  v_rank = vec_rank.get(rid, vec_penalty)
@@ -282,6 +281,13 @@ def _true_rrf_merge(
282
281
  return [row for row, _ in scored[:match_count]]
283
282
 
284
283
 
284
+ # Tombstone statuses: rows with these values are excluded from default reads.
285
+ # Tasks 5/9/10 create tombstones; this constant ensures they stay hidden unless
286
+ # explicitly requested via include_tombstones=True on by-id getters, or an
287
+ # explicit status_filter on list/count methods.
288
+ _TOMBSTONE_STATUS_VALUES = (Status.MERGED.value, Status.SUPERSEDED.value)
289
+
290
+
285
291
  def _status_value(status: Status | None) -> str | None:
286
292
  """Convert a Status enum (or None) to its DB string value."""
287
293
  if status is None:
@@ -340,14 +346,32 @@ def _iso_to_epoch(iso_str: str | None) -> int:
340
346
  return _epoch_now()
341
347
  try:
342
348
  cleaned = iso_str.replace("Z", "+00:00")
343
- return int(datetime.fromisoformat(cleaned).timestamp())
349
+ parsed = datetime.fromisoformat(cleaned)
350
+ if parsed.tzinfo is None:
351
+ parsed = parsed.replace(tzinfo=UTC)
352
+ return int(parsed.timestamp())
344
353
  except (ValueError, TypeError):
345
354
  return _epoch_now()
346
355
 
347
356
 
357
+ # Bounds that ``datetime.fromtimestamp(tz=UTC)`` can represent (year 1..9999).
358
+ # Callers pass sentinel "open" bounds — e.g. ``to_ts=10**12`` for "no upper
359
+ # limit" or ``0`` for "from the beginning" — which would otherwise overflow
360
+ # ``datetime.fromtimestamp`` with a ``ValueError``. Clamping to these bounds
361
+ # yields the same query semantics (the ISO string still sorts before/after every
362
+ # stored row) with a valid value.
363
+ _MAX_SAFE_EPOCH_TS = 253_402_300_799 # 9999-12-31T23:59:59Z
364
+ _MIN_SAFE_EPOCH_TS = 0 # 1970-01-01T00:00:00Z
365
+
366
+
348
367
  def _epoch_to_iso(ts: int) -> str:
349
- """Convert a Unix timestamp to ISO 8601 string."""
350
- return datetime.fromtimestamp(ts, tz=UTC).isoformat()
368
+ """Convert a Unix timestamp (seconds) to an ISO 8601 string.
369
+
370
+ Out-of-range sentinel bounds are clamped to the representable range so that
371
+ callers passing "open" window bounds never trigger a ``ValueError``.
372
+ """
373
+ clamped = max(_MIN_SAFE_EPOCH_TS, min(ts, _MAX_SAFE_EPOCH_TS))
374
+ return datetime.fromtimestamp(clamped, tz=UTC).isoformat()
351
375
 
352
376
 
353
377
  # ---------------------------------------------------------------------------
@@ -373,6 +397,10 @@ def _row_to_profile(row: sqlite3.Row) -> UserProfile:
373
397
  source_span=d.get("source_span"),
374
398
  notes=d.get("notes"),
375
399
  reader_angle=d.get("reader_angle"),
400
+ tags=_json_loads(d.get("tags")),
401
+ source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
402
+ merged_into=d.get("merged_into"),
403
+ superseded_by=d.get("superseded_by"),
376
404
  )
377
405
 
378
406
 
@@ -400,6 +428,7 @@ def _row_to_interaction(row: sqlite3.Row) -> Interaction:
400
428
  user_action=UserActionType(d["user_action"]),
401
429
  user_action_description=d["user_action_description"],
402
430
  interacted_image_url=d["interacted_image_url"],
431
+ image_encoding=d.get("image_encoding") or "",
403
432
  shadow_content=d.get("shadow_content") or "",
404
433
  expert_content=d.get("expert_content") or "",
405
434
  tools_used=tools_used,
@@ -409,24 +438,14 @@ def _row_to_interaction(row: sqlite3.Row) -> Interaction:
409
438
 
410
439
  def _row_to_request(row: sqlite3.Row) -> Request:
411
440
  d = dict(row)
412
- metadata_raw = d.get("metadata") or "{}"
413
- try:
414
- parsed = _json_loads(metadata_raw)
415
- metadata = parsed if isinstance(parsed, dict) else {}
416
- except json.JSONDecodeError:
417
- logger.warning(
418
- "Malformed metadata JSON for request %s; defaulting to empty dict",
419
- d.get("request_id"),
420
- )
421
- metadata = {}
422
441
  return Request(
423
442
  request_id=d["request_id"],
424
443
  user_id=d["user_id"],
425
444
  created_at=_iso_to_epoch(d["created_at"]),
426
445
  source=d.get("source") or "",
427
446
  agent_version=d.get("agent_version") or "",
428
- session_id=d.get("session_id"),
429
- metadata=metadata,
447
+ session_id=require_non_empty_session_id(d.get("session_id")),
448
+ evaluation_only=bool(d.get("evaluation_only", 0)),
430
449
  )
431
450
 
432
451
 
@@ -455,11 +474,14 @@ def _row_to_user_playbook(
455
474
  status=Status(d["status"]) if d.get("status") else None,
456
475
  source=d.get("source"),
457
476
  source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
477
+ tags=_json_loads(d.get("tags")),
458
478
  embedding=embedding,
459
479
  expanded_terms=d.get("expanded_terms"),
460
480
  source_span=d.get("source_span"),
461
481
  notes=d.get("notes"),
462
482
  reader_angle=d.get("reader_angle"),
483
+ merged_into=d.get("merged_into"),
484
+ superseded_by=d.get("superseded_by"),
463
485
  )
464
486
 
465
487
 
@@ -480,9 +502,12 @@ def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
480
502
  if d.get("playbook_status")
481
503
  else PlaybookStatus.PENDING,
482
504
  playbook_metadata=d.get("playbook_metadata") or "",
505
+ tags=_json_loads(d.get("tags")),
483
506
  embedding=[],
484
507
  status=Status(d["status"]) if d.get("status") else None,
485
508
  expanded_terms=d.get("expanded_terms"),
509
+ merged_into=d.get("merged_into"),
510
+ superseded_by=d.get("superseded_by"),
486
511
  )
487
512
 
488
513
 
@@ -490,6 +515,7 @@ def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
490
515
  d = dict(row)
491
516
  return AgentSuccessEvaluationResult(
492
517
  result_id=d["result_id"],
518
+ user_id=d.get("user_id") or "",
493
519
  session_id=d["session_id"],
494
520
  agent_version=d["agent_version"],
495
521
  evaluation_name=d.get("evaluation_name"),
@@ -509,53 +535,6 @@ def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
509
535
  )
510
536
 
511
537
 
512
- def _row_to_profile_change_log(row: sqlite3.Row) -> ProfileChangeLog:
513
- d = dict(row)
514
- return ProfileChangeLog(
515
- id=d["id"],
516
- user_id=d["user_id"],
517
- request_id=d["request_id"],
518
- created_at=d["created_at"],
519
- added_profiles=[
520
- UserProfile(**p) for p in (_json_loads(d["added_profiles"]) or [])
521
- ],
522
- removed_profiles=[
523
- UserProfile(**p) for p in (_json_loads(d["removed_profiles"]) or [])
524
- ],
525
- mentioned_profiles=[
526
- UserProfile(**p) for p in (_json_loads(d["mentioned_profiles"]) or [])
527
- ],
528
- )
529
-
530
-
531
- def _row_to_playbook_aggregation_change_log(
532
- row: sqlite3.Row,
533
- ) -> PlaybookAggregationChangeLog:
534
- d = dict(row)
535
- return PlaybookAggregationChangeLog(
536
- id=d["id"],
537
- created_at=d["created_at"],
538
- playbook_name=d["playbook_name"],
539
- agent_version=d["agent_version"],
540
- run_mode=d["run_mode"],
541
- added_agent_playbooks=[
542
- AgentPlaybookSnapshot(**fb)
543
- for fb in (_json_loads(d.get("added_playbooks")) or [])
544
- ],
545
- removed_agent_playbooks=[
546
- AgentPlaybookSnapshot(**fb)
547
- for fb in (_json_loads(d.get("removed_playbooks")) or [])
548
- ],
549
- updated_agent_playbooks=[
550
- AgentPlaybookUpdateEntry(
551
- before=AgentPlaybookSnapshot(**entry["before"]),
552
- after=AgentPlaybookSnapshot(**entry["after"]),
553
- )
554
- for entry in (_json_loads(d.get("updated_playbooks")) or [])
555
- ],
556
- )
557
-
558
-
559
538
  # ---------------------------------------------------------------------------
560
539
  # SQLiteStorageBase
561
540
  # ---------------------------------------------------------------------------
@@ -652,6 +631,14 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
652
631
  def migrate(self) -> bool:
653
632
  self._migrate_feedback_schema()
654
633
  self._migrate_interactions_schema()
634
+ # Backfill columns that _DDL indexes depend on BEFORE running _DDL.
635
+ # _DDL builds idx_eval_identity_created_at_desc on
636
+ # agent_success_evaluation_result(user_id, ...). On a pre-existing DB that
637
+ # table predates user_id, so executescript(_DDL) raises "no such column:
638
+ # user_id" and migrate() never reaches the backfill below — leaving the DB
639
+ # permanently stuck. The helper is guarded (no-ops when the table is
640
+ # absent), so running it before _DDL is safe on fresh databases too.
641
+ self._migrate_eval_result_user_id()
655
642
  with self._lock:
656
643
  cur = self.conn.cursor()
657
644
  cur.executescript(_DDL)
@@ -663,11 +650,22 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
663
650
  self._migrate_agent_runs_schema()
664
651
  self._migrate_pending_tool_calls_schema()
665
652
  self._migrate_expanded_terms()
653
+ self._migrate_tags()
654
+ self._migrate_profile_source_interaction_ids()
655
+ self._migrate_interaction_window_indexes()
666
656
  self._migrate_agentic_signals()
667
657
  self._migrate_agent_playbook_source_windows()
668
- self._migrate_request_metadata()
658
+ self._migrate_request_evaluation_only()
659
+ self._migrate_request_session_id_required()
660
+ # _migrate_eval_result_user_id() runs before _DDL (see above).
669
661
  self._migrate_shadow_comparison_verdicts()
670
662
  self._migrate_user_playbook_polarity()
663
+ self._migrate_lineage()
664
+ self._migrate_retired_at()
665
+ self._migrate_lineage_event_table()
666
+ self._migrate_playbook_optimization_candidate_metadata()
667
+ self._migrate_retire_profile_change_logs()
668
+ self._migrate_retire_playbook_aggregation_change_logs()
671
669
  init_stall_state_table(self.conn)
672
670
  return True
673
671
 
@@ -706,9 +704,13 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
706
704
  # Wrap dependency + target deletes in a single critical section so
707
705
  # concurrent writers see either both or neither.
708
706
  with self._lock:
709
- self._retention_delete_dependencies(target, keys)
710
- self._retention_delete_target_rows(target, keys)
711
- self.conn.commit()
707
+ try:
708
+ self._retention_delete_dependencies(target, keys)
709
+ self._retention_delete_target_rows(target, keys)
710
+ self.conn.commit()
711
+ except Exception:
712
+ self.conn.rollback()
713
+ raise
712
714
 
713
715
  def _retention_delete_dependencies(
714
716
  self, target: RetentionTarget, keys: list[tuple[Any, ...]]
@@ -723,10 +725,14 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
723
725
  self._delete_profile_search_rows([str(v) for v in ids])
724
726
  elif target_name == "user_playbooks":
725
727
  self._delete_source_windows_for_user_playbook_ids([int(v) for v in ids])
726
- self._delete_playbook_search_rows("user", [int(v) for v in ids])
728
+ self._delete_playbook_search_rows(
729
+ "user", [int(v) for v in ids], commit=False
730
+ )
727
731
  elif target_name == "agent_playbooks":
728
732
  self._delete_source_windows_for_agent_playbook_ids([int(v) for v in ids])
729
- self._delete_playbook_search_rows("agent", [int(v) for v in ids])
733
+ self._delete_playbook_search_rows(
734
+ "agent", [int(v) for v in ids], commit=False
735
+ )
730
736
  elif target_name == "playbook_optimization_jobs":
731
737
  self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
732
738
  elif target_name == "playbook_optimization_candidates":
@@ -801,29 +807,63 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
801
807
  self._delete_in_chunks("interactions", "request_id", request_ids)
802
808
 
803
809
  def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
810
+ """Remove fts + vec index rows for the given interaction IDs.
811
+
812
+ Non-committing: participates in the caller's transaction. Only called
813
+ from inside the retention atomic block (_retention_perform_delete).
814
+ """
804
815
  if not interaction_ids:
805
816
  return
806
817
  self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
807
- for interaction_id in interaction_ids:
808
- self._vec_delete("interactions_vec", interaction_id)
818
+ if self._has_sqlite_vec:
819
+ self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
809
820
 
810
821
  def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
822
+ """Remove fts + vec index rows for the given profile IDs.
823
+
824
+ Non-committing: participates in the caller's transaction. Only called
825
+ from inside the retention atomic block (_retention_perform_delete).
826
+ profiles_fts is keyed by profile_id (TEXT); profiles_vec by rowid (INT).
827
+ """
811
828
  if not profile_ids:
812
829
  return
813
- rows = self._select_in_chunks(
814
- "SELECT rowid, profile_id FROM profiles WHERE profile_id IN ({placeholders})",
815
- profile_ids,
816
- )
817
- for row in rows:
818
- self._fts_delete_profile(row["profile_id"])
819
- self._vec_delete("profiles_vec", row["rowid"])
830
+ self._delete_in_chunks("profiles_fts", "profile_id", profile_ids)
831
+ if self._has_sqlite_vec:
832
+ rows = self._select_in_chunks(
833
+ "SELECT rowid FROM profiles WHERE profile_id IN ({placeholders})",
834
+ profile_ids,
835
+ )
836
+ rowids = [row["rowid"] for row in rows]
837
+ if rowids:
838
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
820
839
 
821
- def _delete_playbook_search_rows(self, kind: str, ids: list[int]) -> None:
840
+ def _delete_playbook_search_rows(
841
+ self, kind: str, ids: list[int], *, commit: bool = True
842
+ ) -> None:
843
+ """Remove fts + vec index rows for the given playbook IDs.
844
+
845
+ Args:
846
+ kind: ``"user"`` or ``"agent"``.
847
+ ids: Playbook row IDs to remove from the search indexes.
848
+ commit: When ``True`` (default) commits after the deletes so the
849
+ after-commit callers in ``_playbook.py`` get a clean, durable
850
+ cleanup. Pass ``commit=False`` from inside the retention atomic
851
+ block so the deletes participate in the single block-level commit
852
+ (``_retention_perform_delete``).
853
+
854
+ Note: callers may already hold ``self._lock`` when calling this (the
855
+ ``commit=False`` retention/atomic-delete call sites do). The internal
856
+ ``with self._lock:`` re-acquire is safe ONLY because ``self._lock`` is a
857
+ reentrant ``threading.RLock``; a non-reentrant lock would deadlock here.
858
+ """
822
859
  if not ids:
823
860
  return
824
- self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
825
- for item_id in ids:
826
- self._vec_delete(f"{kind}_playbooks_vec", item_id)
861
+ with self._lock:
862
+ self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
863
+ if self._has_sqlite_vec:
864
+ self._delete_in_chunks(f"{kind}_playbooks_vec", "rowid", ids)
865
+ if commit:
866
+ self.conn.commit()
827
867
 
828
868
  def _delete_source_windows_for_agent_playbook_ids(
829
869
  self, agent_playbook_ids: list[int]
@@ -947,6 +987,14 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
947
987
  self.conn.execute("ALTER TABLE interactions ADD COLUMN citations TEXT")
948
988
  self.conn.commit()
949
989
 
990
+ if "image_encoding" not in columns:
991
+ logger.info("Adding image_encoding column to interactions table.")
992
+ with self._lock:
993
+ self.conn.execute(
994
+ "ALTER TABLE interactions ADD COLUMN image_encoding TEXT NOT NULL DEFAULT ''"
995
+ )
996
+ self.conn.commit()
997
+
950
998
  def _migrate_feedback_schema(self) -> None:
951
999
  """Drop old-schema feedback/playbook tables so _DDL can recreate them.
952
1000
 
@@ -1109,6 +1157,39 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1109
1157
  logger.info("Added expanded_terms column to %s", table)
1110
1158
  self.conn.commit()
1111
1159
 
1160
+ def _migrate_tags(self) -> None:
1161
+ """Add tags column if missing."""
1162
+ for table in ("profiles", "user_playbooks", "agent_playbooks"):
1163
+ cols = {
1164
+ row["name"]
1165
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1166
+ }
1167
+ if "tags" not in cols:
1168
+ self.conn.execute(f"ALTER TABLE {table} ADD COLUMN tags TEXT")
1169
+ logger.info("Added tags column to %s", table)
1170
+ self.conn.commit()
1171
+
1172
+ def _migrate_profile_source_interaction_ids(self) -> None:
1173
+ """Add profile source interaction ids for provenance on existing DBs."""
1174
+ cols = {
1175
+ row["name"]
1176
+ for row in self.conn.execute("PRAGMA table_info(profiles)").fetchall()
1177
+ }
1178
+ if "source_interaction_ids" not in cols:
1179
+ self.conn.execute(
1180
+ "ALTER TABLE profiles ADD COLUMN source_interaction_ids TEXT"
1181
+ )
1182
+ logger.info("Added source_interaction_ids column to profiles")
1183
+ self.conn.commit()
1184
+
1185
+ def _migrate_interaction_window_indexes(self) -> None:
1186
+ """Add composite indexes used by sliding-window provenance lookups."""
1187
+ self.conn.execute(
1188
+ "CREATE INDEX IF NOT EXISTS idx_interactions_user_created_at_desc "
1189
+ "ON interactions(user_id, created_at DESC, interaction_id DESC)"
1190
+ )
1191
+ self.conn.commit()
1192
+
1112
1193
  def _migrate_agent_runs_schema(self) -> None:
1113
1194
  """Add resumable-agent run columns if missing from existing SQLite DBs."""
1114
1195
  cols = {
@@ -1178,6 +1259,194 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1178
1259
  logger.info("Dropped legacy polarity column from user_playbooks")
1179
1260
  self.conn.commit()
1180
1261
 
1262
+ def _migrate_lineage(self) -> None:
1263
+ """Add merged_into/superseded_by forward-pointer columns if missing.
1264
+
1265
+ Backfill-safe: columns are nullable with no default. INTEGER for playbook
1266
+ tables (int foreign-key pointers), TEXT for profiles (str profile_id pointers).
1267
+ """
1268
+ int_tables = {"user_playbooks": "INTEGER", "agent_playbooks": "INTEGER"}
1269
+ str_tables = {"profiles": "TEXT"}
1270
+ for table, coltype in {**int_tables, **str_tables}.items():
1271
+ cols = {
1272
+ row["name"]
1273
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1274
+ }
1275
+ for col in ("merged_into", "superseded_by"):
1276
+ if col not in cols:
1277
+ self.conn.execute(
1278
+ f"ALTER TABLE {table} ADD COLUMN {col} {coltype}" # noqa: S608
1279
+ )
1280
+ logger.info("Added %s column to %s", col, table)
1281
+ self.conn.commit()
1282
+
1283
+ def _migrate_retired_at(self) -> None:
1284
+ """Add nullable ``retired_at INTEGER`` GC column to tombstone-bearing tables.
1285
+
1286
+ Backfill-safe: column is nullable with no default. Existing tombstones
1287
+ will have ``retired_at = NULL`` (conservative — GC T2 uses ``retired_at``
1288
+ as the age signal, so old tombstones without it are simply not yet eligible
1289
+ by the new clock; ops can backfill via T4 if needed).
1290
+ Also creates the covering index for GC queries (idempotent).
1291
+ """
1292
+ with self._lock:
1293
+ for table in ("profiles", "user_playbooks", "agent_playbooks"):
1294
+ cols = {
1295
+ row["name"]
1296
+ for row in self.conn.execute(
1297
+ f"PRAGMA table_info({table})" # noqa: S608
1298
+ ).fetchall()
1299
+ }
1300
+ if "retired_at" not in cols:
1301
+ self.conn.execute(
1302
+ f"ALTER TABLE {table} ADD COLUMN retired_at INTEGER" # noqa: S608
1303
+ )
1304
+ logger.info("Added retired_at column to %s", table)
1305
+ self.conn.execute(
1306
+ f"CREATE INDEX IF NOT EXISTS idx_{table}_retired_at " # noqa: S608
1307
+ f"ON {table}(status, retired_at)"
1308
+ )
1309
+ self.conn.commit()
1310
+
1311
+ def _migrate_lineage_event_table(self) -> None:
1312
+ """Create the lineage_event table + index for existing databases (idempotent)."""
1313
+ with self._lock:
1314
+ self.conn.executescript("""
1315
+ CREATE TABLE IF NOT EXISTS lineage_event (
1316
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
1317
+ org_id TEXT NOT NULL,
1318
+ entity_type TEXT NOT NULL,
1319
+ entity_id TEXT NOT NULL,
1320
+ op TEXT NOT NULL,
1321
+ prov_relation TEXT NOT NULL DEFAULT '',
1322
+ source_ids TEXT NOT NULL DEFAULT '[]',
1323
+ actor TEXT NOT NULL DEFAULT '',
1324
+ request_id TEXT NOT NULL DEFAULT '',
1325
+ reason TEXT NOT NULL DEFAULT '',
1326
+ created_at INTEGER NOT NULL,
1327
+ UNIQUE (org_id, entity_type, entity_id, op, request_id)
1328
+ );
1329
+ CREATE INDEX IF NOT EXISTS idx_lineage_entity
1330
+ ON lineage_event (entity_type, entity_id);
1331
+ """)
1332
+ existing_cols = {
1333
+ row["name"]
1334
+ for row in self.conn.execute(
1335
+ "PRAGMA table_info(lineage_event)"
1336
+ ).fetchall()
1337
+ }
1338
+ for col in ("from_status", "to_status", "status_namespace"):
1339
+ if col not in existing_cols:
1340
+ self.conn.execute(
1341
+ f"ALTER TABLE lineage_event ADD COLUMN {col} TEXT" # noqa: S608
1342
+ )
1343
+ logger.info("Added %s column to lineage_event", col)
1344
+ self.conn.commit()
1345
+
1346
+ def _migrate_playbook_optimization_candidate_metadata(self) -> None:
1347
+ """Add metadata_json to legacy optimizer candidate tables when missing."""
1348
+ cols = {
1349
+ row["name"]
1350
+ for row in self.conn.execute(
1351
+ "PRAGMA table_info(playbook_optimization_candidates)"
1352
+ ).fetchall()
1353
+ }
1354
+ if not cols:
1355
+ return
1356
+ if "metadata_json" not in cols:
1357
+ self.conn.execute(
1358
+ "ALTER TABLE playbook_optimization_candidates "
1359
+ "ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'"
1360
+ )
1361
+ logger.info(
1362
+ "Added metadata_json column to playbook_optimization_candidates"
1363
+ )
1364
+ self.conn.commit()
1365
+
1366
+ def _migrate_retire_profile_change_logs(self) -> None:
1367
+ """Retire the frozen ``profile_change_logs`` table via a reversible RENAME.
1368
+
1369
+ Lineage B3 Task 8: the legacy change log is fully de-referenced (no
1370
+ readers, writers, or GDPR delete callers remain) and the view is served
1371
+ from reconstruction. We rename the table out of the way now and DROP it
1372
+ in a later migration after the recovery window — keeping the data
1373
+ recoverable in the interim.
1374
+
1375
+ Idempotent: SQLite has no ``RENAME ... IF EXISTS``, so we guard on the
1376
+ source table's presence and no-op once it has been renamed. The
1377
+ ``CREATE TABLE`` for ``profile_change_logs`` was deleted from ``_DDL`` so
1378
+ ``executescript(_DDL)`` does not recreate an empty table after the rename.
1379
+ """
1380
+ with self._lock:
1381
+ row = self.conn.execute(
1382
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1383
+ ("profile_change_logs",),
1384
+ ).fetchone()
1385
+ if row is None:
1386
+ return
1387
+ target_row = self.conn.execute(
1388
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1389
+ ("profile_change_logs_retired_20260623",),
1390
+ ).fetchone()
1391
+ if target_row is not None:
1392
+ logger.info(
1393
+ "Retired target profile_change_logs_retired_20260623 already exists;"
1394
+ " skipping rename (idempotent no-op)."
1395
+ )
1396
+ return
1397
+ self.conn.execute(
1398
+ "ALTER TABLE profile_change_logs "
1399
+ "RENAME TO profile_change_logs_retired_20260623"
1400
+ )
1401
+ logger.info(
1402
+ "Renamed profile_change_logs to "
1403
+ "profile_change_logs_retired_20260623 (B3 Task 8 retirement)"
1404
+ )
1405
+ self.conn.commit()
1406
+
1407
+ def _migrate_retire_playbook_aggregation_change_logs(self) -> None:
1408
+ """Retire the frozen ``playbook_aggregation_change_logs`` table via a reversible RENAME.
1409
+
1410
+ Lineage Track B Task 4: the legacy change log is fully de-referenced (no
1411
+ readers, writers, or GDPR delete callers remain) and the view is served
1412
+ from reconstruction. We rename the table out of the way now and DROP it
1413
+ in a later migration after the recovery window — keeping the data
1414
+ recoverable in the interim.
1415
+
1416
+ Idempotent: SQLite has no ``RENAME ... IF EXISTS``, so we guard on the
1417
+ source table's presence and no-op once it has been renamed. The
1418
+ ``CREATE TABLE`` for ``playbook_aggregation_change_logs`` was deleted from
1419
+ ``_DDL`` so ``executescript(_DDL)`` does not recreate an empty table after
1420
+ the rename.
1421
+ """
1422
+ with self._lock:
1423
+ row = self.conn.execute(
1424
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1425
+ ("playbook_aggregation_change_logs",),
1426
+ ).fetchone()
1427
+ if row is None:
1428
+ return
1429
+ target_row = self.conn.execute(
1430
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1431
+ ("playbook_aggregation_change_logs_retired_20260624",),
1432
+ ).fetchone()
1433
+ if target_row is not None:
1434
+ logger.info(
1435
+ "Retired target playbook_aggregation_change_logs_retired_20260624"
1436
+ " already exists; skipping rename (idempotent no-op)."
1437
+ )
1438
+ return
1439
+ self.conn.execute(
1440
+ "ALTER TABLE playbook_aggregation_change_logs "
1441
+ "RENAME TO playbook_aggregation_change_logs_retired_20260624"
1442
+ )
1443
+ logger.info(
1444
+ "Renamed playbook_aggregation_change_logs to "
1445
+ "playbook_aggregation_change_logs_retired_20260624 "
1446
+ "(Track B Task 4 retirement)"
1447
+ )
1448
+ self.conn.commit()
1449
+
1181
1450
  def _migrate_agent_playbook_source_windows(self) -> None:
1182
1451
  """Add source window snapshots to existing agent source mappings."""
1183
1452
  cols = {
@@ -1203,24 +1472,122 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1203
1472
  )
1204
1473
  self.conn.commit()
1205
1474
 
1206
- def _migrate_request_metadata(self) -> None:
1207
- """Add metadata column to requests for databases created before F2.
1475
+ def _migrate_request_evaluation_only(self) -> None:
1476
+ """Add evaluation_only column to requests for learning exclusion."""
1477
+ cols = {
1478
+ row["name"]
1479
+ for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
1480
+ }
1481
+ if not cols:
1482
+ return
1483
+ if "evaluation_only" not in cols:
1484
+ self.conn.execute(
1485
+ "ALTER TABLE requests ADD COLUMN evaluation_only INTEGER NOT NULL DEFAULT 0"
1486
+ )
1487
+ logger.info("Added evaluation_only column to requests")
1488
+ self.conn.commit()
1208
1489
 
1209
- The column stores a JSON-encoded dict per request (see Request.metadata
1210
- in the Pydantic model). Backfill-safe: existing rows pick up the
1211
- ``'{}'`` default and round-trip as an empty dict.
1490
+ def _migrate_request_session_id_required(self) -> None:
1491
+ """Require non-empty session ids on ``requests``.
1492
+
1493
+ SQLite cannot add a ``NOT NULL`` or ``CHECK`` constraint to an
1494
+ existing column, so existing databases are rebuilt in place. Historical
1495
+ null/blank sessions are intentionally backfilled per request to avoid
1496
+ inventing conversation groupings that were never recorded.
1212
1497
  """
1213
1498
  cols = {
1214
- row["name"]
1499
+ row["name"]: row
1215
1500
  for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
1216
1501
  }
1502
+ if not cols or "session_id" not in cols:
1503
+ return
1504
+
1505
+ table_sql_row = self.conn.execute(
1506
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'requests'"
1507
+ ).fetchone()
1508
+ table_sql = (table_sql_row["sql"] if table_sql_row else "") or ""
1509
+ blank_count = self.conn.execute(
1510
+ "SELECT COUNT(*) FROM requests WHERE session_id IS NULL OR trim(session_id) = ''"
1511
+ ).fetchone()[0]
1512
+ has_required_schema = (
1513
+ bool(cols["session_id"]["notnull"])
1514
+ and "CHECK (trim(session_id) != '')" in table_sql
1515
+ )
1516
+ if has_required_schema and blank_count == 0:
1517
+ return
1518
+
1519
+ evaluation_only_expr = (
1520
+ "COALESCE(evaluation_only, 0)" if "evaluation_only" in cols else "0"
1521
+ )
1522
+ # NOTE: this rebuild hardcodes the full `requests` column set. If a
1523
+ # future migration adds a column to `requests`, it MUST be added here
1524
+ # too (and to the SELECT below) or the rebuild will silently drop it.
1525
+ self.conn.executescript(
1526
+ f"""
1527
+ CREATE TABLE requests_new (
1528
+ request_id TEXT PRIMARY KEY,
1529
+ user_id TEXT NOT NULL,
1530
+ created_at TEXT NOT NULL,
1531
+ source TEXT NOT NULL DEFAULT '',
1532
+ agent_version TEXT NOT NULL DEFAULT '',
1533
+ session_id TEXT NOT NULL CHECK (trim(session_id) != ''),
1534
+ evaluation_only INTEGER NOT NULL DEFAULT 0
1535
+ );
1536
+ INSERT INTO requests_new
1537
+ (
1538
+ request_id,
1539
+ user_id,
1540
+ created_at,
1541
+ source,
1542
+ agent_version,
1543
+ session_id,
1544
+ evaluation_only
1545
+ )
1546
+ SELECT
1547
+ request_id,
1548
+ user_id,
1549
+ created_at,
1550
+ COALESCE(source, ''),
1551
+ COALESCE(agent_version, ''),
1552
+ CASE
1553
+ WHEN session_id IS NULL OR trim(session_id) = ''
1554
+ THEN 'legacy-' || lower(hex(randomblob(16)))
1555
+ ELSE trim(session_id)
1556
+ END,
1557
+ {evaluation_only_expr}
1558
+ FROM requests;
1559
+ DROP TABLE requests;
1560
+ ALTER TABLE requests_new RENAME TO requests;
1561
+ CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
1562
+ CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
1563
+ CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
1564
+ """
1565
+ )
1566
+ self.conn.commit()
1567
+ logger.info("Migrated requests.session_id to required non-empty values")
1568
+
1569
+ def _migrate_eval_result_user_id(self) -> None:
1570
+ """Add user_id to session evaluation results for per-user identity."""
1571
+ cols = {
1572
+ row["name"]
1573
+ for row in self.conn.execute(
1574
+ "PRAGMA table_info(agent_success_evaluation_result)"
1575
+ ).fetchall()
1576
+ }
1217
1577
  if not cols:
1218
1578
  return
1219
- if "metadata" not in cols:
1579
+ if "user_id" not in cols:
1220
1580
  self.conn.execute(
1221
- "ALTER TABLE requests ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'"
1581
+ "ALTER TABLE agent_success_evaluation_result "
1582
+ "ADD COLUMN user_id TEXT NOT NULL DEFAULT ''"
1222
1583
  )
1223
- logger.info("Added metadata column to requests")
1584
+ logger.info("Added user_id column to agent_success_evaluation_result")
1585
+ self.conn.execute("DROP INDEX IF EXISTS idx_eval_identity_created_at_desc")
1586
+ self.conn.execute(
1587
+ "CREATE INDEX IF NOT EXISTS idx_eval_identity_created_at_desc "
1588
+ "ON agent_success_evaluation_result"
1589
+ "(user_id, session_id, evaluation_name, agent_version, created_at DESC)"
1590
+ )
1224
1591
  self.conn.commit()
1225
1592
 
1226
1593
  def _migrate_shadow_comparison_verdicts(self) -> None:
@@ -1228,8 +1595,7 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1228
1595
 
1229
1596
  Idempotent; safe to run on every startup. The PRAGMA-LBYL guard
1230
1597
  avoids running the CREATE statements on every boot for
1231
- already-migrated DBs mirrors the :meth:`_migrate_request_metadata`
1232
- pattern. The ``CREATE TABLE IF NOT EXISTS`` in :data:`_DDL` will
1598
+ already-migrated DBs. The ``CREATE TABLE IF NOT EXISTS`` in :data:`_DDL` will
1233
1599
  also create this table on a fresh database, so this helper is a
1234
1600
  no-op there; its purpose is explicit symmetry with the per-feature
1235
1601
  migration convention and a single named hook the disk/supabase
@@ -1461,28 +1827,51 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1461
1827
  # ------------------------------------------------------------------
1462
1828
 
1463
1829
  def clear_user_data(self, user_id: str) -> dict[str, int]:
1464
- """Atomic per-``user_id`` row deletion across all user-scoped tables.
1830
+ """Per-``user_id`` row deletion across all user-scoped tables.
1465
1831
 
1466
- Overrides the BaseStorage default with a single-transaction SQL
1467
- implementation. Removes interactions, user playbooks, profiles,
1468
- and requests scoped to the user. Intentionally does NOT touch
1469
- ``agent_playbooks`` — they are the cross-project rollup of
1470
- skills and have no ``user_id`` column.
1832
+ Overrides the BaseStorage default with an optimized SQL implementation.
1833
+ Removes interactions, user playbooks, profiles, and requests scoped to
1834
+ the user. Intentionally does NOT touch ``agent_playbooks`` — they are
1835
+ the cross-project rollup of skills and have no ``user_id`` column.
1471
1836
 
1472
1837
  Also cleans up FTS and vector sidecars for the user's rows so
1473
1838
  subsequent searches don't surface deleted data.
1474
1839
 
1840
+ **Lineage-aware erasure for profiles and user_playbooks:**
1841
+ Rows that are tombstones (``merged_into`` or ``superseded_by`` is set)
1842
+ *or* are pointed-to by another row (``has_inbound_lineage_refs`` returns
1843
+ True) are **content-purged** (skeleton kept, body blanked) rather than
1844
+ hard-deleted. This preserves chain resolution across user erasures.
1845
+ Standalone rows with no lineage involvement are hard-deleted as before.
1846
+
1847
+ The purge/delete decision delegates to
1848
+ ``BaseStorage._partition_purge_vs_delete`` so the logic is defined once
1849
+ and shared with the default ``clear_user_data`` implementation used by
1850
+ Supabase/Postgres backends.
1851
+
1852
+ **Commit ordering (atomicity invariant):**
1853
+ The hard-deletes for interactions, requests, and the delete-sets of
1854
+ profiles/user_playbooks are committed in one transaction first. Then
1855
+ ``purge_content`` is called for each purge-eligible row — each call
1856
+ commits atomically on its own. This two-phase approach is required
1857
+ because ``purge_content`` issues its own ``conn.commit()``, and nesting
1858
+ it inside the outer transaction would prematurely flush the still-pending
1859
+ hard-DELETEs.
1860
+
1475
1861
  Args:
1476
1862
  user_id (str): The user id whose rows should be deleted.
1477
1863
 
1478
1864
  Returns:
1479
- dict[str, int]: Per-entity deletion counts with keys
1480
- ``interactions``, ``user_playbooks``, ``profiles``, and
1481
- ``requests``.
1865
+ dict[str, int]: Per-entity counts with keys ``interactions``,
1866
+ ``user_playbooks``, ``profiles``, ``requests``,
1867
+ ``purged_profiles``, and ``purged_user_playbooks``.
1868
+ ``profiles`` and ``user_playbooks`` reflect hard-deleted counts;
1869
+ purged rows are counted separately.
1482
1870
  """
1483
1871
  with self._lock:
1484
- # Snapshot rowids/ids that need FTS or vector cleanup before
1485
- # the DELETE removes them from the main tables.
1872
+ # ------------------------------------------------------------------
1873
+ # Phase 1: snapshot all user-scoped ids before any mutations.
1874
+ # ------------------------------------------------------------------
1486
1875
  interaction_ids = [
1487
1876
  r["interaction_id"]
1488
1877
  for r in self.conn.execute(
@@ -1490,7 +1879,7 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1490
1879
  (user_id,),
1491
1880
  ).fetchall()
1492
1881
  ]
1493
- user_playbook_ids = [
1882
+ raw_upb_ids = [
1494
1883
  r["user_playbook_id"]
1495
1884
  for r in self.conn.execute(
1496
1885
  "SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
@@ -1501,67 +1890,97 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1501
1890
  "SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
1502
1891
  (user_id,),
1503
1892
  ).fetchall()
1504
- profile_rowids = [r["rowid"] for r in profile_rows]
1505
- profile_ids = [r["profile_id"] for r in profile_rows]
1506
1893
 
1507
- # FTS cleanup
1508
- if interaction_ids:
1509
- ph = ",".join("?" for _ in interaction_ids)
1510
- self.conn.execute(
1511
- f"DELETE FROM interactions_fts WHERE rowid IN ({ph})",
1512
- interaction_ids,
1513
- )
1514
- if user_playbook_ids:
1515
- ph = ",".join("?" for _ in user_playbook_ids)
1516
- self.conn.execute(
1517
- f"DELETE FROM user_playbooks_fts WHERE rowid IN ({ph})",
1518
- user_playbook_ids,
1519
- )
1520
- if profile_ids:
1521
- ph = ",".join("?" for _ in profile_ids)
1522
- self.conn.execute(
1523
- f"DELETE FROM profiles_fts WHERE profile_id IN ({ph})",
1524
- profile_ids,
1525
- )
1894
+ # Build a rowid lookup for FTS/vec cleanup (SQLite-specific need).
1895
+ profile_rowid_by_id: dict[str, int] = {
1896
+ r["profile_id"]: r["rowid"] for r in profile_rows
1897
+ }
1898
+ all_profile_ids = list(profile_rowid_by_id.keys())
1899
+
1900
+ # ------------------------------------------------------------------
1901
+ # Phase 2: partition profiles and user_playbooks into purge vs delete.
1902
+ # Delegates to the shared BaseStorage helper so the decision logic
1903
+ # is defined once and reused by all backends.
1904
+ # ------------------------------------------------------------------
1905
+ purge_profile_ids, delete_profile_ids = self._partition_purge_vs_delete(
1906
+ "profile", all_profile_ids
1907
+ )
1908
+ purge_upb_str_ids, delete_upb_str_ids = self._partition_purge_vs_delete(
1909
+ "user_playbook", [str(uid) for uid in raw_upb_ids]
1910
+ )
1911
+ purge_upb_ids = [int(s) for s in purge_upb_str_ids]
1912
+ delete_upb_ids = [int(s) for s in delete_upb_str_ids]
1913
+
1914
+ # Rowids for the delete-set only (purge_content handles its own cleanup).
1915
+ delete_profile_rowids = [
1916
+ profile_rowid_by_id[pid]
1917
+ for pid in delete_profile_ids
1918
+ if pid in profile_rowid_by_id
1919
+ ]
1920
+
1921
+ # ------------------------------------------------------------------
1922
+ # Phase 3: FTS and vector cleanup — only for the delete-sets
1923
+ # (purge_content handles its own index cleanup for purged rows).
1924
+ # Use _delete_in_chunks to stay under SQLite's SQLITE_MAX_VARIABLE_NUMBER
1925
+ # limit for large user datasets.
1926
+ # ------------------------------------------------------------------
1927
+ self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
1928
+ self._delete_in_chunks("user_playbooks_fts", "rowid", delete_upb_ids)
1929
+ self._delete_in_chunks("profiles_fts", "profile_id", delete_profile_ids)
1526
1930
 
1527
- # Vector index cleanup (best-effort: only if sqlite-vec loaded)
1528
1931
  if self._has_sqlite_vec:
1529
- vec_targets = (
1530
- ("interactions_vec", interaction_ids),
1531
- ("user_playbooks_vec", user_playbook_ids),
1532
- ("profiles_vec", profile_rowids),
1533
- )
1534
- for vec_table, rowids in vec_targets:
1535
- if rowids:
1536
- ph = ",".join("?" for _ in rowids)
1537
- self.conn.execute(
1538
- f"DELETE FROM {vec_table} WHERE rowid IN ({ph})",
1539
- rowids,
1540
- )
1932
+ self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
1933
+ self._delete_in_chunks("user_playbooks_vec", "rowid", delete_upb_ids)
1934
+ self._delete_in_chunks("profiles_vec", "rowid", delete_profile_rowids)
1541
1935
 
1542
- # Main-table deletes. Order matters only for foreign key
1543
- # integrity; SQLite default has FK off for most tables here
1544
- # so order is chosen for readability.
1936
+ # ------------------------------------------------------------------
1937
+ # Phase 4: hard-delete the delete-sets and all interactions/requests.
1938
+ # ------------------------------------------------------------------
1545
1939
  interactions_cur = self.conn.execute(
1546
1940
  "DELETE FROM interactions WHERE user_id = ?", (user_id,)
1547
1941
  )
1548
- user_playbooks_cur = self.conn.execute(
1549
- "DELETE FROM user_playbooks WHERE user_id = ?", (user_id,)
1550
- )
1551
- profiles_cur = self.conn.execute(
1552
- "DELETE FROM profiles WHERE user_id = ?", (user_id,)
1553
- )
1554
1942
  requests_cur = self.conn.execute(
1555
1943
  "DELETE FROM requests WHERE user_id = ?", (user_id,)
1556
1944
  )
1945
+ upb_deleted_count = 0
1946
+ if delete_upb_ids:
1947
+ # Clean up source-window join rows before deleting the parent rows.
1948
+ self._delete_source_windows_for_user_playbook_ids(delete_upb_ids)
1949
+ self._delete_in_chunks(
1950
+ "user_playbooks", "user_playbook_id", delete_upb_ids
1951
+ )
1952
+ # rowcount not available from _delete_in_chunks; derive from list length
1953
+ # (all ids came from a pre-snapshot so they exist at delete time).
1954
+ upb_deleted_count = len(delete_upb_ids)
1955
+ profile_deleted_count = 0
1956
+ if delete_profile_ids:
1957
+ self._delete_in_chunks("profiles", "profile_id", delete_profile_ids)
1958
+ profile_deleted_count = len(delete_profile_ids)
1959
+
1960
+ # Commit the hard-deletes before calling purge_content, because
1961
+ # purge_content issues its own conn.commit() and nesting it here
1962
+ # would prematurely flush the still-pending deletes.
1557
1963
  self.conn.commit()
1558
1964
 
1559
- return {
1560
- "interactions": interactions_cur.rowcount,
1561
- "user_playbooks": user_playbooks_cur.rowcount,
1562
- "profiles": profiles_cur.rowcount,
1563
- "requests": requests_cur.rowcount,
1564
- }
1965
+ # Phase 5: content-purge the purge-sets WITHOUT releasing the lock,
1966
+ # so erase-eligible rows are never observable by another thread with
1967
+ # PII still intact between the hard-delete commit and the purge. Each
1968
+ # purge_content call self-commits; self._lock is an RLock so its
1969
+ # internal ``with self._lock`` re-acquires cleanly, and the commit
1970
+ # above already closed the outer transaction (no flush hazard).
1971
+ for pid in purge_profile_ids:
1972
+ self.purge_content(entity_type="profile", entity_id=str(pid))
1973
+ for upid in purge_upb_ids:
1974
+ self.purge_content(entity_type="user_playbook", entity_id=str(upid))
1975
+
1976
+ return {
1977
+ "interactions": interactions_cur.rowcount,
1978
+ "user_playbooks": upb_deleted_count,
1979
+ "profiles": profile_deleted_count,
1980
+ "requests": requests_cur.rowcount,
1981
+ "purged_profiles": len(purge_profile_ids),
1982
+ "purged_user_playbooks": len(purge_upb_ids),
1983
+ }
1565
1984
 
1566
1985
 
1567
1986
  # ---------------------------------------------------------------------------
@@ -1583,10 +2002,15 @@ CREATE TABLE IF NOT EXISTS profiles (
1583
2002
  status TEXT,
1584
2003
  extractor_names TEXT,
1585
2004
  expanded_terms TEXT,
2005
+ tags TEXT,
2006
+ source_interaction_ids TEXT,
1586
2007
  source_span TEXT,
1587
2008
  notes TEXT,
1588
2009
  reader_angle TEXT,
1589
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
2010
+ merged_into TEXT,
2011
+ superseded_by TEXT,
2012
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
2013
+ retired_at INTEGER
1590
2014
  );
1591
2015
  CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
1592
2016
  CREATE INDEX IF NOT EXISTS idx_profiles_status ON profiles(status);
@@ -1601,6 +2025,7 @@ CREATE TABLE IF NOT EXISTS interactions (
1601
2025
  user_action TEXT NOT NULL DEFAULT 'none',
1602
2026
  user_action_description TEXT NOT NULL DEFAULT '',
1603
2027
  interacted_image_url TEXT NOT NULL DEFAULT '',
2028
+ image_encoding TEXT NOT NULL DEFAULT '',
1604
2029
  shadow_content TEXT NOT NULL DEFAULT '',
1605
2030
  expert_content TEXT NOT NULL DEFAULT '',
1606
2031
  tools_used TEXT,
@@ -1610,6 +2035,8 @@ CREATE TABLE IF NOT EXISTS interactions (
1610
2035
  CREATE INDEX IF NOT EXISTS idx_interactions_user_id ON interactions(user_id);
1611
2036
  CREATE INDEX IF NOT EXISTS idx_interactions_request_id ON interactions(request_id);
1612
2037
  CREATE INDEX IF NOT EXISTS idx_interactions_created_at ON interactions(created_at);
2038
+ CREATE INDEX IF NOT EXISTS idx_interactions_user_created_at_desc
2039
+ ON interactions(user_id, created_at DESC, interaction_id DESC);
1613
2040
 
1614
2041
  CREATE TABLE IF NOT EXISTS requests (
1615
2042
  request_id TEXT PRIMARY KEY,
@@ -1617,12 +2044,14 @@ CREATE TABLE IF NOT EXISTS requests (
1617
2044
  created_at TEXT NOT NULL,
1618
2045
  source TEXT NOT NULL DEFAULT '',
1619
2046
  agent_version TEXT NOT NULL DEFAULT '',
1620
- session_id TEXT,
1621
- metadata TEXT NOT NULL DEFAULT '{}'
2047
+ session_id TEXT NOT NULL CHECK (trim(session_id) != ''),
2048
+ evaluation_only INTEGER NOT NULL DEFAULT 0
1622
2049
  );
1623
2050
  CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
1624
2051
  CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
1625
2052
  CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
2053
+ CREATE INDEX IF NOT EXISTS idx_requests_session_created_at_asc
2054
+ ON requests(session_id, created_at ASC, request_id ASC);
1626
2055
 
1627
2056
  CREATE TABLE IF NOT EXISTS user_playbooks (
1628
2057
  user_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1640,9 +2069,13 @@ CREATE TABLE IF NOT EXISTS user_playbooks (
1640
2069
  source TEXT,
1641
2070
  embedding TEXT,
1642
2071
  expanded_terms TEXT,
2072
+ tags TEXT,
1643
2073
  source_span TEXT,
1644
2074
  notes TEXT,
1645
- reader_angle TEXT
2075
+ reader_angle TEXT,
2076
+ merged_into INTEGER,
2077
+ superseded_by INTEGER,
2078
+ retired_at INTEGER
1646
2079
  );
1647
2080
  CREATE INDEX IF NOT EXISTS idx_user_playbooks_playbook_name ON user_playbooks(playbook_name);
1648
2081
  CREATE INDEX IF NOT EXISTS idx_user_playbooks_agent_version ON user_playbooks(agent_version);
@@ -1662,7 +2095,11 @@ CREATE TABLE IF NOT EXISTS agent_playbooks (
1662
2095
  playbook_metadata TEXT NOT NULL DEFAULT '',
1663
2096
  embedding TEXT,
1664
2097
  expanded_terms TEXT,
1665
- status TEXT
2098
+ tags TEXT,
2099
+ status TEXT,
2100
+ merged_into INTEGER,
2101
+ superseded_by INTEGER,
2102
+ retired_at INTEGER
1666
2103
  );
1667
2104
  CREATE INDEX IF NOT EXISTS idx_agent_playbooks_playbook_name ON agent_playbooks(playbook_name);
1668
2105
  CREATE INDEX IF NOT EXISTS idx_agent_playbooks_agent_version ON agent_playbooks(agent_version);
@@ -1671,6 +2108,7 @@ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_created_at ON agent_playbooks(cre
1671
2108
 
1672
2109
  CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
1673
2110
  result_id INTEGER PRIMARY KEY AUTOINCREMENT,
2111
+ user_id TEXT NOT NULL DEFAULT '',
1674
2112
  session_id TEXT NOT NULL,
1675
2113
  agent_version TEXT NOT NULL DEFAULT '',
1676
2114
  evaluation_name TEXT,
@@ -1686,31 +2124,12 @@ CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
1686
2124
  );
1687
2125
  CREATE INDEX IF NOT EXISTS idx_eval_agent_version ON agent_success_evaluation_result(agent_version);
1688
2126
  CREATE INDEX IF NOT EXISTS idx_eval_created_at ON agent_success_evaluation_result(created_at);
1689
-
1690
- CREATE TABLE IF NOT EXISTS profile_change_logs (
1691
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1692
- user_id TEXT NOT NULL,
1693
- request_id TEXT NOT NULL,
1694
- created_at INTEGER NOT NULL,
1695
- added_profiles TEXT NOT NULL DEFAULT '[]',
1696
- removed_profiles TEXT NOT NULL DEFAULT '[]',
1697
- mentioned_profiles TEXT NOT NULL DEFAULT '[]'
1698
- );
1699
- CREATE INDEX IF NOT EXISTS idx_pcl_user_id ON profile_change_logs(user_id);
1700
- CREATE INDEX IF NOT EXISTS idx_pcl_created_at ON profile_change_logs(created_at);
1701
-
1702
- CREATE TABLE IF NOT EXISTS playbook_aggregation_change_logs (
1703
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1704
- created_at INTEGER NOT NULL,
1705
- playbook_name TEXT NOT NULL,
1706
- agent_version TEXT NOT NULL,
1707
- run_mode TEXT NOT NULL,
1708
- added_playbooks TEXT NOT NULL DEFAULT '[]',
1709
- removed_playbooks TEXT NOT NULL DEFAULT '[]',
1710
- updated_playbooks TEXT NOT NULL DEFAULT '[]'
1711
- );
1712
- CREATE INDEX IF NOT EXISTS idx_pacl_playbook_name ON playbook_aggregation_change_logs(playbook_name);
1713
- CREATE INDEX IF NOT EXISTS idx_pacl_agent_version ON playbook_aggregation_change_logs(agent_version);
2127
+ CREATE INDEX IF NOT EXISTS idx_eval_created_at_desc
2128
+ ON agent_success_evaluation_result(created_at DESC);
2129
+ CREATE INDEX IF NOT EXISTS idx_eval_agent_version_created_at_desc
2130
+ ON agent_success_evaluation_result(agent_version, created_at DESC);
2131
+ CREATE INDEX IF NOT EXISTS idx_eval_identity_created_at_desc
2132
+ ON agent_success_evaluation_result(user_id, session_id, evaluation_name, agent_version, created_at DESC);
1714
2133
 
1715
2134
  CREATE TABLE IF NOT EXISTS agent_playbook_source_user_playbooks (
1716
2135
  agent_playbook_id INTEGER NOT NULL,
@@ -1745,6 +2164,7 @@ CREATE TABLE IF NOT EXISTS playbook_optimization_candidates (
1745
2164
  parent_candidate_ids TEXT NOT NULL DEFAULT '[]',
1746
2165
  aggregate_score REAL,
1747
2166
  is_winner INTEGER NOT NULL DEFAULT 0,
2167
+ metadata_json TEXT NOT NULL DEFAULT '{}',
1748
2168
  created_at INTEGER NOT NULL
1749
2169
  );
1750
2170
  CREATE INDEX IF NOT EXISTS idx_poc_job ON playbook_optimization_candidates(job_id);
@@ -1941,5 +2361,30 @@ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
1941
2361
  ON shadow_comparison_verdicts (created_at);
1942
2362
  CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
1943
2363
  ON shadow_comparison_verdicts (judge_prompt_version);
2364
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_created_at_desc
2365
+ ON shadow_comparison_verdicts (judge_prompt_version, created_at DESC);
2366
+
2367
+ -- ============================================================================
2368
+ -- Append-only, content-free lineage event log
2369
+ -- ============================================================================
2370
+
2371
+ CREATE TABLE IF NOT EXISTS lineage_event (
2372
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
2373
+ org_id TEXT NOT NULL,
2374
+ entity_type TEXT NOT NULL,
2375
+ entity_id TEXT NOT NULL,
2376
+ op TEXT NOT NULL,
2377
+ prov_relation TEXT NOT NULL DEFAULT '',
2378
+ source_ids TEXT NOT NULL DEFAULT '[]',
2379
+ actor TEXT NOT NULL DEFAULT '',
2380
+ request_id TEXT NOT NULL DEFAULT '',
2381
+ reason TEXT NOT NULL DEFAULT '',
2382
+ created_at INTEGER NOT NULL,
2383
+ from_status TEXT,
2384
+ to_status TEXT,
2385
+ status_namespace TEXT,
2386
+ UNIQUE (org_id, entity_type, entity_id, op, request_id)
2387
+ );
2388
+ CREATE INDEX IF NOT EXISTS idx_lineage_entity ON lineage_event (entity_type, entity_id);
1944
2389
 
1945
2390
  """