claude-smart 0.2.44 → 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 (323) 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 +77 -29
  35. package/plugin/scripts/backend-service.sh +27 -5
  36. package/plugin/scripts/dashboard-service.sh +7 -5
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +3 -3
  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 +10 -8
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +304 -6
  45. package/plugin/src/claude_smart/env_config.py +12 -4
  46. package/plugin/src/claude_smart/events/session_start.py +26 -7
  47. package/plugin/src/claude_smart/events/stop.py +1 -1
  48. package/plugin/src/claude_smart/ids.py +1 -1
  49. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  50. package/plugin/src/claude_smart/runtime.py +7 -1
  51. package/plugin/uv.lock +1 -1
  52. package/plugin/vendor/reflexio/.env.example +54 -30
  53. package/plugin/vendor/reflexio/README.md +14 -8
  54. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  55. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  57. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  64. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  66. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  67. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  68. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  142. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  143. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  144. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  145. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  146. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  147. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  148. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  149. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  158. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  159. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  160. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  161. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  162. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  163. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  164. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  165. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  167. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  168. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  169. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  170. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  171. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  172. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  173. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  174. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  176. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  178. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  179. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  198. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  208. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  209. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  216. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  221. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  222. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  238. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  239. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  244. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  248. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  249. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  250. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  252. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  285. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  286. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  287. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  289. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  290. package/scripts/setup-claude-smart.sh +8 -3
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  313. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  314. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  318. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  323. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -13,7 +13,10 @@ logger = logging.getLogger(__name__)
13
13
 
14
14
  from pydantic import BaseModel, ConfigDict, Field, ValidationError
15
15
 
16
- from reflexio.server.llm.llm_utils import make_strict_json_schema
16
+ from reflexio.server.llm.llm_utils import (
17
+ assert_provider_safe_schema,
18
+ make_strict_json_schema,
19
+ )
17
20
  from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
18
21
 
19
22
  if TYPE_CHECKING:
@@ -72,6 +75,11 @@ class Tool(BaseModel):
72
75
 
73
76
  def openai_spec(self) -> dict:
74
77
  parameters = self.args_model.model_json_schema()
78
+ # Boundary guard: tool-arg schemas bypass the response_format path and the
79
+ # registry contract test, so enforce provider-safety here too (raises under
80
+ # tests, warns in prod). Checked on the native schema to catch a model that
81
+ # forgot StrictStructuredOutput even when ``strict`` would later fold it.
82
+ assert_provider_safe_schema(parameters, name=self.args_model.__name__)
75
83
  if self.strict:
76
84
  parameters = make_strict_json_schema(parameters)
