claude-smart 0.2.45 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +22 -2
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +5 -1
  35. package/plugin/scripts/backend-service.sh +22 -2
  36. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  37. package/plugin/scripts/hook_entry.sh +1 -1
  38. package/plugin/scripts/opencode-claude-compat +4 -0
  39. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  40. package/plugin/scripts/opencode-claude-compat.js +225 -0
  41. package/plugin/scripts/smart-install.sh +10 -8
  42. package/plugin/src/README.md +1 -1
  43. package/plugin/src/claude_smart/cli.py +304 -6
  44. package/plugin/src/claude_smart/env_config.py +12 -4
  45. package/plugin/src/claude_smart/events/session_start.py +26 -7
  46. package/plugin/src/claude_smart/events/stop.py +1 -1
  47. package/plugin/src/claude_smart/ids.py +1 -1
  48. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  49. package/plugin/src/claude_smart/runtime.py +7 -1
  50. package/plugin/uv.lock +1 -1
  51. package/plugin/vendor/reflexio/.env.example +54 -30
  52. package/plugin/vendor/reflexio/README.md +14 -8
  53. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  54. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  56. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  57. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  65. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  66. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  67. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  68. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  141. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  142. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  143. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  144. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  146. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  147. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  148. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  149. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  158. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  159. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  160. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  161. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  162. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  163. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  164. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  165. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  167. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  168. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  169. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  170. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  171. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  172. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  173. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  174. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  176. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  178. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  179. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  197. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  198. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  208. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  209. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  211. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  216. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  221. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  222. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  238. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  244. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  248. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  249. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  250. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  252. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  281. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  285. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  286. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  287. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  289. package/scripts/setup-claude-smart.sh +8 -3
  290. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  317. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  318. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -4,6 +4,7 @@ import hashlib
4
4
  import logging
5
5
  import os
6
6
  import time
7
+ import uuid
7
8
  from collections.abc import Sequence
8
9
  from typing import TYPE_CHECKING
9
10
 
@@ -12,13 +13,9 @@ if TYPE_CHECKING:
12
13
 
13
14
  from reflexio.models.api_schema.service_schemas import (
14
15
  AgentPlaybook,
15
- AgentPlaybookSnapshot,
16
16
  AgentPlaybookSourceWindow,
17
- AgentPlaybookUpdateEntry,
18
- PlaybookAggregationChangeLog,
19
17
  PlaybookStatus,
20
18
  UserPlaybook,
21
- agent_playbook_to_snapshot,
22
19
  )
