alysis-code 0.13.0__py3-none-any.whl
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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class DirectionChange:
|
|
10
|
+
old_terms: tuple[str, ...]
|
|
11
|
+
new_terms: tuple[str, ...]
|
|
12
|
+
reason: str
|
|
13
|
+
raw_text: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_TERM_TOKEN_RE = re.compile(r"[a-z0-9_]+")
|
|
17
|
+
_CLAUSE_END_RE = r"(?=[;,\n]|(?:\.\s)|\.$|$)"
|
|
18
|
+
_DIRECTION_BOUNDARY_RE = (
|
|
19
|
+
r"(?=(?:\s+entirely|\s+from\b|\s+and\s+use\b|\s+use\b|\s+any\s+more|"
|
|
20
|
+
r"\s+anymore|\s+instead\b|[;,\n]|(?:\.\s)|\.$|$))"
|
|
21
|
+
)
|
|
22
|
+
_STOPWORDS = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"a",
|
|
25
|
+
"an",
|
|
26
|
+
"and",
|
|
27
|
+
"any",
|
|
28
|
+
"anymore",
|
|
29
|
+
"approach",
|
|
30
|
+
"entirely",
|
|
31
|
+
"from",
|
|
32
|
+
"more",
|
|
33
|
+
"old",
|
|
34
|
+
"plan",
|
|
35
|
+
"previous",
|
|
36
|
+
"the",
|
|
37
|
+
"to",
|
|
38
|
+
"use",
|
|
39
|
+
"using",
|
|
40
|
+
"with",
|
|
41
|
+
"work",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
_GENERIC_MATCH_TOKENS = frozenset(
|
|
45
|
+
{
|
|
46
|
+
"config",
|
|
47
|
+
"configuration",
|
|
48
|
+
"file",
|
|
49
|
+
"flag",
|
|
50
|
+
"implementation",
|
|
51
|
+
"setting",
|
|
52
|
+
"settings",
|
|
53
|
+
"task",
|
|
54
|
+
"tasks",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_DIRECTION_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
59
|
+
(
|
|
60
|
+
"replace",
|
|
61
|
+
re.compile(
|
|
62
|
+
rf"\breplace\s+(?:the\s+)?(?P<old>.+?)\s+with\s+(?P<new>.+?){_CLAUSE_END_RE}",
|
|
63
|
+
re.IGNORECASE | re.DOTALL,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"switch",
|
|
68
|
+
re.compile(
|
|
69
|
+
rf"\bswitch\s+from\s+(?:the\s+)?(?P<old>.+?)\s+to\s+(?P<new>.+?){_CLAUSE_END_RE}",
|
|
70
|
+
re.IGNORECASE | re.DOTALL,
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"instead_of",
|
|
75
|
+
re.compile(
|
|
76
|
+
rf"\b(?:use|using)\s+(?P<new>.+?)\s+instead\s+of\s+(?:the\s+)?(?P<old>.+?){_CLAUSE_END_RE}",
|
|
77
|
+
re.IGNORECASE | re.DOTALL,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
"drop",
|
|
82
|
+
re.compile(
|
|
83
|
+
rf"\b(?:drop|remove|abandon|forget)\s+(?:the\s+)?(?P<old>.+?){_DIRECTION_BOUNDARY_RE}",
|
|
84
|
+
re.IGNORECASE | re.DOTALL,
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"do_not_implement",
|
|
89
|
+
re.compile(
|
|
90
|
+
rf"\bdo\s+not\s+(?:implement|use|build|add|create)\s+(?:the\s+)?(?P<old>.+?){_DIRECTION_BOUNDARY_RE}",
|
|
91
|
+
re.IGNORECASE | re.DOTALL,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"no_longer",
|
|
96
|
+
re.compile(
|
|
97
|
+
r"\bno\s+(?P<old>[a-z0-9_ .+\-/]{2,120}?)\s+(?:anymore|any\s+more)\b",
|
|
98
|
+
re.IGNORECASE,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
_USE_INSTEAD_RE = re.compile(
|
|
103
|
+
r"\b(?:use|using)\s+(?P<new>.+?)\s+instead\b",
|
|
104
|
+
re.IGNORECASE | re.DOTALL,
|
|
105
|
+
)
|
|
106
|
+
_NEGATED_DROP_PREFIX_RE = re.compile(
|
|
107
|
+
r"(?:^|[\s.;:,])(?:do\s+not|don't|dont|never|must\s+not|should\s+not|without|not)\s+$",
|
|
108
|
+
re.IGNORECASE,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _dedupe_keep_order(values: list[str]) -> tuple[str, ...]:
|
|
113
|
+
seen: set[str] = set()
|
|
114
|
+
out: list[str] = []
|
|
115
|
+
for value in values:
|
|
116
|
+
text = value.strip()
|
|
117
|
+
key = text.casefold()
|
|
118
|
+
if not text or key in seen:
|
|
119
|
+
continue
|
|
120
|
+
seen.add(key)
|
|
121
|
+
out.append(text)
|
|
122
|
+
return tuple(out)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _clean_term(value: str) -> str:
|
|
126
|
+
text = re.sub(r"\s+", " ", str(value or "").replace("`", " ").replace('"', " ")).strip()
|
|
127
|
+
text = re.sub(r"^(?:the|a|an)\s+", "", text, flags=re.IGNORECASE)
|
|
128
|
+
text = re.sub(
|
|
129
|
+
r"\s+(?:from\s+the\s+plan|from\s+plan|any\s+more|anymore|entirely|instead)$",
|
|
130
|
+
"",
|
|
131
|
+
text,
|
|
132
|
+
flags=re.IGNORECASE,
|
|
133
|
+
).strip()
|
|
134
|
+
return text
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _tokens(value: str) -> tuple[str, ...]:
|
|
138
|
+
normalized = str(value or "").casefold().replace("-", " ").replace("/", " ")
|
|
139
|
+
tokens: list[str] = []
|
|
140
|
+
for token in _TERM_TOKEN_RE.findall(normalized):
|
|
141
|
+
if token in _STOPWORDS:
|
|
142
|
+
continue
|
|
143
|
+
tokens.append(token)
|
|
144
|
+
if token == "env":
|
|
145
|
+
tokens.append("environment")
|
|
146
|
+
elif token == "environment":
|
|
147
|
+
tokens.append("env")
|
|
148
|
+
elif token == "var":
|
|
149
|
+
tokens.append("variable")
|
|
150
|
+
elif token == "variable":
|
|
151
|
+
tokens.append("var")
|
|
152
|
+
return _dedupe_keep_order(tokens)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _term_groups(terms: tuple[str, ...]) -> tuple[tuple[str, ...], ...]:
|
|
156
|
+
groups: list[tuple[str, ...]] = []
|
|
157
|
+
for term in terms:
|
|
158
|
+
tokens = _tokens(term)
|
|
159
|
+
if tokens:
|
|
160
|
+
groups.append(tokens)
|
|
161
|
+
return tuple(groups)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _group_matches(tokens: set[str], group: tuple[str, ...]) -> bool:
|
|
165
|
+
if not group:
|
|
166
|
+
return False
|
|
167
|
+
rare_tokens = [token for token in group if token not in _GENERIC_MATCH_TOKENS]
|
|
168
|
+
if rare_tokens:
|
|
169
|
+
return any(token in tokens for token in rare_tokens)
|
|
170
|
+
return all(token in tokens for token in group)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _text_tokens(text: str) -> set[str]:
|
|
174
|
+
return set(_tokens(text))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_new_terms(text: str) -> tuple[str, ...]:
|
|
178
|
+
found: list[str] = []
|
|
179
|
+
for match in _USE_INSTEAD_RE.finditer(text or ""):
|
|
180
|
+
cleaned = _clean_term(match.group("new"))
|
|
181
|
+
if cleaned:
|
|
182
|
+
found.append(cleaned)
|
|
183
|
+
return _dedupe_keep_order(found)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def detect_direction_change(text: str) -> DirectionChange | None:
|
|
187
|
+
raw_text = str(text or "").strip()
|
|
188
|
+
if not raw_text:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
old_terms: list[str] = []
|
|
192
|
+
new_terms: list[str] = []
|
|
193
|
+
reasons: list[str] = []
|
|
194
|
+
for reason, pattern in _DIRECTION_PATTERNS:
|
|
195
|
+
for match in pattern.finditer(raw_text):
|
|
196
|
+
if reason == "drop" and _match_has_negated_drop_prefix(raw_text, match.start()):
|
|
197
|
+
continue
|
|
198
|
+
old = _clean_term(match.groupdict().get("old", ""))
|
|
199
|
+
if not old:
|
|
200
|
+
continue
|
|
201
|
+
old_terms.append(old)
|
|
202
|
+
reasons.append(reason)
|
|
203
|
+
new = _clean_term(match.groupdict().get("new", ""))
|
|
204
|
+
if new:
|
|
205
|
+
new_terms.append(new)
|
|
206
|
+
|
|
207
|
+
new_terms.extend(_extract_new_terms(raw_text))
|
|
208
|
+
deduped_old = _dedupe_keep_order(old_terms)
|
|
209
|
+
if not deduped_old:
|
|
210
|
+
return None
|
|
211
|
+
return DirectionChange(
|
|
212
|
+
old_terms=deduped_old,
|
|
213
|
+
new_terms=_dedupe_keep_order(new_terms),
|
|
214
|
+
reason=", ".join(_dedupe_keep_order(reasons)) or "direction_change",
|
|
215
|
+
raw_text=raw_text,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _match_has_negated_drop_prefix(text: str, start: int) -> bool:
|
|
220
|
+
prefix = str(text or "")[: max(0, start)]
|
|
221
|
+
return _NEGATED_DROP_PREFIX_RE.search(prefix[-40:]) is not None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def direction_change_to_record(change: DirectionChange) -> dict[str, Any]:
|
|
225
|
+
return {
|
|
226
|
+
"type": "direction_change",
|
|
227
|
+
"reason": change.reason,
|
|
228
|
+
"old_terms": list(change.old_terms),
|
|
229
|
+
"new_terms": list(change.new_terms),
|
|
230
|
+
"user_text": change.raw_text,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def direction_change_from_record(record: Any) -> DirectionChange | None:
|
|
235
|
+
if not isinstance(record, dict):
|
|
236
|
+
return None
|
|
237
|
+
old_terms = _dedupe_keep_order([str(item) for item in record.get("old_terms") or []])
|
|
238
|
+
if not old_terms:
|
|
239
|
+
return None
|
|
240
|
+
new_terms = _dedupe_keep_order([str(item) for item in record.get("new_terms") or []])
|
|
241
|
+
return DirectionChange(
|
|
242
|
+
old_terms=old_terms,
|
|
243
|
+
new_terms=new_terms,
|
|
244
|
+
reason=str(record.get("reason") or "direction_change").strip() or "direction_change",
|
|
245
|
+
raw_text=str(record.get("user_text") or "").strip(),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def text_matches_obsolete_direction(text: str, change: DirectionChange) -> bool:
|
|
250
|
+
tokens = _text_tokens(text)
|
|
251
|
+
if not tokens:
|
|
252
|
+
return False
|
|
253
|
+
old_match = any(_group_matches(tokens, group) for group in _term_groups(change.old_terms))
|
|
254
|
+
if not old_match:
|
|
255
|
+
return False
|
|
256
|
+
new_match = any(_group_matches(tokens, group) for group in _term_groups(change.new_terms))
|
|
257
|
+
return not new_match
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _direction_changes_for_path_filter(
|
|
261
|
+
*,
|
|
262
|
+
latest_user_text: str = "",
|
|
263
|
+
task_text: str = "",
|
|
264
|
+
) -> tuple[DirectionChange, ...]:
|
|
265
|
+
changes: list[DirectionChange] = []
|
|
266
|
+
for text in (latest_user_text, task_text):
|
|
267
|
+
change = detect_direction_change(text)
|
|
268
|
+
if change is not None and change not in changes:
|
|
269
|
+
changes.append(change)
|
|
270
|
+
return tuple(changes)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def filter_obsolete_direction_paths(
|
|
274
|
+
paths: list[str],
|
|
275
|
+
*,
|
|
276
|
+
latest_user_text: str = "",
|
|
277
|
+
task_text: str = "",
|
|
278
|
+
) -> tuple[list[str], list[str]]:
|
|
279
|
+
changes = _direction_changes_for_path_filter(
|
|
280
|
+
latest_user_text=latest_user_text,
|
|
281
|
+
task_text=task_text,
|
|
282
|
+
)
|
|
283
|
+
if not changes:
|
|
284
|
+
return paths, []
|
|
285
|
+
|
|
286
|
+
kept: list[str] = []
|
|
287
|
+
dropped: list[str] = []
|
|
288
|
+
for path in paths:
|
|
289
|
+
if any(text_matches_obsolete_direction(path, change) for change in changes):
|
|
290
|
+
dropped.append(path)
|
|
291
|
+
continue
|
|
292
|
+
kept.append(path)
|
|
293
|
+
return kept, dropped
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Dispatch timing policy: cancellable waits, promotion thresholds, overhead.
|
|
2
|
+
|
|
3
|
+
This module is deliberately dependency-light -- standard library only, and no
|
|
4
|
+
imports from the rest of the package -- for the same two reasons as
|
|
5
|
+
``budget_policy``:
|
|
6
|
+
|
|
7
|
+
* it is loaded by ``agent/tools_assembly.py`` on the shell dispatch hot path
|
|
8
|
+
and must not drag anything in;
|
|
9
|
+
* it holds the decision logic that has to be unit-testable in a bare
|
|
10
|
+
interpreter, without executing ``alysis_code/__init__`` (which pulls
|
|
11
|
+
in httpx/pydantic/rich). ``tests/test_dispatch_timing.py`` loads this file
|
|
12
|
+
directly by path.
|
|
13
|
+
|
|
14
|
+
What went wrong in production
|
|
15
|
+
----------------------------
|
|
16
|
+
Terminal-Bench telemetry showed ``tool_dispatch`` averaging 30.4s over 72
|
|
17
|
+
dispatches on ``compile-compcert`` -- 2,190s of a 3,484s run -- with the
|
|
18
|
+
maximum pinned at 60.08s. That bucket is not generic dispatch overhead:
|
|
19
|
+
``_deadline_operation_for_tool_name`` special-cases only ``shell_run`` and
|
|
20
|
+
``shell_background``, so **``shell_wait`` falls through into
|
|
21
|
+
``tool_dispatch``**. The 60.08s ceiling is ``shell_wait``'s own cap
|
|
22
|
+
(``min(wait_seconds, 60.0)``, schema ``maximum: 60``).
|
|
23
|
+
|
|
24
|
+
Two defects follow from that, and this module addresses both:
|
|
25
|
+
|
|
26
|
+
1. **A wait in flight could not be preempted.** The terminal manager waits on a
|
|
27
|
+
``threading.Condition`` that knows nothing about the run budget, so PR2's
|
|
28
|
+
watchdog -- which sets a ``threading.Event`` -- could not interrupt a wait
|
|
29
|
+
already running. A 60s wait armed just before the deadline outlived it.
|
|
30
|
+
:func:`run_cancellable_wait` fixes this by driving the *existing*
|
|
31
|
+
completion-driven wait in short slices and re-reading the cancellation token
|
|
32
|
+
between them, so a budget stop is observed within one slice instead of at
|
|
33
|
+
the end of the full wait. The wait stays completion-driven: each slice
|
|
34
|
+
returns the instant the process emits output or exits, so nothing is made
|
|
35
|
+
slower, and no busy-spinning is introduced.
|
|
36
|
+
|
|
37
|
+
2. **Dispatch overhead was unmeasurable.** ``shell_tool`` timed the whole
|
|
38
|
+
dispatch, including the workspace walks that mutation detection performs
|
|
39
|
+
before and after every command, so the cost of the machinery could not be
|
|
40
|
+
separated from the cost of the command.
|
|
41
|
+
:class:`DispatchOverheadAccount` does that subtraction, and the result is
|
|
42
|
+
reported under :data:`DISPATCH_OVERHEAD_OPERATION` as its own
|
|
43
|
+
``duration_observations`` category.
|
|
44
|
+
|
|
45
|
+
Environment lookups go through :data:`os.environ` rather than
|
|
46
|
+
``branding.env_get`` to keep this module free of package imports; ``env_get``
|
|
47
|
+
is itself just ``os.environ.get``, so the behaviour is identical.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import math
|
|
53
|
+
import os
|
|
54
|
+
import time
|
|
55
|
+
from collections.abc import Callable, Mapping
|
|
56
|
+
from dataclasses import dataclass
|
|
57
|
+
|
|
58
|
+
# How long a foreground command may hold a blocking dispatch slot before it is
|
|
59
|
+
# considered "long running". Small on purpose: the point of the threshold is
|
|
60
|
+
# that a build or install stops occupying the agent loop almost immediately.
|
|
61
|
+
DEFAULT_BACKGROUND_PROMOTION_SECONDS = 10.0
|
|
62
|
+
|
|
63
|
+
# Granularity at which a long wait re-reads the cancellation token. Each slice
|
|
64
|
+
# is a real completion-driven wait, not a sleep: it returns early the moment
|
|
65
|
+
# output arrives or the process exits, so the slice size bounds *cancellation
|
|
66
|
+
# latency* only, never how quickly a finished command is noticed.
|
|
67
|
+
DEFAULT_WAIT_SLICE_SECONDS = 0.5
|
|
68
|
+
|
|
69
|
+
BACKGROUND_PROMOTION_SECONDS_ENV = "ALYSIS_SHELL_BACKGROUND_PROMOTION_SECONDS"
|
|
70
|
+
WAIT_SLICE_SECONDS_ENV = "ALYSIS_SHELL_WAIT_SLICE_SECONDS"
|
|
71
|
+
|
|
72
|
+
# ``duration_observations`` category for time spent inside the dispatch
|
|
73
|
+
# machinery, excluding the runtime of the command being dispatched. Kept as one
|
|
74
|
+
# constant so the telemetry key cannot drift from the allowlist in
|
|
75
|
+
# ``crash_diagnostics``.
|
|
76
|
+
DISPATCH_OVERHEAD_OPERATION = "dispatch_overhead_seconds"
|
|
77
|
+
|
|
78
|
+
WAIT_OUTCOME_COMPLETED = "completed"
|
|
79
|
+
WAIT_OUTCOME_TIMED_OUT = "timed_out"
|
|
80
|
+
WAIT_OUTCOME_CANCELLED = "cancelled"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _env_value(name: str, environ: Mapping[str, str] | None = None) -> str | None:
|
|
84
|
+
source: Mapping[str, str] = os.environ if environ is None else environ
|
|
85
|
+
raw = source.get(name)
|
|
86
|
+
if raw is None and name.startswith("ALYSIS_"):
|
|
87
|
+
raw = source.get("SYLLIPTOR_" + name.removeprefix("ALYSIS_"))
|
|
88
|
+
if raw is None:
|
|
89
|
+
return None
|
|
90
|
+
cleaned = str(raw).strip()
|
|
91
|
+
return cleaned or None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _positive_float(raw: str | None, fallback: float) -> float:
|
|
95
|
+
"""Parse a strictly positive finite float, falling back on anything else.
|
|
96
|
+
|
|
97
|
+
A misconfigured knob must never be able to disable the mechanism it
|
|
98
|
+
configures: every bad value -- unparseable, negative, zero, NaN, inf --
|
|
99
|
+
resolves to the documented default.
|
|
100
|
+
"""
|
|
101
|
+
if raw is None:
|
|
102
|
+
return fallback
|
|
103
|
+
try:
|
|
104
|
+
value = float(raw)
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
return fallback
|
|
107
|
+
if not math.isfinite(value) or value <= 0.0:
|
|
108
|
+
return fallback
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _finite_or(value: object, fallback: float) -> float:
|
|
113
|
+
"""Coerce ``value`` to a finite float, else ``fallback``."""
|
|
114
|
+
try:
|
|
115
|
+
number = float(value) # type: ignore[arg-type]
|
|
116
|
+
except (TypeError, ValueError):
|
|
117
|
+
return fallback
|
|
118
|
+
if not math.isfinite(number):
|
|
119
|
+
return fallback
|
|
120
|
+
return number
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def resolve_background_promotion_seconds(environ: Mapping[str, str] | None = None) -> float:
|
|
124
|
+
"""Seconds a foreground command may block dispatch before it counts as long.
|
|
125
|
+
|
|
126
|
+
``ALYSIS_SHELL_BACKGROUND_PROMOTION_SECONDS`` overrides
|
|
127
|
+
:data:`DEFAULT_BACKGROUND_PROMOTION_SECONDS`.
|
|
128
|
+
"""
|
|
129
|
+
return _positive_float(
|
|
130
|
+
_env_value(BACKGROUND_PROMOTION_SECONDS_ENV, environ),
|
|
131
|
+
DEFAULT_BACKGROUND_PROMOTION_SECONDS,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolve_wait_slice_seconds(environ: Mapping[str, str] | None = None) -> float:
|
|
136
|
+
"""Cancellation-check granularity for a long wait, in seconds.
|
|
137
|
+
|
|
138
|
+
``ALYSIS_SHELL_WAIT_SLICE_SECONDS`` overrides
|
|
139
|
+
:data:`DEFAULT_WAIT_SLICE_SECONDS`.
|
|
140
|
+
"""
|
|
141
|
+
return _positive_float(
|
|
142
|
+
_env_value(WAIT_SLICE_SECONDS_ENV, environ),
|
|
143
|
+
DEFAULT_WAIT_SLICE_SECONDS,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def should_promote_to_background(
|
|
148
|
+
elapsed_seconds: float,
|
|
149
|
+
threshold_seconds: float | None = None,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""True when a command has blocked dispatch for at least the threshold.
|
|
152
|
+
|
|
153
|
+
Boundary is inclusive: a command that has run *exactly* the threshold has
|
|
154
|
+
already held the slot for as long as the policy allows.
|
|
155
|
+
"""
|
|
156
|
+
threshold = (
|
|
157
|
+
resolve_background_promotion_seconds()
|
|
158
|
+
if threshold_seconds is None
|
|
159
|
+
else _finite_or(threshold_seconds, DEFAULT_BACKGROUND_PROMOTION_SECONDS)
|
|
160
|
+
)
|
|
161
|
+
elapsed = _finite_or(elapsed_seconds, 0.0)
|
|
162
|
+
if elapsed < 0.0:
|
|
163
|
+
return False
|
|
164
|
+
return elapsed >= max(0.0, threshold)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def clamp_wait_seconds(
|
|
168
|
+
requested_seconds: float,
|
|
169
|
+
*,
|
|
170
|
+
remaining_seconds: float | None = None,
|
|
171
|
+
reserve_seconds: float = 0.0,
|
|
172
|
+
minimum_seconds: float = 0.0,
|
|
173
|
+
) -> float:
|
|
174
|
+
"""Clamp a requested wait to what the deadline can still afford.
|
|
175
|
+
|
|
176
|
+
``remaining_seconds`` of ``None`` means "no deadline applies", in which case
|
|
177
|
+
the request stands. Otherwise the wait is capped at the remaining time less
|
|
178
|
+
``reserve_seconds`` (the cleanup reserve), floored at ``minimum_seconds`` so
|
|
179
|
+
a caller can always make one non-blocking probe rather than being told to
|
|
180
|
+
wait for a negative duration.
|
|
181
|
+
"""
|
|
182
|
+
requested = max(0.0, _finite_or(requested_seconds, 0.0))
|
|
183
|
+
floor = max(0.0, _finite_or(minimum_seconds, 0.0))
|
|
184
|
+
if remaining_seconds is None:
|
|
185
|
+
return max(requested, floor)
|
|
186
|
+
remaining = _finite_or(remaining_seconds, 0.0)
|
|
187
|
+
reserve = max(0.0, _finite_or(reserve_seconds, 0.0))
|
|
188
|
+
affordable = remaining - reserve
|
|
189
|
+
if affordable <= 0.0:
|
|
190
|
+
return floor
|
|
191
|
+
return max(floor, min(requested, affordable))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def next_wait_slice(remaining_seconds: float, slice_seconds: float | None = None) -> float:
|
|
195
|
+
"""Duration of the next wait slice.
|
|
196
|
+
|
|
197
|
+
Slicing exists solely to bound cancellation latency, so a non-positive
|
|
198
|
+
slice size means "do not slice" and the whole remaining wait is taken in
|
|
199
|
+
one step -- which is exactly the pre-fix behaviour.
|
|
200
|
+
"""
|
|
201
|
+
remaining = max(0.0, _finite_or(remaining_seconds, 0.0))
|
|
202
|
+
if slice_seconds is None:
|
|
203
|
+
step = resolve_wait_slice_seconds()
|
|
204
|
+
else:
|
|
205
|
+
step = _finite_or(slice_seconds, 0.0)
|
|
206
|
+
if step <= 0.0:
|
|
207
|
+
return remaining
|
|
208
|
+
return min(remaining, step)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True)
|
|
212
|
+
class WaitResult:
|
|
213
|
+
"""Outcome of a sliced, cancellable wait."""
|
|
214
|
+
|
|
215
|
+
outcome: str
|
|
216
|
+
elapsed_seconds: float
|
|
217
|
+
slices: int
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def completed(self) -> bool:
|
|
221
|
+
return self.outcome == WAIT_OUTCOME_COMPLETED
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def cancelled(self) -> bool:
|
|
225
|
+
return self.outcome == WAIT_OUTCOME_CANCELLED
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def timed_out(self) -> bool:
|
|
229
|
+
return self.outcome == WAIT_OUTCOME_TIMED_OUT
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_cancellable_wait(
|
|
233
|
+
*,
|
|
234
|
+
wait_once: Callable[[float], bool],
|
|
235
|
+
total_seconds: float,
|
|
236
|
+
is_cancelled: Callable[[], bool] | None = None,
|
|
237
|
+
slice_seconds: float | None = None,
|
|
238
|
+
monotonic: Callable[[], float] = time.monotonic,
|
|
239
|
+
) -> WaitResult:
|
|
240
|
+
"""Wait up to ``total_seconds``, re-reading cancellation between slices.
|
|
241
|
+
|
|
242
|
+
``wait_once`` performs one real completion-driven wait of the given
|
|
243
|
+
duration and returns ``True`` when the condition it waits on is satisfied.
|
|
244
|
+
Because each slice returns early on completion, slicing costs nothing in
|
|
245
|
+
responsiveness to the process; it only bounds how long a cancelled run
|
|
246
|
+
stays parked here.
|
|
247
|
+
|
|
248
|
+
``wait_once`` is always invoked at least once, even for a zero-length wait,
|
|
249
|
+
so a ``wait_seconds=0`` probe keeps its existing "read what is there right
|
|
250
|
+
now" semantics.
|
|
251
|
+
"""
|
|
252
|
+
started = monotonic()
|
|
253
|
+
total = max(0.0, _finite_or(total_seconds, 0.0))
|
|
254
|
+
slices = 0
|
|
255
|
+
# Tracked against the original total rather than recomputed from a deadline,
|
|
256
|
+
# so the first slice is exactly what the caller asked for. That matters for
|
|
257
|
+
# the unsliced path: it must hand the wait primitive the identical timeout
|
|
258
|
+
# it received today, not that value minus the microseconds spent getting
|
|
259
|
+
# here.
|
|
260
|
+
remaining = total
|
|
261
|
+
|
|
262
|
+
def _elapsed() -> float:
|
|
263
|
+
return max(0.0, monotonic() - started)
|
|
264
|
+
|
|
265
|
+
# Cancelled before we ever blocked: report it without touching the process.
|
|
266
|
+
if is_cancelled is not None and is_cancelled():
|
|
267
|
+
return WaitResult(WAIT_OUTCOME_CANCELLED, _elapsed(), slices)
|
|
268
|
+
|
|
269
|
+
while True:
|
|
270
|
+
step = next_wait_slice(remaining, slice_seconds)
|
|
271
|
+
slices += 1
|
|
272
|
+
if wait_once(step):
|
|
273
|
+
return WaitResult(WAIT_OUTCOME_COMPLETED, _elapsed(), slices)
|
|
274
|
+
if is_cancelled is not None and is_cancelled():
|
|
275
|
+
return WaitResult(WAIT_OUTCOME_CANCELLED, _elapsed(), slices)
|
|
276
|
+
remaining = total - _elapsed()
|
|
277
|
+
if remaining <= 0.0:
|
|
278
|
+
return WaitResult(WAIT_OUTCOME_TIMED_OUT, _elapsed(), slices)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True)
|
|
282
|
+
class DispatchOverheadAccount:
|
|
283
|
+
"""Split a dispatch into the command's own runtime and the machinery around it.
|
|
284
|
+
|
|
285
|
+
``overhead_seconds`` is what the next campaign should watch: it excludes the
|
|
286
|
+
command, so it stays flat when a build legitimately takes twenty minutes and
|
|
287
|
+
rises only when the dispatch path itself is doing expensive work (the
|
|
288
|
+
pre/post workspace walks for mutation detection, evidence classification).
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
total_seconds: float
|
|
292
|
+
command_seconds: float
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def overhead_seconds(self) -> float:
|
|
296
|
+
# Clamped at zero: a command clock read slightly after the dispatch
|
|
297
|
+
# clock must never report negative overhead.
|
|
298
|
+
return max(0.0, self.total_seconds - self.command_seconds)
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def from_totals(cls, total_seconds: float, command_seconds: float) -> DispatchOverheadAccount:
|
|
302
|
+
total = max(0.0, _finite_or(total_seconds, 0.0))
|
|
303
|
+
command = max(0.0, _finite_or(command_seconds, 0.0))
|
|
304
|
+
# A command cannot have run longer than the dispatch that contains it;
|
|
305
|
+
# if the clocks disagree, trust the enclosing measurement.
|
|
306
|
+
return cls(total_seconds=total, command_seconds=min(command, total))
|