claude-smart 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +47 -10
  3. package/bin/claude-smart.js +535 -23
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +27 -6
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/layout.tsx +20 -0
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  17. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  18. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  19. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  20. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  21. package/plugin/dashboard/lib/status.ts +10 -3
  22. package/plugin/dashboard/lib/types.ts +18 -6
  23. package/plugin/hooks/codex-hooks.json +0 -1
  24. package/plugin/opencode/assistant-buffer.ts +108 -0
  25. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  26. package/plugin/opencode/dist/internal.js +12 -0
  27. package/plugin/opencode/dist/payload.js +85 -0
  28. package/plugin/opencode/dist/server.mjs +232 -0
  29. package/plugin/opencode/internal.ts +18 -0
  30. package/plugin/opencode/package.json +3 -0
  31. package/plugin/opencode/payload.ts +90 -0
  32. package/plugin/opencode/server.mts +251 -0
  33. package/plugin/opencode/tsconfig.json +13 -0
  34. package/plugin/pyproject.toml +25 -4
  35. package/plugin/scripts/_lib.sh +5 -1
  36. package/plugin/scripts/backend-service.sh +22 -2
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +1 -1
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +17 -9
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +498 -12
  45. package/plugin/src/claude_smart/context_format.py +9 -9
  46. package/plugin/src/claude_smart/cs_cite.py +66 -19
  47. package/plugin/src/claude_smart/env_config.py +12 -4
  48. package/plugin/src/claude_smart/events/session_start.py +26 -7
  49. package/plugin/src/claude_smart/events/stop.py +1 -1
  50. package/plugin/src/claude_smart/ids.py +1 -1
  51. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  52. package/plugin/src/claude_smart/runtime.py +7 -1
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +54 -30
  55. package/plugin/vendor/reflexio/README.md +17 -11
  56. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  57. package/plugin/vendor/reflexio/reflexio/README.md +4 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  59. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  60. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  64. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
  65. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  66. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  67. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  71. package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
  146. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  147. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  148. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
  149. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
  151. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  152. package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
  153. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  158. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  159. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  160. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  161. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
  162. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  164. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
  165. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  166. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  167. package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
  168. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  169. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  170. package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
  171. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  172. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
  173. package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
  174. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  175. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  176. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  177. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  178. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  179. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  180. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  181. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  182. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  183. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  185. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  186. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  187. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  188. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  189. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  198. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
  199. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  200. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  201. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  202. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  203. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  204. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  205. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  206. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  207. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  208. package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
  209. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  212. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  215. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  216. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  217. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  218. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  219. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  220. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  221. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  222. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  225. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  226. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  227. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  229. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  230. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  231. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
  232. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  233. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  234. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
  238. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
  241. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  242. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  243. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
  244. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  245. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  246. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  248. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  249. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  250. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  252. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  255. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
  256. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  257. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  258. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  259. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  261. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  262. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  263. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
  264. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  265. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  266. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  277. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
  278. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
  279. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
  280. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  281. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  282. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
  289. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  290. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
  292. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
  296. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
  297. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
  311. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  312. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  313. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  314. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  315. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  316. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  317. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  318. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  319. package/scripts/setup-claude-smart.sh +8 -3
  320. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  321. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  322. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  323. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  324. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  325. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  326. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  327. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  328. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  329. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  330. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  331. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  332. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  333. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  334. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  335. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  336. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  337. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  338. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  339. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  340. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  341. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  342. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  343. package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
  344. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  345. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  346. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  347. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  348. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  349. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  350. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  351. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  352. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  353. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -17,6 +17,7 @@ from reflexio.models.config_schema import (
17
17
  DeduplicationConfig,
18
18
  SearchOptions,
19
19
  )
20
+ from reflexio.models.structured_output import StrictStructuredOutput
20
21
  from reflexio.server.api_endpoints.request_context import RequestContext
21
22
  from reflexio.server.llm.litellm_client import LiteLLMClient