77
85
  return {
@@ -170,10 +178,17 @@ class ToolLoopResult(BaseModel):
170
178
 
171
179
  ctx: Any
172
180
  trace: ToolLoopTrace
173
- finished_reason: Literal["finish_tool", "no_tool_call", "max_steps", "error"]
181
+ finished_reason: Literal[
182
+ "finish_tool", "structured_output", "no_tool_call", "max_steps", "error"
183
+ ]
174
184
  messages: list[dict[str, Any]] = Field(default_factory=list)
175
185
  pending_tool_call_ids: list[str] = Field(default_factory=list)
176
186
  max_steps_remaining: int = 0
187
+ # Set when finished_reason == "structured_output": the model ended the turn
188
+ # with a plain response parsed into the caller's ``response_format`` schema
189
+ # (the structured-output terminus, used by the extraction agent instead of a
190
+ # finish-sentinel tool call).
191
+ structured_output: BaseModel | None = None
177
192
 
178
193
 
179
194
  # Models we know support function calling per vendor docs but that litellm's
@@ -268,6 +283,36 @@ def _serialize_tool_result_for_history(result: dict[str, Any]) -> str:
268
283
  return f"{payload[:_MULTI_STAGE_RESULT_CHAR_CAP]}... [truncated]"
269
284
 
270
285
 
286
+ def _normalize_tool_call_for_history(tool_call: Any) -> dict[str, Any]:
287
+ """Return an OpenAI-compatible plain dict for assistant tool-call history."""
288
+
289
+ if isinstance(tool_call, dict):
290
+ function = tool_call.get("function") or {}
291
+ if not isinstance(function, dict):
292
+ function = {
293
+ "name": getattr(function, "name", None),
294
+ "arguments": getattr(function, "arguments", None),
295
+ }
296
+ return {
297
+ "id": tool_call.get("id"),
298
+ "type": tool_call.get("type") or "function",
299
+ "function": {
300
+ "name": function.get("name"),
301
+ "arguments": function.get("arguments") or "{}",
302
+ },
303
+ }
304
+
305
+ function = getattr(tool_call, "function", None)
306
+ return {
307
+ "id": getattr(tool_call, "id", None),
308
+ "type": getattr(tool_call, "type", None) or "function",
309
+ "function": {
310
+ "name": getattr(function, "name", None),
311
+ "arguments": getattr(function, "arguments", None) or "{}",
312
+ },
313
+ }
314
+
315
+
271
316
  def _run_multi_stage_fallback(
272
317
  *,
273
318
  client: LiteLLMClient,
@@ -431,6 +476,7 @@ def run_tool_loop(
431
476
  fallback_schema: type[BaseModel] | None = None,
432
477
  fallback_tool_name: str | None = None,
433
478
  multi_stage_schema: type[BaseModel] | None = None,
479
+ response_format: type[BaseModel] | None = None,
434
480
  tool_choice: str | dict[str, Any] = "auto",
435
481
  log_label: str | None = None,
436
482
  ) -> ToolLoopResult:
@@ -471,6 +517,14 @@ def run_tool_loop(
471
517
  ``tool`` discriminator literal — that literal names the tool
472
518
  to dispatch, all other fields become its args. Takes priority
473
519
  over ``fallback_schema``.
520
+ response_format (type[BaseModel] | None): When set, the native tool loop
521
+ requests this schema as the model's structured response and treats a
522
+ turn with no tool call as the SUCCESS terminus — the parsed result is
523
+ returned on ``ToolLoopResult.structured_output`` with
524
+ ``finished_reason="structured_output"``. This is how the extraction
525
+ agent finishes (a direct structured answer) while still letting the
526
+ model call intermediate tools such as ``ask_human``. Leave unset for
527
+ finish-sentinel-tool loops.
474
528
  tool_choice (str | dict): Forwarded to each native tool-calling turn.
475
529
  Defaults to ``"auto"``. Pass an OpenAI tool-choice dict (e.g.
476
530
  ``{"type": "function", "function": {"name": "finish"}}``) to force a
@@ -500,7 +554,7 @@ def run_tool_loop(
500
554
  pending_tool_call_ids: list[str] = []
501
555
 
502
556
  # Lazily import the llm_io helpers only when logging is requested —
503
- # matches classic's per-call lazy-import pattern in profile_deduplicator.py.
557
+ # matches classic's per-call lazy-import pattern in profile/components/consolidator.py.
504
558
  if log_label:
505
559
  from reflexio.server.services.service_utils import (
506
560
  log_llm_messages,
@@ -583,14 +637,16 @@ def run_tool_loop(
583
637
 
584
638
  local_msgs = list(messages)
585
639
  try:
640
+ tool_specs = registry.openai_specs()
586
641
  for _step in range(max_steps):
587
642
  if log_label:
588
643
  log_llm_messages(logger, f"{log_label} (turn {_step + 1})", local_msgs)
589
644
  resp = client.generate_chat_response(
590
645
  messages=local_msgs,
591
- tools=registry.openai_specs(),
592
- tool_choice=tool_choice,
646
+ tools=tool_specs or None,
647
+ tool_choice=tool_choice if tool_specs else None,
593
648
  model_role=model_role,
649
+ response_format=response_format,
594
650
  )
595
651
  if log_label:
596
652
  log_model_response(logger, f"{log_label} (turn {_step + 1})", resp)
@@ -611,12 +667,36 @@ def run_tool_loop(
611
667
 
612
668
  tool_calls = getattr(resp, "tool_calls", None)
613
669
  if not tool_calls:
614
- # The model returned a plain-text turn with no tool calls. For a
615
- # text-output agent this means "done", but the finish handler did
616
- # NOT run, so no structured output was committed. Report a
617
- # distinct reason so callers (and logs) don't conflate this with
618
- # an actual finish_extraction call. Callers that require output
619
- # (extraction) already gate success on committed output, so this
670
+ # No tool call this turn. When response_format was requested, this
671
+ # is the SUCCESS terminus: the model returned the final answer
672
+ # directly and the client parsed it into the schema (on the
673
+ # no-tools path the client returns the parsed BaseModel itself;
674
+ # with tools present it sets ``parsed_output``). This is the
675
+ # structured-output equivalent of a finish-sentinel tool call and
676
+ # is how the extraction agent commits its result.
677
+ if response_format is not None:
678
+ structured = (
679
+ resp
680
+ if isinstance(resp, BaseModel)
681
+ else getattr(resp, "parsed_output", None)
682
+ )
683
+ if isinstance(structured, BaseModel):
684
+ trace.finished = True
685
+ return ToolLoopResult(
686
+ ctx=ctx,
687
+ trace=trace,
688
+ finished_reason="structured_output",
689
+ structured_output=structured,
690
+ messages=local_msgs,
691
+ # The structured answer is committed on this turn —
692
+ # one LLM call consumed, mirroring the finish_tool path.
693
+ max_steps_remaining=max_steps - _step - 1,
694
+ )
695
+ # No response_format requested (or nothing parseable): the finish
696
+ # handler did NOT run, so no structured output was committed.
697
+ # Report a distinct reason so callers (and logs) don't conflate
698
+ # this with an actual finish tool call. Callers that require
699
+ # output already gate success on committed output, so this
620
700
  # surfaces accurately as a non-finish termination.
621
701
  trace.finished = True
622
702
  return ToolLoopResult(
@@ -627,21 +707,28 @@ def run_tool_loop(
627
707
  pending_tool_call_ids=pending_tool_call_ids,
628
708
  max_steps_remaining=max_steps - _step,
629
709
  )
710
+ normalized_tool_calls = [
711
+ _normalize_tool_call_for_history(tc) for tc in tool_calls
712
+ ]
630
713
  # Emit ONE assistant message carrying ALL tool_calls from this turn.
631
714
  # OpenAI/Anthropic strict mode requires this shape.
632
715
  local_msgs.append(
633
- {"role": "assistant", "content": None, "tool_calls": list(tool_calls)}
716
+ {
717
+ "role": "assistant",
718
+ "content": None,
719
+ "tool_calls": normalized_tool_calls,
720
+ }
634
721
  )
635
722
  # Process every tool call and append per-call tool result messages.
636
723
  # A single response's usage is attached to every turn it produced —
637
724
  # the summary helpers dedup by (model, prompt_tokens, completion_tokens).
638
- for tc in tool_calls:
725
+ for tc in normalized_tool_calls:
639
726
  # Time each tool individually — using the turn-start clock
640
727
  # would inflate later tools' latencies with model time and
641
728
  # earlier tools' work, masking the actual per-tool cost.
642
729
  tool_t0 = time.monotonic()
643
- name = tc.function.name
644
- args_json = tc.function.arguments
730
+ name = tc["function"]["name"]
731
+ args_json = tc["function"]["arguments"]
645
732
  outcome = registry.handle_outcome(name, args_json, ctx)
646
733
  result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
647
734
  try:
@@ -664,13 +751,16 @@ def run_tool_loop(
664
751
  local_msgs.append(
665
752
  {
666
753
  "role": "tool",
667
- "tool_call_id": tc.id,
754
+ "tool_call_id": tc["id"],
668
755
  "content": json.dumps(result),
669
756
  }
670
757
  )
671
758
  # After processing ALL tool calls, check whether the finish sentinel
672
759
  # appeared in this turn (may be alongside sibling calls).
673
- if any(tc.function.name == finish_tool_name for tc in tool_calls):
760
+ if any(
761
+ tc["function"]["name"] == finish_tool_name
762
+ for tc in normalized_tool_calls
763
+ ):
674
764
  trace.finished = True
675
765
  return ToolLoopResult(
676
766
  ctx=ctx,
@@ -2,20 +2,25 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import os
6
7
  import threading
7
8
  import time
8
9
  from collections.abc import Callable
9
10
  from contextlib import contextmanager
10
11
  from dataclasses import dataclass
12
+ from pathlib import Path
11
13
  from typing import Any, Literal
12
14
 
13
15
  from fastapi import HTTPException, status
14
16
 
17
+ from reflexio.server.tracing import profile_step
15
18
  from reflexio.server.usage_metrics import record_usage_event
16
19
 
17
20
  OperationName = Literal["search", "publish", "aggregation"]
18
21
 
22
+ logger = logging.getLogger(__name__)
23
+
19
24
  _DEFAULT_LIMITS: dict[OperationName, int] = {
20
25
  "search": 8,
21
26
  "publish": 4,
@@ -26,6 +31,12 @@ _DEFAULT_TIMEOUT_SECONDS: dict[OperationName, float] = {
26
31
  "publish": 5.0,
27
32
  "aggregation": 1.0,
28
33
  }
34
+ _ESTIMATED_THREADS_PER_PUBLISH = 3
35
+ _PUBLISH_WAIT_LOG_THRESHOLD_MS = 250
36
+ _PUBLISH_PRESSURE_LOG_INTERVAL_SECONDS = 60.0
37
+ _CGROUP_ROOT = Path("/sys/fs/cgroup")
38
+ _PROC_SELF_CGROUP = Path("/proc/self/cgroup")
39
+ _last_publish_pressure_log_by_org: dict[str, float] = {}
29
40
 
30
41
 
31
42
  @dataclass
@@ -33,6 +44,7 @@ class _LimiterState:
33
44
  semaphore: threading.BoundedSemaphore
34
45
  limit: int
35
46
  waiting: int = 0
47
+ active: int = 0
36
48
 
37
49
 
38
50
  _limiters: dict[tuple[str, OperationName], _LimiterState] = {}
@@ -60,6 +72,277 @@ def _timeout_for(operation: OperationName) -> float:
60
72
  return _DEFAULT_TIMEOUT_SECONDS[operation]
61
73
 
62
74
 
75
+ def _resource_limit_nproc() -> int | str | None:
76
+ try:
77
+ import resource
78
+ except ImportError: # pragma: no cover - platform-specific
79
+ return None
80
+ try:
81
+ soft, _hard = resource.getrlimit(resource.RLIMIT_NPROC)
82
+ except (OSError, ValueError, AttributeError):
83
+ return None
84
+ if soft == resource.RLIM_INFINITY:
85
+ return "unlimited"
86
+ return int(soft)
87
+
88
+
89
+ def _linux_threads_max() -> int | None:
90
+ try:
91
+ with Path("/proc/sys/kernel/threads-max").open() as threads_max_file:
92
+ return int(threads_max_file.read().strip())
93
+ except (OSError, ValueError):
94
+ return None
95
+
96
+
97
+ def _process_os_thread_count() -> int | None:
98
+ try:
99
+ with Path("/proc/self/status").open() as status_file:
100
+ for line in status_file:
101
+ if line.startswith("Threads:"):
102
+ return int(line.split(":", maxsplit=1)[1].strip())
103
+ except (OSError, ValueError):
104
+ return None
105
+ return None
106
+
107
+
108
+ def _available_memory_bytes() -> int | None:
109
+ try:
110
+ page_size = os.sysconf("SC_PAGE_SIZE")
111
+ available_pages = os.sysconf("SC_AVPHYS_PAGES")
112
+ except (OSError, ValueError, AttributeError):
113
+ return None
114
+ if isinstance(page_size, int) and isinstance(available_pages, int):
115
+ return page_size * available_pages
116
+ return None
117
+
118
+
119
+ def _read_path_text(path: Path) -> str | None:
120
+ try:
121
+ return path.read_text().strip()
122
+ except OSError:
123
+ return None
124
+
125
+
126
+ def _positive_int_from_text(text: str | None) -> int | None:
127
+ if text is None or text == "max":
128
+ return None
129
+ try:
130
+ value = int(text)
131
+ except ValueError:
132
+ return None
133
+ return value if value > 0 else None
134
+
135
+
136
+ def _cgroup_candidate_paths(controller: str, filename: str) -> list[Path]:
137
+ candidates = [
138
+ _CGROUP_ROOT / filename,
139
+ _CGROUP_ROOT / controller / filename,
140
+ ]
141
+ cgroup_text = _read_path_text(_PROC_SELF_CGROUP)
142
+ if not cgroup_text:
143
+ return candidates
144
+
145
+ for line in cgroup_text.splitlines():
146
+ parts = line.split(":", maxsplit=2)
147
+ if len(parts) != 3:
148
+ continue
149
+ controllers_text = parts[1]
150
+ relative = parts[2].strip().lstrip("/")
151
+ relative_path = Path(relative) if relative else Path()
152
+ controllers = [item for item in controllers_text.split(",") if item]
153
+ if controllers_text == "":
154
+ candidates.append(_CGROUP_ROOT / relative_path / filename)
155
+ continue
156
+ if controller not in controllers:
157
+ continue
158
+ candidates.append(_CGROUP_ROOT / controller / relative_path / filename)
159
+ candidates.append(
160
+ _CGROUP_ROOT / ",".join(controllers) / relative_path / filename
161
+ )
162
+ candidates.append(_CGROUP_ROOT / relative_path / filename)
163
+ return candidates
164
+
165
+
166
+ def _read_first_cgroup_value(controller: str, filename: str) -> str | None:
167
+ seen: set[Path] = set()
168
+ for path in _cgroup_candidate_paths(controller, filename):
169
+ if path in seen:
170
+ continue
171
+ seen.add(path)
172
+ value = _read_path_text(path)
173
+ if value:
174
+ return value
175
+ return None
176
+
177
+
178
+ def _cgroup_v2_cpu_count() -> float | None:
179
+ raw = _read_first_cgroup_value("unified", "cpu.max")
180
+ if raw is None:
181
+ return None
182
+ parts = raw.split()
183
+ if len(parts) < 2 or parts[0] == "max":
184
+ return None
185
+ try:
186
+ quota = int(parts[0])
187
+ period = int(parts[1])
188
+ except ValueError:
189
+ return None
190
+ if quota <= 0 or period <= 0:
191
+ return None
192
+ return quota / period
193
+
194
+
195
+ def _cgroup_v1_cpu_count() -> float | None:
196
+ quota = _positive_int_from_text(_read_first_cgroup_value("cpu", "cpu.cfs_quota_us"))
197
+ period = _positive_int_from_text(
198
+ _read_first_cgroup_value("cpu", "cpu.cfs_period_us")
199
+ )
200
+ if quota is None or period is None:
201
+ return None
202
+ return quota / period
203
+
204
+
205
+ def _cgroup_cpu_count() -> float | None:
206
+ return _cgroup_v2_cpu_count() or _cgroup_v1_cpu_count()
207
+
208
+
209
+ def _cgroup_memory_limit_bytes() -> int | None:
210
+ v2_limit = _positive_int_from_text(
211
+ _read_first_cgroup_value("unified", "memory.max")
212
+ )
213
+ if v2_limit is not None:
214
+ return v2_limit
215
+ return _positive_int_from_text(
216
+ _read_first_cgroup_value("memory", "memory.limit_in_bytes")
217
+ )
218
+
219
+
220
+ def _cgroup_memory_current_bytes() -> int | None:
221
+ v2_current = _positive_int_from_text(
222
+ _read_first_cgroup_value("unified", "memory.current")
223
+ )
224
+ if v2_current is not None:
225
+ return v2_current
226
+ return _positive_int_from_text(
227
+ _read_first_cgroup_value("memory", "memory.usage_in_bytes")
228
+ )
229
+
230
+
231
+ def _cgroup_pids_limit() -> int | None:
232
+ v2_limit = _positive_int_from_text(_read_first_cgroup_value("unified", "pids.max"))
233
+ if v2_limit is not None:
234
+ return v2_limit
235
+ return _positive_int_from_text(_read_first_cgroup_value("pids", "pids.max"))
236
+
237
+
238
+ def _cgroup_pids_current() -> int | None:
239
+ v2_current = _positive_int_from_text(
240
+ _read_first_cgroup_value("unified", "pids.current")
241
+ )
242
+ if v2_current is not None:
243
+ return v2_current
244
+ return _positive_int_from_text(_read_first_cgroup_value("pids", "pids.current"))
245
+
246
+
247
+ def _anyio_threadpool_stats() -> dict[str, int | None]:
248
+ try:
249
+ from anyio.to_thread import current_default_thread_limiter
250
+
251
+ stats = current_default_thread_limiter().statistics()
252
+ return {
253
+ "anyio_threadpool_total": int(stats.total_tokens),
254
+ "anyio_threadpool_borrowed": int(stats.borrowed_tokens),
255
+ "anyio_threadpool_waiting": int(stats.tasks_waiting),
256
+ }
257
+ except Exception: # noqa: BLE001 - best-effort diagnostics only
258
+ return {
259
+ "anyio_threadpool_total": None,
260
+ "anyio_threadpool_borrowed": None,
261
+ "anyio_threadpool_waiting": None,
262
+ }
263
+
264
+
265
+ def publish_hardware_capacity_snapshot() -> dict[str, int | str | float | None]:
266
+ """Return hardware/threadpool diagnostics for publish concurrency tuning."""
267
+ host_cpu_count = os.cpu_count()
268
+ cgroup_cpu_count = _cgroup_cpu_count()
269
+ effective_cpu_count = cgroup_cpu_count or host_cpu_count
270
+ process_os_threads = _process_os_thread_count()
271
+ nproc_limit = _resource_limit_nproc()
272
+ threads_max = _linux_threads_max()
273
+ cgroup_pids_limit = _cgroup_pids_limit()
274
+ cgroup_pids_current = _cgroup_pids_current()
275
+ cgroup_memory_limit = _cgroup_memory_limit_bytes()
276
+ cgroup_memory_current = _cgroup_memory_current_bytes()
277
+ cgroup_available_memory = (
278
+ max(0, cgroup_memory_limit - cgroup_memory_current)
279
+ if cgroup_memory_limit is not None and cgroup_memory_current is not None
280
+ else None
281
+ )
282
+ anyio_stats = _anyio_threadpool_stats()
283
+ host_thread_budget_candidates = [
284
+ value - process_os_threads
285
+ for value in (nproc_limit, threads_max)
286
+ if isinstance(value, int) and process_os_threads is not None
287
+ ]
288
+ cgroup_pid_budget = (
289
+ cgroup_pids_limit - cgroup_pids_current
290
+ if cgroup_pids_limit is not None and cgroup_pids_current is not None
291
+ else None
292
+ )
293
+ thread_budget_candidates = [
294
+ value
295
+ for value in (cgroup_pid_budget, *host_thread_budget_candidates)
296
+ if isinstance(value, int)
297
+ ]
298
+ max_by_os_threads = (
299
+ max(1, min(thread_budget_candidates) // _ESTIMATED_THREADS_PER_PUBLISH)
300
+ if thread_budget_candidates
301
+ else None
302
+ )
303
+ guidance_candidates = [
304
+ value
305
+ for value in (
306
+ anyio_stats["anyio_threadpool_total"],
307
+ max_by_os_threads,
308
+ max(1, int(effective_cpu_count * 2))
309
+ if effective_cpu_count is not None
310
+ else None,
311
+ )
312
+ if isinstance(value, int) and value > 0
313
+ ]
314
+ hardware_guidance = min(guidance_candidates) if guidance_candidates else None
315
+ return {
316
+ "configured_publish_limit": _limit_for("publish"),
317
+ "publish_timeout_seconds": _timeout_for("publish"),
318
+ "hardware_guidance_publish_limit": hardware_guidance,
319
+ "estimated_threads_per_publish": _ESTIMATED_THREADS_PER_PUBLISH,
320
+ "effective_cpu_count": effective_cpu_count,
321
+ "logical_cpu_count": host_cpu_count,
322
+ "cgroup_cpu_count": cgroup_cpu_count,
323
+ "python_active_threads": threading.active_count(),
324
+ "process_os_threads": process_os_threads,
325
+ "rlimit_nproc_soft": nproc_limit,
326
+ "linux_threads_max": threads_max,
327
+ "cgroup_pids_limit": cgroup_pids_limit,
328
+ "cgroup_pids_current": cgroup_pids_current,
329
+ "cgroup_memory_limit_bytes": cgroup_memory_limit,
330
+ "cgroup_memory_current_bytes": cgroup_memory_current,
331
+ "available_memory_bytes": cgroup_available_memory
332
+ if cgroup_available_memory is not None
333
+ else _available_memory_bytes(),
334
+ **anyio_stats,
335
+ }
336
+
337
+
338
+ def log_publish_hardware_capacity() -> None:
339
+ """Log one startup snapshot for publish concurrency hardware tuning."""
340
+ logger.info(
341
+ "publish_concurrency_hardware_capacity %s",
342
+ publish_hardware_capacity_snapshot(),
343
+ )
344
+
345
+
63
346
  def _state_for(org_id: str, operation: OperationName) -> _LimiterState:
64
347
  key = (str(org_id), operation)
65
348
  desired_limit = _limit_for(operation)
@@ -83,6 +366,7 @@ def _record_limiter_event(
83
366
  wait_ms: int,
84
367
  waiting: int,
85
368
  limit: int,
369
+ active: int,
86
370
  ) -> None:
87
371
  record_usage_event(
88
372
  org_id=org_id,
@@ -95,31 +379,64 @@ def _record_limiter_event(
95
379
  "operation": operation,
96
380
  "waiting": waiting,
97
381
  "limit": limit,
382
+ "active": active,
98
383
  },
99
384
  )
100
385
 
101
386
 
387
+ def _should_log_publish_pressure(org_id: str, now: float) -> bool:
388
+ with _limiters_lock:
389
+ last_logged = _last_publish_pressure_log_by_org.get(org_id)
390
+ if (
391
+ last_logged is not None
392
+ and now - last_logged < _PUBLISH_PRESSURE_LOG_INTERVAL_SECONDS
393
+ ):
394
+ return False
395
+ _last_publish_pressure_log_by_org[org_id] = now
396
+ return True
397
+
398
+
102
399
  @contextmanager
103
400
  def operation_limit(
104
401
  org_id: str,
105
402
  operation: OperationName,
106
403
  *,
107
404
  timeout_seconds: float | None = None,
405
+ wait_forever: bool = False,
108
406
  ) -> Any:
109
407
  """Acquire the per-org limiter for an operation, recording wait metrics."""
110
408
  state = _state_for(org_id, operation)
111
- timeout = _timeout_for(operation) if timeout_seconds is None else timeout_seconds
409
+ timeout = (
410
+ None
411
+ if wait_forever
412
+ else (_timeout_for(operation) if timeout_seconds is None else timeout_seconds)
413
+ )
112
414
  start = time.perf_counter()
113
415
  with _limiters_lock:
114
416
  state.waiting += 1
115
417
  waiting = state.waiting
116
418
  acquired = False
117
419
  try:
118
- acquired = state.semaphore.acquire(timeout=timeout)
119
- wait_ms = int((time.perf_counter() - start) * 1000)
420
+ with profile_step(
421
+ f"{operation}.operation_limit.acquire",
422
+ limit=state.limit,
423
+ waiting=waiting,
424
+ wait_forever=wait_forever,
425
+ ) as span:
426
+ acquired = (
427
+ state.semaphore.acquire()
428
+ if timeout is None
429
+ else state.semaphore.acquire(timeout=timeout)
430
+ )
431
+ wait_ms = int((time.perf_counter() - start) * 1000)
432
+ span.set_data("wait_ms", wait_ms)
433
+ span.set_data("acquired", acquired)
120
434
  with _limiters_lock:
121
435
  state.waiting -= 1
122
436
  waiting_after = state.waiting
437
+ if acquired:
438
+ state.active += 1
439
+ active = state.active
123
440
  if not acquired:
124
441
  _record_limiter_event(
125
442
  org_id=org_id,
@@ -129,10 +446,39 @@ def operation_limit(
129
446
  wait_ms=wait_ms,
130
447
  waiting=waiting_after,
131
448
  limit=state.limit,
449
+ active=active,
132
450
  )
451
+ if operation == "publish":
452
+ logger.warning(
453
+ "publish_limiter_timeout org_id=%s limit=%s active=%s "
454
+ "waiting=%s wait_ms=%s timeout_seconds=%s hardware=%s",
455
+ org_id,
456
+ state.limit,
457
+ active,
458
+ waiting_after,
459
+ wait_ms,
460
+ timeout,
461
+ publish_hardware_capacity_snapshot(),
462
+ )
133
463
  raise TimeoutError(
134
464
  f"{operation} concurrency limit reached for org {org_id}"
135
465
  )
466
+ if (
467
+ operation == "publish"
468
+ and wait_ms >= _PUBLISH_WAIT_LOG_THRESHOLD_MS
469
+ and _should_log_publish_pressure(str(org_id), time.monotonic())
470
+ ):
471
+ logger.info(
472
+ "publish_limiter_wait org_id=%s limit=%s active=%s waiting=%s "
473
+ "wait_ms=%s timeout_seconds=%s hardware=%s",
474
+ org_id,
475
+ state.limit,
476
+ active,
477
+ waiting_after,
478
+ wait_ms,
479
+ timeout,
480
+ publish_hardware_capacity_snapshot(),
481
+ )
136
482
  _record_limiter_event(
137
483
  org_id=org_id,
138
484
  operation=operation,
@@ -141,10 +487,13 @@ def operation_limit(
141
487
  wait_ms=wait_ms,
142
488
  waiting=waiting,
143
489
  limit=state.limit,
490
+ active=active,
144
491
  )
145
492
  yield
146
493
  finally:
147
494
  if acquired:
495
+ with _limiters_lock:
496
+ state.active = max(0, state.active - 1)
148
497
  state.semaphore.release()
149
498
 
150
499
 
@@ -154,8 +503,14 @@ def run_with_operation_limit[T](
154
503
  operation: OperationName,
155
504
  fn: Callable[[], T],
156
505
  timeout_seconds: float | None = None,
506
+ wait_forever: bool = False,
157
507
  ) -> T:
158
- with operation_limit(org_id, operation, timeout_seconds=timeout_seconds):
508
+ with operation_limit(
509
+ org_id,
510
+ operation,
511
+ timeout_seconds=timeout_seconds,
512
+ wait_forever=wait_forever,
513
+ ):
159
514
  return fn()
160
515
 
161
516
 
@@ -177,3 +532,4 @@ def limiter_http_exception(operation: OperationName) -> HTTPException:
177
532
  def reset_operation_limiters_for_tests() -> None:
178
533
  with _limiters_lock:
179
534
  _limiters.clear()
535
+ _last_publish_pressure_log_by_org.clear()