23
20
  from reflexio.models.config_schema import (
24
21
  SINGLETON_USER_PLAYBOOK_NAME,
@@ -33,9 +30,12 @@ from reflexio.server.services.playbook.playbook_service_constants import (
33
30
  from reflexio.server.services.playbook.playbook_service_utils import (
34
31
  PlaybookAggregationOutput,
35
32
  PlaybookAggregatorRequest,
33
+ StructuredPlaybookContent,
36
34
  ensure_playbook_content,
37
35
  )
36
+ from reflexio.server.services.playbook.user_detail_stripping import UserDetailStripper
38
37
  from reflexio.server.services.service_utils import log_model_response
38
+ from reflexio.server.tracing import capture_anomaly, sentry_tags
39
39
  from reflexio.server.usage_metrics import record_usage_event
40
40
 
41
41
  logger = logging.getLogger(__name__)
@@ -52,17 +52,33 @@ class PlaybookAggregator:
52
52
  llm_client: LiteLLMClient,
53
53
  request_context: RequestContext,
54
54
  agent_version: str,
55
+ user_detail_stripper: UserDetailStripper | None = None,
55
56
  ) -> None:
56
57
  self.client = llm_client
57
58
  self.storage = request_context.storage
58
59
  self.configurator = request_context.configurator
59
60
  self.request_context = request_context
60
61
  self.agent_version = agent_version
62
+ self.user_detail_stripper = user_detail_stripper
63
+ prompt_extra_instructions = getattr(
64
+ user_detail_stripper, "prompt_extra_instructions", None
65
+ )
66
+ if not isinstance(prompt_extra_instructions, str):
67
+ prompt_extra_instructions = None
68
+ self.aggregation_prompt_extra_instructions = (
69
+ self._format_prompt_extra_instructions(prompt_extra_instructions)
70
+ )
61
71
 
62
72
  # ===============================
63
73
  # private methods - operation state
64
74
  # ===============================
65
75
 
76
+ @staticmethod
77
+ def _format_prompt_extra_instructions(instructions: str | None) -> str:
78
+ if not instructions or not instructions.strip():
79
+ return ""
80
+ return f"{instructions.strip()}\n"
81
+
66
82
  def _create_state_manager(self) -> OperationStateManager:
67
83
  """
68
84
  Create an OperationStateManager for the playbook aggregator.
@@ -103,7 +119,7 @@ class PlaybookAggregator:
103
119
  # Count user playbooks with ID greater than last processed using efficient count query
104
120
  # Only count current user playbooks (status=None), not archived or pending ones.
105
121
  # Singleton aggregation operates on the user's whole playbook set — no name filter.
106
- new_count = self.storage.count_user_playbooks( # type: ignore[reportOptionalMemberAccess]
122
+ new_count = self.storage.count_user_playbooks( # pyright: ignore[reportOptionalMemberAccess]
107
123
  min_user_playbook_id=last_processed_id,
108
124
  agent_version=self.agent_version,
109
125
  status_filter=[None],
@@ -194,6 +210,108 @@ class PlaybookAggregator:
194
210
  blocks.append("\n".join(lines))
195
211
  return "\n\n".join(blocks) if blocks else "(No playbook items)"
196
212
 
213
+ def _strip_prompt_field(
214
+ self,
215
+ text: str | None,
216
+ shared_mapping: dict[str, int],
217
+ ) -> str | None:
218
+ if text is None or self.user_detail_stripper is None:
219
+ return text
220
+ return self.user_detail_stripper.strip_user_details(
221
+ text, shared_mapping=shared_mapping
222
+ ).text
223
+
224
+ def _strip_user_playbook_for_prompt(
225
+ self,
226
+ playbook: UserPlaybook,
227
+ shared_mapping: dict[str, int],
228
+ ) -> UserPlaybook:
229
+ if self.user_detail_stripper is None:
230
+ return playbook
231
+ return playbook.model_copy(
232
+ update={
233
+ "content": self._strip_prompt_field(playbook.content, shared_mapping)
234
+ or "",
235
+ "trigger": self._strip_prompt_field(playbook.trigger, shared_mapping),
236
+ "rationale": self._strip_prompt_field(
237
+ playbook.rationale, shared_mapping
238
+ ),
239
+ }
240
+ )
241
+
242
+ def _sanitize_aggregation_output_text(
243
+ self,
244
+ text: str | None,
245
+ ) -> tuple[str | None, int]:
246
+ if self.user_detail_stripper is None:
247
+ return text, 0
248
+ return self.user_detail_stripper.sanitize_aggregation_output_text(text)
249
+
250
+ def _sanitize_aggregation_response(
251
+ self, response: PlaybookAggregationOutput
252
+ ) -> tuple[PlaybookAggregationOutput, int]:
253
+ structured = response.playbook
254
+ if structured is None:
255
+ return response, 0
256
+
257
+ updates: dict[str, str | None] = {}
258
+ placeholder_count = 0
259
+ for field_name, value in structured.model_dump().items():
260
+ if not isinstance(value, str):
261
+ continue
262
+ sanitized, field_count = self._sanitize_aggregation_output_text(value)
263
+ placeholder_count += field_count
264
+ if sanitized != value:
265
+ updates[field_name] = sanitized
266
+
267
+ if not updates:
268
+ return response, 0
269
+ return response.model_copy(
270
+ update={"playbook": structured.model_copy(update=updates)}
271
+ ), placeholder_count
272
+
273
+ def _sanitize_aggregation_log_value(self, value: object) -> tuple[object, int]:
274
+ if isinstance(value, str):
275
+ return self._sanitize_aggregation_output_text(value)
276
+
277
+ if isinstance(value, dict):
278
+ sanitized: dict[object, object] = {}
279
+ placeholder_count = 0
280
+ for key, item in value.items():
281
+ sanitized_key, key_count = self._sanitize_aggregation_log_value(key)
282
+ sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
283
+ sanitized[sanitized_key] = sanitized_item
284
+ placeholder_count += key_count + item_count
285
+ return sanitized, placeholder_count
286
+
287
+ if isinstance(value, list):
288
+ sanitized_items: list[object] = []
289
+ placeholder_count = 0
290
+ for item in value:
291
+ sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
292
+ sanitized_items.append(sanitized_item)
293
+ placeholder_count += item_count
294
+ return sanitized_items, placeholder_count
295
+
296
+ if isinstance(value, tuple):
297
+ sanitized_items: list[object] = []
298
+ placeholder_count = 0
299
+ for item in value:
300
+ sanitized_item, item_count = self._sanitize_aggregation_log_value(item)
301
+ sanitized_items.append(sanitized_item)
302
+ placeholder_count += item_count
303
+ return tuple(sanitized_items), placeholder_count
304
+
305
+ return value, 0
306
+
307
+ def _record_placeholder_leakage(self, placeholder_count: int) -> None:
308
+ if placeholder_count <= 0:
309
+ return
310
+ logger.warning(
311
+ "Replaced %d residual user-detail placeholders in aggregated playbook output",
312
+ placeholder_count,
313
+ )
314
+
197
315
  @staticmethod
198
316
  def _get_direction_key(fb: UserPlaybook) -> str:
199
317
  """
@@ -465,102 +583,6 @@ class PlaybookAggregator:
465
583
 
466
584
  return changed_clusters, playbook_ids_to_archive
467
585
 
468
- def _build_change_log(
469
- self,
470
- playbook_name: str,
471
- full_archive: bool,
472
- before_playbooks_by_id: dict[int, AgentPlaybook],
473
- saved_playbooks: list[AgentPlaybook],
474
- archived_playbook_ids: list[int],
475
- prev_fingerprints: dict,
476
- ) -> PlaybookAggregationChangeLog:
477
- """Build a PlaybookAggregationChangeLog from the aggregation run results.
478
-
479
- Args:
480
- playbook_name: The playbook name being aggregated
481
- full_archive: Whether this was a full archive (rerun/first run)
482
- before_playbooks_by_id: Snapshot of playbooks before archiving, keyed by playbook_id
483
- saved_playbooks: Newly saved playbooks from this run
484
- archived_playbook_ids: AgentPlaybook IDs that were selectively archived (incremental mode)
485
- prev_fingerprints: Previous cluster fingerprints (empty for full archive)
486
-
487
- Returns:
488
- PlaybookAggregationChangeLog with added/removed/updated lists populated
489
- """
490
- added: list[AgentPlaybookSnapshot] = []
491
- removed: list[AgentPlaybookSnapshot] = []
492
- updated: list[AgentPlaybookUpdateEntry] = []
493
-
494
- if full_archive:
495
- # No 1:1 mapping — all old playbooks are removed, all new are added
496
- removed = [
497
- agent_playbook_to_snapshot(fb) for fb in before_playbooks_by_id.values()
498
- ]
499
- added = [agent_playbook_to_snapshot(fb) for fb in saved_playbooks if fb]
500
- else:
501
- # Incremental mode: map old playbook_ids to new playbooks via fingerprints
502
- # Build a set of old playbook_ids that were archived
503
- archived_id_set = set(archived_playbook_ids)
504
-
505
- # Build mapping: old_playbook_id -> new_playbook_id via fingerprint changes
506
- # prev_fingerprints maps fp_hash -> {playbook_id, user_playbook_ids}
507
- # new_fingerprints maps fp_hash -> {playbook_id, user_playbook_ids}
508
- # If an old fingerprint disappeared and a new one appeared, and
509
- # the old fp had a playbook_id in archived_id_set, we can try to pair them.
510
- # However, without a direct cluster-level old->new mapping, we use a simpler approach:
511
- # archived playbooks that have a corresponding new playbook (by position in saved list) are updates.
512
-
513
- # Collect old playbook_ids from disappeared fingerprints
514
- old_fp_playbook_ids = {}
515
- for fp, fp_data in prev_fingerprints.items():
516
- fid = fp_data.get("agent_playbook_id")
517
- if fid is not None and fid in archived_id_set:
518
- old_fp_playbook_ids[fid] = fp
519
-
520
- # For each saved playbook, try to match with an archived old playbook
521
- matched_old_ids: set[int] = set()
522
- for saved_fb in saved_playbooks:
523
- if not saved_fb:
524
- continue
525
- # Try to find an old playbook from the archived set to pair with
526
- paired_old_id = None
527
- for old_id in list(old_fp_playbook_ids.keys()):
528
- if old_id not in matched_old_ids:
529
- paired_old_id = old_id
530
- matched_old_ids.add(old_id)
531
- break
532
-
533
- if (
534
- paired_old_id is not None
535
- and paired_old_id in before_playbooks_by_id
536
- ):
537
- updated.append(
538
- AgentPlaybookUpdateEntry(
539
- before=agent_playbook_to_snapshot(
540
- before_playbooks_by_id[paired_old_id]
541
- ),
542
- after=agent_playbook_to_snapshot(saved_fb),
543
- )
544
- )
545
- else:
546
- added.append(agent_playbook_to_snapshot(saved_fb))
547
-
548
- # Remaining archived playbooks that weren't paired are removals
549
- for old_id in archived_id_set:
550
- if old_id not in matched_old_ids and old_id in before_playbooks_by_id:
551
- removed.append(
552
- agent_playbook_to_snapshot(before_playbooks_by_id[old_id])
553
- )
554
-
555
- return PlaybookAggregationChangeLog(
556
- playbook_name=playbook_name,
557
- agent_version=self.agent_version,
558
- run_mode="full_archive" if full_archive else "incremental",
559
- added_agent_playbooks=added,
560
- removed_agent_playbooks=removed,
561
- updated_agent_playbooks=updated,
562
- )
563
-
564
586
  # ===============================
565
587
  # public methods
566
588
  # ===============================
@@ -572,6 +594,8 @@ class PlaybookAggregator:
572
594
  dict: Aggregation stats with keys: clusters_found, user_playbooks_processed, playbooks_generated, skipped (optional)
573
595
  """
574
596
  aggregation_start = time.perf_counter()
597
+ # Stable id for this aggregation run — groups all lineage events produced below.
598
+ _run_id = str(uuid.uuid4())
575
599
  _empty_stats = {
576
600
  "clusters_found": 0,
577
601
  "user_playbooks_processed": 0,
@@ -667,6 +691,12 @@ class PlaybookAggregator:
667
691
  playbook_name,
668
692
  self.agent_version,
669
693
  )
694
+ logger.info(
695
+ "User detail stripping for aggregation: %s",
696
+ type(self.user_detail_stripper).__name__
697
+ if self.user_detail_stripper is not None
698
+ else "disabled",
699
+ )
670
700
 
671
701
  # Get existing APPROVED and PENDING playbooks before archiving (to pass to LLM for deduplication).
672
702
  # Singleton aggregation pulls the user's whole set — no name filter.
@@ -694,11 +724,6 @@ class PlaybookAggregator:
694
724
  )
695
725
  clusters = self.get_clusters(user_playbooks, playbook_aggregator_config)
696
726
 
697
- # Capture all current playbooks before archiving (for change log)
698
- before_playbooks_by_id: dict[int, AgentPlaybook] = {
699
- fb.agent_playbook_id: fb for fb in existing_playbooks
700
- }
701
-
702
727
  # Determine which clusters changed (skip for rerun)
703
728
  mgr = self._create_state_manager()
704
729
  archived_playbook_ids = []
@@ -764,10 +789,6 @@ class PlaybookAggregator:
764
789
  len(archived_playbook_ids),
765
790
  )
766
791
 
767
- # Selectively archive only playbooks from changed/disappeared clusters
768
- if archived_playbook_ids:
769
- self.storage.archive_agent_playbooks_by_ids(archived_playbook_ids) # type: ignore[reportOptionalMemberAccess]
770
-
771
792
  try:
772
793
  # Emit the started event inside the protected block so any failure
773
794
  # from here on is paired with an aggregation_failed event.
@@ -789,6 +810,37 @@ class PlaybookAggregator:
789
810
  )
790
811
  new_playbooks = [playbook for playbook, _ in generated_pairs]
791
812
 
813
+ previous_fingerprints_for_changed_clusters = {}
814
+ changed_fps_by_previous_fp = {}
815
+ changed_fps_with_replacements = set()
816
+ previous_playbook_id_by_fp = {}
817
+ if not playbook_aggregator_request.rerun and prev_fingerprints:
818
+ for cluster_playbooks in changed_clusters.values():
819
+ fp = self._compute_cluster_fingerprint(cluster_playbooks)
820
+ current_user_ids = {
821
+ fb.user_playbook_id
822
+ for fb in cluster_playbooks
823
+ if fb.user_playbook_id is not None
824
+ }
825
+ matched_prev_fingerprints = {
826
+ prev_fp: fp_data
827
+ for prev_fp, fp_data in prev_fingerprints.items()
828
+ if fp_data.get("agent_playbook_id") is not None
829
+ and current_user_ids
830
+ & set(fp_data.get("user_playbook_ids") or [])
831
+ }
832
+ if matched_prev_fingerprints:
833
+ previous_fingerprints_for_changed_clusters[fp] = (
834
+ matched_prev_fingerprints
835
+ )
836
+ for prev_fp, fp_data in matched_prev_fingerprints.items():
837
+ changed_fps_by_previous_fp.setdefault(prev_fp, set()).add(
838
+ fp
839
+ )
840
+ playbook_id = fp_data.get("agent_playbook_id")
841
+ if playbook_id is not None:
842
+ previous_playbook_id_by_fp[prev_fp] = playbook_id
843
+
792
844
  # Lazy archive: only full-archive when the LLM produced replacements.
793
845
  # Skipping the archive when new_playbooks is empty preserves existing
794
846
  # PENDING/APPROVED playbooks that the LLM identified as duplicates.
@@ -806,17 +858,12 @@ class PlaybookAggregator:
806
858
  )
807
859
  full_archive = False
808
860
 
809
- # Save playbooks (returns playbooks with playbook_id populated)
810
- saved_playbooks = self.storage.save_agent_playbooks(new_playbooks) # type: ignore[reportOptionalMemberAccess]
811
-
812
861
  # Build new fingerprint state
813
862
  new_fingerprints = {}
814
863
 
815
864
  if not playbook_aggregator_request.rerun:
816
865
  # Carry forward unchanged fingerprints from previous state
817
- prev_fps = mgr.get_cluster_fingerprints(
818
- name=playbook_name, version=self.agent_version
819
- )
866
+ prev_fps = prev_fingerprints
820
867
  current_fp_set = set()
821
868
  for cluster_playbooks in clusters.values():
822
869
  fp = self._compute_cluster_fingerprint(cluster_playbooks)
@@ -837,31 +884,47 @@ class PlaybookAggregator:
837
884
  }
838
885
  )
839
886
 
840
- # Initialize changed cluster fingerprints before assigning saved IDs
841
- # below. ``generated_pairs`` preserves the exact source cluster for
842
- # every non-duplicate playbook the LLM produced.
843
- for cluster_playbooks in changed_clusters.values():
844
- fp = self._compute_cluster_fingerprint(cluster_playbooks)
845
- raw_ids = sorted(fb.user_playbook_id for fb in cluster_playbooks)
846
-
847
- new_fingerprints[fp] = {
848
- "agent_playbook_id": None,
849
- "user_playbook_ids": raw_ids,
850
- }
851
-
852
- saved_playbook_list = list(saved_playbooks)
853
-
854
- # Assign saved playbook ids to the exact source cluster that generated them.
855
- for saved_fb, (_, cluster_playbooks) in zip(
856
- saved_playbook_list, generated_pairs, strict=False
857
- ):
887
+ saved_playbook_list: list[AgentPlaybook] = []
888
+ selective_supersede_playbook_ids = set()
889
+ replaced_previous_fingerprints = set()
890
+
891
+ # Save each playbook + its aggregate event atomically, then assign
892
+ # fingerprints and source-windows for the saved row.
893
+ for playbook, cluster_playbooks in generated_pairs:
894
+ run_mode = "full_archive" if full_archive else "incremental"
895
+ member_ids = [
896
+ str(fb.user_playbook_id)
897
+ for fb in cluster_playbooks
898
+ if fb.user_playbook_id
899
+ ]
900
+ saved_fb = self.storage.save_agent_playbook_with_aggregate_event( # type: ignore[reportOptionalMemberAccess]
901
+ playbook,
902
+ source_ids=member_ids,
903
+ request_id=_run_id,
904
+ run_mode=run_mode,
905
+ )
906
+ saved_playbook_list.append(saved_fb)
858
907
  if saved_fb and saved_fb.agent_playbook_id:
859
908
  fp_key = self._compute_cluster_fingerprint(cluster_playbooks)
909
+ changed_fps_with_replacements.add(fp_key)
860
910
  raw_ids = sorted(fb.user_playbook_id for fb in cluster_playbooks)
861
911
  new_fingerprints[fp_key] = {
862
912
  "agent_playbook_id": saved_fb.agent_playbook_id,
863
913
  "user_playbook_ids": raw_ids,
864
914
  }
915
+ for prev_fp in previous_fingerprints_for_changed_clusters.get(
916
+ fp_key, {}
917
+ ):
918
+ all_overlapping_clusters_replaced = (
919
+ changed_fps_by_previous_fp.get(prev_fp, set()).issubset(
920
+ changed_fps_with_replacements
921
+ )
922
+ )
923
+ playbook_id = previous_playbook_id_by_fp.get(prev_fp)
924
+ if all_overlapping_clusters_replaced:
925
+ replaced_previous_fingerprints.add(prev_fp)
926
+ if playbook_id is not None:
927
+ selective_supersede_playbook_ids.add(playbook_id)
865
928
  self.storage.set_source_windows_for_agent_playbook( # type: ignore[reportOptionalMemberAccess]
866
929
  saved_fb.agent_playbook_id,
867
930
  [
@@ -876,6 +939,33 @@ class PlaybookAggregator:
876
939
  ],
877
940
  )
878
941
 
942
+ # Changed clusters that did not get a replacement keep their previous
943
+ # fingerprint/playbook mapping so a later successful replacement can
944
+ # supersede the old playbook. Brand-new duplicate clusters still get
945
+ # a None marker to avoid repeated LLM calls for the same fingerprint.
946
+ for cluster_playbooks in changed_clusters.values():
947
+ fp = self._compute_cluster_fingerprint(cluster_playbooks)
948
+ if fp in new_fingerprints:
949
+ continue
950
+
951
+ previous_for_cluster = previous_fingerprints_for_changed_clusters.get(
952
+ fp, {}
953
+ )
954
+ preserved_previous = {
955
+ prev_fp: fp_data
956
+ for prev_fp, fp_data in previous_for_cluster.items()
957
+ if prev_fp not in replaced_previous_fingerprints
958
+ }
959
+ if preserved_previous:
960
+ new_fingerprints.update(preserved_previous)
961
+ continue
962
+
963
+ raw_ids = sorted(fb.user_playbook_id for fb in cluster_playbooks)
964
+ new_fingerprints[fp] = {
965
+ "agent_playbook_id": None,
966
+ "user_playbook_ids": raw_ids,
967
+ }
968
+
879
969
  # Store fingerprints in operation state
880
970
  mgr.update_cluster_fingerprints(
881
971
  name=playbook_name,
@@ -886,39 +976,54 @@ class PlaybookAggregator:
886
976
  # Update operation state with the highest user_playbook_id processed
887
977
  self._update_operation_state(playbook_name, user_playbooks)
888
978
 
889
- # Build and save change log
890
- try:
891
- change_log = self._build_change_log(
892
- playbook_name=playbook_name,
893
- full_archive=full_archive,
894
- before_playbooks_by_id=before_playbooks_by_id,
895
- saved_playbooks=saved_playbook_list,
896
- archived_playbook_ids=archived_playbook_ids,
897
- prev_fingerprints=(prev_fingerprints if not full_archive else {}),
979
+ # Remove archived playbooks after successful aggregation. ALWAYS soft-supersede
980
+ # (never hard-delete) so the removal is reconstructable from lineage — mirrors the
981
+ # profile dedup always-soft path (#206).
982
+ if not _run_id:
983
+ # Empty request_id makes the removal unreconstructable (lineage events are keyed
984
+ # on it). Fail loud and skip removal — never silently hard-delete.
985
+ capture_anomaly(
986
+ "lineage.aggregation.missing_request_id",
987
+ level="error",
988
+ org_id=self.request_context.org_id,
898
989
  )
899
- self.storage.add_playbook_aggregation_change_log(change_log) # type: ignore[reportOptionalMemberAccess]
900
- logger.info(
901
- "User playbook aggregation change log for '%s' (agent_version=%s): %d agent playbooks added, %d removed, %d updated",
902
- playbook_name,
903
- self.agent_version,
904
- len(change_log.added_agent_playbooks),
905
- len(change_log.removed_agent_playbooks),
906
- len(change_log.updated_agent_playbooks),
907
- )
908
- except Exception:
909
- logger.exception(
910
- "Failed to save playbook aggregation change log for '%s', continuing",
911
- playbook_name,
912
- )
913
-
914
- # Delete archived playbooks after successful aggregation
915
- if full_archive:
916
- for name in full_archive_playbook_names:
917
- self.storage.delete_archived_agent_playbooks_by_playbook_name( # type: ignore[reportOptionalMemberAccess]
918
- name, agent_version=self.agent_version
990
+ else:
991
+ try:
992
+ if full_archive:
993
+ for name in full_archive_playbook_names:
994
+ self.storage.supersede_agent_playbooks_by_playbook_name( # type: ignore[reportOptionalMemberAccess]
995
+ name,
996
+ agent_version=self.agent_version,
997
+ request_id=_run_id,
998
+ )
999
+ elif selective_supersede_playbook_ids:
1000
+ self.storage.supersede_agent_playbooks_by_ids( # type: ignore[reportOptionalMemberAccess]
1001
+ sorted(selective_supersede_playbook_ids),
1002
+ request_id=_run_id,
1003
+ )
1004
+ elif archived_playbook_ids:
1005
+ logger.info(
1006
+ "Skipping selective supersede of %s (agent_version=%s): LLM produced 0 new playbooks; existing PENDING/APPROVED playbooks preserved",
1007
+ archived_playbook_ids,
1008
+ self.agent_version,
1009
+ )
1010
+ except Exception:
1011
+ with sentry_tags(
1012
+ subsystem="playbook_aggregation",
1013
+ op="supersede_agent_playbooks",
1014
+ org_id=self.request_context.org_id,
1015
+ request_id=_run_id,
1016
+ ):
1017
+ logger.exception(
1018
+ "Failed to soft-supersede archived agent playbooks (run %s)",
1019
+ _run_id,
1020
+ )
1021
+ capture_anomaly(
1022
+ "lineage.aggregation.supersede_failed",
1023
+ level="error",
1024
+ org_id=self.request_context.org_id,
1025
+ request_id=_run_id,
919
1026
  )
920
- elif archived_playbook_ids:
921
- self.storage.delete_agent_playbooks_by_ids(archived_playbook_ids) # type: ignore[reportOptionalMemberAccess]
922
1027
 
923
1028
  self._enqueue_playbook_optimization(saved_playbook_list)
924
1029
 
@@ -1197,17 +1302,24 @@ class PlaybookAggregator:
1197
1302
  direction_overlap_threshold: float = 0.6,
1198
1303
  ) -> list[tuple[AgentPlaybook, list[UserPlaybook]]]:
1199
1304
  """Generate agent playbooks while preserving their exact source cluster."""
1200
- # Format existing approved playbooks for the prompt
1305
+ new_playbooks: list[tuple[AgentPlaybook, list[UserPlaybook]]] = []
1201
1306
  approved_playbooks_str = (
1202
1307
  "\n".join([f"- {fb.content}" for fb in existing_approved_playbooks])
1203
1308
  if existing_approved_playbooks
1204
1309
  else "None"
1205
1310
  )
1206
-
1207
- new_playbooks: list[tuple[AgentPlaybook, list[UserPlaybook]]] = []
1208
1311
  for cluster_playbooks in clusters.values():
1312
+ shared_mapping: dict[str, int] = {}
1313
+ if self.user_detail_stripper is None:
1314
+ prompt_cluster_playbooks = cluster_playbooks
1315
+ else:
1316
+ prompt_cluster_playbooks = [
1317
+ self._strip_user_playbook_for_prompt(playbook, shared_mapping)
1318
+ for playbook in cluster_playbooks
1319
+ ]
1320
+
1209
1321
  playbook = self._generate_playbook_from_cluster(
1210
- cluster_playbooks,
1322
+ prompt_cluster_playbooks,
1211
1323
  approved_playbooks_str,
1212
1324
  direction_overlap_threshold=direction_overlap_threshold,
1213
1325
  )
@@ -1289,14 +1401,18 @@ class PlaybookAggregator:
1289
1401
  # Build content directly as a freeform summary
1290
1402
  content_text = f"When {trigger}, {first_content}."
1291
1403
 
1292
- return AgentPlaybook(
1293
- playbook_name=cluster_playbooks[0].playbook_name,
1294
- agent_version=cluster_playbooks[0].agent_version,
1295
- content=content_text,
1296
- trigger=trigger,
1297
- playbook_status=PlaybookStatus.PENDING,
1298
- playbook_metadata="mock_generated",
1404
+ response = PlaybookAggregationOutput(
1405
+ playbook=StructuredPlaybookContent(
1406
+ content=content_text,
1407
+ trigger=trigger,
1408
+ )
1299
1409
  )
1410
+ response, placeholder_count = self._sanitize_aggregation_response(response)
1411
+ self._record_placeholder_leakage(placeholder_count)
1412
+ playbook = self._process_aggregation_response(response, cluster_playbooks)
1413
+ if playbook is None:
1414
+ return None
1415
+ return playbook.model_copy(update={"playbook_metadata": "mock_generated"})
1300
1416
 
1301
1417
  # Format raw playbooks for prompt using structured format
1302
1418
  raw_playbooks_str = self._format_structured_cluster_input(
@@ -1312,6 +1428,7 @@ class PlaybookAggregator:
1312
1428
  {
1313
1429
  "user_playbooks": raw_playbooks_str,
1314
1430
  "existing_approved_playbooks": existing_approved_playbooks_str,
1431
+ "aggregation_prompt_extra_instructions": self.aggregation_prompt_extra_instructions,
1315
1432
  },
1316
1433
  ),
1317
1434
  }
@@ -1324,6 +1441,16 @@ class PlaybookAggregator:
1324
1441
  response_format=PlaybookAggregationOutput,
1325
1442
  parse_structured_output=True,
1326
1443
  )
1444
+ if isinstance(response, PlaybookAggregationOutput):
1445
+ response, placeholder_count = self._sanitize_aggregation_response(
1446
+ response
1447
+ )
1448
+ self._record_placeholder_leakage(placeholder_count)
1449
+ else:
1450
+ response, placeholder_count = self._sanitize_aggregation_log_value(
1451
+ response
1452
+ )
1453
+ self._record_placeholder_leakage(placeholder_count)
1327
1454
  log_model_response(logger, "Aggregation structured response", response)
1328
1455
 
1329
1456
  if not isinstance(response, PlaybookAggregationOutput):
@@ -1335,9 +1462,13 @@ class PlaybookAggregator:
1335
1462
 
1336
1463
  return self._process_aggregation_response(response, cluster_playbooks)
1337
1464
  except Exception as exc:
1465
+ sanitized_error, placeholder_count = self._sanitize_aggregation_log_value(
1466
+ str(exc)
1467
+ )
1468
+ self._record_placeholder_leakage(placeholder_count)
1338
1469
  logger.error(
1339
1470
  "AgentPlaybook aggregation failed due to %s, returning None.",
1340
- str(exc),
1471
+ sanitized_error,
1341
1472
  )
1342
1473
  return None
1343
1474
 
@@ -1349,13 +1480,17 @@ class PlaybookAggregator:
1349
1480
 
1350
1481
  Args:
1351
1482
  response: Parsed PlaybookAggregationOutput from LLM
1352
- cluster_playbooks: Original cluster playbooks for metadata
1483
+ cluster_playbooks: Cluster playbooks used only for non-user metadata
1484
+ such as playbook name and agent version. Callers may pass
1485
+ prompt-sanitized copies here, so this method must not read
1486
+ user-authored fields from them.
1353
1487
 
1354
1488
  Returns:
1355
1489
  AgentPlaybook or None if no playbook should be generated
1356
1490
  """
1357
1491
  if not response:
1358
1492
  return None
1493
+ response, _placeholder_count = self._sanitize_aggregation_response(response)
1359
1494
 
1360
1495
  structured = response.playbook
1361
1496
  if structured is None:
@@ -1365,6 +1500,9 @@ class PlaybookAggregator:
1365
1500
  # content is always the LLM's freeform summary;
1366
1501
  # fall back to formatted structured fields for backward compatibility
1367
1502
  playbook_content = ensure_playbook_content(structured.content, structured)
1503
+ if not playbook_content.strip():
1504
+ logger.info("Aggregated playbook has no valid content, skipping")
1505
+ return None
1368
1506
  logger.info(
1369
1507
  "Aggregated playbook content (freeform): %.200s",
1370
1508
  playbook_content,