22
23
  from reflexio.server.services.deduplication_utils import (
@@ -36,18 +37,19 @@ logger = logging.getLogger(__name__)
36
37
  def _coerce_existing_position(value: object) -> int:
37
38
  """Accept either a bare int position or an ``"EXISTING-N"`` label.
38
39
 
39
- Wired ONLY to ``UnifyDecision.archive_existing_ids`` the one field whose
40
- int contract is genuinely a list **position** (resolved via
41
- ``existing_by_position`` as ``f"EXISTING-{idx}"`` in ``_apply_unify``).
40
+ Wired to all three EXISTING-id integer fields
41
+ ``UnifyDecision.archive_existing_ids``,
42
42
  ``RejectNewDecision.superseded_by_existing_id`` and
43
- ``DifferentiateDecision.existing_id`` are DB ``user_playbook_id`` values
44
- (resolved via ``existing_by_id`` in ``_apply_reject_new`` /
45
- ``_apply_differentiate``) coercing ``"EXISTING-N"`` to ``N`` on those
46
- fields would silently misroute decisions when the position int doesn't
47
- map to a real DB id.
43
+ ``DifferentiateDecision.existing_id``. The consolidation prompt instructs
44
+ the model to emit a list **position** for every one of them, and the apply
45
+ path resolves each position-first via ``existing_by_position``
46
+ (``f"EXISTING-{idx}"``), falling back to ``existing_by_id`` only for older
47
+ prompt outputs. Coercing the label here keeps all three fields consistent
48
+ so a weak model that returns ``"EXISTING-0"`` for any of them does not kill
49
+ the whole batch.
48
50
 
49
51
  The consolidation prompt labels rows as ``[EXISTING-0]``, ``[EXISTING-1]``
50
- etc. (see ``_format_playbooks_with_prefix``) and ``_apply_unify``
52
+ etc. (see ``_format_playbooks_with_prefix``) and the apply path
51
53
  reconstructs ``f"EXISTING-{position}"`` from the integer the LLM returns.
52
54
  Strong structured-output models (GPT-4o, Claude) honor the ``list[int]``
53
55
  schema and return the bare integer ``0``; weaker models (e.g. MiniMax-M3)
@@ -132,12 +134,11 @@ class UnifyDecision(BaseModel):
132
134
  class RejectNewDecision(BaseModel):
133
135
  """The new candidate is redundant; an existing row supersedes it (storage no-op).
134
136
 
135
- ``superseded_by_existing_id`` is a DB ``user_playbook_id`` (resolved
136
- against ``existing_by_id`` in ``_apply_reject_new``), NOT a list
137
- position — no ``"EXISTING-N"`` coercion is wired here. If the LLM
138
- returns ``"EXISTING-N"`` for this field, pydantic rejects it loudly
139
- rather than silently misrouting the decision (see
140
- ``_coerce_existing_position`` docstring).
137
+ ``superseded_by_existing_id`` is a bare integer that normally refers to the
138
+ rendered ``EXISTING-N`` list position. The apply path also accepts a DB
139
+ ``user_playbook_id`` fallback for older prompts/tests, but list position is
140
+ resolved first because it is the only identifier visible in the rendered
141
+ consolidation prompt.
141
142
  """
142
143
 
143
144
  kind: Literal["reject_new"] = "reject_new"
@@ -145,16 +146,22 @@ class RejectNewDecision(BaseModel):
145
146
  superseded_by_existing_id: int
146
147
  reason: str = ""
147
148
 
149
+ @field_validator("superseded_by_existing_id", mode="before")
150
+ @classmethod
151
+ def _coerce_superseded_id(cls, value: object) -> int:
152
+ return _coerce_existing_position(value)
153
+
148
154
  model_config = ConfigDict(json_schema_extra={"additionalProperties": False})
149
155
 
150
156
 
151
157
  class DifferentiateDecision(BaseModel):
152
158
  """Both rules valid in distinct contexts: refine both triggers.
153
159
 
154
- ``existing_id`` is a DB ``user_playbook_id`` (resolved against
155
- ``existing_by_id`` in ``_apply_differentiate``), NOT a list position
156
- no ``"EXISTING-N"`` coercion is wired here. See
157
- ``_coerce_existing_position`` docstring.
160
+ ``existing_id`` is a bare integer that normally refers to the rendered
161
+ ``EXISTING-N`` list position. The apply path also accepts a DB
162
+ ``user_playbook_id`` fallback for older prompts/tests, but list position is
163
+ resolved first because it is the only identifier visible in the rendered
164
+ consolidation prompt.
158
165
  """
159
166
 
160
167
  kind: Literal["differentiate"] = "differentiate"
@@ -164,6 +171,11 @@ class DifferentiateDecision(BaseModel):
164
171
  refined_existing_trigger: str
165
172
  reason: str = ""
166
173
 
174
+ @field_validator("existing_id", mode="before")
175
+ @classmethod
176
+ def _coerce_existing_id(cls, value: object) -> int:
177
+ return _coerce_existing_position(value)
178
+
167
179
  @field_validator("refined_new_trigger", "refined_existing_trigger")
168
180
  @classmethod
169
181
  def _non_empty(cls, v: str) -> str:
@@ -190,11 +202,17 @@ ConsolidationDecision = Annotated[
190
202
  ]
191
203
 
192
204
 
193
- class PlaybookConsolidationOutput(BaseModel):
194
- """Output schema for playbook consolidation as a 4-kind discriminated union.
205
+ # ``PlaybookConsolidationOutput`` inherits ``StrictStructuredOutput`` so the
206
+ # emitted JSON schema is folded into the provider-accepted union shape while the
207
+ # discriminator is kept for keyed validation at parse time (Sentry
208
+ # PYTHON-FASTAPI-9J). This note is a comment, NOT part of the docstring, because
209
+ # the docstring is serialized into the wire schema's ``description`` sent to the
210
+ # model — keep implementation tokens out of it.
211
+ class PlaybookConsolidationOutput(StrictStructuredOutput):
212
+ """Output schema for playbook consolidation as a 4-kind tagged union.
195
213
 
196
214
  Each decision is one of ``UnifyDecision``, ``RejectNewDecision``,
197
- ``DifferentiateDecision``, or ``IndependentDecision``; the ``kind`` literal
215
+ ``DifferentiateDecision``, or ``IndependentDecision``; the ``kind`` field
198
216
  selects the concrete shape.
199
217
  """
200
218
 
@@ -503,7 +521,7 @@ class PlaybookConsolidator(BaseDeduplicator):
503
521
  request_id: str,
504
522
  agent_version: str,
505
523
  user_id: str | None = None,
506
- ) -> tuple[list[UserPlaybook], list[int]]:
524
+ ) -> tuple[list[UserPlaybook], list[int], list[tuple[int, list[int]]]]:
507
525
  """
508
526
  Consolidate user playbook entries across extractors and against existing entries in DB.
509
527
 
@@ -514,7 +532,18 @@ class PlaybookConsolidator(BaseDeduplicator):
514
532
  user_id: Optional user ID to scope the existing entry search
515
533
 
516
534
  Returns:
517
- Tuple of (consolidated entries, list of existing entry IDs to delete after save)
535
+ Tuple of ``(consolidated entries, existing ids to delete after save,
536
+ merge_groups)``. ``merge_groups`` is a list of
537
+ ``(survivor_index, source_existing_ids)`` where ``survivor_index``
538
+ indexes into the returned entries list and identifies the row that
539
+ supersedes the given existing source ids (one entry per ``unify``
540
+ decision that archives at least one existing row). Callers persist
541
+ the entries first (assigning survivor ids), then route each merge
542
+ group through ``storage.merge_records`` so each source becomes a
543
+ MERGED tombstone pointing at its survivor. The "existing ids to
544
+ delete" set still includes ALL archived ids (merge sources +
545
+ non-merge archives such as ``differentiate``'s split source); the
546
+ caller subtracts the merge-covered ids to find pure-delete leftovers.
518
547
  """
