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,800 @@
1
+ """Sliding-window reflection: critique-and-revise of cited memories.
2
+
3
+ Invoked from ``GenerationService.run`` after the publish has saved its
4
+ interactions and before the extractor pool spins up. Mirrors the
5
+ extractor pattern: window of size ``window_size`` (global), advanced
6
+ every ``stride_size`` interactions per ``OperationStateManager``
7
+ bookmark. When the gate is open and at least one Assistant interaction
8
+ in the window cites a *current* user playbook / user profile row, the
9
+ service asks an LLM whether any of those cited rows should be replaced
10
+ in light of how they were applied across the window. Replacements
11
+ archive the cited row and insert a new current row with copied identity
12
+ and metadata.
13
+
14
+ Runs *before* extraction on purpose: extractors then read the
15
+ post-reflection state when computing existing-data context.
16
+
17
+ Failure isolation:
18
+ - Per-decision apply errors are caught locally so one bad decision
19
+ cannot block the others.
20
+ - Top-level failures (extractor LLM call) are caught and logged; the
21
+ bookmark is *not* advanced so the next publish retries the window.
22
+ - The caller (``GenerationService.run``) wraps reflection in its own
23
+ try/except so a reflection bug never breaks the publish.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import uuid
30
+ from dataclasses import dataclass
31
+ from datetime import UTC, datetime
32
+ from typing import TYPE_CHECKING
33
+
34
+ from reflexio.models.api_schema.domain.entities import (
35
+ Citation,
36
+ Interaction,
37
+ UserPlaybook,
38
+ UserProfile,
39
+ )
40
+ from reflexio.server.llm.litellm_client import LiteLLMClient
41
+ from reflexio.server.services.operation_state_utils import OperationStateManager
42
+ from reflexio.server.services.reflection.reflection_extractor import (
43
+ ReflectionExtractor,
44
+ )
45
+ from reflexio.server.services.reflection.reflection_service_utils import (
46
+ REFLECTION_OPERATION_NAME,
47
+ ReflectionDecision,
48
+ ReflectionResult,
49
+ ReflectionServiceRequest,
50
+ )
51
+ from reflexio.server.tracing import sentry_tags
52
+
53
+ if TYPE_CHECKING:
54
+ from reflexio.models.api_schema.internal_schema import (
55
+ RequestInteractionDataModel,
56
+ )
57
+ from reflexio.server.api_endpoints.request_context import RequestContext
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ # Fallback per-pass revision cap when no ReflectionConfig is available in
62
+ # the apply path. Mirrors ReflectionConfig.max_revisions_per_pass default.
63
+ _DEFAULT_MAX_REVISIONS_PER_PASS = 8
64
+
65
+
66
+ class ReflectionService:
67
+ """Sliding-window reflection step.
68
+
69
+ Args:
70
+ request_context (RequestContext): Provides storage, prompt
71
+ manager, and configurator.
72
+ llm_client (LiteLLMClient): Shared LLM client.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ request_context: RequestContext,
78
+ llm_client: LiteLLMClient,
79
+ ):
80
+ self.request_context = request_context
81
+ self.client = llm_client
82
+
83
+ def run(self, request: ReflectionServiceRequest) -> ReflectionResult:
84
+ """Execute one reflection pass.
85
+
86
+ Always returns a ``ReflectionResult`` describing what happened.
87
+ Routine failure modes (gate closed, no citations, missing rows,
88
+ LLM error) do not raise. Storage exceptions propagate.
89
+ """
90
+ result = ReflectionResult()
91
+ config = self.request_context.configurator.get_config()
92
+ reflection_config = config.reflection_config if config else None
93
+ if reflection_config is not None and not reflection_config.enabled:
94
+ return result
95
+
96
+ storage = self.request_context.storage
97
+ if storage is None:
98
+ return result
99
+
100
+ window_size = config.window_size if config else 10
101
+ stride_size = config.stride_size if config else 5
102
+ sources = [request.source] if request.source else None
103
+
104
+ mgr = OperationStateManager(
105
+ storage, # type: ignore[arg-type]
106
+ self.request_context.org_id,
107
+ REFLECTION_OPERATION_NAME,
108
+ )
109
+
110
+ # Gate: how many new interactions since last bookmark?
111
+ _, new_models = mgr.get_extractor_state_with_new_interactions(
112
+ REFLECTION_OPERATION_NAME,
113
+ user_id=request.user_id,
114
+ sources=sources,
115
+ )
116
+ new_count = sum(len(m.interactions) for m in new_models)
117
+ if new_count < stride_size:
118
+ return result
119
+ result.gate_open = True
120
+
121
+ # Pull window of last window_size interactions for the user.
122
+ window_models, _flat = storage.get_last_k_interactions_grouped(
123
+ user_id=request.user_id,
124
+ k=window_size,
125
+ sources=sources,
126
+ )
127
+ window_interactions = _flatten(window_models)
128
+ if not window_interactions:
129
+ mgr.update_extractor_bookmark(
130
+ REFLECTION_OPERATION_NAME,
131
+ processed_interactions=[],
132
+ user_id=request.user_id,
133
+ )
134
+ return result
135
+
136
+ citations = _collect_citations(window_interactions)
137
+ result.cited_count = len(citations)
138
+ if not citations:
139
+ # Advance the bookmark — we did examine this window.
140
+ mgr.update_extractor_bookmark(
141
+ REFLECTION_OPERATION_NAME,
142
+ processed_interactions=window_interactions,
143
+ user_id=request.user_id,
144
+ )
145
+ return result
146
+
147
+ # Post-horizon filter: only send citations with enough follow-up context.
148
+ post_horizon_size = (
149
+ reflection_config.post_horizon_size if reflection_config else 3
150
+ )
151
+ eligible = _filter_citations_by_horizon(
152
+ citations=citations,
153
+ window=window_interactions,
154
+ post_horizon_size=post_horizon_size,
155
+ stride_size=stride_size,
156
+ )
157
+ deferred_count = len(citations) - len(eligible)
158
+ result.skipped_count += deferred_count
159
+ if not eligible:
160
+ mgr.update_extractor_bookmark(
161
+ REFLECTION_OPERATION_NAME,
162
+ processed_interactions=window_interactions,
163
+ user_id=request.user_id,
164
+ )
165
+ return result
166
+
167
+ eligible_citations = [e.citation for e in eligible]
168
+ horizon_by_key = {
169
+ (e.citation.kind, e.citation.real_id): e.has_full_horizon for e in eligible
170
+ }
171
+ cited_profiles, cited_playbooks, missing = self._resolve_cited_rows(
172
+ user_id=request.user_id,
173
+ citations=eligible_citations,
174
+ )
175
+ result.skipped_count += missing
176
+ result.considered_count = len(cited_profiles) + len(cited_playbooks)
177
+ if result.considered_count == 0:
178
+ mgr.update_extractor_bookmark(
179
+ REFLECTION_OPERATION_NAME,
180
+ processed_interactions=window_interactions,
181
+ user_id=request.user_id,
182
+ )
183
+ return result
184
+
185
+ agent_context = (config.agent_context_prompt or "") if config else ""
186
+ model_override = reflection_config.model if reflection_config else None
187
+ extractor = ReflectionExtractor(
188
+ request_context=self.request_context,
189
+ llm_client=self.client,
190
+ agent_context=agent_context,
191
+ model_override=model_override,
192
+ )
193
+
194
+ try:
195
+ output = extractor.run(
196
+ window_interactions=window_interactions,
197
+ cited_profiles=cited_profiles,
198
+ cited_user_playbooks=cited_playbooks,
199
+ horizon_by_key=horizon_by_key,
200
+ )
201
+ except Exception as exc: # noqa: BLE001 — best-effort
202
+ with sentry_tags(
203
+ subsystem="reflection",
204
+ op="extractor",
205
+ org_id=self.request_context.org_id,
206
+ user_id=request.user_id,
207
+ error_type=type(exc).__name__,
208
+ ):
209
+ logger.exception(
210
+ "event=reflection_extractor_failed user_id=%s",
211
+ request.user_id,
212
+ )
213
+ # Don't advance bookmark — let the next publish retry.
214
+ return result
215
+
216
+ result.ran = True
217
+ profiles_by_id = {p.profile_id: p for p in cited_profiles}
218
+ playbooks_by_id = {p.user_playbook_id: p for p in cited_playbooks}
219
+
220
+ max_revisions_per_pass = (
221
+ reflection_config.max_revisions_per_pass
222
+ if reflection_config is not None
223
+ else _DEFAULT_MAX_REVISIONS_PER_PASS
224
+ )
225
+
226
+ for decision in output.decisions:
227
+ if not _is_revision(decision):
228
+ result.no_change_count += 1
229
+ continue
230
+ # Per-pass cap: once we've applied max_revisions_per_pass
231
+ # revisions, skip any further revision-intent decisions.
232
+ if result.revised_count >= max_revisions_per_pass:
233
+ result.capped_count += 1
234
+ continue
235
+ try:
236
+ self._validate_decision(decision, profiles_by_id, playbooks_by_id)
237
+ applied = self._apply_revision(
238
+ request=request,
239
+ decision=decision,
240
+ profiles_by_id=profiles_by_id,
241
+ playbooks_by_id=playbooks_by_id,
242
+ )
243
+ except Exception as exc: # noqa: BLE001 — per-decision isolation
244
+ result.failed_count += 1
245
+ with sentry_tags(
246
+ subsystem="reflection",
247
+ op="apply_decision",
248
+ org_id=self.request_context.org_id,
249
+ user_id=request.user_id,
250
+ target_kind=decision.target_kind,
251
+ target_id=decision.target_id,
252
+ error_type=type(exc).__name__,
253
+ ):
254
+ logger.exception(
255
+ "event=reflection_apply_failed kind=%s target_id=%s",
256
+ decision.target_kind, decision.target_id,
257
+ )
258
+ continue
259
+ if applied:
260
+ result.revised_count += 1
261
+ # Field-derivable granular counters (no mode label exists).
262
+ if decision.new_trigger is not None:
263
+ result.trigger_revised_count += 1
264
+ if decision.new_content is not None:
265
+ result.content_revised_count += 1
266
+ if decision.new_profile_time_to_live is not None:
267
+ result.ttl_changed_count += 1
268
+ else:
269
+ result.skipped_count += 1
270
+
271
+ mgr.update_extractor_bookmark(
272
+ REFLECTION_OPERATION_NAME,
273
+ processed_interactions=window_interactions,
274
+ user_id=request.user_id,
275
+ )
276
+
277
+ logger.info(
278
+ "event=reflection_done user_id=%s gate_open=%s ran=%s "
279
+ "cited=%d considered=%d no_change=%d revised=%d "
280
+ "trigger_revised=%d content_revised=%d ttl_changed=%d capped=%d "
281
+ "skipped=%d failed=%d",
282
+ request.user_id,
283
+ result.gate_open,
284
+ result.ran,
285
+ result.cited_count,
286
+ result.considered_count,
287
+ result.no_change_count,
288
+ result.revised_count,
289
+ result.trigger_revised_count,
290
+ result.content_revised_count,
291
+ result.ttl_changed_count,
292
+ result.capped_count,
293
+ result.skipped_count,
294
+ result.failed_count,
295
+ )
296
+ return result
297
+
298
+ # ------------------------------------------------------------------
299
+ # Helpers
300
+ # ------------------------------------------------------------------
301
+
302
+ def _resolve_cited_rows(
303
+ self,
304
+ *,
305
+ user_id: str,
306
+ citations: list[Citation],
307
+ ) -> tuple[list[UserProfile], list[UserPlaybook], int]:
308
+ """Resolve citations to current rows; count missing/archived.
309
+
310
+ Citations whose target row has already been archived (e.g. by
311
+ the deduplicator earlier in the publish flow, or by a prior
312
+ reflection pass) are silently skipped — only ``status IS NULL``
313
+ rows are sent to the LLM. Uses ``get_profiles_by_ids`` /
314
+ ``get_user_playbooks_by_ids`` so storage filters server-side
315
+ rather than scanning every row for the user.
316
+
317
+ Returns:
318
+ Tuple of (cited_profiles, cited_playbooks, missing_count).
319
+ """
320
+ storage = self.request_context.storage
321
+ wanted_profile_ids: set[str] = set()
322
+ wanted_playbook_ids: set[int] = set()
323
+ for c in citations:
324
+ if c.kind == "profile":
325
+ wanted_profile_ids.add(c.real_id)
326
+ elif c.kind == "playbook":
327
+ try:
328
+ wanted_playbook_ids.add(int(c.real_id))
329
+ except (TypeError, ValueError):
330
+ continue
331
+
332
+ cited_profiles: list[UserProfile] = []
333
+ if wanted_profile_ids:
334
+ cited_profiles = storage.get_profiles_by_ids( # type: ignore[union-attr]
335
+ user_id=user_id,
336
+ profile_ids=list(wanted_profile_ids),
337
+ status_filter=[None],
338
+ )
339
+
340
+ cited_playbooks: list[UserPlaybook] = []
341
+ if wanted_playbook_ids:
342
+ cited_playbooks = storage.get_user_playbooks_by_ids( # type: ignore[union-attr]
343
+ user_id=user_id,
344
+ user_playbook_ids=list(wanted_playbook_ids),
345
+ status_filter=[None],
346
+ )
347
+
348
+ found = len(cited_profiles) + len(cited_playbooks)
349
+ missing = max(
350
+ 0,
351
+ len(wanted_profile_ids) + len(wanted_playbook_ids) - found,
352
+ )
353
+ return cited_profiles, cited_playbooks, missing
354
+
355
+ def _validate_decision(
356
+ self,
357
+ decision: ReflectionDecision,
358
+ profiles_by_id: dict[str, UserProfile], # noqa: ARG002 - kept for signature parity with _apply_revision
359
+ playbooks_by_id: dict[int, UserPlaybook],
360
+ ) -> None:
361
+ """Raise if a playbook content rewrite omits ``new_rationale``.
362
+
363
+ Flip is LLM-reported, not derived: the ``memory_reflection`` prompt
364
+ instructs the model to set ``new_rationale`` whenever it changes a
365
+ rule's orientation (a flip), and to set it on substance rewrites of
366
+ playbook content as well. So any playbook revision that sets
367
+ ``new_content`` must carry a ``new_rationale`` — this preserves the
368
+ flip-requires-rationale audit-trail spirit without re-deriving
369
+ polarity from wording. Trigger-only / TTL-only revisions are exempt.
370
+
371
+ Per-decision try/except in the caller catches these and counts
372
+ them as failed_count.
373
+
374
+ Args:
375
+ decision (ReflectionDecision): The decision to validate.
376
+ profiles_by_id (dict[str, UserProfile]): Resolved profile
377
+ rows keyed by profile_id.
378
+ playbooks_by_id (dict[int, UserPlaybook]): Resolved playbook
379
+ rows keyed by user_playbook_id.
380
+
381
+ Raises:
382
+ ValueError: When a playbook ``new_content`` revision omits
383
+ ``new_rationale``.
384
+ """
385
+ if decision.target_kind == "profile":
386
+ return
387
+ # Playbook
388
+ try:
389
+ target_id = int(decision.target_id)
390
+ except (TypeError, ValueError) as exc:
391
+ raise ValueError(
392
+ f"playbook target_id not an int: {decision.target_id!r}"
393
+ ) from exc
394
+ cited = playbooks_by_id.get(target_id)
395
+ if cited is None:
396
+ return # apply step will mark as skipped
397
+ if decision.new_content is not None and not decision.new_rationale:
398
+ raise ValueError("playbook content revision must include new_rationale")
399
+
400
+ def _apply_revision(
401
+ self,
402
+ *,
403
+ request: ReflectionServiceRequest,
404
+ decision: ReflectionDecision,
405
+ profiles_by_id: dict[str, UserProfile],
406
+ playbooks_by_id: dict[int, UserPlaybook],
407
+ ) -> bool:
408
+ """Apply a revision decision.
409
+
410
+ Args:
411
+ request (ReflectionServiceRequest): The current service
412
+ request (for user_id / agent_version).
413
+ decision (ReflectionDecision): The revision to apply.
414
+ profiles_by_id (dict[str, UserProfile]): Resolved profile
415
+ rows keyed by profile_id.
416
+ playbooks_by_id (dict[int, UserPlaybook]): Resolved playbook
417
+ rows keyed by user_playbook_id.
418
+
419
+ Returns:
420
+ bool: ``applied``. ``False`` means the target row could not be
421
+ resolved (target archived between resolve and apply, etc.).
422
+
423
+ Flip is no longer derived here: orientation changes are LLM-reported
424
+ via the rewritten ``new_content`` + ``new_rationale`` the prompt
425
+ emits, not inferred from wording at apply time.
426
+ """
427
+ if decision.target_kind == "profile":
428
+ cited_p = profiles_by_id.get(decision.target_id)
429
+ if cited_p is None:
430
+ return False
431
+ return self._replace_profile(request, decision, cited_p)
432
+ # Playbook
433
+ try:
434
+ target_id = int(decision.target_id)
435
+ except (TypeError, ValueError):
436
+ return False
437
+ cited_pb = playbooks_by_id.get(target_id)
438
+ if cited_pb is None:
439
+ return False
440
+ return self._replace_playbook(request, decision, cited_pb)
441
+
442
+ def _replace_profile(
443
+ self,
444
+ request: ReflectionServiceRequest,
445
+ decision: ReflectionDecision,
446
+ cited: UserProfile,
447
+ ) -> bool:
448
+ """Insert the replacement profile, then archive the cited row.
449
+
450
+ Insert-first ordering means that if ``add_user_profile`` raises,
451
+ the cited row stays current and the per-decision exception
452
+ handler reports ``failed_count``. Only after the new row is
453
+ durable do we flip the cited row to ARCHIVED — and if *that*
454
+ fails we log at ERROR rather than silently dropping the user's
455
+ data, leaving a transient duplicate that downstream dedup can
456
+ clean up.
457
+ """
458
+ storage = self.request_context.storage
459
+ if storage is None:
460
+ return False
461
+ now_ts = int(datetime.now(UTC).timestamp())
462
+ new_profile = UserProfile(
463
+ profile_id=str(uuid.uuid4()),
464
+ user_id=cited.user_id,
465
+ content=decision.new_content or cited.content,
466
+ last_modified_timestamp=now_ts,
467
+ generated_from_request_id=cited.generated_from_request_id,
468
+ profile_time_to_live=(
469
+ decision.new_profile_time_to_live or cited.profile_time_to_live
470
+ ),
471
+ custom_features=cited.custom_features,
472
+ source=cited.source,
473
+ status=None,
474
+ extractor_names=cited.extractor_names,
475
+ )
476
+ _log_edit_magnitude(
477
+ kind="profile",
478
+ target_id=cited.profile_id,
479
+ old_content=cited.content,
480
+ new_content=new_profile.content,
481
+ )
482
+ storage.add_user_profile(cited.user_id, [new_profile])
483
+ try:
484
+ archived = storage.archive_profile_by_id(
485
+ user_id=request.user_id, profile_id=cited.profile_id
486
+ )
487
+ except Exception as exc: # noqa: BLE001
488
+ with sentry_tags(
489
+ subsystem="reflection",
490
+ op="archive_after_insert",
491
+ kind="profile",
492
+ org_id=self.request_context.org_id,
493
+ user_id=cited.user_id,
494
+ cited_id=cited.profile_id,
495
+ new_id=new_profile.profile_id,
496
+ error_type=type(exc).__name__,
497
+ ):
498
+ logger.exception(
499
+ "event=reflection_archive_after_insert_failed kind=profile "
500
+ "cited_id=%s new_id=%s",
501
+ cited.profile_id, new_profile.profile_id,
502
+ )
503
+ return True
504
+ if not archived:
505
+ with sentry_tags(
506
+ subsystem="reflection",
507
+ op="archive_after_insert_noop",
508
+ kind="profile",
509
+ org_id=self.request_context.org_id,
510
+ user_id=cited.user_id,
511
+ cited_id=cited.profile_id,
512
+ new_id=new_profile.profile_id,
513
+ ):
514
+ logger.error(
515
+ "event=reflection_archive_after_insert_noop kind=profile "
516
+ "cited_id=%s new_id=%s",
517
+ cited.profile_id,
518
+ new_profile.profile_id,
519
+ )
520
+ return True
521
+
522
+ def _replace_playbook(
523
+ self,
524
+ request: ReflectionServiceRequest,
525
+ decision: ReflectionDecision,
526
+ cited: UserPlaybook,
527
+ ) -> bool:
528
+ """Insert the replacement playbook, then archive the cited row.
529
+
530
+ Same insert-first ordering as ``_replace_profile``: if insert
531
+ fails, the cited row stays current; if the post-insert archive
532
+ fails, log at ERROR and accept a transient duplicate rather
533
+ than losing data.
534
+ """
535
+ storage = self.request_context.storage
536
+ if storage is None:
537
+ return False
538
+ owning_user_id = cited.user_id or request.user_id
539
+ new_playbook = UserPlaybook(
540
+ user_playbook_id=0, # auto-assigned by storage
541
+ user_id=owning_user_id,
542
+ agent_version=cited.agent_version or request.agent_version,
543
+ request_id=cited.request_id,
544
+ playbook_name=cited.playbook_name,
545
+ content=decision.new_content or cited.content,
546
+ trigger=(
547
+ decision.new_trigger
548
+ if decision.new_trigger is not None
549
+ else cited.trigger
550
+ ),
551
+ rationale=(
552
+ decision.new_rationale
553
+ if decision.new_rationale is not None
554
+ else cited.rationale
555
+ ),
556
+ status=None,
557
+ source=cited.source,
558
+ source_interaction_ids=list(cited.source_interaction_ids),
559
+ )
560
+ _log_edit_magnitude(
561
+ kind="playbook",
562
+ target_id=str(cited.user_playbook_id),
563
+ old_content=cited.content,
564
+ new_content=new_playbook.content,
565
+ )
566
+ # Flip is LLM-reported, not derived: when the model changes a rule's
567
+ # orientation it rewrites ``new_content`` and names the motivating
568
+ # failure/observation in ``new_rationale`` (per the memory_reflection
569
+ # prompt). A rewrite-with-rationale is the LLM-reported flip/revision
570
+ # signal — log it for observability without re-deriving polarity.
571
+ if decision.new_content is not None and decision.new_rationale:
572
+ logger.info(
573
+ "reflection.content_revision playbook_id=%s "
574
+ 'content_excerpt="%s" prior_excerpt="%s" rationale_excerpt="%s"',
575
+ cited.user_playbook_id,
576
+ (new_playbook.content or "")[:120].replace('"', "'"),
577
+ (cited.content or "")[:120].replace('"', "'"),
578
+ (decision.new_rationale or "")[:120].replace('"', "'"),
579
+ )
580
+ storage.save_user_playbooks([new_playbook])
581
+ try:
582
+ archived = storage.archive_user_playbook_by_id(
583
+ user_id=owning_user_id,
584
+ user_playbook_id=cited.user_playbook_id,
585
+ )
586
+ except Exception as exc: # noqa: BLE001
587
+ with sentry_tags(
588
+ subsystem="reflection",
589
+ op="archive_after_insert",
590
+ kind="playbook",
591
+ org_id=self.request_context.org_id,
592
+ user_id=owning_user_id,
593
+ cited_id=cited.user_playbook_id,
594
+ new_id=new_playbook.user_playbook_id,
595
+ error_type=type(exc).__name__,
596
+ ):
597
+ logger.exception(
598
+ "event=reflection_archive_after_insert_failed kind=playbook "
599
+ "cited_id=%s new_id=%s",
600
+ cited.user_playbook_id, new_playbook.user_playbook_id,
601
+ )
602
+ return True
603
+ if not archived:
604
+ with sentry_tags(
605
+ subsystem="reflection",
606
+ op="archive_after_insert_noop",
607
+ kind="playbook",
608
+ org_id=self.request_context.org_id,
609
+ user_id=owning_user_id,
610
+ cited_id=cited.user_playbook_id,
611
+ new_id=new_playbook.user_playbook_id,
612
+ ):
613
+ logger.error(
614
+ "event=reflection_archive_after_insert_noop kind=playbook "
615
+ "cited_id=%s new_id=%s",
616
+ cited.user_playbook_id,
617
+ new_playbook.user_playbook_id,
618
+ )
619
+ return True
620
+
621
+
622
+ _PROFILE_REVISION_FIELDS: tuple[str, ...] = (
623
+ "new_content",
624
+ "new_profile_time_to_live",
625
+ )
626
+ _PLAYBOOK_REVISION_FIELDS: tuple[str, ...] = (
627
+ "new_content",
628
+ "new_trigger",
629
+ "new_rationale",
630
+ )
631
+
632
+
633
+ _REVISION_FIELDS_BY_KIND: dict[str, tuple[str, ...]] = {
634
+ "profile": _PROFILE_REVISION_FIELDS,
635
+ "playbook": _PLAYBOOK_REVISION_FIELDS,
636
+ }
637
+
638
+
639
+ def _log_edit_magnitude(
640
+ *,
641
+ kind: str,
642
+ target_id: str,
643
+ old_content: str | None,
644
+ new_content: str | None,
645
+ ) -> None:
646
+ """Log a cheap edit-magnitude signal for one applied revision.
647
+
648
+ The magnitude is the content-size delta (new length minus old length)
649
+ in characters — a coarse proxy for how large the revision is, useful
650
+ for offline regularization analysis without storing diffs.
651
+
652
+ Args:
653
+ kind (str): ``"profile"`` or ``"playbook"``.
654
+ target_id (str): Id of the cited row being replaced.
655
+ old_content (str | None): Cited row content before the revision.
656
+ new_content (str | None): Replacement row content after the revision.
657
+ """
658
+ old_len = len(old_content or "")
659
+ new_len = len(new_content or "")
660
+ logger.info(
661
+ "event=reflection_edit_magnitude target_kind=%s target_id=%s "
662
+ "old_len=%d new_len=%d delta=%d",
663
+ kind,
664
+ target_id,
665
+ old_len,
666
+ new_len,
667
+ new_len - old_len,
668
+ )
669
+
670
+
671
+ def _is_revision(decision: ReflectionDecision) -> bool:
672
+ """Return True iff a revision field relevant to the target kind is set.
673
+
674
+ Splits the revision-field set by ``target_kind`` so a profile decision
675
+ is not classified as a revision purely because a playbook-only field
676
+ (e.g. ``new_trigger``) happens to be populated. This prevents
677
+ unnecessary replace/archive churn that would otherwise produce no
678
+ effective change.
679
+
680
+ Args:
681
+ decision (ReflectionDecision): The decision to inspect.
682
+
683
+ Returns:
684
+ bool: True if at least one kind-relevant revision field is non-None.
685
+ """
686
+ fields = _REVISION_FIELDS_BY_KIND.get(
687
+ decision.target_kind, _PLAYBOOK_REVISION_FIELDS
688
+ )
689
+ return any(getattr(decision, f) is not None for f in fields)
690
+
691
+
692
+ def _flatten(
693
+ request_models: list[RequestInteractionDataModel],
694
+ ) -> list[Interaction]:
695
+ """Flatten grouped request models into a flat list, oldest first."""
696
+ out: list[Interaction] = []
697
+ for rm in request_models:
698
+ out.extend(rm.interactions)
699
+ out.sort(key=lambda i: i.created_at)
700
+ return out
701
+
702
+
703
+ def _collect_citations(interactions: list[Interaction]) -> list[Citation]:
704
+ """Pull distinct citations off Assistant interactions."""
705
+ seen: set[tuple[str, str]] = set()
706
+ out: list[Citation] = []
707
+ for i in interactions:
708
+ if i.role != "Assistant":
709
+ continue
710
+ for c in i.citations:
711
+ key = (c.kind, c.real_id)
712
+ if key in seen:
713
+ continue
714
+ seen.add(key)
715
+ out.append(c)
716
+ return out
717
+
718
+
719
+ @dataclass(frozen=True)
720
+ class _EligibleCitation:
721
+ """A citation that passed the post-horizon filter.
722
+
723
+ Attributes:
724
+ citation (Citation): The citation itself.
725
+ position (int): 0-indexed position in the window where it
726
+ appeared (earliest occurrence — see deduplication note in
727
+ ``_filter_citations_by_horizon``).
728
+ has_full_horizon (bool): True iff at least
729
+ ``post_horizon_size`` interactions follow the citation in
730
+ the window. False indicates a ``last_chance`` judgment
731
+ with weaker evidence.
732
+ """
733
+
734
+ citation: Citation
735
+ position: int
736
+ has_full_horizon: bool
737
+
738
+
739
+ def _filter_citations_by_horizon(
740
+ citations: list[Citation],
741
+ window: list[Interaction],
742
+ post_horizon_size: int,
743
+ stride_size: int,
744
+ ) -> list[_EligibleCitation]:
745
+ """Filter citations to those that have enough post-citation context.
746
+
747
+ For each unique ``(kind, real_id)``, finds the **earliest** occurrence
748
+ in the window (maximizes follow-up turns) and decides:
749
+
750
+ - ``after_count >= post_horizon_size`` → eligible, ``has_full_horizon=True``.
751
+ - ``position < stride_size`` → eligible (last-chance, about to fall
752
+ out of the window next stride), ``has_full_horizon=False``.
753
+ - otherwise → deferred (excluded from this pass).
754
+
755
+ Only Assistant-role interactions contribute citations to consider —
756
+ user-role interactions are skipped even if their ``citations`` list is
757
+ populated.
758
+
759
+ Args:
760
+ citations (list[Citation]): Distinct citations collected from
761
+ assistant turns in the window.
762
+ window (list[Interaction]): The reflection window, oldest first.
763
+ post_horizon_size (int): Minimum follow-up turns required for a
764
+ full-horizon judgment. Zero disables the horizon check.
765
+ stride_size (int): Used to detect citations about to fall out.
766
+
767
+ Returns:
768
+ list[_EligibleCitation]: Citations to send to the LLM, each
769
+ paired with its window position and horizon flag.
770
+ """
771
+ seen: dict[tuple[str, str], int] = {}
772
+ for idx, interaction in enumerate(window):
773
+ if interaction.role != "Assistant":
774
+ continue
775
+ for c in interaction.citations:
776
+ key = (c.kind, c.real_id)
777
+ seen.setdefault(key, idx) # earliest occurrence only
778
+
779
+ citation_by_key = {(c.kind, c.real_id): c for c in citations}
780
+
781
+ out: list[_EligibleCitation] = []
782
+ for key, position in seen.items():
783
+ cite = citation_by_key.get(key)
784
+ if cite is None:
785
+ continue
786
+ after_count = len(window) - position - 1
787
+ if post_horizon_size <= 0 or after_count >= post_horizon_size:
788
+ out.append(
789
+ _EligibleCitation(
790
+ citation=cite, position=position, has_full_horizon=True
791
+ )
792
+ )
793
+ elif position < stride_size:
794
+ out.append(
795
+ _EligibleCitation(
796
+ citation=cite, position=position, has_full_horizon=False
797
+ )
798
+ )
799
+ # else: deferred — not included
800
+ return out