claude-smart 0.2.41 → 0.2.43

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 (396) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +86 -48
  4. package/package.json +10 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +2 -2
  8. package/plugin/dashboard/next.config.ts +9 -1
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +91 -0
  11. package/plugin/scripts/backend-service.sh +46 -15
  12. package/plugin/scripts/cli.sh +29 -1
  13. package/plugin/scripts/codex-hook.js +72 -4
  14. package/plugin/scripts/dashboard-build.sh +1 -0
  15. package/plugin/scripts/dashboard-service.sh +1 -0
  16. package/plugin/scripts/ensure-plugin-root.sh +7 -14
  17. package/plugin/scripts/hook_entry.sh +1 -0
  18. package/plugin/scripts/smart-install.sh +18 -2
  19. package/plugin/src/claude_smart/cli.py +72 -38
  20. package/plugin/src/claude_smart/context_format.py +11 -12
  21. package/plugin/src/claude_smart/cs_cite.py +26 -12
  22. package/plugin/src/claude_smart/ids.py +13 -5
  23. package/plugin/uv.lock +1 -1
  24. package/plugin/vendor/reflexio/.env.example +53 -0
  25. package/plugin/vendor/reflexio/LICENSE +201 -0
  26. package/plugin/vendor/reflexio/README.md +338 -0
  27. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  28. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  29. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  40. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  71. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  72. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  73. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  74. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  75. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  151. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  165. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  166. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  183. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  184. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  185. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  186. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  187. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  189. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  190. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  385. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  386. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  387. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  388. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  389. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  390. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  391. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  392. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  393. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  394. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  395. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  396. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,1945 @@