519
548
  # Check if mock mode is enabled
520
549
  if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
@@ -523,7 +552,7 @@ class PlaybookConsolidator(BaseDeduplicator):
523
552
  for result in results:
524
553
  if isinstance(result, list):
525
554
  all_playbooks.extend(result)
526
- return all_playbooks, []
555
+ return all_playbooks, [], []
527
556
 
528
557
  # Flatten all new entries
529
558
  new_playbooks: list[UserPlaybook] = []
@@ -532,7 +561,7 @@ class PlaybookConsolidator(BaseDeduplicator):
532
561
  new_playbooks.extend(result)
533
562
 
534
563
  if not new_playbooks:
535
- return [], []
564
+ return [], [], []
536
565
 
537
566
  # Retrieve existing entries via hybrid search
538
567
  existing_playbooks = self._retrieve_existing_playbooks(
@@ -555,13 +584,13 @@ class PlaybookConsolidator(BaseDeduplicator):
555
584
  error_type=type(e).__name__,
556
585
  ):
557
586
  logger.exception("Failed to identify duplicates")
558
- return new_playbooks, []
587
+ return new_playbooks, [], []
559
588
 
560
589
  if not dedup_output.decisions:
561
590
  logger.info(
562
591
  "No consolidation decisions returned for request %s", request_id
563
592
  )
564
- return new_playbooks, []
593
+ return new_playbooks, [], []
565
594
 
