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
@@ -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,
@@ -63,6 +62,7 @@ from reflexio.server.services.storage.retention_mixin import (
63
62
  from reflexio.server.services.storage.storage_base import BaseStorage
64
63
  from reflexio.server.site_var.site_var_manager import SiteVarManager
65
64
 
65
+ from ._governance import init_governance_tables
66
66
  from ._stall_state import init_stall_state_table
67
67
 
68
68
  logger = logging.getLogger(__name__)
@@ -182,10 +182,10 @@ def _effective_search_mode(
182
182
 
183
183
 
184
184
  def _vector_rank_rows(
185
- rows: list[sqlite3.Row],
185
+ rows: Sequence[Any],
186
186
  query_embedding: list[float],
187
187
  match_count: int,
188
- ) -> list[sqlite3.Row]:
188
+ ) -> list[Any]:
189
189
  """Rank rows by cosine similarity to the query embedding.
190
190
 
191
191
  Args:
@@ -196,7 +196,7 @@ def _vector_rank_rows(
196
196
  Returns:
197
197
  Top ``match_count`` rows sorted by cosine similarity descending.
198
198
  """
199
- scored: list[tuple[sqlite3.Row, float]] = []
199
+ scored: list[tuple[Any, float]] = []
200
200
  for row in rows:
201
201
  raw_emb = row["embedding"] if "embedding" in row.keys() else None # noqa: SIM118
202
202
  emb = _json_loads(raw_emb) if raw_emb else None
@@ -225,14 +225,14 @@ def _vector_rank_rows(
225
225
 
226
226
 
227
227
  def _true_rrf_merge(
228
- fts_rows: list[sqlite3.Row],
229
- vec_rows: list[sqlite3.Row],
228
+ fts_rows: Sequence[Any],
229
+ vec_rows: Sequence[Any],
230
230
  id_column: str,
231
231
  match_count: int,
232
232
  rrf_k: int = 60,
233
233
  vector_weight: float = 1.0,
234
234
  fts_weight: float = 1.0,
235
- ) -> list[sqlite3.Row]:
235
+ ) -> list[Any]:
236
236
  """Merge independent FTS and vector result sets via Reciprocal Rank Fusion.
237
237
 
238
238
  Unlike ``_rrf_rerank`` (which re-ranks FTS results only), this function
@@ -255,7 +255,7 @@ def _true_rrf_merge(
255
255
  return []
256
256
 
257
257
  # Collect unique rows by ID (first-seen wins for the Row object)
258
- row_by_id: dict[str | int, sqlite3.Row] = {}
258
+ row_by_id: dict[str | int, Any] = {}
259
259
  for row in (*fts_rows, *vec_rows):
260
260
  rid = row[id_column]
261
261
  if rid not in row_by_id:
@@ -271,7 +271,7 @@ def _true_rrf_merge(
271
271
  fts_penalty = len(fts_rows) + 1
272
272
  vec_penalty = len(vec_rows) + 1
273
273
 
274
- scored: list[tuple[sqlite3.Row, float]] = []
274
+ scored: list[tuple[Any, float]] = []
275
275
  for rid, row in row_by_id.items():
276
276
  f_rank = fts_rank.get(rid, fts_penalty)
277
277
  v_rank = vec_rank.get(rid, vec_penalty)
@@ -282,6 +282,13 @@ def _true_rrf_merge(
282
282
  return [row for row, _ in scored[:match_count]]
283
283
 
284
284
 
285
+ # Tombstone statuses: rows with these values are excluded from default reads.
286
+ # Tasks 5/9/10 create tombstones; this constant ensures they stay hidden unless
287
+ # explicitly requested via include_tombstones=True on by-id getters, or an
288
+ # explicit status_filter on list/count methods.
289
+ _TOMBSTONE_STATUS_VALUES = (Status.MERGED.value, Status.SUPERSEDED.value)
290
+
291
+
285
292
  def _status_value(status: Status | None) -> str | None:
286
293
  """Convert a Status enum (or None) to its DB string value."""
287
294
  if status is None:
@@ -340,14 +347,32 @@ def _iso_to_epoch(iso_str: str | None) -> int:
340
347
  return _epoch_now()
341
348
  try:
342
349
  cleaned = iso_str.replace("Z", "+00:00")
343
- return int(datetime.fromisoformat(cleaned).timestamp())
350
+ parsed = datetime.fromisoformat(cleaned)
351
+ if parsed.tzinfo is None:
352
+ parsed = parsed.replace(tzinfo=UTC)
353
+ return int(parsed.timestamp())
344
354
  except (ValueError, TypeError):
345
355
  return _epoch_now()
346
356
 
347
357
 
358
+ # Bounds that ``datetime.fromtimestamp(tz=UTC)`` can represent (year 1..9999).
359
+ # Callers pass sentinel "open" bounds — e.g. ``to_ts=10**12`` for "no upper
360
+ # limit" or ``0`` for "from the beginning" — which would otherwise overflow
361
+ # ``datetime.fromtimestamp`` with a ``ValueError``. Clamping to these bounds
362
+ # yields the same query semantics (the ISO string still sorts before/after every
363
+ # stored row) with a valid value.
364
+ _MAX_SAFE_EPOCH_TS = 253_402_300_799 # 9999-12-31T23:59:59Z
365
+ _MIN_SAFE_EPOCH_TS = 0 # 1970-01-01T00:00:00Z
366
+
367
+
348
368
  def _epoch_to_iso(ts: int) -> str:
349
- """Convert a Unix timestamp to ISO 8601 string."""
350
- return datetime.fromtimestamp(ts, tz=UTC).isoformat()
369
+ """Convert a Unix timestamp (seconds) to an ISO 8601 string.
370
+
371
+ Out-of-range sentinel bounds are clamped to the representable range so that
372
+ callers passing "open" window bounds never trigger a ``ValueError``.
373
+ """
374
+ clamped = max(_MIN_SAFE_EPOCH_TS, min(ts, _MAX_SAFE_EPOCH_TS))
375
+ return datetime.fromtimestamp(clamped, tz=UTC).isoformat()
351
376
 
352
377
 
353
378
  # ---------------------------------------------------------------------------
@@ -373,6 +398,10 @@ def _row_to_profile(row: sqlite3.Row) -> UserProfile:
373
398
  source_span=d.get("source_span"),
374
399
  notes=d.get("notes"),
375
400
  reader_angle=d.get("reader_angle"),
401
+ tags=_json_loads(d.get("tags")),
402
+ source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
403
+ merged_into=d.get("merged_into"),
404
+ superseded_by=d.get("superseded_by"),
376
405
  )
377
406
 
378
407
 
@@ -400,6 +429,7 @@ def _row_to_interaction(row: sqlite3.Row) -> Interaction:
400
429
  user_action=UserActionType(d["user_action"]),
401
430
  user_action_description=d["user_action_description"],
402
431
  interacted_image_url=d["interacted_image_url"],
432
+ image_encoding=d.get("image_encoding") or "",
403
433
  shadow_content=d.get("shadow_content") or "",
404
434
  expert_content=d.get("expert_content") or "",
405
435
  tools_used=tools_used,
@@ -409,24 +439,14 @@ def _row_to_interaction(row: sqlite3.Row) -> Interaction:
409
439
 
410
440
  def _row_to_request(row: sqlite3.Row) -> Request:
411
441
  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
442
  return Request(
423
443
  request_id=d["request_id"],
424
444
  user_id=d["user_id"],
425
445
  created_at=_iso_to_epoch(d["created_at"]),
426
446
  source=d.get("source") or "",
427
447
  agent_version=d.get("agent_version") or "",
428
- session_id=d.get("session_id"),
429
- metadata=metadata,
448
+ session_id=require_non_empty_session_id(d.get("session_id")),
449
+ evaluation_only=bool(d.get("evaluation_only", 0)),
430
450
  )
431
451
 
432
452
 
@@ -455,11 +475,14 @@ def _row_to_user_playbook(
455
475
  status=Status(d["status"]) if d.get("status") else None,
456
476
  source=d.get("source"),
457
477
  source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
478
+ tags=_json_loads(d.get("tags")),
458
479
  embedding=embedding,
459
480
  expanded_terms=d.get("expanded_terms"),
460
481
  source_span=d.get("source_span"),
461
482
  notes=d.get("notes"),
462
483
  reader_angle=d.get("reader_angle"),
484
+ merged_into=d.get("merged_into"),
485
+ superseded_by=d.get("superseded_by"),
463
486
  )
464
487
 
465
488
 
@@ -480,9 +503,12 @@ def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
480
503
  if d.get("playbook_status")
481
504
  else PlaybookStatus.PENDING,
482
505
  playbook_metadata=d.get("playbook_metadata") or "",
506
+ tags=_json_loads(d.get("tags")),
483
507
  embedding=[],
484
508
  status=Status(d["status"]) if d.get("status") else None,
485
509
  expanded_terms=d.get("expanded_terms"),
510
+ merged_into=d.get("merged_into"),
511
+ superseded_by=d.get("superseded_by"),
486
512
  )
487
513
 
488
514
 
@@ -490,6 +516,7 @@ def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
490
516
  d = dict(row)
491
517
  return AgentSuccessEvaluationResult(
492
518
  result_id=d["result_id"],
519
+ user_id=d.get("user_id") or "",
493
520
  session_id=d["session_id"],
494
521
  agent_version=d["agent_version"],
495
522
  evaluation_name=d.get("evaluation_name"),
@@ -509,53 +536,6 @@ def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
509
536
  )
510
537
 
511
538
 
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
539
  # ---------------------------------------------------------------------------
560
540
  # SQLiteStorageBase
561
541
  # ---------------------------------------------------------------------------
@@ -652,9 +632,18 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
652
632
  def migrate(self) -> bool:
653
633
  self._migrate_feedback_schema()
654
634
  self._migrate_interactions_schema()
635
+ # Backfill columns that _DDL indexes depend on BEFORE running _DDL.
636
+ # _DDL builds idx_eval_identity_created_at_desc on
637
+ # agent_success_evaluation_result(user_id, ...). On a pre-existing DB that
638
+ # table predates user_id, so executescript(_DDL) raises "no such column:
639
+ # user_id" and migrate() never reaches the backfill below — leaving the DB
640
+ # permanently stuck. The helper is guarded (no-ops when the table is
641
+ # absent), so running it before _DDL is safe on fresh databases too.
642
+ self._migrate_eval_result_user_id()
655
643
  with self._lock:
656
644
  cur = self.conn.cursor()
657
645
  cur.executescript(_DDL)
646
+ init_governance_tables(self.conn)
658
647
  self.conn.commit()
659
648
  if self._has_sqlite_vec:
660
649
  self._create_vec_tables()
@@ -663,11 +652,22 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
663
652
  self._migrate_agent_runs_schema()
664
653
  self._migrate_pending_tool_calls_schema()
665
654
  self._migrate_expanded_terms()
655
+ self._migrate_tags()
656
+ self._migrate_profile_source_interaction_ids()
657
+ self._migrate_interaction_window_indexes()
666
658
  self._migrate_agentic_signals()
667
659
  self._migrate_agent_playbook_source_windows()
668
- self._migrate_request_metadata()
660
+ self._migrate_request_evaluation_only()
661
+ self._migrate_request_session_id_required()
662
+ # _migrate_eval_result_user_id() runs before _DDL (see above).
669
663
  self._migrate_shadow_comparison_verdicts()
670
664
  self._migrate_user_playbook_polarity()
665
+ self._migrate_lineage()
666
+ self._migrate_retired_at()
667
+ self._migrate_lineage_event_table()
668
+ self._migrate_playbook_optimization_candidate_metadata()
669
+ self._migrate_retire_profile_change_logs()
670
+ self._migrate_retire_playbook_aggregation_change_logs()
671
671
  init_stall_state_table(self.conn)
672
672
  return True
673
673
 
@@ -706,9 +706,13 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
706
706
  # Wrap dependency + target deletes in a single critical section so
707
707
  # concurrent writers see either both or neither.
708
708
  with self._lock:
709
- self._retention_delete_dependencies(target, keys)
710
- self._retention_delete_target_rows(target, keys)
711
- self.conn.commit()
709
+ try:
710
+ self._retention_delete_dependencies(target, keys)
711
+ self._retention_delete_target_rows(target, keys)
712
+ self.conn.commit()
713
+ except Exception:
714
+ self.conn.rollback()
715
+ raise
712
716
 
713
717
  def _retention_delete_dependencies(
714
718
  self, target: RetentionTarget, keys: list[tuple[Any, ...]]
@@ -723,10 +727,14 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
723
727
  self._delete_profile_search_rows([str(v) for v in ids])
724
728
  elif target_name == "user_playbooks":
725
729
  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])
730
+ self._delete_playbook_search_rows(
731
+ "user", [int(v) for v in ids], commit=False
732
+ )
727
733
  elif target_name == "agent_playbooks":
728
734
  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])
735
+ self._delete_playbook_search_rows(
736
+ "agent", [int(v) for v in ids], commit=False
737
+ )
730
738
  elif target_name == "playbook_optimization_jobs":
731
739
  self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
732
740
  elif target_name == "playbook_optimization_candidates":
@@ -801,29 +809,63 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
801
809
  self._delete_in_chunks("interactions", "request_id", request_ids)
802
810
 
803
811
  def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
812
+ """Remove fts + vec index rows for the given interaction IDs.
813
+
814
+ Non-committing: participates in the caller's transaction. Only called
815
+ from inside the retention atomic block (_retention_perform_delete).
816
+ """
804
817
  if not interaction_ids:
805
818
  return
806
819
  self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
807
- for interaction_id in interaction_ids:
808
- self._vec_delete("interactions_vec", interaction_id)
820
+ if self._has_sqlite_vec:
821
+ self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
809
822
 
810
823
  def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
824
+ """Remove fts + vec index rows for the given profile IDs.
825
+
826
+ Non-committing: participates in the caller's transaction. Only called
827
+ from inside the retention atomic block (_retention_perform_delete).
828
+ profiles_fts is keyed by profile_id (TEXT); profiles_vec by rowid (INT).
829
+ """
811
830
  if not profile_ids:
812
831
  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"])
832
+ self._delete_in_chunks("profiles_fts", "profile_id", profile_ids)
833
+ if self._has_sqlite_vec:
834
+ rows = self._select_in_chunks(
835
+ "SELECT rowid FROM profiles WHERE profile_id IN ({placeholders})",
836
+ profile_ids,
837
+ )
838
+ rowids = [row["rowid"] for row in rows]
839
+ if rowids:
840
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
820
841
 
821
- def _delete_playbook_search_rows(self, kind: str, ids: list[int]) -> None:
842
+ def _delete_playbook_search_rows(
843
+ self, kind: str, ids: list[int], *, commit: bool = True
844
+ ) -> None:
845
+ """Remove fts + vec index rows for the given playbook IDs.
846
+
847
+ Args:
848
+ kind: ``"user"`` or ``"agent"``.
849
+ ids: Playbook row IDs to remove from the search indexes.
850
+ commit: When ``True`` (default) commits after the deletes so the
851
+ after-commit callers in ``_playbook.py`` get a clean, durable
852
+ cleanup. Pass ``commit=False`` from inside the retention atomic
853
+ block so the deletes participate in the single block-level commit
854
+ (``_retention_perform_delete``).
855
+
856
+ Note: callers may already hold ``self._lock`` when calling this (the
857
+ ``commit=False`` retention/atomic-delete call sites do). The internal
858
+ ``with self._lock:`` re-acquire is safe ONLY because ``self._lock`` is a
859
+ reentrant ``threading.RLock``; a non-reentrant lock would deadlock here.
860
+ """
822
861
  if not ids:
823
862
  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)
863
+ with self._lock:
864
+ self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
865
+ if self._has_sqlite_vec:
866
+ self._delete_in_chunks(f"{kind}_playbooks_vec", "rowid", ids)
867
+ if commit:
868
+ self.conn.commit()
827
869
 
828
870
  def _delete_source_windows_for_agent_playbook_ids(
829
871
  self, agent_playbook_ids: list[int]
@@ -947,6 +989,14 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
947
989
  self.conn.execute("ALTER TABLE interactions ADD COLUMN citations TEXT")
948
990
  self.conn.commit()
949
991
 
992
+ if "image_encoding" not in columns:
993
+ logger.info("Adding image_encoding column to interactions table.")
994
+ with self._lock:
995
+ self.conn.execute(
996
+ "ALTER TABLE interactions ADD COLUMN image_encoding TEXT NOT NULL DEFAULT ''"
997
+ )
998
+ self.conn.commit()
999
+
950
1000
  def _migrate_feedback_schema(self) -> None:
951
1001
  """Drop old-schema feedback/playbook tables so _DDL can recreate them.
952
1002
 
@@ -1109,6 +1159,39 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1109
1159
  logger.info("Added expanded_terms column to %s", table)
1110
1160
  self.conn.commit()
1111
1161
 
1162
+ def _migrate_tags(self) -> None:
1163
+ """Add tags column if missing."""
1164
+ for table in ("profiles", "user_playbooks", "agent_playbooks"):
1165
+ cols = {
1166
+ row["name"]
1167
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1168
+ }
1169
+ if "tags" not in cols:
1170
+ self.conn.execute(f"ALTER TABLE {table} ADD COLUMN tags TEXT")
1171
+ logger.info("Added tags column to %s", table)
1172
+ self.conn.commit()
1173
+
1174
+ def _migrate_profile_source_interaction_ids(self) -> None:
1175
+ """Add profile source interaction ids for provenance on existing DBs."""
1176
+ cols = {
1177
+ row["name"]
1178
+ for row in self.conn.execute("PRAGMA table_info(profiles)").fetchall()
1179
+ }
1180
+ if "source_interaction_ids" not in cols:
1181
+ self.conn.execute(
1182
+ "ALTER TABLE profiles ADD COLUMN source_interaction_ids TEXT"
1183
+ )
1184
+ logger.info("Added source_interaction_ids column to profiles")
1185
+ self.conn.commit()
1186
+
1187
+ def _migrate_interaction_window_indexes(self) -> None:
1188
+ """Add composite indexes used by sliding-window provenance lookups."""
1189
+ self.conn.execute(
1190
+ "CREATE INDEX IF NOT EXISTS idx_interactions_user_created_at_desc "
1191
+ "ON interactions(user_id, created_at DESC, interaction_id DESC)"
1192
+ )
1193
+ self.conn.commit()
1194
+
1112
1195
  def _migrate_agent_runs_schema(self) -> None:
1113
1196
  """Add resumable-agent run columns if missing from existing SQLite DBs."""
1114
1197
  cols = {
@@ -1178,6 +1261,194 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1178
1261
  logger.info("Dropped legacy polarity column from user_playbooks")
1179
1262
  self.conn.commit()
1180
1263
 
1264
+ def _migrate_lineage(self) -> None:
1265
+ """Add merged_into/superseded_by forward-pointer columns if missing.
1266
+
1267
+ Backfill-safe: columns are nullable with no default. INTEGER for playbook
1268
+ tables (int foreign-key pointers), TEXT for profiles (str profile_id pointers).
1269
+ """
1270
+ int_tables = {"user_playbooks": "INTEGER", "agent_playbooks": "INTEGER"}
1271
+ str_tables = {"profiles": "TEXT"}
1272
+ for table, coltype in {**int_tables, **str_tables}.items():
1273
+ cols = {
1274
+ row["name"]
1275
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1276
+ }
1277
+ for col in ("merged_into", "superseded_by"):
1278
+ if col not in cols:
1279
+ self.conn.execute(
1280
+ f"ALTER TABLE {table} ADD COLUMN {col} {coltype}" # noqa: S608
1281
+ )
1282
+ logger.info("Added %s column to %s", col, table)
1283
+ self.conn.commit()
1284
+
1285
+ def _migrate_retired_at(self) -> None:
1286
+ """Add nullable ``retired_at INTEGER`` GC column to tombstone-bearing tables.
1287
+
1288
+ Backfill-safe: column is nullable with no default. Existing tombstones
1289
+ will have ``retired_at = NULL`` (conservative — GC T2 uses ``retired_at``
1290
+ as the age signal, so old tombstones without it are simply not yet eligible
1291
+ by the new clock; ops can backfill via T4 if needed).
1292
+ Also creates the covering index for GC queries (idempotent).
1293
+ """
1294
+ with self._lock:
1295
+ for table in ("profiles", "user_playbooks", "agent_playbooks"):
1296
+ cols = {
1297
+ row["name"]
1298
+ for row in self.conn.execute(
1299
+ f"PRAGMA table_info({table})" # noqa: S608
1300
+ ).fetchall()
1301
+ }
1302
+ if "retired_at" not in cols:
1303
+ self.conn.execute(
1304
+ f"ALTER TABLE {table} ADD COLUMN retired_at INTEGER" # noqa: S608
1305
+ )
1306
+ logger.info("Added retired_at column to %s", table)
1307
+ self.conn.execute(
1308
+ f"CREATE INDEX IF NOT EXISTS idx_{table}_retired_at " # noqa: S608
1309
+ f"ON {table}(status, retired_at)"
1310
+ )
1311
+ self.conn.commit()
1312
+
1313
+ def _migrate_lineage_event_table(self) -> None:
1314
+ """Create the lineage_event table + index for existing databases (idempotent)."""
1315
+ with self._lock:
1316
+ self.conn.executescript("""
1317
+ CREATE TABLE IF NOT EXISTS lineage_event (
1318
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
1319
+ org_id TEXT NOT NULL,
1320
+ entity_type TEXT NOT NULL,
1321
+ entity_id TEXT NOT NULL,
1322
+ op TEXT NOT NULL,
1323
+ prov_relation TEXT NOT NULL DEFAULT '',
1324
+ source_ids TEXT NOT NULL DEFAULT '[]',
1325
+ actor TEXT NOT NULL DEFAULT '',
1326
+ request_id TEXT NOT NULL DEFAULT '',
1327
+ reason TEXT NOT NULL DEFAULT '',
1328
+ created_at INTEGER NOT NULL,
1329
+ UNIQUE (org_id, entity_type, entity_id, op, request_id)
1330
+ );
1331
+ CREATE INDEX IF NOT EXISTS idx_lineage_entity
1332
+ ON lineage_event (entity_type, entity_id);
1333
+ """)
1334
+ existing_cols = {
1335
+ row["name"]
1336
+ for row in self.conn.execute(
1337
+ "PRAGMA table_info(lineage_event)"
1338
+ ).fetchall()
1339
+ }
1340
+ for col in ("from_status", "to_status", "status_namespace"):
1341
+ if col not in existing_cols:
1342
+ self.conn.execute(
1343
+ f"ALTER TABLE lineage_event ADD COLUMN {col} TEXT" # noqa: S608
1344
+ )
1345
+ logger.info("Added %s column to lineage_event", col)
1346
+ self.conn.commit()
1347
+
1348
+ def _migrate_playbook_optimization_candidate_metadata(self) -> None:
1349
+ """Add metadata_json to legacy optimizer candidate tables when missing."""
1350
+ cols = {
1351
+ row["name"]
1352
+ for row in self.conn.execute(
1353
+ "PRAGMA table_info(playbook_optimization_candidates)"
1354
+ ).fetchall()
1355
+ }
1356
+ if not cols:
1357
+ return
1358
+ if "metadata_json" not in cols:
1359
+ self.conn.execute(
1360
+ "ALTER TABLE playbook_optimization_candidates "
1361
+ "ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'"
1362
+ )
1363
+ logger.info(
1364
+ "Added metadata_json column to playbook_optimization_candidates"
1365
+ )
1366
+ self.conn.commit()
1367
+
1368
+ def _migrate_retire_profile_change_logs(self) -> None:
1369
+ """Retire the frozen ``profile_change_logs`` table via a reversible RENAME.
1370
+
1371
+ Lineage B3 Task 8: the legacy change log is fully de-referenced (no
1372
+ readers, writers, or GDPR delete callers remain) and the view is served
1373
+ from reconstruction. We rename the table out of the way now and DROP it
1374
+ in a later migration after the recovery window — keeping the data
1375
+ recoverable in the interim.
1376
+
1377
+ Idempotent: SQLite has no ``RENAME ... IF EXISTS``, so we guard on the
1378
+ source table's presence and no-op once it has been renamed. The
1379
+ ``CREATE TABLE`` for ``profile_change_logs`` was deleted from ``_DDL`` so
1380
+ ``executescript(_DDL)`` does not recreate an empty table after the rename.
1381
+ """
1382
+ with self._lock:
1383
+ row = self.conn.execute(
1384
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1385
+ ("profile_change_logs",),
1386
+ ).fetchone()
1387
+ if row is None:
1388
+ return
1389
+ target_row = self.conn.execute(
1390
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1391
+ ("profile_change_logs_retired_20260623",),
1392
+ ).fetchone()
1393
+ if target_row is not None:
1394
+ logger.info(
1395
+ "Retired target profile_change_logs_retired_20260623 already exists;"
1396
+ " skipping rename (idempotent no-op)."
1397
+ )
1398
+ return
1399
+ self.conn.execute(
1400
+ "ALTER TABLE profile_change_logs "
1401
+ "RENAME TO profile_change_logs_retired_20260623"
1402
+ )
1403
+ logger.info(
1404
+ "Renamed profile_change_logs to "
1405
+ "profile_change_logs_retired_20260623 (B3 Task 8 retirement)"
1406
+ )
1407
+ self.conn.commit()
1408
+
1409
+ def _migrate_retire_playbook_aggregation_change_logs(self) -> None:
1410
+ """Retire the frozen ``playbook_aggregation_change_logs`` table via a reversible RENAME.
1411
+
1412
+ Lineage Track B Task 4: the legacy change log is fully de-referenced (no
1413
+ readers, writers, or GDPR delete callers remain) and the view is served
1414
+ from reconstruction. We rename the table out of the way now and DROP it
1415
+ in a later migration after the recovery window — keeping the data
1416
+ recoverable in the interim.
1417
+
1418
+ Idempotent: SQLite has no ``RENAME ... IF EXISTS``, so we guard on the
1419
+ source table's presence and no-op once it has been renamed. The
1420
+ ``CREATE TABLE`` for ``playbook_aggregation_change_logs`` was deleted from
1421
+ ``_DDL`` so ``executescript(_DDL)`` does not recreate an empty table after
1422
+ the rename.
1423
+ """
1424
+ with self._lock:
1425
+ row = self.conn.execute(
1426
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1427
+ ("playbook_aggregation_change_logs",),
1428
+ ).fetchone()
1429
+ if row is None:
1430
+ return
1431
+ target_row = self.conn.execute(
1432
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
1433
+ ("playbook_aggregation_change_logs_retired_20260624",),
1434
+ ).fetchone()
1435
+ if target_row is not None:
1436
+ logger.info(
1437
+ "Retired target playbook_aggregation_change_logs_retired_20260624"
1438
+ " already exists; skipping rename (idempotent no-op)."
1439
+ )
1440
+ return
1441
+ self.conn.execute(
1442
+ "ALTER TABLE playbook_aggregation_change_logs "
1443
+ "RENAME TO playbook_aggregation_change_logs_retired_20260624"
1444
+ )
1445
+ logger.info(
1446
+ "Renamed playbook_aggregation_change_logs to "
1447
+ "playbook_aggregation_change_logs_retired_20260624 "
1448
+ "(Track B Task 4 retirement)"
1449
+ )
1450
+ self.conn.commit()
1451
+
1181
1452
  def _migrate_agent_playbook_source_windows(self) -> None:
1182
1453
  """Add source window snapshots to existing agent source mappings."""
1183
1454
  cols = {
@@ -1203,24 +1474,122 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1203
1474
  )
1204
1475
  self.conn.commit()
1205
1476
 
1206
- def _migrate_request_metadata(self) -> None:
1207
- """Add metadata column to requests for databases created before F2.
1477
+ def _migrate_request_evaluation_only(self) -> None:
1478
+ """Add evaluation_only column to requests for learning exclusion."""
1479
+ cols = {
1480
+ row["name"]
1481
+ for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
1482
+ }
1483
+ if not cols:
1484
+ return
1485
+ if "evaluation_only" not in cols:
1486
+ self.conn.execute(
1487
+ "ALTER TABLE requests ADD COLUMN evaluation_only INTEGER NOT NULL DEFAULT 0"
1488
+ )
1489
+ logger.info("Added evaluation_only column to requests")
1490
+ self.conn.commit()
1208
1491
 
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.
1492
+ def _migrate_request_session_id_required(self) -> None:
1493
+ """Require non-empty session ids on ``requests``.
1494
+
1495
+ SQLite cannot add a ``NOT NULL`` or ``CHECK`` constraint to an
1496
+ existing column, so existing databases are rebuilt in place. Historical
1497
+ null/blank sessions are intentionally backfilled per request to avoid
1498
+ inventing conversation groupings that were never recorded.
1212
1499
  """
1213
1500
  cols = {
1214
- row["name"]
1501
+ row["name"]: row
1215
1502
  for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
1216
1503
  }
1504
+ if not cols or "session_id" not in cols:
1505
+ return
1506
+
1507
+ table_sql_row = self.conn.execute(
1508
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'requests'"
1509
+ ).fetchone()
1510
+ table_sql = (table_sql_row["sql"] if table_sql_row else "") or ""
1511
+ blank_count = self.conn.execute(
1512
+ "SELECT COUNT(*) FROM requests WHERE session_id IS NULL OR trim(session_id) = ''"
1513
+ ).fetchone()[0]
1514
+ has_required_schema = (
1515
+ bool(cols["session_id"]["notnull"])
1516
+ and "CHECK (trim(session_id) != '')" in table_sql
1517
+ )
1518
+ if has_required_schema and blank_count == 0:
1519
+ return
1520
+
1521
+ evaluation_only_expr = (
1522
+ "COALESCE(evaluation_only, 0)" if "evaluation_only" in cols else "0"
1523
+ )
1524
+ # NOTE: this rebuild hardcodes the full `requests` column set. If a
1525
+ # future migration adds a column to `requests`, it MUST be added here
1526
+ # too (and to the SELECT below) or the rebuild will silently drop it.
1527
+ self.conn.executescript(
1528
+ f"""
1529
+ CREATE TABLE requests_new (
1530
+ request_id TEXT PRIMARY KEY,
1531
+ user_id TEXT NOT NULL,
1532
+ created_at TEXT NOT NULL,
1533
+ source TEXT NOT NULL DEFAULT '',
1534
+ agent_version TEXT NOT NULL DEFAULT '',
1535
+ session_id TEXT NOT NULL CHECK (trim(session_id) != ''),
1536
+ evaluation_only INTEGER NOT NULL DEFAULT 0
1537
+ );
1538
+ INSERT INTO requests_new
1539
+ (
1540
+ request_id,
1541
+ user_id,
1542
+ created_at,
1543
+ source,
1544
+ agent_version,
1545
+ session_id,
1546
+ evaluation_only
1547
+ )
1548
+ SELECT
1549
+ request_id,
1550
+ user_id,
1551
+ created_at,
1552
+ COALESCE(source, ''),
1553
+ COALESCE(agent_version, ''),
1554
+ CASE
1555
+ WHEN session_id IS NULL OR trim(session_id) = ''
1556
+ THEN 'legacy-' || lower(hex(randomblob(16)))
1557
+ ELSE trim(session_id)
1558
+ END,
1559
+ {evaluation_only_expr}
1560
+ FROM requests;
1561
+ DROP TABLE requests;
1562
+ ALTER TABLE requests_new RENAME TO requests;
1563
+ CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
1564
+ CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
1565
+ CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
1566
+ """
1567
+ )
1568
+ self.conn.commit()
1569
+ logger.info("Migrated requests.session_id to required non-empty values")
1570
+
1571
+ def _migrate_eval_result_user_id(self) -> None:
1572
+ """Add user_id to session evaluation results for per-user identity."""
1573
+ cols = {
1574
+ row["name"]
1575
+ for row in self.conn.execute(
1576
+ "PRAGMA table_info(agent_success_evaluation_result)"
1577
+ ).fetchall()
1578
+ }
1217
1579
  if not cols:
1218
1580
  return
1219
- if "metadata" not in cols:
1581
+ if "user_id" not in cols:
1220
1582
  self.conn.execute(
1221
- "ALTER TABLE requests ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'"
1583
+ "ALTER TABLE agent_success_evaluation_result "
1584
+ "ADD COLUMN user_id TEXT NOT NULL DEFAULT ''"
1222
1585
  )
1223
- logger.info("Added metadata column to requests")
1586
+ logger.info("Added user_id column to agent_success_evaluation_result")
1587
+ self.conn.execute("DROP INDEX IF EXISTS idx_eval_identity_created_at_desc")
1588
+ self.conn.execute(
1589
+ "CREATE INDEX IF NOT EXISTS idx_eval_identity_created_at_desc "
1590
+ "ON agent_success_evaluation_result"
1591
+ "(user_id, session_id, evaluation_name, agent_version, created_at DESC)"
1592
+ )
1224
1593
  self.conn.commit()
1225
1594
 
1226
1595
  def _migrate_shadow_comparison_verdicts(self) -> None:
@@ -1228,8 +1597,7 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1228
1597
 
1229
1598
  Idempotent; safe to run on every startup. The PRAGMA-LBYL guard
1230
1599
  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
1600
+ already-migrated DBs. The ``CREATE TABLE IF NOT EXISTS`` in :data:`_DDL` will
1233
1601
  also create this table on a fresh database, so this helper is a
1234
1602
  no-op there; its purpose is explicit symmetry with the per-feature
1235
1603
  migration convention and a single named hook the disk/supabase
@@ -1461,28 +1829,51 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1461
1829
  # ------------------------------------------------------------------
1462
1830
 
1463
1831
  def clear_user_data(self, user_id: str) -> dict[str, int]:
1464
- """Atomic per-``user_id`` row deletion across all user-scoped tables.
1832
+ """Per-``user_id`` row deletion across all user-scoped tables.
1465
1833
 
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.
1834
+ Overrides the BaseStorage default with an optimized SQL implementation.
1835
+ Removes interactions, user playbooks, profiles, and requests scoped to
1836
+ the user. Intentionally does NOT touch ``agent_playbooks`` — they are
1837
+ the cross-project rollup of skills and have no ``user_id`` column.
1471
1838
 
1472
1839
  Also cleans up FTS and vector sidecars for the user's rows so
1473
1840
  subsequent searches don't surface deleted data.
1474
1841
 
1842
+ **Lineage-aware erasure for profiles and user_playbooks:**
1843
+ Rows that are tombstones (``merged_into`` or ``superseded_by`` is set)
1844
+ *or* are pointed-to by another row (``has_inbound_lineage_refs`` returns
1845
+ True) are **content-purged** (skeleton kept, body blanked) rather than
1846
+ hard-deleted. This preserves chain resolution across user erasures.
1847
+ Standalone rows with no lineage involvement are hard-deleted as before.
1848
+
1849
+ The purge/delete decision delegates to
1850
+ ``BaseStorage._partition_purge_vs_delete`` so the logic is defined once
1851
+ and shared with the default ``clear_user_data`` implementation used by
1852
+ Supabase/Postgres backends.
1853
+
1854
+ **Commit ordering (atomicity invariant):**
1855
+ The hard-deletes for interactions, requests, and the delete-sets of
1856
+ profiles/user_playbooks are committed in one transaction first. Then
1857
+ ``purge_content`` is called for each purge-eligible row — each call
1858
+ commits atomically on its own. This two-phase approach is required
1859
+ because ``purge_content`` issues its own ``conn.commit()``, and nesting
1860
+ it inside the outer transaction would prematurely flush the still-pending
1861
+ hard-DELETEs.
1862
+
1475
1863
  Args:
1476
1864
  user_id (str): The user id whose rows should be deleted.
1477
1865
 
1478
1866
  Returns:
1479
- dict[str, int]: Per-entity deletion counts with keys
1480
- ``interactions``, ``user_playbooks``, ``profiles``, and
1481
- ``requests``.
1867
+ dict[str, int]: Per-entity counts with keys ``interactions``,
1868
+ ``user_playbooks``, ``profiles``, ``requests``,
1869
+ ``purged_profiles``, and ``purged_user_playbooks``.
1870
+ ``profiles`` and ``user_playbooks`` reflect hard-deleted counts;
1871
+ purged rows are counted separately.
1482
1872
  """
1483
1873
  with self._lock:
1484
- # Snapshot rowids/ids that need FTS or vector cleanup before
1485
- # the DELETE removes them from the main tables.
1874
+ # ------------------------------------------------------------------
1875
+ # Phase 1: snapshot all user-scoped ids before any mutations.
1876
+ # ------------------------------------------------------------------
1486
1877
  interaction_ids = [
1487
1878
  r["interaction_id"]
1488
1879
  for r in self.conn.execute(
@@ -1490,7 +1881,7 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1490
1881
  (user_id,),
1491
1882
  ).fetchall()
1492
1883
  ]
1493
- user_playbook_ids = [
1884
+ raw_upb_ids = [
1494
1885
  r["user_playbook_id"]
1495
1886
  for r in self.conn.execute(
1496
1887
  "SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
@@ -1501,67 +1892,97 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
1501
1892
  "SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
1502
1893
  (user_id,),
1503
1894
  ).fetchall()
1504
- profile_rowids = [r["rowid"] for r in profile_rows]
1505
- profile_ids = [r["profile_id"] for r in profile_rows]
1506
1895
 
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
- )
1896
+ # Build a rowid lookup for FTS/vec cleanup (SQLite-specific need).
1897
+ profile_rowid_by_id: dict[str, int] = {
1898
+ r["profile_id"]: r["rowid"] for r in profile_rows
1899
+ }
1900
+ all_profile_ids = list(profile_rowid_by_id.keys())
1901
+
1902
+ # ------------------------------------------------------------------
1903
+ # Phase 2: partition profiles and user_playbooks into purge vs delete.
1904
+ # Delegates to the shared BaseStorage helper so the decision logic
1905
+ # is defined once and reused by all backends.
1906
+ # ------------------------------------------------------------------
1907
+ purge_profile_ids, delete_profile_ids = self._partition_purge_vs_delete(
1908
+ "profile", all_profile_ids
1909
+ )
1910
+ purge_upb_str_ids, delete_upb_str_ids = self._partition_purge_vs_delete(
1911
+ "user_playbook", [str(uid) for uid in raw_upb_ids]
1912
+ )
1913
+ purge_upb_ids = [int(s) for s in purge_upb_str_ids]
1914
+ delete_upb_ids = [int(s) for s in delete_upb_str_ids]
1915
+
1916
+ # Rowids for the delete-set only (purge_content handles its own cleanup).
1917
+ delete_profile_rowids = [
1918
+ profile_rowid_by_id[pid]
1919
+ for pid in delete_profile_ids
1920
+ if pid in profile_rowid_by_id
1921
+ ]
1922
+
1923
+ # ------------------------------------------------------------------
1924
+ # Phase 3: FTS and vector cleanup — only for the delete-sets
1925
+ # (purge_content handles its own index cleanup for purged rows).
1926
+ # Use _delete_in_chunks to stay under SQLite's SQLITE_MAX_VARIABLE_NUMBER
1927
+ # limit for large user datasets.
1928
+ # ------------------------------------------------------------------
1929
+ self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
1930
+ self._delete_in_chunks("user_playbooks_fts", "rowid", delete_upb_ids)
1931
+ self._delete_in_chunks("profiles_fts", "profile_id", delete_profile_ids)
1526
1932
 
1527
- # Vector index cleanup (best-effort: only if sqlite-vec loaded)
1528
1933
  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
- )
1934
+ self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
1935
+ self._delete_in_chunks("user_playbooks_vec", "rowid", delete_upb_ids)
1936
+ self._delete_in_chunks("profiles_vec", "rowid", delete_profile_rowids)
1541
1937
 
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.
1938
+ # ------------------------------------------------------------------
1939
+ # Phase 4: hard-delete the delete-sets and all interactions/requests.
1940
+ # ------------------------------------------------------------------
1545
1941
  interactions_cur = self.conn.execute(
1546
1942
  "DELETE FROM interactions WHERE user_id = ?", (user_id,)
1547
1943
  )
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
1944
  requests_cur = self.conn.execute(
1555
1945
  "DELETE FROM requests WHERE user_id = ?", (user_id,)
1556
1946
  )
1947
+ upb_deleted_count = 0
1948
+ if delete_upb_ids:
1949
+ # Clean up source-window join rows before deleting the parent rows.
1950
+ self._delete_source_windows_for_user_playbook_ids(delete_upb_ids)
1951
+ self._delete_in_chunks(
1952
+ "user_playbooks", "user_playbook_id", delete_upb_ids
1953
+ )
1954
+ # rowcount not available from _delete_in_chunks; derive from list length
1955
+ # (all ids came from a pre-snapshot so they exist at delete time).
1956
+ upb_deleted_count = len(delete_upb_ids)
1957
+ profile_deleted_count = 0
1958
+ if delete_profile_ids:
1959
+ self._delete_in_chunks("profiles", "profile_id", delete_profile_ids)
1960
+ profile_deleted_count = len(delete_profile_ids)
1961
+
1962
+ # Commit the hard-deletes before calling purge_content, because
1963
+ # purge_content issues its own conn.commit() and nesting it here
1964
+ # would prematurely flush the still-pending deletes.
1557
1965
  self.conn.commit()
1558
1966
 
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
- }
1967
+ # Phase 5: content-purge the purge-sets WITHOUT releasing the lock,
1968
+ # so erase-eligible rows are never observable by another thread with
1969
+ # PII still intact between the hard-delete commit and the purge. Each
1970
+ # purge_content call self-commits; self._lock is an RLock so its
1971
+ # internal ``with self._lock`` re-acquires cleanly, and the commit
1972
+ # above already closed the outer transaction (no flush hazard).
1973
+ for pid in purge_profile_ids:
1974
+ self.purge_content(entity_type="profile", entity_id=str(pid))
1975
+ for upid in purge_upb_ids:
1976
+ self.purge_content(entity_type="user_playbook", entity_id=str(upid))
1977
+
1978
+ return {
1979
+ "interactions": interactions_cur.rowcount,
1980
+ "user_playbooks": upb_deleted_count,
1981
+ "profiles": profile_deleted_count,
1982
+ "requests": requests_cur.rowcount,
1983
+ "purged_profiles": len(purge_profile_ids),
1984
+ "purged_user_playbooks": len(purge_upb_ids),
1985
+ }
1565
1986
 
1566
1987
 
1567
1988
  # ---------------------------------------------------------------------------
@@ -1583,10 +2004,15 @@ CREATE TABLE IF NOT EXISTS profiles (
1583
2004
  status TEXT,
1584
2005
  extractor_names TEXT,
1585
2006
  expanded_terms TEXT,
2007
+ tags TEXT,
2008
+ source_interaction_ids TEXT,
1586
2009
  source_span TEXT,
1587
2010
  notes TEXT,
1588
2011
  reader_angle TEXT,
1589
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
2012
+ merged_into TEXT,
2013
+ superseded_by TEXT,
2014
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
2015
+ retired_at INTEGER
1590
2016
  );
1591
2017
  CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
1592
2018
  CREATE INDEX IF NOT EXISTS idx_profiles_status ON profiles(status);
@@ -1601,6 +2027,7 @@ CREATE TABLE IF NOT EXISTS interactions (
1601
2027
  user_action TEXT NOT NULL DEFAULT 'none',
1602
2028
  user_action_description TEXT NOT NULL DEFAULT '',
1603
2029
  interacted_image_url TEXT NOT NULL DEFAULT '',
2030
+ image_encoding TEXT NOT NULL DEFAULT '',
1604
2031
  shadow_content TEXT NOT NULL DEFAULT '',
1605
2032
  expert_content TEXT NOT NULL DEFAULT '',
1606
2033
  tools_used TEXT,
@@ -1610,6 +2037,8 @@ CREATE TABLE IF NOT EXISTS interactions (
1610
2037
  CREATE INDEX IF NOT EXISTS idx_interactions_user_id ON interactions(user_id);
1611
2038
  CREATE INDEX IF NOT EXISTS idx_interactions_request_id ON interactions(request_id);
1612
2039
  CREATE INDEX IF NOT EXISTS idx_interactions_created_at ON interactions(created_at);
2040
+ CREATE INDEX IF NOT EXISTS idx_interactions_user_created_at_desc
2041
+ ON interactions(user_id, created_at DESC, interaction_id DESC);
1613
2042
 
1614
2043
  CREATE TABLE IF NOT EXISTS requests (
1615
2044
  request_id TEXT PRIMARY KEY,
@@ -1617,12 +2046,14 @@ CREATE TABLE IF NOT EXISTS requests (
1617
2046
  created_at TEXT NOT NULL,
1618
2047
  source TEXT NOT NULL DEFAULT '',
1619
2048
  agent_version TEXT NOT NULL DEFAULT '',
1620
- session_id TEXT,
1621
- metadata TEXT NOT NULL DEFAULT '{}'
2049
+ session_id TEXT NOT NULL CHECK (trim(session_id) != ''),
2050
+ evaluation_only INTEGER NOT NULL DEFAULT 0
1622
2051
  );
1623
2052
  CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
1624
2053
  CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
1625
2054
  CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
2055
+ CREATE INDEX IF NOT EXISTS idx_requests_session_created_at_asc
2056
+ ON requests(session_id, created_at ASC, request_id ASC);
1626
2057
 
1627
2058
  CREATE TABLE IF NOT EXISTS user_playbooks (
1628
2059
  user_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1640,9 +2071,13 @@ CREATE TABLE IF NOT EXISTS user_playbooks (
1640
2071
  source TEXT,
1641
2072
  embedding TEXT,
1642
2073
  expanded_terms TEXT,
2074
+ tags TEXT,
1643
2075
  source_span TEXT,
1644
2076
  notes TEXT,
1645
- reader_angle TEXT
2077
+ reader_angle TEXT,
2078
+ merged_into INTEGER,
2079
+ superseded_by INTEGER,
2080
+ retired_at INTEGER
1646
2081
  );
1647
2082
  CREATE INDEX IF NOT EXISTS idx_user_playbooks_playbook_name ON user_playbooks(playbook_name);
1648
2083
  CREATE INDEX IF NOT EXISTS idx_user_playbooks_agent_version ON user_playbooks(agent_version);
@@ -1662,7 +2097,11 @@ CREATE TABLE IF NOT EXISTS agent_playbooks (
1662
2097
  playbook_metadata TEXT NOT NULL DEFAULT '',
1663
2098
  embedding TEXT,
1664
2099
  expanded_terms TEXT,
1665
- status TEXT
2100
+ tags TEXT,
2101
+ status TEXT,
2102
+ merged_into INTEGER,
2103
+ superseded_by INTEGER,
2104
+ retired_at INTEGER
1666
2105
  );
1667
2106
  CREATE INDEX IF NOT EXISTS idx_agent_playbooks_playbook_name ON agent_playbooks(playbook_name);
1668
2107
  CREATE INDEX IF NOT EXISTS idx_agent_playbooks_agent_version ON agent_playbooks(agent_version);
@@ -1671,6 +2110,7 @@ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_created_at ON agent_playbooks(cre
1671
2110
 
1672
2111
  CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
1673
2112
  result_id INTEGER PRIMARY KEY AUTOINCREMENT,
2113
+ user_id TEXT NOT NULL DEFAULT '',
1674
2114
  session_id TEXT NOT NULL,
1675
2115
  agent_version TEXT NOT NULL DEFAULT '',
1676
2116
  evaluation_name TEXT,
@@ -1686,31 +2126,12 @@ CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
1686
2126
  );
1687
2127
  CREATE INDEX IF NOT EXISTS idx_eval_agent_version ON agent_success_evaluation_result(agent_version);
1688
2128
  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);
2129
+ CREATE INDEX IF NOT EXISTS idx_eval_created_at_desc
2130
+ ON agent_success_evaluation_result(created_at DESC);
2131
+ CREATE INDEX IF NOT EXISTS idx_eval_agent_version_created_at_desc
2132
+ ON agent_success_evaluation_result(agent_version, created_at DESC);
2133
+ CREATE INDEX IF NOT EXISTS idx_eval_identity_created_at_desc
2134
+ ON agent_success_evaluation_result(user_id, session_id, evaluation_name, agent_version, created_at DESC);
1714
2135
 
1715
2136
  CREATE TABLE IF NOT EXISTS agent_playbook_source_user_playbooks (
1716
2137
  agent_playbook_id INTEGER NOT NULL,
@@ -1745,6 +2166,7 @@ CREATE TABLE IF NOT EXISTS playbook_optimization_candidates (
1745
2166
  parent_candidate_ids TEXT NOT NULL DEFAULT '[]',
1746
2167
  aggregate_score REAL,
1747
2168
  is_winner INTEGER NOT NULL DEFAULT 0,
2169
+ metadata_json TEXT NOT NULL DEFAULT '{}',
1748
2170
  created_at INTEGER NOT NULL
1749
2171
  );
1750
2172
  CREATE INDEX IF NOT EXISTS idx_poc_job ON playbook_optimization_candidates(job_id);
@@ -1941,5 +2363,30 @@ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
1941
2363
  ON shadow_comparison_verdicts (created_at);
1942
2364
  CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
1943
2365
  ON shadow_comparison_verdicts (judge_prompt_version);
2366
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_created_at_desc
2367
+ ON shadow_comparison_verdicts (judge_prompt_version, created_at DESC);
2368
+
2369
+ -- ============================================================================
2370
+ -- Append-only, content-free lineage event log
2371
+ -- ============================================================================
2372
+
2373
+ CREATE TABLE IF NOT EXISTS lineage_event (
2374
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
2375
+ org_id TEXT NOT NULL,
2376
+ entity_type TEXT NOT NULL,
2377
+ entity_id TEXT NOT NULL,
2378
+ op TEXT NOT NULL,
2379
+ prov_relation TEXT NOT NULL DEFAULT '',
2380
+ source_ids TEXT NOT NULL DEFAULT '[]',
2381
+ actor TEXT NOT NULL DEFAULT '',
2382
+ request_id TEXT NOT NULL DEFAULT '',
2383
+ reason TEXT NOT NULL DEFAULT '',
2384
+ created_at INTEGER NOT NULL,
2385
+ from_status TEXT,
2386
+ to_status TEXT,
2387
+ status_namespace TEXT,
2388
+ UNIQUE (org_id, entity_type, entity_id, op, request_id)
2389
+ );
2390
+ CREATE INDEX IF NOT EXISTS idx_lineage_entity ON lineage_event (entity_type, entity_id);
1944
2391
 
1945
2392
  """