1
+ """
2
+ Base class and module-level helpers for SQLite storage.
3
+
4
+ Supports hybrid search combining FTS5 (BM25) with embedding cosine similarity
5
+ via Reciprocal Rank Fusion (RRF). Falls back to FTS-only when no embeddings
6
+ are available.
7
+
8
+ """
9
+
10
+ import functools
11
+ import json
12
+ import logging
13
+ import math
14
+ import re
15
+ import sqlite3
16
+ import threading
17
+ from collections.abc import Callable
18
+ from datetime import UTC, datetime
19
+ from pathlib import Path
20
+ from typing import Any, ClassVar, Literal
21
+
22
+ from reflexio.models.api_schema.common import BlockingIssue
23
+ from reflexio.models.api_schema.service_schemas import (
24
+ AgentPlaybook,
25
+ AgentPlaybookSnapshot,
26
+ AgentPlaybookUpdateEntry,
27
+ AgentSuccessEvaluationResult,
28
+ Citation,
29
+ Interaction,
30
+ PlaybookAggregationChangeLog,
31
+ PlaybookStatus,
32
+ ProfileChangeLog,
33
+ ProfileTimeToLive,
34
+ RegularVsShadow,
35
+ Request,
36
+ Status,
37
+ ToolUsed,
38
+ UserActionType,
39
+ UserPlaybook,
40
+ UserProfile,
41
+ )
42
+ from reflexio.models.config_schema import (
43
+ EMBEDDING_DIMENSIONS,
44
+ APIKeyConfig,
45
+ LLMConfig,
46
+ SearchMode,
47
+ )
48
+ from reflexio.server.llm.litellm_client import LiteLLMClient, LiteLLMConfig
49
+ from reflexio.server.llm.model_defaults import (
50
+ ModelRole,
51
+ resolve_model_name,
52
+ )
53
+ from reflexio.server.llm.providers.embedding_service_provider import (
54
+ EmbeddingUnavailableError,
55
+ )
56
+ from reflexio.server.services.storage.error import StorageError
57
+ from reflexio.server.services.storage.retention import RetentionTarget
58
+ from reflexio.server.services.storage.retention_mixin import (
59
+ RETENTION_DELETE_CHUNK,
60
+ RetentionMixin,
61
+ chunked,
62
+ )
63
+ from reflexio.server.services.storage.storage_base import BaseStorage
64
+ from reflexio.server.site_var.site_var_manager import SiteVarManager
65
+
66
+ from ._stall_state import init_stall_state_table
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Module-level helpers
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def _json_dumps(obj: Any) -> str | None:
77
+ """Serialize a Python object to a JSON string, or None if the object is None."""
78
+ if obj is None:
79
+ return None
80
+ return json.dumps(obj, default=str)
81
+
82
+
83
+ def _json_loads(text: str | None) -> Any:
84
+ """Deserialize a JSON string, returning None for None/empty input."""
85
+ if not text:
86
+ return None
87
+ return json.loads(text)
88
+
89
+
90
+ _FTS5_OPERATORS = frozenset({"OR", "AND", "NOT"})
91
+ _FTS5_RESERVED = _FTS5_OPERATORS | {"NEAR"}
92
+ _TOKEN_RE = re.compile(r"[a-zA-Z0-9_]+")
93
+
94
+
95
+ def _sanitize_fts_query(text: str) -> str:
96
+ """Sanitize a query string for FTS5, defaulting to OR between tokens.
97
+
98
+ Bare (unquoted) tokens preserve Porter stemming. Explicit OR/AND/NOT
99
+ operators are passed through. A trailing ``*`` is appended to the last
100
+ token for prefix matching.
101
+
102
+ Args:
103
+ text: Raw user query string (may contain FTS5 boolean operators like OR)
104
+
105
+ Returns:
106
+ FTS5-safe query string with stemming enabled and OR default
107
+ """
108
+ tokens = _TOKEN_RE.findall(text)
109
+ if not tokens:
110
+ return '""'
111
+
112
+ has_explicit_operator = any(t in _FTS5_OPERATORS for t in tokens)
113
+
114
+ parts: list[str] = []
115
+ for t in tokens:
116
+ if t in _FTS5_OPERATORS:
117
+ if not parts or parts[-1] in _FTS5_OPERATORS:
118
+ continue
119
+ parts.append(t)
120
+ elif t in _FTS5_RESERVED:
121
+ continue
122
+ else:
123
+ if not has_explicit_operator and parts and parts[-1] not in _FTS5_OPERATORS:
124
+ parts.append("OR")
125
+ parts.append(t)
126
+
127
+ if parts and parts[-1] in _FTS5_OPERATORS:
128
+ parts.pop()
129
+ if not parts:
130
+ return '""'
131
+
132
+ # Append prefix wildcard to last token for partial-word matching
133
+ parts[-1] = parts[-1] + "*"
134
+ return " ".join(parts)
135
+
136
+
137
+ def _cosine_similarity(a: list[float], b: list[float]) -> float:
138
+ """Compute cosine similarity between two vectors.
139
+
140
+ Args:
141
+ a: First embedding vector.
142
+ b: Second embedding vector.
143
+
144
+ Returns:
145
+ Cosine similarity in [-1, 1], or 0.0 for degenerate inputs.
146
+ """
147
+ if len(a) != len(b) or not a:
148
+ return 0.0
149
+ dot = sum(x * y for x, y in zip(a, b, strict=True))
150
+ mag_a = math.sqrt(sum(x * x for x in a))
151
+ mag_b = math.sqrt(sum(x * x for x in b))
152
+ if mag_a == 0.0 or mag_b == 0.0:
153
+ return 0.0
154
+ return dot / (mag_a * mag_b)
155
+
156
+
157
+ def _effective_search_mode(
158
+ mode: SearchMode,
159
+ query_embedding: list[float] | None,
160
+ query: str | None,
161
+ ) -> SearchMode:
162
+ """Downgrade search mode when the required embedding is unavailable.
163
+
164
+ Args:
165
+ mode: Requested search mode.
166
+ query_embedding: Pre-computed query embedding, or None.
167
+ query: Original query text. The fallback warning is suppressed when
168
+ this is falsy, since an empty-query HYBRID/VECTOR request has no
169
+ semantic intent to lose.
170
+
171
+ Returns:
172
+ The effective SearchMode — falls back to FTS when HYBRID/VECTOR lacks an embedding.
173
+ """
174
+ if mode in (SearchMode.HYBRID, SearchMode.VECTOR) and not query_embedding:
175
+ if query:
176
+ logger.warning(
177
+ "Search mode '%s' requested but no query embedding provided — falling back to FTS",
178
+ mode,
179
+ )
180
+ return SearchMode.FTS
181
+ return mode
182
+
183
+
184
+ def _vector_rank_rows(
185
+ rows: list[sqlite3.Row],
186
+ query_embedding: list[float],
187
+ match_count: int,
188
+ ) -> list[sqlite3.Row]:
189
+ """Rank rows by cosine similarity to the query embedding.
190
+
191
+ Args:
192
+ rows: Candidate rows with stored embeddings.
193
+ query_embedding: The query's embedding vector.
194
+ match_count: Number of results to return.
195
+
196
+ Returns:
197
+ Top ``match_count`` rows sorted by cosine similarity descending.
198
+ """
199
+ scored: list[tuple[sqlite3.Row, float]] = []
200
+ for row in rows:
201
+ raw_emb = row["embedding"] if "embedding" in row.keys() else None # noqa: SIM118
202
+ emb = _json_loads(raw_emb) if raw_emb else None
203
+ if emb:
204
+ sim = _cosine_similarity(query_embedding, emb)
205
+ scored.append((row, sim))
206
+
207
+ scored.sort(key=lambda x: x[1], reverse=True)
208
+ # Diagnostic: log the full pre-cut score distribution so retrieval
209
+ # misses are debuggable. Without this, the only signal callers see is
210
+ # "K results returned" with no way to tell whether a relevant row
211
+ # scored 0.39 (close, just below an upstream threshold filter) vs
212
+ # 0.05 (semantic mismatch, no threshold tuning will save it). Top 10
213
+ # is sufficient context; logs at INFO so it shows up in production
214
+ # backend.log without an explicit debug flag. Cost is one log line
215
+ # per vector search call (~200 bytes).
216
+ if scored:
217
+ top = [round(s, 3) for _, s in scored[:10]]
218
+ logger.info(
219
+ "vector_rank: candidates=%d match_count=%d top_scores=%s",
220
+ len(scored),
221
+ match_count,
222
+ top,
223
+ )
224
+ return [row for row, _ in scored[:match_count]]
225
+
226
+
227
+ def _true_rrf_merge(
228
+ fts_rows: list[sqlite3.Row],
229
+ vec_rows: list[sqlite3.Row],
230
+ id_column: str,
231
+ match_count: int,
232
+ rrf_k: int = 60,
233
+ vector_weight: float = 1.0,
234
+ fts_weight: float = 1.0,
235
+ ) -> list[sqlite3.Row]:
236
+ """Merge independent FTS and vector result sets via Reciprocal Rank Fusion.
237
+
238
+ Unlike ``_rrf_rerank`` (which re-ranks FTS results only), this function
239
+ takes two independently-produced result lists and unions them so that
240
+ documents appearing in *either* modality can surface.
241
+
242
+ Args:
243
+ fts_rows: Rows from an FTS query, in BM25-ranked order.
244
+ vec_rows: Rows from a vector query, in cosine-similarity order.
245
+ id_column: Column name used as primary key to deduplicate rows.
246
+ match_count: Number of results to return.
247
+ rrf_k: RRF smoothing constant (default 60).
248
+ vector_weight: Weight for vector similarity contribution.
249
+ fts_weight: Weight for FTS contribution.
250
+
251
+ Returns:
252
+ Top ``match_count`` rows sorted by combined RRF score.
253
+ """
254
+ if not fts_rows and not vec_rows:
255
+ return []
256
+
257
+ # Collect unique rows by ID (first-seen wins for the Row object)
258
+ row_by_id: dict[str | int, sqlite3.Row] = {}
259
+ for row in (*fts_rows, *vec_rows):
260
+ rid = row[id_column]
261
+ if rid not in row_by_id:
262
+ row_by_id[rid] = row
263
+
264
+ # Build rank maps (1-based); missing entries get a penalty rank
265
+ fts_rank: dict[str | int, int] = {
266
+ row[id_column]: i + 1 for i, row in enumerate(fts_rows)
267
+ }
268
+ vec_rank: dict[str | int, int] = {
269
+ row[id_column]: i + 1 for i, row in enumerate(vec_rows)
270
+ }
271
+ fts_penalty = len(fts_rows) + 1
272
+ vec_penalty = len(vec_rows) + 1
273
+
274
+ scored: list[tuple[sqlite3.Row, float]] = []
275
+ for rid, row in row_by_id.items():
276
+ f_rank = fts_rank.get(rid, fts_penalty)
277
+ v_rank = vec_rank.get(rid, vec_penalty)
278
+ score = fts_weight / (rrf_k + f_rank) + vector_weight / (rrf_k + v_rank)
279
+ scored.append((row, score))
280
+
281
+ scored.sort(key=lambda x: x[1], reverse=True)
282
+ return [row for row, _ in scored[:match_count]]
283
+
284
+
285
+ def _status_value(status: Status | None) -> str | None:
286
+ """Convert a Status enum (or None) to its DB string value."""
287
+ if status is None:
288
+ return None
289
+ if hasattr(status, "value"):
290
+ return status.value
291
+ return None
292
+
293
+
294
+ def _build_status_sql(
295
+ status_filter: list[Status | None],
296
+ col: str = "status",
297
+ ) -> tuple[str, list[Any]]:
298
+ """Build a SQL WHERE fragment for a list of status values.
299
+
300
+ Args:
301
+ status_filter: List of Status enum values (may include None for CURRENT)
302
+ col: Column name to filter on
303
+
304
+ Returns:
305
+ Tuple of (SQL fragment, parameter list) ready for AND-chaining
306
+ """
307
+ has_none = False
308
+ values: list[str] = []
309
+ for s in status_filter:
310
+ v = _status_value(s)
311
+ if v is None:
312
+ has_none = True
313
+ else:
314
+ values.append(v)
315
+
316
+ if has_none and values:
317
+ placeholders = ",".join("?" for _ in values)
318
+ return f"({col} IS NULL OR {col} IN ({placeholders}))", values
319
+ if has_none:
320
+ return f"{col} IS NULL", []
321
+ if values:
322
+ placeholders = ",".join("?" for _ in values)
323
+ return f"{col} IN ({placeholders})", values
324
+ return "1=1", []
325
+
326
+
327
+ def _iso_now() -> str:
328
+ """Return current UTC time as ISO 8601 string."""
329
+ return datetime.now(UTC).isoformat()
330
+
331
+
332
+ def _epoch_now() -> int:
333
+ """Return current UTC Unix timestamp."""
334
+ return int(datetime.now(UTC).timestamp())
335
+
336
+
337
+ def _iso_to_epoch(iso_str: str | None) -> int:
338
+ """Convert an ISO datetime string to Unix timestamp."""
339
+ if not iso_str:
340
+ return _epoch_now()
341
+ try:
342
+ cleaned = iso_str.replace("Z", "+00:00")
343
+ return int(datetime.fromisoformat(cleaned).timestamp())
344
+ except (ValueError, TypeError):
345
+ return _epoch_now()
346
+
347
+
348
+ def _epoch_to_iso(ts: int) -> str:
349
+ """Convert a Unix timestamp to ISO 8601 string."""
350
+ return datetime.fromtimestamp(ts, tz=UTC).isoformat()
351
+
352
+
353
+ # ---------------------------------------------------------------------------
354
+ # Row-to-model converters
355
+ # ---------------------------------------------------------------------------
356
+
357
+
358
+ def _row_to_profile(row: sqlite3.Row) -> UserProfile:
359
+ d = dict(row)
360
+ return UserProfile(
361
+ profile_id=d["profile_id"],
362
+ user_id=d["user_id"],
363
+ content=d["content"],
364
+ last_modified_timestamp=d["last_modified_timestamp"],
365
+ generated_from_request_id=d["generated_from_request_id"],
366
+ profile_time_to_live=ProfileTimeToLive(d["profile_time_to_live"]),
367
+ expiration_timestamp=d["expiration_timestamp"],
368
+ custom_features=_json_loads(d.get("custom_features")),
369
+ source=d.get("source") or "",
370
+ status=Status(d["status"]) if d.get("status") else None,
371
+ extractor_names=_json_loads(d.get("extractor_names")),
372
+ expanded_terms=d.get("expanded_terms"),
373
+ source_span=d.get("source_span"),
374
+ notes=d.get("notes"),
375
+ reader_angle=d.get("reader_angle"),
376
+ )
377
+
378
+
379
+ def _row_to_interaction(row: sqlite3.Row) -> Interaction:
380
+ d = dict(row)
381
+ tools_used_raw = _json_loads(d.get("tools_used"))
382
+ tools_used = (
383
+ [ToolUsed(**t) for t in tools_used_raw if isinstance(t, dict)]
384
+ if tools_used_raw and isinstance(tools_used_raw, list)
385
+ else []
386
+ )
387
+ citations_raw = _json_loads(d.get("citations"))
388
+ citations = (
389
+ [Citation(**c) for c in citations_raw if isinstance(c, dict)]
390
+ if citations_raw and isinstance(citations_raw, list)
391
+ else []
392
+ )
393
+ return Interaction(
394
+ interaction_id=d["interaction_id"],
395
+ user_id=d["user_id"],
396
+ content=d["content"],
397
+ request_id=d["request_id"],
398
+ created_at=_iso_to_epoch(d["created_at"]),
399
+ role=d.get("role") or "User",
400
+ user_action=UserActionType(d["user_action"]),
401
+ user_action_description=d["user_action_description"],
402
+ interacted_image_url=d["interacted_image_url"],
403
+ shadow_content=d.get("shadow_content") or "",
404
+ expert_content=d.get("expert_content") or "",
405
+ tools_used=tools_used,
406
+ citations=citations,
407
+ )
408
+
409
+
410
+ def _row_to_request(row: sqlite3.Row) -> Request:
411
+ 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
+ return Request(
423
+ request_id=d["request_id"],
424
+ user_id=d["user_id"],
425
+ created_at=_iso_to_epoch(d["created_at"]),
426
+ source=d.get("source") or "",
427
+ agent_version=d.get("agent_version") or "",
428
+ session_id=d.get("session_id"),
429
+ metadata=metadata,
430
+ )
431
+
432
+
433
+ def _row_to_user_playbook(
434
+ row: sqlite3.Row, include_embedding: bool = False
435
+ ) -> UserPlaybook:
436
+ d = dict(row)
437
+ embedding: list[float] = []
438
+ if include_embedding and d.get("embedding"):
439
+ raw_emb = _json_loads(d["embedding"])
440
+ if isinstance(raw_emb, list):
441
+ embedding = [float(x) for x in raw_emb]
442
+ return UserPlaybook(
443
+ user_playbook_id=d["user_playbook_id"],
444
+ user_id=d.get("user_id"),
445
+ playbook_name=d["playbook_name"],
446
+ created_at=_iso_to_epoch(d["created_at"]),
447
+ request_id=d["request_id"],
448
+ agent_version=d["agent_version"],
449
+ content=d["content"],
450
+ trigger=d.get("trigger"),
451
+ rationale=d.get("rationale"),
452
+ blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
453
+ if d.get("blocking_issue")
454
+ else None,
455
+ status=Status(d["status"]) if d.get("status") else None,
456
+ source=d.get("source"),
457
+ source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
458
+ embedding=embedding,
459
+ expanded_terms=d.get("expanded_terms"),
460
+ source_span=d.get("source_span"),
461
+ notes=d.get("notes"),
462
+ reader_angle=d.get("reader_angle"),
463
+ )
464
+
465
+
466
+ def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
467
+ d = dict(row)
468
+ return AgentPlaybook(
469
+ agent_playbook_id=d["agent_playbook_id"],
470
+ playbook_name=d["playbook_name"],
471
+ created_at=_iso_to_epoch(d["created_at"]),
472
+ agent_version=d["agent_version"],
473
+ content=d["content"],
474
+ trigger=d.get("trigger"),
475
+ rationale=d.get("rationale"),
476
+ blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
477
+ if d.get("blocking_issue")
478
+ else None,
479
+ playbook_status=PlaybookStatus(d["playbook_status"])
480
+ if d.get("playbook_status")
481
+ else PlaybookStatus.PENDING,
482
+ playbook_metadata=d.get("playbook_metadata") or "",
483
+ embedding=[],
484
+ status=Status(d["status"]) if d.get("status") else None,
485
+ expanded_terms=d.get("expanded_terms"),
486
+ )
487
+
488
+
489
+ def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
490
+ d = dict(row)
491
+ return AgentSuccessEvaluationResult(
492
+ result_id=d["result_id"],
493
+ session_id=d["session_id"],
494
+ agent_version=d["agent_version"],
495
+ evaluation_name=d.get("evaluation_name"),
496
+ is_success=bool(d["is_success"]),
497
+ failure_type=d.get("failure_type"),
498
+ failure_reason=d.get("failure_reason"),
499
+ created_at=_iso_to_epoch(d["created_at"]),
500
+ regular_vs_shadow=(
501
+ RegularVsShadow(d["regular_vs_shadow"])
502
+ if d.get("regular_vs_shadow")
503
+ else None
504
+ ),
505
+ number_of_correction_per_session=d.get("number_of_correction_per_session") or 0,
506
+ user_turns_to_resolution=d.get("user_turns_to_resolution"),
507
+ is_escalated=bool(d.get("is_escalated", False)),
508
+ embedding=[],
509
+ )
510
+
511
+
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
+ # ---------------------------------------------------------------------------
560
+ # SQLiteStorageBase
561
+ # ---------------------------------------------------------------------------
562
+
563
+
564
+ class SQLiteStorageBase(RetentionMixin, BaseStorage):
565
+ """SQLite-backed storage base class for local/self-hosted deployments."""
566
+
567
+ supports_embedding: ClassVar[bool] = True
568
+
569
+ @staticmethod
570
+ def handle_exceptions(func: Callable[..., Any]) -> Callable[..., Any]:
571
+ @functools.wraps(func)
572
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
573
+ try:
574
+ return func(*args, **kwargs)
575
+ except StorageError:
576
+ raise
577
+ except Exception as e:
578
+ import traceback
579
+
580
+ stack_trace = traceback.format_exc()
581
+ logger.error(
582
+ "Error in %s: %s\nStack trace:\n%s",
583
+ func.__name__,
584
+ str(e),
585
+ stack_trace,
586
+ )
587
+ raise StorageError(message=f"{e}\nStack trace:\n{stack_trace}") from e
588
+
589
+ return wrapper
590
+
591
+ def __init__(
592
+ self,
593
+ org_id: str,
594
+ db_path: str | None = None,
595
+ api_key_config: APIKeyConfig | None = None,
596
+ llm_config: LLMConfig | None = None,
597
+ enable_document_expansion: bool = False,
598
+ ) -> None:
599
+ super().__init__(org_id)
600
+ self.api_key_config = api_key_config
601
+ self._enable_document_expansion = enable_document_expansion
602
+
603
+ # Resolve db_path: explicit arg > LOCAL_STORAGE_PATH env var > ~/.reflexio/data/
604
+ if db_path is None:
605
+ from reflexio.server import LOCAL_STORAGE_PATH
606
+
607
+ db_path = str(Path(LOCAL_STORAGE_PATH) / "reflexio.db")
608
+
609
+ self.db_path = db_path
610
+ self._lock = threading.RLock()
611
+
612
+ logger.info("SQLite Storage for org %s using db_path: %s", org_id, db_path)
613
+
614
+ # Ensure parent directory exists
615
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
616
+
617
+ # Open connection
618
+ self.conn = sqlite3.connect(db_path, check_same_thread=False)
619
+ self.conn.row_factory = sqlite3.Row
620
+ self.conn.execute("PRAGMA journal_mode=WAL")
621
+ self.conn.execute("PRAGMA foreign_keys=ON")
622
+
623
+ # LLM client for embeddings
624
+ model_setting = SiteVarManager().get_site_var("llm_model_setting")
625
+ site_var = model_setting if isinstance(model_setting, dict) else {}
626
+
627
+ self.embedding_model_name = resolve_model_name(
628
+ ModelRole.EMBEDDING,
629
+ site_var_value=site_var.get("embedding_model_name"),
630
+ config_override=llm_config.embedding_model_name if llm_config else None,
631
+ api_key_config=self.api_key_config,
632
+ )
633
+ self.embedding_dimensions = EMBEDDING_DIMENSIONS
634
+
635
+ litellm_config = LiteLLMConfig(
636
+ model=self.embedding_model_name,
637
+ temperature=0.0,
638
+ api_key_config=self.api_key_config,
639
+ )
640
+ self.llm_client = LiteLLMClient(litellm_config)
641
+
642
+ # Optionally load sqlite-vec for native KNN vector search
643
+ self._has_sqlite_vec = self._try_load_sqlite_vec()
644
+
645
+ # Create tables
646
+ self.migrate()
647
+
648
+ # ------------------------------------------------------------------
649
+ # DDL / migration
650
+ # ------------------------------------------------------------------
651
+
652
+ def migrate(self) -> bool:
653
+ self._migrate_feedback_schema()
654
+ self._migrate_interactions_schema()
655
+ with self._lock:
656
+ cur = self.conn.cursor()
657
+ cur.executescript(_DDL)
658
+ self.conn.commit()
659
+ if self._has_sqlite_vec:
660
+ self._create_vec_tables()
661
+ self._migrate_vec_tables()
662
+ # Run after DDL so tables exist on fresh databases
663
+ self._migrate_agent_runs_schema()
664
+ self._migrate_pending_tool_calls_schema()
665
+ self._migrate_expanded_terms()
666
+ self._migrate_agentic_signals()
667
+ self._migrate_agent_playbook_source_windows()
668
+ self._migrate_request_metadata()
669
+ self._migrate_shadow_comparison_verdicts()
670
+ self._migrate_user_playbook_polarity()
671
+ init_stall_state_table(self.conn)
672
+ return True
673
+
674
+ # -- Retention hooks (see RetentionMixin) --
675
+
676
+ @handle_exceptions
677
+ def _retention_table_exists(self, table_name: str) -> bool:
678
+ row = self._fetchone(
679
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
680
+ (table_name,),
681
+ )
682
+ return row is not None
683
+
684
+ @handle_exceptions
685
+ def _retention_count_rows(self, target: RetentionTarget) -> int:
686
+ row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {target.table_name}") # noqa: S608
687
+ return int(row["cnt"]) if row else 0
688
+
689
+ @handle_exceptions
690
+ def _retention_select_oldest_keys(
691
+ self, target: RetentionTarget, count: int
692
+ ) -> list[tuple[Any, ...]]:
693
+ id_sql = ", ".join(target.id_columns)
694
+ tiebreak_sql = id_sql
695
+ rows = self._fetchall(
696
+ f"SELECT {id_sql} FROM {target.table_name} " # noqa: S608
697
+ f"ORDER BY {target.order_column} ASC, {tiebreak_sql} ASC LIMIT ?",
698
+ (count,),
699
+ )
700
+ return [tuple(row[col] for col in target.id_columns) for row in rows]
701
+
702
+ @handle_exceptions
703
+ def _retention_perform_delete(
704
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
705
+ ) -> None:
706
+ # Wrap dependency + target deletes in a single critical section so
707
+ # concurrent writers see either both or neither.
708
+ with self._lock:
709
+ self._retention_delete_dependencies(target, keys)
710
+ self._retention_delete_target_rows(target, keys)
711
+ self.conn.commit()
712
+
713
+ def _retention_delete_dependencies(
714
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
715
+ ) -> None:
716
+ ids = [key[0] for key in keys]
717
+ target_name = target.name
718
+ if target_name == "requests":
719
+ self._delete_interactions_for_request_ids([str(v) for v in ids])
720
+ elif target_name == "interactions":
721
+ self._delete_interaction_search_rows([int(v) for v in ids])
722
+ elif target_name == "profiles":
723
+ self._delete_profile_search_rows([str(v) for v in ids])
724
+ elif target_name == "user_playbooks":
725
+ 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])
727
+ elif target_name == "agent_playbooks":
728
+ 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])
730
+ elif target_name == "playbook_optimization_jobs":
731
+ self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
732
+ elif target_name == "playbook_optimization_candidates":
733
+ self._delete_optimizer_evaluations_for_candidate_ids([int(v) for v in ids])
734
+
735
+ def _retention_delete_target_rows(
736
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
737
+ ) -> None:
738
+ if len(target.id_columns) == 1:
739
+ self._delete_in_chunks(
740
+ target.table_name,
741
+ target.id_columns[0],
742
+ [key[0] for key in keys],
743
+ )
744
+ return
745
+ # Composite-key delete: chunk by row to bound parameter count.
746
+ params_per_key = len(target.id_columns)
747
+ rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
748
+ for chunk in chunked(keys, rows_per_chunk):
749
+ where = " OR ".join(
750
+ "("
751
+ + " AND ".join(f"{column} = ?" for column in target.id_columns)
752
+ + ")"
753
+ for _ in chunk
754
+ )
755
+ params = [value for key in chunk for value in key]
756
+ self.conn.execute(
757
+ f"DELETE FROM {target.table_name} WHERE {where}", # noqa: S608
758
+ params,
759
+ )
760
+
761
+ # -- Chunked-delete primitives shared by the cascade helpers --
762
+
763
+ def _delete_in_chunks(
764
+ self, table_name: str, column_name: str, values: list[Any]
765
+ ) -> None:
766
+ """Chunked ``DELETE FROM table WHERE col IN (...)``.
767
+
768
+ Chunking keeps parameter count under ``SQLITE_MAX_VARIABLE_NUMBER``
769
+ on older sqlite builds (default 999) and avoids degenerate plans
770
+ on very large IN lists.
771
+ """
772
+ if not values:
773
+ return
774
+ for chunk in chunked(values):
775
+ placeholders = ",".join("?" for _ in chunk)
776
+ self.conn.execute(
777
+ f"DELETE FROM {table_name} WHERE {column_name} IN ({placeholders})", # noqa: S608
778
+ chunk,
779
+ )
780
+
781
+ def _select_in_chunks(self, sql_template: str, values: list[Any]) -> list[Any]:
782
+ """Run ``sql_template`` (containing ``{placeholders}``) over chunks of
783
+ ``values`` and aggregate the result rows."""
784
+ results: list[Any] = []
785
+ for chunk in chunked(values):
786
+ placeholders = ",".join("?" for _ in chunk)
787
+ stmt = sql_template.format(placeholders=placeholders)
788
+ results.extend(self.conn.execute(stmt, chunk).fetchall())
789
+ return results
790
+
791
+ def _delete_interactions_for_request_ids(self, request_ids: list[str]) -> None:
792
+ if not request_ids:
793
+ return
794
+ rows = self._select_in_chunks(
795
+ "SELECT interaction_id FROM interactions WHERE request_id IN ({placeholders})",
796
+ request_ids,
797
+ )
798
+ self._delete_interaction_search_rows(
799
+ [int(row["interaction_id"]) for row in rows]
800
+ )
801
+ self._delete_in_chunks("interactions", "request_id", request_ids)
802
+
803
+ def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
804
+ if not interaction_ids:
805
+ return
806
+ self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
807
+ for interaction_id in interaction_ids:
808
+ self._vec_delete("interactions_vec", interaction_id)
809
+
810
+ def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
811
+ if not profile_ids:
812
+ 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"])
820
+
821
+ def _delete_playbook_search_rows(self, kind: str, ids: list[int]) -> None:
822
+ if not ids:
823
+ 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)
827
+
828
+ def _delete_source_windows_for_agent_playbook_ids(
829
+ self, agent_playbook_ids: list[int]
830
+ ) -> None:
831
+ self._delete_in_chunks(
832
+ "agent_playbook_source_user_playbooks",
833
+ "agent_playbook_id",
834
+ agent_playbook_ids,
835
+ )
836
+
837
+ def _delete_source_windows_for_user_playbook_ids(
838
+ self, user_playbook_ids: list[int]
839
+ ) -> None:
840
+ self._delete_in_chunks(
841
+ "agent_playbook_source_user_playbooks",
842
+ "user_playbook_id",
843
+ user_playbook_ids,
844
+ )
845
+
846
+ def _delete_optimizer_rows_for_job_ids(self, job_ids: list[int]) -> None:
847
+ if not job_ids:
848
+ return
849
+ for table in (
850
+ "playbook_optimization_evaluations",
851
+ "playbook_optimization_events",
852
+ "playbook_optimization_candidates",
853
+ ):
854
+ self._delete_in_chunks(table, "job_id", job_ids)
855
+
856
+ def _delete_optimizer_evaluations_for_candidate_ids(
857
+ self, candidate_ids: list[int]
858
+ ) -> None:
859
+ self._delete_in_chunks(
860
+ "playbook_optimization_evaluations",
861
+ "candidate_id",
862
+ candidate_ids,
863
+ )
864
+
865
+ def _try_load_sqlite_vec(self) -> bool:
866
+ """Attempt to load the sqlite-vec extension for native KNN search.
867
+
868
+ Returns:
869
+ True if the extension was loaded successfully, False otherwise.
870
+ """
871
+ try:
872
+ import sqlite_vec # type: ignore[import-untyped]
873
+
874
+ # AttributeError covers Python builds without loadable-extension
875
+ # support (common with pyenv/Homebrew on macOS) — the method
876
+ # itself is absent rather than raising at runtime.
877
+ self.conn.enable_load_extension(True)
878
+ sqlite_vec.load(self.conn)
879
+ self.conn.enable_load_extension(False)
880
+ logger.info("sqlite-vec extension loaded — native KNN search enabled")
881
+ return True
882
+ except (ImportError, OSError, AttributeError, sqlite3.OperationalError) as e:
883
+ logger.info("sqlite-vec not available, using Python fallback: %s", e)
884
+ return False
885
+
886
+ def _create_vec_tables(self) -> None:
887
+ """Create vec0 virtual tables for each entity that stores embeddings."""
888
+ dim = self.embedding_dimensions
889
+ vec_ddl = f"""
890
+ CREATE VIRTUAL TABLE IF NOT EXISTS interactions_vec USING vec0(
891
+ embedding float[{dim}]
892
+ );
893
+ CREATE VIRTUAL TABLE IF NOT EXISTS profiles_vec USING vec0(
894
+ embedding float[{dim}]
895
+ );
896
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_playbooks_vec USING vec0(
897
+ embedding float[{dim}]
898
+ );
899
+ CREATE VIRTUAL TABLE IF NOT EXISTS agent_playbooks_vec USING vec0(
900
+ embedding float[{dim}]
901
+ );
902
+ """
903
+ with self._lock:
904
+ self.conn.executescript(vec_ddl)
905
+ self.conn.commit()
906
+
907
+ def _migrate_vec_tables(self) -> None:
908
+ """Backfill vec tables from existing embedding TEXT columns (idempotent)."""
909
+ entity_map = [
910
+ ("interactions", "interactions_vec", "interaction_id"),
911
+ ("profiles", "profiles_vec", "profile_id"),
912
+ ("user_playbooks", "user_playbooks_vec", "user_playbook_id"),
913
+ ("agent_playbooks", "agent_playbooks_vec", "agent_playbook_id"),
914
+ ]
915
+ for main_table, vec_table, _id_col in entity_map:
916
+ row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {vec_table}")
917
+ if row and row["cnt"] > 0:
918
+ continue # Already populated
919
+ rows = self._fetchall(
920
+ f"SELECT rowid AS rid, embedding FROM {main_table} WHERE embedding IS NOT NULL"
921
+ )
922
+ for r in rows:
923
+ emb = _json_loads(r["embedding"])
924
+ if emb:
925
+ self._vec_upsert(vec_table, r["rid"], emb)
926
+
927
+ def _migrate_interactions_schema(self) -> None:
928
+ """Add new columns to existing interactions table if missing."""
929
+ with self._lock:
930
+ cur = self.conn.execute("PRAGMA table_info(interactions)")
931
+ columns = {row[1] for row in cur.fetchall()}
932
+
933
+ if not columns:
934
+ return
935
+
936
+ if "expert_content" not in columns:
937
+ logger.info("Adding expert_content column to interactions table.")
938
+ with self._lock:
939
+ self.conn.execute(
940
+ "ALTER TABLE interactions ADD COLUMN expert_content TEXT NOT NULL DEFAULT ''"
941
+ )
942
+ self.conn.commit()
943
+
944
+ if "citations" not in columns:
945
+ logger.info("Adding citations column to interactions table.")
946
+ with self._lock:
947
+ self.conn.execute("ALTER TABLE interactions ADD COLUMN citations TEXT")
948
+ self.conn.commit()
949
+
950
+ def _migrate_feedback_schema(self) -> None:
951
+ """Drop old-schema feedback/playbook tables so _DDL can recreate them.
952
+
953
+ Checks for two migration scenarios:
954
+ 1. Old column layout (missing ``trigger``) -- drop data tables + FTS.
955
+ 2. Old FTS column name (``feedback_content`` instead of ``search_text``)
956
+ -- drop only the FTS tables so they are recreated with the new column.
957
+
958
+ Also handles migration from old table names (raw_feedbacks/feedbacks)
959
+ to new names (user_playbooks/agent_playbooks), renames
960
+ feedback_aggregation_change_logs to playbook_aggregation_change_logs,
961
+ and renames columns on related tables (skills, profiles,
962
+ playbook_aggregation_change_logs).
963
+
964
+ Since SQLite is used only for local development, data loss is acceptable.
965
+ """
966
+ # Check for old table names and rename if needed
967
+ with self._lock:
968
+ old_tables = {
969
+ row[0]
970
+ for row in self.conn.execute(
971
+ "SELECT name FROM sqlite_master WHERE type='table'"
972
+ ).fetchall()
973
+ }
974
+
975
+ if "raw_feedbacks" in old_tables and "user_playbooks" not in old_tables:
976
+ logger.warning(
977
+ "Detected old table names (raw_feedbacks/feedbacks). "
978
+ "Dropping old tables so they can be recreated with the new schema."
979
+ )
980
+ with self._lock:
981
+ self.conn.executescript("""
982
+ DROP TABLE IF EXISTS raw_feedbacks_fts;
983
+ DROP TABLE IF EXISTS feedbacks_fts;
984
+ DROP TABLE IF EXISTS raw_feedbacks;
985
+ DROP TABLE IF EXISTS feedbacks;
986
+ """)
987
+ self.conn.commit()
988
+
989
+ if (
990
+ "feedback_aggregation_change_logs" in old_tables
991
+ and "playbook_aggregation_change_logs" not in old_tables
992
+ ):
993
+ logger.warning(
994
+ "Renaming table feedback_aggregation_change_logs → playbook_aggregation_change_logs."
995
+ )
996
+ with self._lock:
997
+ self.conn.execute(
998
+ "ALTER TABLE feedback_aggregation_change_logs RENAME TO playbook_aggregation_change_logs"
999
+ )
1000
+ self.conn.commit()
1001
+
1002
+ # Migrate renamed columns on related tables (skills, profiles, change_logs)
1003
+ self._migrate_renamed_columns()
1004
+
1005
+ with self._lock:
1006
+ cur = self.conn.execute("PRAGMA table_info(user_playbooks)")
1007
+ columns = {row[1] for row in cur.fetchall()}
1008
+
1009
+ # Table doesn't exist yet -- nothing to migrate
1010
+ if not columns:
1011
+ return
1012
+
1013
+ # Scenario 1: old data schema (missing trigger column — pre-flattening)
1014
+ if "trigger" not in columns:
1015
+ logger.warning(
1016
+ "Detected old playbook schema (missing trigger column). "
1017
+ "Dropping playbook tables so they can be recreated with the new schema."
1018
+ )
1019
+ with self._lock:
1020
+ self.conn.executescript("""
1021
+ DROP TABLE IF EXISTS user_playbooks_fts;
1022
+ DROP TABLE IF EXISTS agent_playbooks_fts;
1023
+ DROP TABLE IF EXISTS user_playbooks;
1024
+ DROP TABLE IF EXISTS agent_playbooks;
1025
+ """)
1026
+ self.conn.commit()
1027
+ return
1028
+
1029
+ # Scenario 2: old FTS column name (feedback_content -> search_text)
1030
+ with self._lock:
1031
+ cur = self.conn.execute("PRAGMA table_info(user_playbooks_fts)")
1032
+ fts_columns = {row[1] for row in cur.fetchall()}
1033
+
1034
+ if fts_columns and "search_text" not in fts_columns:
1035
+ logger.warning(
1036
+ "Detected old FTS column name. "
1037
+ "Dropping FTS tables so they can be recreated with the new schema."
1038
+ )
1039
+ with self._lock:
1040
+ self.conn.executescript("""
1041
+ DROP TABLE IF EXISTS user_playbooks_fts;
1042
+ DROP TABLE IF EXISTS agent_playbooks_fts;
1043
+ """)
1044
+ self.conn.commit()
1045
+
1046
+ def _migrate_renamed_columns(self) -> None:
1047
+ """Rename columns on tables affected by the feedback→playbook rename.
1048
+
1049
+ Handles: skills (feedback_name→playbook_name, raw_feedback_ids→user_playbook_ids),
1050
+ profiles (profile_content→content), playbook_aggregation_change_logs (feedback_name→playbook_name).
1051
+
1052
+ Since SQLite is used only for local development, we drop and recreate if needed.
1053
+ """
1054
+ renames = [
1055
+ ("skills", "feedback_name", "playbook_name"),
1056
+ ("skills", "raw_feedback_ids", "user_playbook_ids"),
1057
+ ("profiles", "profile_content", "content"),
1058
+ ("playbook_aggregation_change_logs", "feedback_name", "playbook_name"),
1059
+ ]
1060
+
1061
+ for table, old_col, new_col in renames:
1062
+ with self._lock:
1063
+ try:
1064
+ cols = {
1065
+ row[1]
1066
+ for row in self.conn.execute(
1067
+ f"PRAGMA table_info({table})"
1068
+ ).fetchall() # noqa: S608
1069
+ }
1070
+ except Exception: # noqa: S112
1071
+ continue # Table doesn't exist yet
1072
+
1073
+ if not cols:
1074
+ continue # Table doesn't exist
1075
+
1076
+ if old_col in cols and new_col not in cols:
1077
+ logger.info(
1078
+ "Renaming column %s.%s -> %s",
1079
+ table,
1080
+ old_col,
1081
+ new_col,
1082
+ )
1083
+ try:
1084
+ self.conn.execute(
1085
+ f"ALTER TABLE {table} RENAME COLUMN {old_col} TO {new_col}" # noqa: S608
1086
+ )
1087
+ self.conn.commit()
1088
+ except Exception as e:
1089
+ logger.warning(
1090
+ "Could not rename %s.%s -> %s: %s. "
1091
+ "Dropping table so it can be recreated.",
1092
+ table,
1093
+ old_col,
1094
+ new_col,
1095
+ e,
1096
+ )
1097
+ self.conn.execute(f"DROP TABLE IF EXISTS {table}") # noqa: S608
1098
+ self.conn.commit()
1099
+
1100
+ def _migrate_expanded_terms(self) -> None:
1101
+ """Add expanded_terms column if missing (for databases created before this feature)."""
1102
+ for table in ("profiles", "user_playbooks", "agent_playbooks"):
1103
+ cols = {
1104
+ row["name"]
1105
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1106
+ }
1107
+ if "expanded_terms" not in cols:
1108
+ self.conn.execute(f"ALTER TABLE {table} ADD COLUMN expanded_terms TEXT")
1109
+ logger.info("Added expanded_terms column to %s", table)
1110
+ self.conn.commit()
1111
+
1112
+ def _migrate_agent_runs_schema(self) -> None:
1113
+ """Add resumable-agent run columns if missing from existing SQLite DBs."""
1114
+ cols = {
1115
+ row["name"]
1116
+ for row in self.conn.execute("PRAGMA table_info(_agent_runs)").fetchall()
1117
+ }
1118
+ if not cols:
1119
+ return
1120
+ if "max_steps_remaining" not in cols:
1121
+ self.conn.execute(
1122
+ "ALTER TABLE _agent_runs ADD COLUMN max_steps_remaining INTEGER"
1123
+ )
1124
+ logger.info("Added max_steps_remaining column to _agent_runs")
1125
+ self.conn.commit()
1126
+
1127
+ def _migrate_pending_tool_calls_schema(self) -> None:
1128
+ """Add pending-tool-call columns if missing from existing SQLite DBs."""
1129
+ cols = {
1130
+ row["name"]
1131
+ for row in self.conn.execute(
1132
+ "PRAGMA table_info(_pending_tool_calls)"
1133
+ ).fetchall()
1134
+ }
1135
+ if not cols:
1136
+ return
1137
+ if "superseded_by" not in cols:
1138
+ self.conn.execute(
1139
+ "ALTER TABLE _pending_tool_calls ADD COLUMN superseded_by TEXT"
1140
+ )
1141
+ logger.info("Added superseded_by column to _pending_tool_calls")
1142
+ self.conn.commit()
1143
+
1144
+ def _migrate_agentic_signals(self) -> None:
1145
+ """Add source_span/notes/reader_angle columns if missing.
1146
+
1147
+ Backfill-safe: columns are nullable with no default. Applies to both
1148
+ the profiles and user_playbooks tables — the agentic extraction
1149
+ pipeline populates them per-row; classic extraction leaves them NULL.
1150
+ """
1151
+ for table in ("profiles", "user_playbooks"):
1152
+ cols = {
1153
+ row["name"]
1154
+ for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
1155
+ }
1156
+ for col in ("source_span", "notes", "reader_angle"):
1157
+ if col not in cols:
1158
+ self.conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} TEXT") # noqa: S608
1159
+ logger.info("Added %s column to %s", col, table)
1160
+ self.conn.commit()
1161
+
1162
+ def _migrate_user_playbook_polarity(self) -> None:
1163
+ """Drop the legacy ``polarity`` column from ``user_playbooks`` if present.
1164
+
1165
+ Polarity is retired under Option B (orientation lives in rule wording and
1166
+ is LLM-judged, never a stored field). This mirrors the Supabase drop
1167
+ migration and brings databases created while the column existed back in
1168
+ line with the current schema, which no longer defines it.
1169
+ """
1170
+ cols = {
1171
+ row["name"]
1172
+ for row in self.conn.execute("PRAGMA table_info(user_playbooks)").fetchall()
1173
+ }
1174
+ if not cols:
1175
+ return
1176
+ if "polarity" in cols:
1177
+ self.conn.execute("ALTER TABLE user_playbooks DROP COLUMN polarity")
1178
+ logger.info("Dropped legacy polarity column from user_playbooks")
1179
+ self.conn.commit()
1180
+
1181
+ def _migrate_agent_playbook_source_windows(self) -> None:
1182
+ """Add source window snapshots to existing agent source mappings."""
1183
+ cols = {
1184
+ row["name"]
1185
+ for row in self.conn.execute(
1186
+ "PRAGMA table_info(agent_playbook_source_user_playbooks)"
1187
+ ).fetchall()
1188
+ }
1189
+ if not cols:
1190
+ return
1191
+ if "source_interaction_ids" not in cols:
1192
+ self.conn.execute(
1193
+ "ALTER TABLE agent_playbook_source_user_playbooks "
1194
+ "ADD COLUMN source_interaction_ids TEXT NOT NULL DEFAULT '[]'"
1195
+ )
1196
+ logger.info(
1197
+ "Added source_interaction_ids column to "
1198
+ "agent_playbook_source_user_playbooks"
1199
+ )
1200
+ self.conn.execute(
1201
+ "CREATE INDEX IF NOT EXISTS idx_apsup_user "
1202
+ "ON agent_playbook_source_user_playbooks(user_playbook_id)"
1203
+ )
1204
+ self.conn.commit()
1205
+
1206
+ def _migrate_request_metadata(self) -> None:
1207
+ """Add metadata column to requests for databases created before F2.
1208
+
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.
1212
+ """
1213
+ cols = {
1214
+ row["name"]
1215
+ for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
1216
+ }
1217
+ if not cols:
1218
+ return
1219
+ if "metadata" not in cols:
1220
+ self.conn.execute(
1221
+ "ALTER TABLE requests ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'"
1222
+ )
1223
+ logger.info("Added metadata column to requests")
1224
+ self.conn.commit()
1225
+
1226
+ def _migrate_shadow_comparison_verdicts(self) -> None:
1227
+ """F1: create the shadow_comparison_verdicts table if missing.
1228
+
1229
+ Idempotent; safe to run on every startup. The PRAGMA-LBYL guard
1230
+ 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
1233
+ also create this table on a fresh database, so this helper is a
1234
+ no-op there; its purpose is explicit symmetry with the per-feature
1235
+ migration convention and a single named hook the disk/supabase
1236
+ backends in Tasks 6/7 can mirror.
1237
+ """
1238
+ cur = self.conn.execute("PRAGMA table_info(shadow_comparison_verdicts)")
1239
+ cols = cur.fetchall()
1240
+ if cols:
1241
+ return
1242
+ self.conn.executescript("""
1243
+ CREATE TABLE IF NOT EXISTS shadow_comparison_verdicts (
1244
+ verdict_id INTEGER PRIMARY KEY AUTOINCREMENT,
1245
+ interaction_id TEXT NOT NULL,
1246
+ session_id TEXT NOT NULL,
1247
+ agent_version TEXT NOT NULL,
1248
+ reflexio_is_request_1 INTEGER NOT NULL,
1249
+ better_request TEXT NOT NULL CHECK (better_request IN ('1','2','tie')),
1250
+ is_significantly_better INTEGER NOT NULL,
1251
+ comparison_reason TEXT,
1252
+ judge_prompt_version TEXT NOT NULL,
1253
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
1254
+ );
1255
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_session
1256
+ ON shadow_comparison_verdicts (session_id, agent_version);
1257
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
1258
+ ON shadow_comparison_verdicts (created_at);
1259
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
1260
+ ON shadow_comparison_verdicts (judge_prompt_version);
1261
+ """)
1262
+ self.conn.commit()
1263
+ logger.info("Created shadow_comparison_verdicts table (F1 migration)")
1264
+
1265
+ # ------------------------------------------------------------------
1266
+ # Internal helpers
1267
+ # ------------------------------------------------------------------
1268
+
1269
+ def _execute(
1270
+ self, sql: str, params: tuple[Any, ...] | list[Any] = ()
1271
+ ) -> sqlite3.Cursor:
1272
+ with self._lock:
1273
+ cur = self.conn.execute(sql, params)
1274
+ self.conn.commit()
1275
+ return cur
1276
+
1277
+ def _fetchone(
1278
+ self, sql: str, params: tuple[Any, ...] | list[Any] = ()
1279
+ ) -> sqlite3.Row | None:
1280
+ with self._lock:
1281
+ return self.conn.execute(sql, params).fetchone()
1282
+
1283
+ def _fetchall(
1284
+ self, sql: str, params: tuple[Any, ...] | list[Any] = ()
1285
+ ) -> list[sqlite3.Row]:
1286
+ with self._lock:
1287
+ return self.conn.execute(sql, params).fetchall()
1288
+
1289
+ def _get_embedding(
1290
+ self, text: str, purpose: Literal["document", "query"] = "document"
1291
+ ) -> list[float]:
1292
+ """Generate an embedding with a purpose-specific prefix.
1293
+
1294
+ Args:
1295
+ text: The text to embed.
1296
+ purpose: Either ``"document"`` (stored embeddings) or ``"query"``
1297
+ (search-time embeddings). The prefix improves asymmetric
1298
+ retrieval quality for models that support it.
1299
+
1300
+ Returns:
1301
+ The embedding vector as a list of floats.
1302
+ """
1303
+ prefix = "search_document: " if purpose == "document" else "search_query: "
1304
+ try:
1305
+ return self.llm_client.get_embedding(
1306
+ prefix + text, self.embedding_model_name, self.embedding_dimensions
1307
+ )
1308
+ except EmbeddingUnavailableError as exc:
1309
+ logger.warning(
1310
+ "Embedding unavailable for %s text; continuing without vector: %s",
1311
+ purpose,
1312
+ exc,
1313
+ )
1314
+ return []
1315
+
1316
+ def _should_expand_documents(self) -> bool:
1317
+ """Check if document expansion is enabled."""
1318
+ return self._enable_document_expansion
1319
+
1320
+ def _expand_document(self, content: str) -> str | None:
1321
+ """Expand document content with synonyms for FTS recall.
1322
+
1323
+ Uses DocumentExpander to generate synonym groups. Returns the
1324
+ expanded_terms string (e.g., "backup, sync; failure, error")
1325
+ or None on failure.
1326
+
1327
+ Args:
1328
+ content (str): Document text to expand
1329
+
1330
+ Returns:
1331
+ str or None: Expanded terms text, or None if expansion fails/disabled
1332
+ """
1333
+ if not content:
1334
+ return None
1335
+ try:
1336
+ from reflexio.server.prompt.prompt_manager import PromptManager
1337
+ from reflexio.server.services.pre_retrieval import DocumentExpander
1338
+
1339
+ expander = DocumentExpander(
1340
+ llm_client=self.llm_client,
1341
+ prompt_manager=PromptManager(),
1342
+ )
1343
+ result = expander.expand(content)
1344
+ return result.expanded_text or None
1345
+ except Exception:
1346
+ logger.warning("Document expansion failed", exc_info=True)
1347
+ return None
1348
+
1349
+ def _current_timestamp(self) -> str:
1350
+ return datetime.now(UTC).isoformat()
1351
+
1352
+ # FTS helpers
1353
+ def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
1354
+ """Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
1355
+ with self._lock:
1356
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
1357
+ cols = list(text_fields.keys())
1358
+ vals = [text_fields[c] or "" for c in cols]
1359
+ placeholders = ",".join("?" for _ in cols)
1360
+ col_str = ",".join(cols)
1361
+ self.conn.execute(
1362
+ f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})",
1363
+ [rowid, *vals],
1364
+ )
1365
+ self.conn.commit()
1366
+
1367
+ def _fts_delete(self, table: str, rowid: int) -> None:
1368
+ with self._lock:
1369
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
1370
+ self.conn.commit()
1371
+
1372
+ def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
1373
+ """FTS for profiles uses profile_id TEXT as key column."""
1374
+ with self._lock:
1375
+ self.conn.execute(
1376
+ "DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
1377
+ )
1378
+ self.conn.execute(
1379
+ "INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
1380
+ (profile_id, content),
1381
+ )
1382
+ self.conn.commit()
1383
+
1384
+ def _fts_delete_profile(self, profile_id: str) -> None:
1385
+ with self._lock:
1386
+ self.conn.execute(
1387
+ "DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
1388
+ )
1389
+ self.conn.commit()
1390
+
1391
+ # Vec helpers (sqlite-vec)
1392
+ def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
1393
+ """Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
1394
+ if not self._has_sqlite_vec:
1395
+ return
1396
+ with self._lock:
1397
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
1398
+ self.conn.execute(
1399
+ f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)",
1400
+ (rowid, json.dumps(embedding)),
1401
+ )
1402
+ self.conn.commit()
1403
+
1404
+ def _vec_delete(self, table: str, rowid: int) -> None:
1405
+ """Delete a vec table row. No-op when sqlite-vec is unavailable."""
1406
+ if not self._has_sqlite_vec:
1407
+ return
1408
+ with self._lock:
1409
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
1410
+ self.conn.commit()
1411
+
1412
+ def _vec_knn_search(
1413
+ self,
1414
+ vec_table: str,
1415
+ main_table: str,
1416
+ query_embedding: list[float],
1417
+ match_count: int,
1418
+ conditions: list[str] | None = None,
1419
+ params: list[Any] | None = None,
1420
+ ) -> list[sqlite3.Row]:
1421
+ """Run a native KNN search via sqlite-vec and join back to the main table.
1422
+
1423
+ Over-fetches from the KNN index (5x ``match_count``) so that post-filter
1424
+ WHERE conditions (org, user, status, etc.) don't silently reduce the
1425
+ result set below the requested count.
1426
+
1427
+ Args:
1428
+ vec_table: Name of the vec0 virtual table.
1429
+ main_table: Name of the main data table.
1430
+ query_embedding: Query embedding vector.
1431
+ match_count: Number of results to return.
1432
+ conditions: Optional WHERE conditions for the main table.
1433
+ params: Parameters for the conditions.
1434
+
1435
+ Returns:
1436
+ Up to ``match_count`` rows from the main table, ordered by vector
1437
+ distance (ascending).
1438
+ """
1439
+ knn_overfetch = match_count * 5
1440
+ where_clause = " AND ".join(conditions) if conditions else "1=1"
1441
+ sql = f"""SELECT m.* FROM {main_table} m
1442
+ JOIN (
1443
+ SELECT rowid, distance FROM {vec_table}
1444
+ WHERE embedding MATCH ?
1445
+ ORDER BY distance
1446
+ LIMIT ?
1447
+ ) v ON m.rowid = v.rowid
1448
+ WHERE {where_clause}
1449
+ ORDER BY v.distance
1450
+ LIMIT ?"""
1451
+ all_params = [
1452
+ json.dumps(query_embedding),
1453
+ knn_overfetch,
1454
+ *(params or []),
1455
+ match_count,
1456
+ ]
1457
+ return self._fetchall(sql, all_params)
1458
+
1459
+ # ------------------------------------------------------------------
1460
+ # Per-user data clear
1461
+ # ------------------------------------------------------------------
1462
+
1463
+ def clear_user_data(self, user_id: str) -> dict[str, int]:
1464
+ """Atomic per-``user_id`` row deletion across all user-scoped tables.
1465
+
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.
1471
+
1472
+ Also cleans up FTS and vector sidecars for the user's rows so
1473
+ subsequent searches don't surface deleted data.
1474
+
1475
+ Args:
1476
+ user_id (str): The user id whose rows should be deleted.
1477
+
1478
+ Returns:
1479
+ dict[str, int]: Per-entity deletion counts with keys
1480
+ ``interactions``, ``user_playbooks``, ``profiles``, and
1481
+ ``requests``.
1482
+ """
1483
+ with self._lock:
1484
+ # Snapshot rowids/ids that need FTS or vector cleanup before
1485
+ # the DELETE removes them from the main tables.
1486
+ interaction_ids = [
1487
+ r["interaction_id"]
1488
+ for r in self.conn.execute(
1489
+ "SELECT interaction_id FROM interactions WHERE user_id = ?",
1490
+ (user_id,),
1491
+ ).fetchall()
1492
+ ]
1493
+ user_playbook_ids = [
1494
+ r["user_playbook_id"]
1495
+ for r in self.conn.execute(
1496
+ "SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
1497
+ (user_id,),
1498
+ ).fetchall()
1499
+ ]
1500
+ profile_rows = self.conn.execute(
1501
+ "SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
1502
+ (user_id,),
1503
+ ).fetchall()
1504
+ profile_rowids = [r["rowid"] for r in profile_rows]
1505
+ profile_ids = [r["profile_id"] for r in profile_rows]
1506
+
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
+ )
1526
+
1527
+ # Vector index cleanup (best-effort: only if sqlite-vec loaded)
1528
+ 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
+ )
1541
+
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.
1545
+ interactions_cur = self.conn.execute(
1546
+ "DELETE FROM interactions WHERE user_id = ?", (user_id,)
1547
+ )
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
+ requests_cur = self.conn.execute(
1555
+ "DELETE FROM requests WHERE user_id = ?", (user_id,)
1556
+ )
1557
+ self.conn.commit()
1558
+
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
+ }
1565
+
1566
+
1567
+ # ---------------------------------------------------------------------------
1568
+ # DDL — table and FTS definitions
1569
+ # ---------------------------------------------------------------------------
1570
+
1571
+ _DDL = """
1572
+ CREATE TABLE IF NOT EXISTS profiles (
1573
+ profile_id TEXT PRIMARY KEY,
1574
+ user_id TEXT NOT NULL,
1575
+ content TEXT NOT NULL DEFAULT '',
1576
+ last_modified_timestamp INTEGER NOT NULL,
1577
+ generated_from_request_id TEXT NOT NULL DEFAULT '',
1578
+ profile_time_to_live TEXT NOT NULL DEFAULT 'infinity',
1579
+ expiration_timestamp INTEGER NOT NULL DEFAULT 4102444800,
1580
+ custom_features TEXT,
1581
+ embedding TEXT,
1582
+ source TEXT DEFAULT '',
1583
+ status TEXT,
1584
+ extractor_names TEXT,
1585
+ expanded_terms TEXT,
1586
+ source_span TEXT,
1587
+ notes TEXT,
1588
+ reader_angle TEXT,
1589
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
1590
+ );
1591
+ CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
1592
+ CREATE INDEX IF NOT EXISTS idx_profiles_status ON profiles(status);
1593
+
1594
+ CREATE TABLE IF NOT EXISTS interactions (
1595
+ interaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
1596
+ user_id TEXT NOT NULL,
1597
+ content TEXT NOT NULL DEFAULT '',
1598
+ request_id TEXT NOT NULL,
1599
+ created_at TEXT NOT NULL,
1600
+ role TEXT NOT NULL DEFAULT 'User',
1601
+ user_action TEXT NOT NULL DEFAULT 'none',
1602
+ user_action_description TEXT NOT NULL DEFAULT '',
1603
+ interacted_image_url TEXT NOT NULL DEFAULT '',
1604
+ shadow_content TEXT NOT NULL DEFAULT '',
1605
+ expert_content TEXT NOT NULL DEFAULT '',
1606
+ tools_used TEXT,
1607
+ citations TEXT,
1608
+ embedding TEXT
1609
+ );
1610
+ CREATE INDEX IF NOT EXISTS idx_interactions_user_id ON interactions(user_id);
1611
+ CREATE INDEX IF NOT EXISTS idx_interactions_request_id ON interactions(request_id);
1612
+ CREATE INDEX IF NOT EXISTS idx_interactions_created_at ON interactions(created_at);
1613
+
1614
+ CREATE TABLE IF NOT EXISTS requests (
1615
+ request_id TEXT PRIMARY KEY,
1616
+ user_id TEXT NOT NULL,
1617
+ created_at TEXT NOT NULL,
1618
+ source TEXT NOT NULL DEFAULT '',
1619
+ agent_version TEXT NOT NULL DEFAULT '',
1620
+ session_id TEXT,
1621
+ metadata TEXT NOT NULL DEFAULT '{}'
1622
+ );
1623
+ CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
1624
+ CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
1625
+ CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
1626
+
1627
+ CREATE TABLE IF NOT EXISTS user_playbooks (
1628
+ user_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
1629
+ user_id TEXT,
1630
+ playbook_name TEXT NOT NULL DEFAULT '',
1631
+ created_at TEXT NOT NULL,
1632
+ request_id TEXT NOT NULL,
1633
+ agent_version TEXT NOT NULL DEFAULT '',
1634
+ content TEXT NOT NULL DEFAULT '',
1635
+ trigger TEXT,
1636
+ rationale TEXT,
1637
+ blocking_issue TEXT,
1638
+ source_interaction_ids TEXT,
1639
+ status TEXT,
1640
+ source TEXT,
1641
+ embedding TEXT,
1642
+ expanded_terms TEXT,
1643
+ source_span TEXT,
1644
+ notes TEXT,
1645
+ reader_angle TEXT
1646
+ );
1647
+ CREATE INDEX IF NOT EXISTS idx_user_playbooks_playbook_name ON user_playbooks(playbook_name);
1648
+ CREATE INDEX IF NOT EXISTS idx_user_playbooks_agent_version ON user_playbooks(agent_version);
1649
+ CREATE INDEX IF NOT EXISTS idx_user_playbooks_status ON user_playbooks(status);
1650
+ CREATE INDEX IF NOT EXISTS idx_user_playbooks_created_at ON user_playbooks(created_at);
1651
+
1652
+ CREATE TABLE IF NOT EXISTS agent_playbooks (
1653
+ agent_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
1654
+ playbook_name TEXT NOT NULL DEFAULT '',
1655
+ created_at TEXT NOT NULL,
1656
+ agent_version TEXT NOT NULL DEFAULT '',
1657
+ content TEXT NOT NULL DEFAULT '',
1658
+ trigger TEXT,
1659
+ rationale TEXT,
1660
+ blocking_issue TEXT,
1661
+ playbook_status TEXT NOT NULL DEFAULT 'pending',
1662
+ playbook_metadata TEXT NOT NULL DEFAULT '',
1663
+ embedding TEXT,
1664
+ expanded_terms TEXT,
1665
+ status TEXT
1666
+ );
1667
+ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_playbook_name ON agent_playbooks(playbook_name);
1668
+ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_agent_version ON agent_playbooks(agent_version);
1669
+ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_status ON agent_playbooks(status);
1670
+ CREATE INDEX IF NOT EXISTS idx_agent_playbooks_created_at ON agent_playbooks(created_at);
1671
+
1672
+ CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
1673
+ result_id INTEGER PRIMARY KEY AUTOINCREMENT,
1674
+ session_id TEXT NOT NULL,
1675
+ agent_version TEXT NOT NULL DEFAULT '',
1676
+ evaluation_name TEXT,
1677
+ is_success INTEGER NOT NULL DEFAULT 0,
1678
+ failure_type TEXT,
1679
+ failure_reason TEXT,
1680
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1681
+ regular_vs_shadow TEXT,
1682
+ number_of_correction_per_session INTEGER NOT NULL DEFAULT 0,
1683
+ user_turns_to_resolution INTEGER,
1684
+ is_escalated INTEGER NOT NULL DEFAULT 0,
1685
+ embedding TEXT
1686
+ );
1687
+ CREATE INDEX IF NOT EXISTS idx_eval_agent_version ON agent_success_evaluation_result(agent_version);
1688
+ 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);
1714
+
1715
+ CREATE TABLE IF NOT EXISTS agent_playbook_source_user_playbooks (
1716
+ agent_playbook_id INTEGER NOT NULL,
1717
+ user_playbook_id INTEGER NOT NULL,
1718
+ source_interaction_ids TEXT NOT NULL DEFAULT '[]',
1719
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1720
+ PRIMARY KEY (agent_playbook_id, user_playbook_id)
1721
+ );
1722
+ CREATE INDEX IF NOT EXISTS idx_apsup_agent ON agent_playbook_source_user_playbooks(agent_playbook_id);
1723
+ CREATE INDEX IF NOT EXISTS idx_apsup_user ON agent_playbook_source_user_playbooks(user_playbook_id);
1724
+
1725
+ CREATE TABLE IF NOT EXISTS playbook_optimization_jobs (
1726
+ job_id INTEGER PRIMARY KEY AUTOINCREMENT,
1727
+ target_kind TEXT NOT NULL,
1728
+ target_id INTEGER NOT NULL,
1729
+ status TEXT NOT NULL DEFAULT 'pending',
1730
+ best_candidate_id INTEGER,
1731
+ successor_target_id INTEGER,
1732
+ decision_reason TEXT NOT NULL DEFAULT '',
1733
+ metadata_json TEXT NOT NULL DEFAULT '{}',
1734
+ created_at INTEGER NOT NULL,
1735
+ updated_at INTEGER NOT NULL
1736
+ );
1737
+ CREATE INDEX IF NOT EXISTS idx_poj_target ON playbook_optimization_jobs(target_kind, target_id);
1738
+ CREATE INDEX IF NOT EXISTS idx_poj_status ON playbook_optimization_jobs(status);
1739
+
1740
+ CREATE TABLE IF NOT EXISTS playbook_optimization_candidates (
1741
+ candidate_id INTEGER PRIMARY KEY AUTOINCREMENT,
1742
+ job_id INTEGER NOT NULL,
1743
+ candidate_index INTEGER NOT NULL DEFAULT 0,
1744
+ content TEXT NOT NULL,
1745
+ parent_candidate_ids TEXT NOT NULL DEFAULT '[]',
1746
+ aggregate_score REAL,
1747
+ is_winner INTEGER NOT NULL DEFAULT 0,
1748
+ created_at INTEGER NOT NULL
1749
+ );
1750
+ CREATE INDEX IF NOT EXISTS idx_poc_job ON playbook_optimization_candidates(job_id);
1751
+
1752
+ CREATE TABLE IF NOT EXISTS playbook_optimization_evaluations (
1753
+ evaluation_id INTEGER PRIMARY KEY AUTOINCREMENT,
1754
+ job_id INTEGER NOT NULL,
1755
+ candidate_id INTEGER NOT NULL,
1756
+ target_kind TEXT NOT NULL,
1757
+ target_id INTEGER NOT NULL,
1758
+ scenario_user_playbook_id INTEGER,
1759
+ source_interaction_ids TEXT NOT NULL DEFAULT '[]',
1760
+ score REAL NOT NULL DEFAULT 0.0,
1761
+ verdict TEXT NOT NULL DEFAULT 'tie',
1762
+ likert INTEGER NOT NULL DEFAULT 0,
1763
+ rationale TEXT NOT NULL DEFAULT '',
1764
+ asi_json TEXT NOT NULL DEFAULT '{}',
1765
+ incumbent_rollout_json TEXT NOT NULL DEFAULT '[]',
1766
+ candidate_rollout_json TEXT NOT NULL DEFAULT '[]',
1767
+ created_at INTEGER NOT NULL
1768
+ );
1769
+ CREATE INDEX IF NOT EXISTS idx_poe_job ON playbook_optimization_evaluations(job_id);
1770
+ CREATE INDEX IF NOT EXISTS idx_poe_candidate ON playbook_optimization_evaluations(candidate_id);
1771
+
1772
+ CREATE TABLE IF NOT EXISTS playbook_optimization_events (
1773
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
1774
+ job_id INTEGER NOT NULL,
1775
+ event_type TEXT NOT NULL,
1776
+ payload_json TEXT NOT NULL DEFAULT '{}',
1777
+ created_at INTEGER NOT NULL
1778
+ );
1779
+ CREATE INDEX IF NOT EXISTS idx_poev_job ON playbook_optimization_events(job_id);
1780
+
1781
+ CREATE TABLE IF NOT EXISTS _operation_state (
1782
+ service_name TEXT PRIMARY KEY,
1783
+ operation_state TEXT NOT NULL DEFAULT '{}',
1784
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
1785
+ );
1786
+
1787
+ CREATE TABLE IF NOT EXISTS _agent_runs (
1788
+ id TEXT PRIMARY KEY,
1789
+ org_id TEXT NOT NULL,
1790
+ extractor_kind TEXT NOT NULL,
1791
+ user_id TEXT,
1792
+ request_id TEXT NOT NULL,
1793
+ agent_version TEXT,
1794
+ source TEXT,
1795
+ source_interaction_ids TEXT NOT NULL DEFAULT '[]',
1796
+ window_start_interaction_id INTEGER,
1797
+ window_end_interaction_id INTEGER,
1798
+ extractor_config_hash TEXT,
1799
+ status TEXT NOT NULL,
1800
+ generation_request_snapshot TEXT NOT NULL DEFAULT '{}',
1801
+ service_config_snapshot TEXT,
1802
+ agent_context_snapshot TEXT,
1803
+ committed_output TEXT,
1804
+ pending_tool_call_ids TEXT NOT NULL DEFAULT '[]',
1805
+ max_steps_remaining INTEGER,
1806
+ resume_attempts INTEGER NOT NULL DEFAULT 0,
1807
+ finalization_attempts INTEGER NOT NULL DEFAULT 0,
1808
+ next_resume_at TEXT,
1809
+ claimed_by TEXT,
1810
+ claimed_at TEXT,
1811
+ agent_completed_at TEXT,
1812
+ finalized_at TEXT,
1813
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1814
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1815
+ expires_at TEXT,
1816
+ last_error TEXT
1817
+ );
1818
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_ready ON _agent_runs(status, next_resume_at, updated_at);
1819
+ CREATE INDEX IF NOT EXISTS idx_agent_runs_binding ON _agent_runs(org_id, extractor_kind, user_id);
1820
+
1821
+ CREATE TABLE IF NOT EXISTS _pending_tool_calls (
1822
+ id TEXT PRIMARY KEY,
1823
+ org_id TEXT NOT NULL,
1824
+ user_id TEXT,
1825
+ scope TEXT NOT NULL DEFAULT '{}',
1826
+ scope_hash TEXT NOT NULL,
1827
+ tool_name TEXT NOT NULL,
1828
+ dedup_key TEXT NOT NULL,
1829
+ status TEXT NOT NULL,
1830
+ question_text TEXT NOT NULL,
1831
+ answer_format TEXT,
1832
+ args TEXT NOT NULL DEFAULT '{}',
1833
+ tags TEXT NOT NULL DEFAULT '[]',
1834
+ result TEXT,
1835
+ embedding TEXT,
1836
+ superseded_by TEXT,
1837
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1838
+ resolved_at TEXT,
1839
+ expires_at TEXT NOT NULL,
1840
+ cache_until TEXT NOT NULL,
1841
+ valid_until TEXT
1842
+ );
1843
+ CREATE INDEX IF NOT EXISTS idx_pending_tool_calls_active ON _pending_tool_calls(org_id, scope_hash, tool_name, dedup_key, status, cache_until);
1844
+ CREATE INDEX IF NOT EXISTS idx_pending_tool_calls_prior ON _pending_tool_calls(org_id, scope_hash, tool_name, status, valid_until);
1845
+
1846
+ CREATE TABLE IF NOT EXISTS _run_tool_dependencies (
1847
+ run_id TEXT NOT NULL,
1848
+ pending_tool_call_id TEXT NOT NULL,
1849
+ dependency_kind TEXT NOT NULL DEFAULT 'followup',
1850
+ resolved_at TEXT,
1851
+ consumed_at TEXT,
1852
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1853
+ PRIMARY KEY (run_id, pending_tool_call_id),
1854
+ FOREIGN KEY (run_id) REFERENCES _agent_runs(id) ON DELETE CASCADE,
1855
+ FOREIGN KEY (pending_tool_call_id) REFERENCES _pending_tool_calls(id) ON DELETE CASCADE
1856
+ );
1857
+ CREATE INDEX IF NOT EXISTS idx_run_tool_dependencies_pending ON _run_tool_dependencies(pending_tool_call_id, resolved_at, consumed_at);
1858
+ CREATE INDEX IF NOT EXISTS idx_run_tool_dependencies_ready ON _run_tool_dependencies(run_id, resolved_at, consumed_at);
1859
+
1860
+ -- FTS5 virtual tables
1861
+ CREATE VIRTUAL TABLE IF NOT EXISTS interactions_fts USING fts5(
1862
+ content, user_action_description,
1863
+ tokenize="porter unicode61"
1864
+ );
1865
+
1866
+ CREATE VIRTUAL TABLE IF NOT EXISTS profiles_fts USING fts5(
1867
+ profile_id, content,
1868
+ tokenize="porter unicode61"
1869
+ );
1870
+
1871
+ CREATE VIRTUAL TABLE IF NOT EXISTS user_playbooks_fts USING fts5(
1872
+ search_text,
1873
+ tokenize="porter unicode61"
1874
+ );
1875
+
1876
+ CREATE VIRTUAL TABLE IF NOT EXISTS agent_playbooks_fts USING fts5(
1877
+ search_text,
1878
+ tokenize="porter unicode61"
1879
+ );
1880
+
1881
+ CREATE TABLE IF NOT EXISTS share_links (
1882
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1883
+ org_id TEXT NOT NULL,
1884
+ token TEXT NOT NULL UNIQUE,
1885
+ resource_type TEXT NOT NULL,
1886
+ resource_id TEXT NOT NULL,
1887
+ created_at INTEGER NOT NULL,
1888
+ expires_at INTEGER,
1889
+ created_by_email TEXT
1890
+ );
1891
+ CREATE INDEX IF NOT EXISTS idx_share_links_resource ON share_links(resource_type, resource_id);
1892
+
1893
+ -- ============================================================================
1894
+ -- Braintrust connector (Plan C-backend)
1895
+ -- ============================================================================
1896
+
1897
+ CREATE TABLE IF NOT EXISTS braintrust_connection (
1898
+ org_id TEXT PRIMARY KEY,
1899
+ api_key_enc TEXT NOT NULL,
1900
+ workspace_id TEXT NOT NULL,
1901
+ workspace_name TEXT NOT NULL DEFAULT '',
1902
+ project_ids TEXT NOT NULL DEFAULT '[]',
1903
+ last_sync_ts INTEGER,
1904
+ last_error TEXT
1905
+ );
1906
+
1907
+ CREATE TABLE IF NOT EXISTS imported_score (
1908
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
1909
+ org_id TEXT NOT NULL,
1910
+ source TEXT NOT NULL,
1911
+ source_run_id TEXT NOT NULL,
1912
+ session_id TEXT,
1913
+ scorer_name TEXT NOT NULL,
1914
+ value REAL NOT NULL,
1915
+ ts INTEGER NOT NULL,
1916
+ UNIQUE (org_id, source, source_run_id, scorer_name)
1917
+ );
1918
+ CREATE INDEX IF NOT EXISTS idx_imported_score_session
1919
+ ON imported_score (org_id, session_id) WHERE session_id IS NOT NULL;
1920
+ CREATE INDEX IF NOT EXISTS idx_imported_score_ts ON imported_score (org_id, ts);
1921
+
1922
+ -- ============================================================================
1923
+ -- Per-turn shadow comparison verdicts (F1)
1924
+ -- ============================================================================
1925
+
1926
+ CREATE TABLE IF NOT EXISTS shadow_comparison_verdicts (
1927
+ verdict_id INTEGER PRIMARY KEY AUTOINCREMENT,
1928
+ interaction_id TEXT NOT NULL,
1929
+ session_id TEXT NOT NULL,
1930
+ agent_version TEXT NOT NULL,
1931
+ reflexio_is_request_1 INTEGER NOT NULL,
1932
+ better_request TEXT NOT NULL CHECK (better_request IN ('1','2','tie')),
1933
+ is_significantly_better INTEGER NOT NULL,
1934
+ comparison_reason TEXT,
1935
+ judge_prompt_version TEXT NOT NULL,
1936
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
1937
+ );
1938
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_session
1939
+ ON shadow_comparison_verdicts (session_id, agent_version);
1940
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
1941
+ ON shadow_comparison_verdicts (created_at);
1942
+ CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
1943
+ ON shadow_comparison_verdicts (judge_prompt_version);
1944
+
1945
+ """