566
595
  logger.info(
567
596
  "Received %d consolidation decisions for request %s",
@@ -589,7 +618,7 @@ class PlaybookConsolidator(BaseDeduplicator):
589
618
  dedup_output: PlaybookConsolidationOutput,
590
619
  request_id: str,
591
620
  agent_version: str, # noqa: ARG002
592
- ) -> tuple[list[UserPlaybook], list[int]]:
621
+ ) -> tuple[list[UserPlaybook], list[int], list[tuple[int, list[int]]]]:
593
622
  """
594
623
  Build the deduplicated entry list from LLM decisions.
595
624
 
@@ -606,7 +635,16 @@ class PlaybookConsolidator(BaseDeduplicator):
606
635
  agent_version: Agent version (currently unused, kept for symmetry).
607
636
 
608
637
  Returns:
609
- Tuple of (entries ready to save, existing entry IDs to delete).
638
+ Tuple of ``(entries ready to save, existing entry IDs to delete,
639
+ merge_groups)``. ``merge_groups`` carries one
640
+ ``(survivor_index, source_existing_ids)`` per ``unify`` decision
641
+ that archives >= 1 existing row, where ``survivor_index`` indexes
642
+ into the returned entries list (the unified survivor row) and the
643
+ second element is the existing ids that decision supersedes. Only
644
+ ``unify`` produces merge groups — it collapses N existing rows into
645
+ one survivor. ``differentiate`` archives its split source but emits
646
+ two rows (no single survivor), so its archived id appears in the
647
+ delete set but NOT in any merge group.
610
648
  """
611
649
  candidates_by_id = {
612
650
  f"NEW-{idx}": playbook for idx, playbook in enumerate(new_playbooks)
@@ -626,10 +664,11 @@ class PlaybookConsolidator(BaseDeduplicator):
626
664
  seen_archive: set[int] = set()
627
665
  new_rows: list[UserPlaybook] = []
628
666
  handled_new_ids: set[str] = set()
667
+ merge_groups: list[tuple[int, list[int]]] = []
629
668
 
630
669
  for decision in dedup_output.decisions:
631
670
  try:
632
- rows, marked_new_ids = self._apply_one(
671
+ rows, marked_new_ids, merge_source_ids = self._apply_one(
633
672
  decision=decision,
634
673
  candidates_by_id=candidates_by_id,
635
674
  existing_by_id=existing_by_id,
@@ -659,10 +698,17 @@ class PlaybookConsolidator(BaseDeduplicator):
659
698
  existing_id_str,
660
699
  )
661
700
  continue
701
+ # Record the merge group BEFORE extending: the unified survivor is
702
+ # the first (and only) row a ``unify`` decision emits, so its index
703
+ # in the final list is the current length of ``new_rows``.
704
+ if merge_source_ids:
705
+ merge_groups.append((len(new_rows), merge_source_ids))
662
706
  new_rows.extend(rows)
663
707
  handled_new_ids.update(marked_new_ids)
664
708
  self._bump_counter(result_counters, decision.kind)
665
- self._log_decision(decision, candidates_by_id, existing_by_id)
709
+ self._log_decision(
710
+ decision, candidates_by_id, existing_by_id, existing_by_position
711
+ )
666
712
 
667
713
  # Safety fallback: add any NEW entries the LLM did not reference, so a
668
714
  # misbehaving model cannot silently drop extracted playbooks.
@@ -684,7 +730,7 @@ class PlaybookConsolidator(BaseDeduplicator):
684
730
  result_counters.failed_count,
685
731
  )
686
732
 
687
- return new_rows, archive_ids
733
+ return new_rows, archive_ids, merge_groups
688
734
 
689
735
  def _apply_one(
690
736
  self,
@@ -696,7 +742,7 @@ class PlaybookConsolidator(BaseDeduplicator):
696
742
  archive_ids: list[int],
697
743
  seen_archive: set[int],
698
744
  request_id: str,
699
- ) -> tuple[list[UserPlaybook], list[str]]:
745
+ ) -> tuple[list[UserPlaybook], list[str], list[int]]:
700
746
  """Dispatch a single decision to its kind-specific apply method.
701
747
 
702
748
  Args:
@@ -712,9 +758,14 @@ class PlaybookConsolidator(BaseDeduplicator):
712
758
  request_id: Request ID stamped onto newly-built rows.
713
759
 
714
760
  Returns:
715
- Tuple of ``(rows_to_insert, handled_new_ids)`` where the second
716
- element is the set of ``"NEW-N"`` candidate ids consumed by this
717
- decision (used to suppress the safety fallback).
761
+ Tuple of ``(rows_to_insert, handled_new_ids, merge_source_ids)``.
762
+ ``handled_new_ids`` is the set of ``"NEW-N"`` candidate ids consumed
763
+ by this decision (used to suppress the safety fallback).
764
+ ``merge_source_ids`` is non-empty ONLY for a ``unify`` decision that
765
+ collapses >= 1 existing row into its single survivor (the first row
766
+ in ``rows_to_insert``); for all other kinds it is ``[]`` because they
767
+ either archive nothing or split into multiple rows with no single
768
+ survivor.
718
769
  """
