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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +34 -5
- package/bin/claude-smart.js +295 -5
- package/package.json +22 -3
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +22 -2
- package/plugin/commands/clear-all.md +1 -0
- package/plugin/commands/dashboard.md +1 -0
- package/plugin/commands/learn.md +1 -0
- package/plugin/commands/restart.md +1 -0
- package/plugin/commands/show.md +1 -0
- package/plugin/dashboard/app/configure/env/page.tsx +40 -14
- package/plugin/dashboard/app/configure/server/page.tsx +51 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
- package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
- package/plugin/dashboard/lib/reflexio-client.ts +16 -0
- package/plugin/dashboard/lib/status.ts +10 -3
- package/plugin/dashboard/lib/types.ts +18 -6
- package/plugin/hooks/codex-hooks.json +0 -1
- package/plugin/opencode/assistant-buffer.ts +108 -0
- package/plugin/opencode/dist/assistant-buffer.js +96 -0
- package/plugin/opencode/dist/internal.js +12 -0
- package/plugin/opencode/dist/payload.js +85 -0
- package/plugin/opencode/dist/server.mjs +158 -0
- package/plugin/opencode/internal.ts +18 -0
- package/plugin/opencode/package.json +3 -0
- package/plugin/opencode/payload.ts +90 -0
- package/plugin/opencode/server.mts +174 -0
- package/plugin/opencode/tsconfig.json +13 -0
- package/plugin/pyproject.toml +20 -3
- package/plugin/scripts/_lib.sh +77 -29
- package/plugin/scripts/backend-service.sh +27 -5
- package/plugin/scripts/dashboard-service.sh +7 -5
- package/plugin/scripts/ensure-plugin-root.sh +4 -4
- package/plugin/scripts/hook_entry.sh +3 -3
- package/plugin/scripts/opencode-claude-compat +4 -0
- package/plugin/scripts/opencode-claude-compat.cmd +3 -0
- package/plugin/scripts/opencode-claude-compat.js +225 -0
- package/plugin/scripts/smart-install.sh +10 -8
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +304 -6
- package/plugin/src/claude_smart/env_config.py +12 -4
- package/plugin/src/claude_smart/events/session_start.py +26 -7
- package/plugin/src/claude_smart/events/stop.py +1 -1
- package/plugin/src/claude_smart/ids.py +1 -1
- package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
- package/plugin/src/claude_smart/runtime.py +7 -1
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +54 -30
- package/plugin/vendor/reflexio/README.md +14 -8
- package/plugin/vendor/reflexio/pyproject.toml +13 -1
- package/plugin/vendor/reflexio/reflexio/README.md +1 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
- package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
- package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
- package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
- package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
- package/scripts/setup-claude-smart.sh +8 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
- /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
- /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
|
|
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[
|
|
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
|
|
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=
|
|
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
|
-
#
|
|
615
|
-
#
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
#
|
|
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
|
-
{
|
|
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
|
|
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
|
|
644
|
-
args_json = tc
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
119
|
-
|
|
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(
|
|
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()
|