719
770
  if isinstance(decision, UnifyDecision):
720
771
  return self._apply_unify(
@@ -729,12 +780,14 @@ class PlaybookConsolidator(BaseDeduplicator):
729
780
  return self._apply_reject_new(
730
781
  decision,
731
782
  existing_by_id=existing_by_id,
783
+ existing_by_position=existing_by_position,
732
784
  )
733
785
  if isinstance(decision, DifferentiateDecision):
734
786
  return self._apply_differentiate(
735
787
  decision,
736
788
  candidates_by_id=candidates_by_id,
737
789
  existing_by_id=existing_by_id,
790
+ existing_by_position=existing_by_position,
738
791
  archive_ids=archive_ids,
739
792
  seen_archive=seen_archive,
740
793
  request_id=request_id,
@@ -752,7 +805,7 @@ class PlaybookConsolidator(BaseDeduplicator):
752
805
  archive_ids: list[int],
753
806
  seen_archive: set[int],
754
807
  request_id: str,
755
- ) -> tuple[list[UserPlaybook], list[str]]:
808
+ ) -> tuple[list[UserPlaybook], list[str], list[int]]:
756
809
  """Collapse / compose NEW (+ 0..N EXISTING) into one row.
757
810
 
758
811
  Looks up each ``archive_existing_ids`` entry by position
@@ -774,7 +827,11 @@ class PlaybookConsolidator(BaseDeduplicator):
774
827
  request_id: Request ID stamped on the unified row.
775
828
 
776
829
  Returns:
777
- Tuple of ([unified_row], [consumed NEW-N ids]).
830
+ Tuple of ``([unified_row], [consumed NEW-N ids], merge_source_ids)``
831
+ where ``merge_source_ids`` are the existing ids collapsed into the
832
+ unified survivor (the returned row). The survivor identity is not
833
+ known until the caller persists the row and reads its assigned id,
834
+ so the merge is materialized by the caller, not here.
778
835
 
779
836
  Raises:
780
837
  KeyError: If ``decision.new_id`` does not resolve to a known
@@ -795,11 +852,14 @@ class PlaybookConsolidator(BaseDeduplicator):
795
852
  )
796
853
  existing_members.append(existing)
797
854
 
855
+ merge_source_ids: list[int] = []
798
856
  for existing in existing_members:
799
857
  pid = existing.user_playbook_id
800
858
  if pid and pid not in seen_archive:
801
859
  seen_archive.add(pid)
802
860
  archive_ids.append(pid)
861
+ if pid:
862
+ merge_source_ids.append(pid)
803
863
 
804
864
  budget = self._dedup_config.max_unified_content_chars
805
865
  content_len = len(decision.content)
@@ -830,43 +890,54 @@ class PlaybookConsolidator(BaseDeduplicator):
830
890
  source=candidate.source,
831
891
  source_interaction_ids=combined_source_ids,
832
892
  )
833
- return [unified_row], [decision.new_id]
893
+ return [unified_row], [decision.new_id], merge_source_ids
834
894
 
835
895
  def _apply_reject_new(
836
896
  self,
837
897
  decision: RejectNewDecision,
838
898
  *,
839
899
  existing_by_id: dict[int, UserPlaybook],
840
- ) -> tuple[list[UserPlaybook], list[str]]:
900
+ existing_by_position: dict[str, UserPlaybook],
901
+ ) -> tuple[list[UserPlaybook], list[str], list[int]]:
841
902
  """No-op apply: the existing row wins and the new candidate is dropped.
842
903
 
843
- If ``decision.superseded_by_existing_id`` does not resolve to a known
844
- existing row, the decision is treated as malformed: we log a warning
845
- and return ``([], [])`` so the safety fallback re-inserts the candidate
846
- rather than silently dropping extracted data.
904
+ Resolve the integer against the rendered ``EXISTING-N`` position first,
905
+ then as a DB ``user_playbook_id`` for backwards compatibility. If it
906
+ does not resolve to a known existing row, the decision is treated as
907
+ malformed: we log a warning and return ``([], [])`` so the safety
908
+ fallback re-inserts the candidate rather than silently dropping
909
+ extracted data.
847
910
 
848
911
  Args:
849
912
  decision: The ``RejectNewDecision`` to apply.
850
913
  existing_by_id: Mapping ``user_playbook_id`` -> existing playbook,
851
- used to validate ``decision.superseded_by_existing_id``.
914
+ used as a fallback for ``decision.superseded_by_existing_id``.
915
+ existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
852
916
 
853
917
  Returns:
854
- Tuple of ([], [consumed NEW-N id]) when the existing id resolves,
855
- or ``([], [])`` when the existing id is unknown.
918
+ Tuple of ``([], [consumed NEW-N id], [])`` when the existing id
919
+ resolves, or ``([], [], [])`` when the existing id is unknown.
920
+ Never produces a merge group — the existing row is kept as-is (no
921
+ archive, no survivor).
856
922
  """
857
- if decision.superseded_by_existing_id not in existing_by_id:
923
+ existing = self._resolve_existing_reference(
924
+ decision.superseded_by_existing_id,
925
+ existing_by_position=existing_by_position,
926
+ existing_by_id=existing_by_id,
927
+ )
928
+ if existing is None:
858
929
  logger.warning(
859
930
  "event=consolidation_reject_new_invalid new_id=%s existing_id=%d",
860
931
  decision.new_id,
861
932
  decision.superseded_by_existing_id,
862
933
  )
863
- return [], []
934
+ return [], [], []
864
935
  logger.info(
865
936
  "event=consolidation_reject_new new_id=%s existing_id=%d",
866
937
  decision.new_id,
867
938
  decision.superseded_by_existing_id,
868
939
  )
869
- return [], [decision.new_id]
940
+ return [], [decision.new_id], []
870
941
 
871
942
  def _apply_differentiate(
872
943
  self,
@@ -874,10 +945,11 @@ class PlaybookConsolidator(BaseDeduplicator):
874
945
  *,
875
946
  candidates_by_id: dict[str, UserPlaybook],
876
947
  existing_by_id: dict[int, UserPlaybook],
948
+ existing_by_position: dict[str, UserPlaybook],
877
949
  archive_ids: list[int],
878
950
  seen_archive: set[int],
879
951
  request_id: str,
880
- ) -> tuple[list[UserPlaybook], list[str]]:
952
+ ) -> tuple[list[UserPlaybook], list[str], list[int]]:
881
953
  """Archive the existing row and emit two refined rows in its place.
882
954
 
883
955
  Builds one ``UserPlaybook`` from the candidate's content/polarity with
@@ -889,27 +961,36 @@ class PlaybookConsolidator(BaseDeduplicator):
889
961
  decision: The ``DifferentiateDecision`` to apply.
890
962
  candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
891
963
  existing_by_id: Mapping ``user_playbook_id`` -> existing playbook.
964
+ existing_by_position: Mapping ``"EXISTING-M"`` -> existing playbook.
892
965
  archive_ids: Accumulator mutated with the existing id to archive.
893
966
  seen_archive: Dedup set for ``archive_ids``.
894
967
  request_id: Request ID stamped on both new rows.
895
968
 
896
969
  Returns:
897
- Tuple of ([refined_new_row, refined_existing_row], [NEW-N id]).
970
+ Tuple of ``([refined_new_row, refined_existing_row], [NEW-N id],
971
+ [])``. ``differentiate`` is a SPLIT, not a merge: the existing row
972
+ is archived but maps to no single survivor, so it produces NO merge
973
+ group (its archived id is a pure-delete leftover for the caller).
898
974
  """
899
975
  candidate = candidates_by_id.get(decision.new_id)
900
976
  if candidate is None:
901
977
  raise KeyError(
902
978
  f"differentiate references unknown NEW id: {decision.new_id}"
903
979
  )
904
- existing = existing_by_id.get(decision.existing_id)
980
+ existing = self._resolve_existing_reference(
981
+ decision.existing_id,
982
+ existing_by_position=existing_by_position,
983
+ existing_by_id=existing_by_id,
984
+ )
905
985
  if existing is None:
906
986
  raise KeyError(
907
987
  f"differentiate references unknown EXISTING id: {decision.existing_id}"
908
988
  )
909
989
 
910
- if decision.existing_id not in seen_archive:
911
- seen_archive.add(decision.existing_id)
912
- archive_ids.append(decision.existing_id)
990
+ existing_db_id = existing.user_playbook_id
991
+ if existing_db_id and existing_db_id not in seen_archive:
992
+ seen_archive.add(existing_db_id)
993
+ archive_ids.append(existing_db_id)
913
994
 
914
995
  now_ts = int(datetime.now(UTC).timestamp())
915
996
  refined_candidate = candidate.model_copy(
@@ -929,14 +1010,14 @@ class PlaybookConsolidator(BaseDeduplicator):
929
1010
  "source_interaction_ids": list(existing.source_interaction_ids),
930
1011
  }
931
1012
  )
932
- return [refined_candidate, refined_existing], [decision.new_id]
1013
+ return [refined_candidate, refined_existing], [decision.new_id], []
933
1014
 
934
1015
  def _apply_independent(
935
1016
  self,
936
1017
  decision: IndependentDecision,
937
1018
  *,
938
1019
  candidates_by_id: dict[str, UserPlaybook],
939
- ) -> tuple[list[UserPlaybook], list[str]]:
1020
+ ) -> tuple[list[UserPlaybook], list[str], list[int]]:
940
1021
  """Insert the new candidate unchanged; no archive.
941
1022
 
942
1023
  Args:
@@ -944,12 +1025,13 @@ class PlaybookConsolidator(BaseDeduplicator):
944
1025
  candidates_by_id: Mapping ``"NEW-N"`` -> candidate playbook.
945
1026
 
946
1027
  Returns:
947
- Tuple of ([candidate row], [consumed NEW-N id]).
1028
+ Tuple of ``([candidate row], [consumed NEW-N id], [])`` — no archive,
1029
+ so never a merge group.
948
1030
  """
949
1031
  candidate = candidates_by_id.get(decision.new_id)
950
1032
  if candidate is None:
951
1033
  raise KeyError(f"independent references unknown NEW id: {decision.new_id}")
952
- return [candidate], [decision.new_id]
1034
+ return [candidate], [decision.new_id], []
953
1035
 
954
1036
  @staticmethod
955
1037
  def _merge_source_ids(playbooks: list[UserPlaybook]) -> list[int]:
@@ -970,6 +1052,25 @@ class PlaybookConsolidator(BaseDeduplicator):
970
1052
  combined.append(sid)
971
1053
  return combined
972
1054
 
1055
+ @staticmethod
1056
+ def _resolve_existing_reference(
1057
+ raw_id: int,
1058
+ *,
1059
+ existing_by_position: dict[str, UserPlaybook],
1060
+ existing_by_id: dict[int, UserPlaybook],
1061
+ ) -> UserPlaybook | None:
1062
+ """Resolve an LLM existing-row integer.
1063
+
1064
+ The rendered prompt labels rows as ``EXISTING-N`` and asks the model to
1065
+ emit bare integers, so position is the primary interpretation. DB id is
1066
+ retained as a compatibility fallback for older prompt outputs.
1067
+ """
1068
+ if 0 <= raw_id < len(existing_by_position):
1069
+ existing = existing_by_position.get(f"EXISTING-{raw_id}")
1070
+ if existing is not None:
1071
+ return existing
1072
+ return existing_by_id.get(raw_id)
1073
+
973
1074
  @staticmethod
974
1075
  def _bump_counter(result: PlaybookConsolidationResult, kind: str) -> None:
975
1076
  """Increment the per-kind counter on ``result`` for a successful apply.
@@ -987,6 +1088,7 @@ class PlaybookConsolidator(BaseDeduplicator):
987
1088
  decision: ConsolidationDecision,
988
1089
  candidates_by_id: dict[str, UserPlaybook],
989
1090
  existing_by_id: dict[int, UserPlaybook],
1091
+ existing_by_position: dict[str, UserPlaybook],
990
1092
  ) -> None:
991
1093
  """Emit a structured per-decision log line for probe ingest.
992
1094
 
@@ -1029,7 +1131,11 @@ class PlaybookConsolidator(BaseDeduplicator):
1029
1131
  "existing_id",
1030
1132
  getattr(decision, "superseded_by_existing_id", 0),
1031
1133
  )
1032
- existing_pb = existing_by_id.get(existing_id_raw)
1134
+ existing_pb = PlaybookConsolidator._resolve_existing_reference(
1135
+ existing_id_raw,
1136
+ existing_by_position=existing_by_position,
1137
+ existing_by_id=existing_by_id,
1138
+ )
1033
1139
  trigger_match = (
1034
1140
  new_pb is not None
1035
1141
  and existing_pb is not None
@@ -10,6 +10,7 @@ from reflexio.models.config_schema import PlaybookConfig
10
10
  from reflexio.server.api_endpoints.request_context import RequestContext
11
11
  from reflexio.server.llm.litellm_client import LiteLLMClient
12
12
  from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
13
+ from reflexio.server.llm.token_accounting import RunTokenTotals, sum_trace_tokens
13
14
  from reflexio.server.services.extraction.outcome import ExtractionOutcome
14
15
  from reflexio.server.services.extraction.resumable_agent import (
15
16
  run_resumable_extraction_agent,
@@ -35,7 +36,7 @@ from reflexio.server.services.service_utils import (
35
36
  from reflexio.server.site_var.site_var_manager import SiteVarManager
36
37
 
37
38
  if TYPE_CHECKING:
38
- from reflexio.server.services.playbook.playbook_generation_service import (
39
+ from reflexio.server.services.playbook.service import (
39
40
  PlaybookGenerationServiceConfig,
40
41
  )
41
42
 
@@ -79,6 +80,7 @@ class PlaybookExtractor:
79
80
  self.service_config: PlaybookGenerationServiceConfig = service_config
80
81
  self.agent_context: str = agent_context
81
82
  self._last_resumable_run_id: str | None = None
83
+ self._last_resumable_token_totals: RunTokenTotals | None = None
82
84
 
83
85
  # Get LLM config overrides from configuration
84
86
  config = self.request_context.configurator.get_config()
@@ -223,6 +225,7 @@ class PlaybookExtractor:
223
225
  return ExtractionOutcome.completed(
224
226
  user_playbooks,
225
227
  run_id=self._last_resumable_run_id,
228
+ token_totals=self._last_resumable_token_totals,
226
229
  )
227
230
  return user_playbooks
228
231
 
@@ -318,6 +321,7 @@ class PlaybookExtractor:
318
321
  log_label="Playbook extraction",
319
322
  )
320
323
  self._last_resumable_run_id = result.run_id
324
+ self._last_resumable_token_totals = sum_trace_tokens(result.trace)
321
325
  if not isinstance(result.output, StructuredPlaybookList):
322
326
  logger.warning(
323
327
  "Playbook extraction did not finish: %s",
@@ -0,0 +1,74 @@
1
+ """Shared atomic supersede primitive for applying a playbook edit.
2
+
3
+ Extracted from ReflectionService._replace_playbook so online and background
4
+ playbook repair paths share one lifecycle (insert-then-supersede, no orphan).
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from reflexio.models.api_schema.domain.entities import LineageContext, UserPlaybook
10
+
11
+ if TYPE_CHECKING:
12
+ from reflexio.server.services.storage.storage_base import BaseStorage
13
+
14
+
15
+ def apply_playbook_edit(
16
+ storage: "BaseStorage",
17
+ *,
18
+ incumbent_id: int,
19
+ new_playbook: UserPlaybook,
20
+ source: str,
21
+ request_id: str,
22
+ ) -> int:
23
+ """Insert a replacement playbook then atomically supersede the incumbent.
24
+
25
+ Uses ``storage.supersede_record`` (atomic conditional CAS) so a lost race
26
+ never leaves an orphan CURRENT row:
27
+
28
+ - Insert the new playbook as CURRENT.
29
+ - Call ``supersede_record(incumbent_id → new_id)``, which only succeeds when
30
+ the incumbent is still CURRENT (``status IS NULL``).
31
+ - If ``supersede_record`` returns ``False`` (incumbent already gone), delete
32
+ the just-inserted successor and return ``-1``.
33
+
34
+ Args:
35
+ storage: A BaseStorage instance providing ``save_user_playbooks``,
36
+ ``supersede_record``, and ``delete_user_playbooks_by_ids``.
37
+ incumbent_id: ``user_playbook_id`` of the playbook being replaced.
38
+ new_playbook: The replacement playbook (inserted as CURRENT, i.e.
39
+ ``status=None``).
40
+ source: Provenance label stored on the new playbook row and in the
41
+ lineage event actor field.
42
+ request_id: Operation-run correlation id for the lineage event. Must be
43
+ non-empty; use the reflection run id (``ReflectionServiceRequest.request_id``)
44
+ or another operation-scoped id. Raises ``ValueError`` immediately
45
+ (before any storage write) when empty, preventing orphaned successor rows.
46
+
47
+ Returns:
48
+ The ``user_playbook_id`` of the newly inserted playbook, or ``-1`` if
49
+ the incumbent was not CURRENT (no mutation; no orphan left behind).
50
+
51
+ Raises:
52
+ ValueError: If ``request_id`` is empty or None.
53
+ """
54
+ if not request_id:
55
+ raise ValueError(
56
+ "apply_playbook_edit: request_id must be non-empty (operation-run correlation id)"
57
+ )
58
+ new_playbook.source = source
59
+ storage.save_user_playbooks([new_playbook])
60
+ new_id: int = new_playbook.user_playbook_id
61
+
62
+ ctx = LineageContext(op_kind="revise", actor=source, request_id=request_id)
63
+ superseded = storage.supersede_record(
64
+ entity_type="user_playbook",
65
+ incumbent_id=str(incumbent_id),
66
+ successor_id=str(new_id),
67
+ context=ctx,
68
+ )
69
+ if not superseded:
70
+ # lost the race: delete the just-inserted successor so no orphan CURRENT row
71
+ # remains. It was never live, so this is a rollback — not an audited erasure.
72
+ storage.delete_user_playbooks_by_ids([new_id], emit_hard_delete=False)
73
+ return -1
74
+ return new_id