cli-agent-runner 0.2.9__tar.gz → 0.2.11__tar.gz
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.
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.vulture-whitelist.py +3 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/CHANGELOG.md +30 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_emit.py +7 -1
- cli_agent_runner-0.2.11/agent_runner/_throttle.py +372 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/agent_runtime.py +17 -13
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/api.py +23 -8
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/api_types.py +5 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/claude_rate_limit.py +6 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/codewhale.py +6 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/gemini.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/kimi.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/pi.py +3 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/common.py +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/events_cmd.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/peek_cmd.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/round_cmd.py +7 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/serve_cmd.py +250 -54
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/service_cmd.py +3 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/upgrade_cmd.py +10 -10
- cli_agent_runner-0.2.11/agent_runner/clock.py +79 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/config.py +9 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/defenses.py +3 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/events.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/http_progress.py +1 -0
- cli_agent_runner-0.2.11/agent_runner/lifecycle.py +101 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/metrics.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/monitor.py +6 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/phase_select.py +30 -12
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/remote_relay.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/runner.py +16 -64
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/schedule.py +5 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/service_unit.py +9 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/vcs_state.py +4 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/configuration.md +10 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/migrations/0.2.md +49 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/plugins.md +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/runbook.md +33 -7
- cli_agent_runner-0.2.11/tests/_clock.py +48 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_fresh_eyes_signal.py +3 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_substrate_fingerprint.py +4 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_transient_error_backoff.py +3 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_architecture.py +16 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_atomic_write_enforced.py +3 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_test_pointers_exist.py +14 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_module_boundaries.py +48 -37
- cli_agent_runner-0.2.11/tests/invariants/test_no_raw_time.py +136 -0
- cli_agent_runner-0.2.11/tests/invariants/test_no_vacuous_invariants.py +41 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_peek_schema_version.py +7 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_phase_select_stateless.py +11 -8
- cli_agent_runner-0.2.11/tests/invariants/test_stash_uses_sha_not_index.py +54 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_grace.py +33 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_observation.py +55 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_claude_error_detector.py +6 -6
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_events.py +3 -5
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_events_cmd_tail.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_exit_cause.py +10 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_gemini_plugin.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_kimi_plugin.py +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_lifecycle.py +23 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_metrics.py +2 -7
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detectors.py +19 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_phase_select.py +47 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_pi_plugin.py +1 -1
- cli_agent_runner-0.2.11/tests/unit/test_round_cmd.py +52 -0
- cli_agent_runner-0.2.11/tests/unit/test_runner_throttle.py +618 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_config_broken.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_crash_loop.py +7 -2
- cli_agent_runner-0.2.11/tests/unit/test_serve_phase_schedule.py +553 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_service_unit.py +11 -1
- cli_agent_runner-0.2.9/agent_runner/_throttle.py +0 -142
- cli_agent_runner-0.2.9/agent_runner/lifecycle.py +0 -67
- cli_agent_runner-0.2.9/tests/invariants/test_stash_uses_sha_not_index.py +0 -21
- cli_agent_runner-0.2.9/tests/unit/test_runner_throttle.py +0 -339
- cli_agent_runner-0.2.9/tests/unit/test_serve_phase_schedule.py +0 -193
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/.gitignore +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/LICENSE +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/README.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/README.zh.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/build.sh +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/README.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/events.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.11}/tests/unit/test_vcs_state.py +0 -0
|
@@ -57,6 +57,8 @@ throttled_until_epoch
|
|
|
57
57
|
limit_type
|
|
58
58
|
agent
|
|
59
59
|
since_round
|
|
60
|
+
phase
|
|
61
|
+
throttled_agents
|
|
60
62
|
|
|
61
63
|
# agent_runner.api_types.RoundResult
|
|
62
64
|
round_num
|
|
@@ -106,6 +108,7 @@ reset_at_epoch
|
|
|
106
108
|
classification
|
|
107
109
|
agent
|
|
108
110
|
since_round
|
|
111
|
+
phase
|
|
109
112
|
|
|
110
113
|
# agent_runner.config.AgentConfig
|
|
111
114
|
command
|
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.11] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- systemd `serve` unit now uses `Restart=on-failure` with `RestartPreventExitStatus=78 75`: a deliberate stop (sentinel / stop-file / max-rounds / SIGTERM) and a config give-up stay stopped, while a genuine crash still restarts. Re-render or hand-edit existing units.
|
|
12
|
+
- Throttle-aware skip is keyed on the **agent** (binary basename of `command[0]`), not the phase: every phase sharing a rate-limited agent is stepped over together, so a multi-provider rotation no longer hammers one throttled key while a healthy provider keeps running.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `round` exits non-zero on a genuine crash, so the crash-loop breaker (`crash_loop`, exit 75) sees crashes instead of a masked exit 0; a permanent config failure gives up with exit 78 (`config_broken`) instead of restart-looping.
|
|
16
|
+
- Round deadlines and sleeps are measured on the monotonic clock — an NTP step no longer mis-fires or defers a hard-wall timeout.
|
|
17
|
+
- Back-off and restart sleeps are chunked, so a SIGTERM lands within one chunk instead of after a multi-hour throttle window; an interrupted back-off leaves the throttle active (no false recovered).
|
|
18
|
+
- A grace-killed round still dispatches its dirty work and is no longer miscounted as a timeout.
|
|
19
|
+
- Only one `serve` loop runs per project (loop-lifetime lock); `serve.pid` records a start-time token so `stop`/`kill` never signals a recycled PID.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
- `peek --json` schema `1.10`: `rate_limit.throttled_agents` lists all currently-throttled agents. Additive; no config change.
|
|
23
|
+
|
|
24
|
+
See `docs/migrations/0.2.md`.
|
|
25
|
+
|
|
26
|
+
## [0.2.10] - 2026-08-29
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Throttle-aware skip: under `[phases] phase_policy = "skip"`, a phase whose provider is currently rate-limited/erroring is stepped over in the rotation — a healthy sibling runs instead of the whole loop sleeping on the throttle. No new config; the trigger is the existing `skip` policy.
|
|
30
|
+
- `transient_error_detected` now carries the failing `phase`; `peek --json` `rate_limit` and the HTTP progress state surface which phase is throttled.
|
|
31
|
+
- `transient_error_recovered` is emitted when a skip-around throttle clears (events-derived, restart-safe, no double-emit with the back-off path).
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
- Only `skip` routes around a throttle; `wait`, `--ignore-schedule`, and non-`[phases]` configs keep the 0.2.9 global back-off. When every phase is throttled or window-closed, the loop waits until the earlier of a window opening or the throttle resetting.
|
|
35
|
+
|
|
36
|
+
See `docs/migrations/0.2.md`.
|
|
37
|
+
|
|
8
38
|
## [0.2.9] - 2026-08-28
|
|
9
39
|
|
|
10
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -188,8 +188,13 @@ def emit_transient_error_detected(
|
|
|
188
188
|
reset_at_epoch: int,
|
|
189
189
|
round_num: int,
|
|
190
190
|
raw: str,
|
|
191
|
+
phase: str = "",
|
|
191
192
|
) -> None:
|
|
192
|
-
"""Emit detection of a transient agent error (rate limit / 5xx / timeout).
|
|
193
|
+
"""Emit detection of a transient agent error (rate limit / 5xx / timeout).
|
|
194
|
+
|
|
195
|
+
``phase`` is the rotation phase the failing round ran under ("" when the
|
|
196
|
+
config has no ``[phases]``). It lets a ``phase_policy = "skip"`` serve loop
|
|
197
|
+
step over *this* phase while a healthy sibling keeps running."""
|
|
193
198
|
from agent_runner._redact import redact_secrets
|
|
194
199
|
from agent_runner.events import TRANSIENT_ERROR_DETECTED, emit
|
|
195
200
|
|
|
@@ -202,6 +207,7 @@ def emit_transient_error_detected(
|
|
|
202
207
|
reset_at_epoch=reset_at_epoch,
|
|
203
208
|
round_num=round_num,
|
|
204
209
|
raw=raw,
|
|
210
|
+
phase=phase,
|
|
205
211
|
)
|
|
206
212
|
|
|
207
213
|
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Throttle state helpers — read events.jsonl tail for transient error state,
|
|
2
|
+
and own the supervisor-side back-off sleep.
|
|
3
|
+
|
|
4
|
+
Internal module. Callers: cli/serve_cmd.py (throttle scan + back-off + restart
|
|
5
|
+
delay), api.py (peek). The back-off lives here, NOT in runner.py, to satisfy the
|
|
6
|
+
ouroboros defense: runner.py writes events.jsonl but must never read it back
|
|
7
|
+
(§3 module boundary invariant), and back-off is driven by the events-derived
|
|
8
|
+
throttle state this module scans.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import random
|
|
15
|
+
from datetime import UTC
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from agent_runner.api_types import TransientErrorState
|
|
20
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
21
|
+
from agent_runner.events import (
|
|
22
|
+
TRANSIENT_ERROR_DETECTED,
|
|
23
|
+
TRANSIENT_ERROR_RECOVERED,
|
|
24
|
+
parse_iso_ms,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# _scan_events_for_transient sentinel: file held no transient event at all
|
|
28
|
+
# (distinct from "latest transient was a recovered" → None).
|
|
29
|
+
_NO_TRANSIENT = object()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _iter_events(path: Path):
|
|
33
|
+
"""Yield parsed event dicts from a JSONL file; skip blank / corrupt lines."""
|
|
34
|
+
with path.open() as f:
|
|
35
|
+
for line in f:
|
|
36
|
+
line = line.strip()
|
|
37
|
+
if not line:
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
yield json.loads(line)
|
|
41
|
+
except json.JSONDecodeError:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scan_events_for_transient(path: Path):
|
|
46
|
+
"""The file's LAST transient event: the detected dict, ``None`` (latest was a
|
|
47
|
+
recovered), or ``_NO_TRANSIENT`` (no transient event in the file).
|
|
48
|
+
|
|
49
|
+
Forward single-pass, O(1) memory — it keeps only the latest transient event,
|
|
50
|
+
never a 100-line tail. That matters for throttle-aware skip: the loop keeps
|
|
51
|
+
emitting rounds on healthy phases during a throttle, so the ``detected`` event
|
|
52
|
+
can be thousands of lines back; a bounded tail would scroll it out and make the
|
|
53
|
+
supervisor forget the throttle mid-window."""
|
|
54
|
+
latest: Any = _NO_TRANSIENT
|
|
55
|
+
for ev in _iter_events(path):
|
|
56
|
+
kind = ev.get("event")
|
|
57
|
+
if kind == TRANSIENT_ERROR_DETECTED:
|
|
58
|
+
latest = ev
|
|
59
|
+
elif kind == TRANSIENT_ERROR_RECOVERED:
|
|
60
|
+
latest = None
|
|
61
|
+
return latest
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _latest_unrecovered_detected(log_dir: Path) -> dict[str, Any] | None:
|
|
65
|
+
"""The most recent ``transient_error_detected`` with no
|
|
66
|
+
``transient_error_recovered`` after it, or None. Used by
|
|
67
|
+
:func:`_check_throttle_state` (still-throttled?) — the single-throttle "latest"
|
|
68
|
+
view kept for the non-skip (wait / back_off / stop) paths and peek.
|
|
69
|
+
|
|
70
|
+
Scans the newest monthly ``events-*.jsonl`` and, only if it holds no transient
|
|
71
|
+
event yet, the previous month's — so a throttle that spans a month boundary
|
|
72
|
+
(detected in the old file, cleared in the new) is not orphaned."""
|
|
73
|
+
candidates = sorted(log_dir.glob("events-*.jsonl"))
|
|
74
|
+
for path in reversed(candidates[-2:]):
|
|
75
|
+
result = _scan_events_for_transient(path)
|
|
76
|
+
if result is not _NO_TRANSIENT:
|
|
77
|
+
return result # a detected dict, or None (latest transient was recovered)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _latest_transient_per_agent(log_dir: Path) -> dict[str, Any]:
|
|
82
|
+
"""Per agent label, the last transient event across the newest TWO monthly files:
|
|
83
|
+
the ``detected`` dict, or ``None`` if the latest was a ``recovered``.
|
|
84
|
+
|
|
85
|
+
Unlike :func:`_latest_unrecovered_detected` (which early-exits at the first file
|
|
86
|
+
holding any transient), this always merges BOTH files per agent — a throttle for
|
|
87
|
+
agent A in the old file must not be masked by agent B's event in the newer one.
|
|
88
|
+
Iterates old-then-new so the newest event per agent wins."""
|
|
89
|
+
latest: dict[str, Any] = {}
|
|
90
|
+
for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
|
|
91
|
+
for ev in _iter_events(path):
|
|
92
|
+
kind = ev.get("event")
|
|
93
|
+
if kind == TRANSIENT_ERROR_DETECTED:
|
|
94
|
+
latest[str(ev.get("agent", "unknown"))] = ev
|
|
95
|
+
elif kind == TRANSIENT_ERROR_RECOVERED:
|
|
96
|
+
latest[str(ev.get("agent", "unknown"))] = None
|
|
97
|
+
return latest
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _active_throttles(
|
|
101
|
+
log_dir: Path, *, clock: Clock = SYSTEM_CLOCK
|
|
102
|
+
) -> dict[str, TransientErrorState]:
|
|
103
|
+
"""Currently-active throttles keyed by AGENT label (binary basename), one entry
|
|
104
|
+
per agent whose latest transient event is an unrecovered ``detected`` with its
|
|
105
|
+
reset still in the future.
|
|
106
|
+
|
|
107
|
+
Rate limits are per-provider (per API key / agent binary), not per-phase — so the
|
|
108
|
+
serve skip-policy routes around EVERY phase sharing a throttled agent while a
|
|
109
|
+
healthy agent's phase keeps running. Restart-safe (events-derived)."""
|
|
110
|
+
now = clock.epoch()
|
|
111
|
+
active: dict[str, TransientErrorState] = {}
|
|
112
|
+
for agent, detected in _latest_transient_per_agent(log_dir).items():
|
|
113
|
+
if detected is None:
|
|
114
|
+
continue
|
|
115
|
+
reset_at = int(detected.get("reset_at_epoch", 0))
|
|
116
|
+
if reset_at <= now:
|
|
117
|
+
continue
|
|
118
|
+
active[agent] = TransientErrorState(
|
|
119
|
+
reset_at_epoch=reset_at,
|
|
120
|
+
classification=str(detected.get("classification", "rate_limit_account")),
|
|
121
|
+
agent=agent,
|
|
122
|
+
since_round=int(detected.get("round_num", 0)),
|
|
123
|
+
phase=str(detected.get("phase", "")),
|
|
124
|
+
)
|
|
125
|
+
return active
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _check_throttle_state(
|
|
129
|
+
log_dir: Path, *, clock: Clock = SYSTEM_CLOCK
|
|
130
|
+
) -> TransientErrorState | None:
|
|
131
|
+
"""Scan events.jsonl tail for latest unmatched transient error.
|
|
132
|
+
|
|
133
|
+
Reads `transient_error_detected` / `transient_error_recovered` event names.
|
|
134
|
+
Returns TransientErrorState if currently throttled (reset still in future,
|
|
135
|
+
no matching recovered after). Restart-safe.
|
|
136
|
+
|
|
137
|
+
``clock`` supplies the wall clock the reset is compared against — inject a
|
|
138
|
+
``FakeClock`` in tests to pin an exact instant. Only the reset comparison
|
|
139
|
+
reads it; the event scan is pure.
|
|
140
|
+
"""
|
|
141
|
+
now = clock.epoch()
|
|
142
|
+
latest_detected = _latest_unrecovered_detected(log_dir)
|
|
143
|
+
if latest_detected is None:
|
|
144
|
+
return None
|
|
145
|
+
reset_at = int(latest_detected.get("reset_at_epoch", 0))
|
|
146
|
+
if reset_at <= now:
|
|
147
|
+
return None # Reset already passed without recovery emit; treat as recovered
|
|
148
|
+
|
|
149
|
+
classification = str(latest_detected.get("classification", "rate_limit_account"))
|
|
150
|
+
|
|
151
|
+
return TransientErrorState(
|
|
152
|
+
reset_at_epoch=reset_at,
|
|
153
|
+
classification=classification,
|
|
154
|
+
agent=str(latest_detected.get("agent", "unknown")),
|
|
155
|
+
since_round=int(latest_detected.get("round_num", 0)),
|
|
156
|
+
phase=str(latest_detected.get("phase", "")),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _throttled_for_s(ts: Any, *, clock: Clock = SYSTEM_CLOCK) -> int:
|
|
161
|
+
"""Seconds since a detected event's ``ts`` (best-effort; 0 if unparseable).
|
|
162
|
+
``clock`` supplies now; inject a ``FakeClock`` for exact-value tests."""
|
|
163
|
+
if not ts:
|
|
164
|
+
return 0
|
|
165
|
+
try:
|
|
166
|
+
detected = parse_iso_ms(str(ts)) # events.py owns the Z→+00:00 workaround
|
|
167
|
+
except (ValueError, TypeError):
|
|
168
|
+
return 0
|
|
169
|
+
if detected.tzinfo is None:
|
|
170
|
+
detected = detected.replace(tzinfo=UTC) # hand-injected naive ts → treat as UTC
|
|
171
|
+
return max(0, int(clock.epoch() - detected.timestamp()))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def pending_recovered(log_dir: Path, *, clock: Clock = SYSTEM_CLOCK) -> list[tuple[str, str, int]]:
|
|
175
|
+
"""One ``(agent, classification, throttled_for_s)`` per agent whose throttle
|
|
176
|
+
cleared WITHOUT a recovered breadcrumb — its latest transient event is a
|
|
177
|
+
``detected`` with ``reset_at <= now`` and NO ``transient_error_recovered`` after.
|
|
178
|
+
|
|
179
|
+
This is the throttle-aware-skip path: the loop rotated to a healthy phase instead
|
|
180
|
+
of calling ``_apply_back_off`` (which emits its own recovered), so a multi-hour
|
|
181
|
+
throttle would otherwise close with no breadcrumb. Events-derived → restart-safe
|
|
182
|
+
AND never double-emits: once the caller emits recovered, that event sits after the
|
|
183
|
+
detected one and the agent drops out. Per-agent, so an agent that cleared is
|
|
184
|
+
reported even while a sibling is still throttled (overlapping recovery); an empty
|
|
185
|
+
list means nothing cleared (all still throttled, or the back-off path already left
|
|
186
|
+
a recovered)."""
|
|
187
|
+
now = clock.epoch()
|
|
188
|
+
cleared: list[tuple[str, str, int]] = []
|
|
189
|
+
for agent, detected in _latest_transient_per_agent(log_dir).items():
|
|
190
|
+
if detected is None or int(detected.get("reset_at_epoch", 0)) > now:
|
|
191
|
+
continue # None = latest was a recovered; > now = still throttled
|
|
192
|
+
cleared.append(
|
|
193
|
+
(
|
|
194
|
+
agent,
|
|
195
|
+
str(detected.get("classification", "rate_limit_account")),
|
|
196
|
+
_throttled_for_s(detected.get("ts"), clock=clock),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return cleared
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Module-level supervisor state — bucket → consecutive-failure count.
|
|
203
|
+
# Cleared by reset_counters() or by serve restart.
|
|
204
|
+
_consecutive_failures: dict[str, int] = {}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def compute_adjusted_reset_at(
|
|
208
|
+
*,
|
|
209
|
+
classification: str,
|
|
210
|
+
original_reset_at_epoch: int,
|
|
211
|
+
agent: str,
|
|
212
|
+
log_dir: Path,
|
|
213
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
214
|
+
) -> tuple[int, int, bool]:
|
|
215
|
+
"""Apply exp backoff for estimated-class transient errors.
|
|
216
|
+
|
|
217
|
+
Returns (applied_reset_at_epoch, consecutive_count, capped_by_absolute_max).
|
|
218
|
+
|
|
219
|
+
For server-authoritative classification (``rate_limit_account``): returns
|
|
220
|
+
the original reset epoch verbatim, never increments the counter, and
|
|
221
|
+
never emits an adjustment event. Anthropic's resetsAt is authoritative.
|
|
222
|
+
|
|
223
|
+
For any classification absent from ``_BACK_OFF_DEFAULTS`` — i.e. one a
|
|
224
|
+
third-party plugin defined itself, which ``api_types.py`` types
|
|
225
|
+
``classification`` as ``str`` to permit — the same verbatim path applies:
|
|
226
|
+
core has no base duration for it, so the emitter's reset_at_epoch is the
|
|
227
|
+
only non-invented answer.
|
|
228
|
+
|
|
229
|
+
For estimated classifications (``rate_limit_model``, ``api_transient_5xx``,
|
|
230
|
+
``api_timeout``): increments the counter for this bucket, computes
|
|
231
|
+
duration = base × 2^min(n, _EXP_CAP), caps at _ABSOLUTE_CAP_S, emits
|
|
232
|
+
``transient_error_backoff_capped`` if multiplier > 1 or capped.
|
|
233
|
+
"""
|
|
234
|
+
from agent_runner._emit import emit_transient_error_backoff_capped
|
|
235
|
+
from agent_runner.builtin_plugins._constants import (
|
|
236
|
+
_ABSOLUTE_CAP_S,
|
|
237
|
+
_BACK_OFF_DEFAULTS,
|
|
238
|
+
_EXP_CAP,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if classification == "rate_limit_account" or classification not in _BACK_OFF_DEFAULTS:
|
|
242
|
+
# Server-authoritative, or a plugin's own classification (api_types.py types
|
|
243
|
+
# `classification` as str precisely so plugins can add their own): the emitter
|
|
244
|
+
# supplied reset_at_epoch, so respect it verbatim and never touch the counter.
|
|
245
|
+
return (original_reset_at_epoch, 0, False)
|
|
246
|
+
|
|
247
|
+
# Estimated class: apply exp backoff.
|
|
248
|
+
base = _BACK_OFF_DEFAULTS[classification]
|
|
249
|
+
n = _consecutive_failures.get(classification, 0)
|
|
250
|
+
multiplier = 2 ** min(n, _EXP_CAP)
|
|
251
|
+
extended_duration = base * multiplier
|
|
252
|
+
capped_by_absolute_max = extended_duration > _ABSOLUTE_CAP_S
|
|
253
|
+
applied_duration = min(extended_duration, _ABSOLUTE_CAP_S)
|
|
254
|
+
applied_reset_at = int(clock.epoch()) + applied_duration
|
|
255
|
+
|
|
256
|
+
new_count = n + 1
|
|
257
|
+
_consecutive_failures[classification] = new_count
|
|
258
|
+
|
|
259
|
+
# Emit observability event when supervisor adjusted the wait.
|
|
260
|
+
if multiplier > 1 or capped_by_absolute_max:
|
|
261
|
+
emit_transient_error_backoff_capped(
|
|
262
|
+
log_dir,
|
|
263
|
+
classification=classification,
|
|
264
|
+
agent=agent,
|
|
265
|
+
requested_sleep_s=int(base),
|
|
266
|
+
applied_sleep_s=applied_duration,
|
|
267
|
+
original_reset_at_epoch=original_reset_at_epoch,
|
|
268
|
+
applied_reset_at_epoch=applied_reset_at,
|
|
269
|
+
consecutive_count=new_count,
|
|
270
|
+
capped_by_absolute_max=capped_by_absolute_max,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return (applied_reset_at, new_count, capped_by_absolute_max)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def reset_counters() -> None:
|
|
277
|
+
"""Clear all bucket counters. Called by serve loop when no active throttle."""
|
|
278
|
+
_consecutive_failures.clear()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
_BACK_OFF_CAP_S = 28800 # 8h — defensive cap; 1.6× the 5h-window
|
|
282
|
+
_BACK_OFF_JITTER_MIN_S = 5
|
|
283
|
+
_BACK_OFF_JITTER_MAX_S = 30
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _interruptible_sleep(
|
|
287
|
+
total_s: float, stop: dict[str, bool], *, clock: Clock = SYSTEM_CLOCK, chunk_s: int = 30
|
|
288
|
+
) -> bool:
|
|
289
|
+
"""Sleep ``total_s`` in ``<= chunk_s`` slices, re-checking ``stop`` at each
|
|
290
|
+
boundary; return True iff ``stop["requested"]`` cut it short. Shared by the serve
|
|
291
|
+
restart delay and :func:`_apply_back_off` so a SIGTERM lands within one chunk.
|
|
292
|
+
|
|
293
|
+
Counts down the *intended* nap per slice rather than measuring a wall/monotonic
|
|
294
|
+
deadline: NTP-step immune (no clock read for the deadline) AND does not busy-spin
|
|
295
|
+
when ``clock.sleep`` is a no-op (a test patching ``time.sleep`` runs it instantly
|
|
296
|
+
instead of looping until real time advances)."""
|
|
297
|
+
remaining = float(total_s)
|
|
298
|
+
while remaining > 0:
|
|
299
|
+
if stop["requested"]:
|
|
300
|
+
return True
|
|
301
|
+
nap = min(float(chunk_s), remaining)
|
|
302
|
+
clock.sleep(nap)
|
|
303
|
+
remaining -= nap
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _apply_back_off(
|
|
308
|
+
log_dir: Path,
|
|
309
|
+
throttle: TransientErrorState,
|
|
310
|
+
*,
|
|
311
|
+
stop: dict[str, bool],
|
|
312
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
313
|
+
chunk_s: int = 30,
|
|
314
|
+
) -> bool:
|
|
315
|
+
"""Sleep until adjusted reset_at + jitter, then emit recovered. Returns True iff
|
|
316
|
+
``stop["requested"]`` was set before the sleep completed — an interrupted back-off,
|
|
317
|
+
where the caller must break WITHOUT treating the throttle as recovered: the reset
|
|
318
|
+
has not passed, so a recovered breadcrumb would poison restart-safe state.
|
|
319
|
+
|
|
320
|
+
The sleep is chunked and re-checks ``stop`` at each boundary (see
|
|
321
|
+
:func:`_interruptible_sleep`), so a SIGTERM lands within one chunk instead of after
|
|
322
|
+
a multi-hour window.
|
|
323
|
+
|
|
324
|
+
For estimated-class classifications (rate_limit_model / api_transient_5xx /
|
|
325
|
+
api_timeout), applies exp backoff on consecutive failures via
|
|
326
|
+
:func:`compute_adjusted_reset_at`. For server-authoritative rate_limit_account, the
|
|
327
|
+
original reset_at_epoch is used verbatim. The 8h cap is a last-line defense against a
|
|
328
|
+
malformed (far-future) reset epoch (e.g. a manual event with a far-future ts).
|
|
329
|
+
"""
|
|
330
|
+
from agent_runner._emit import (
|
|
331
|
+
emit_transient_error_backoff_capped,
|
|
332
|
+
emit_transient_error_recovered,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
adjusted_reset_at, _consecutive_count, _capped = compute_adjusted_reset_at(
|
|
336
|
+
classification=throttle.classification,
|
|
337
|
+
original_reset_at_epoch=throttle.reset_at_epoch,
|
|
338
|
+
agent=throttle.agent,
|
|
339
|
+
log_dir=log_dir,
|
|
340
|
+
clock=clock,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
requested = (
|
|
344
|
+
adjusted_reset_at
|
|
345
|
+
- clock.epoch()
|
|
346
|
+
+ random.uniform(_BACK_OFF_JITTER_MIN_S, _BACK_OFF_JITTER_MAX_S)
|
|
347
|
+
)
|
|
348
|
+
if requested > _BACK_OFF_CAP_S:
|
|
349
|
+
# Defensive: malformed reset epoch. The exp-backoff layer caps at 30min, so
|
|
350
|
+
# legitimate flow never reaches this branch.
|
|
351
|
+
emit_transient_error_backoff_capped(
|
|
352
|
+
log_dir,
|
|
353
|
+
classification=throttle.classification,
|
|
354
|
+
agent=throttle.agent,
|
|
355
|
+
requested_sleep_s=int(requested),
|
|
356
|
+
applied_sleep_s=_BACK_OFF_CAP_S,
|
|
357
|
+
)
|
|
358
|
+
sleep_s = float(_BACK_OFF_CAP_S)
|
|
359
|
+
else:
|
|
360
|
+
sleep_s = max(requested, 0.0)
|
|
361
|
+
|
|
362
|
+
sleep_start = clock.epoch()
|
|
363
|
+
if _interruptible_sleep(sleep_s, stop, clock=clock, chunk_s=chunk_s):
|
|
364
|
+
return True # SIGTERM during back-off — leave the throttle active, no breadcrumb
|
|
365
|
+
|
|
366
|
+
emit_transient_error_recovered(
|
|
367
|
+
log_dir,
|
|
368
|
+
classification=throttle.classification,
|
|
369
|
+
agent=throttle.agent,
|
|
370
|
+
throttled_for_s=int(clock.epoch() - sleep_start),
|
|
371
|
+
)
|
|
372
|
+
return False
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.11'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 11)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,13 +15,14 @@ import shutil
|
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
17
17
|
import threading
|
|
18
|
-
import time
|
|
19
18
|
from collections.abc import Callable
|
|
20
19
|
from dataclasses import dataclass, field
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
|
|
23
22
|
import psutil
|
|
24
23
|
|
|
24
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
25
|
+
|
|
25
26
|
REAP_GRACE_S = 5
|
|
26
27
|
|
|
27
28
|
|
|
@@ -68,15 +69,17 @@ def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str)
|
|
|
68
69
|
return list(command) + [a.replace("{prompt}", prompt) for a in prompt_arg_template]
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
72
|
+
def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
72
73
|
pgid = proc.pid
|
|
73
74
|
try:
|
|
74
75
|
os.killpg(pgid, signal.SIGTERM)
|
|
75
76
|
except OSError:
|
|
76
77
|
pass
|
|
77
|
-
deadline =
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
deadline = (
|
|
79
|
+
clock.monotonic() + REAP_GRACE_S
|
|
80
|
+
) # monotonic: an NTP step must not stretch/skip the reap
|
|
81
|
+
while clock.monotonic() < deadline and proc.poll() is None:
|
|
82
|
+
clock.sleep(0.1)
|
|
80
83
|
try:
|
|
81
84
|
os.killpg(pgid, signal.SIGKILL)
|
|
82
85
|
except OSError:
|
|
@@ -173,6 +176,7 @@ def run(
|
|
|
173
176
|
progress_interval_s: int = 0,
|
|
174
177
|
on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
|
|
175
178
|
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
179
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
176
180
|
) -> RunResult:
|
|
177
181
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
178
182
|
|
|
@@ -213,7 +217,7 @@ def run(
|
|
|
213
217
|
env = {**os.environ, **env_extra, "PWD": str(work_dir)}
|
|
214
218
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
219
|
log_file = log_path.open("w", encoding="utf-8")
|
|
216
|
-
start =
|
|
220
|
+
start = clock.monotonic() # all round-duration/deadline math is monotonic (NTP-safe)
|
|
217
221
|
last_progress_at = start
|
|
218
222
|
proc = subprocess.Popen(
|
|
219
223
|
argv,
|
|
@@ -246,13 +250,13 @@ def run(
|
|
|
246
250
|
try:
|
|
247
251
|
while True:
|
|
248
252
|
ret = proc.poll()
|
|
249
|
-
now =
|
|
253
|
+
now = clock.monotonic() # duration / R1128 hard-wall / grace / interval — all monotonic
|
|
250
254
|
if ret is not None:
|
|
251
255
|
duration = now - start
|
|
252
256
|
return RunResult(exit_code=ret, duration_s=duration, timed_out=False, pid=proc.pid)
|
|
253
257
|
if now - start > timeout_s:
|
|
254
|
-
_kill_pgroup(proc)
|
|
255
|
-
duration =
|
|
258
|
+
_kill_pgroup(proc, clock)
|
|
259
|
+
duration = clock.monotonic() - start
|
|
256
260
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
257
261
|
return RunResult(
|
|
258
262
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
@@ -276,8 +280,8 @@ def run(
|
|
|
276
280
|
on_grace_extended(live, ignored)
|
|
277
281
|
grace_extended_emitted = True
|
|
278
282
|
else:
|
|
279
|
-
_kill_pgroup(proc)
|
|
280
|
-
duration =
|
|
283
|
+
_kill_pgroup(proc, clock)
|
|
284
|
+
duration = clock.monotonic() - start
|
|
281
285
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
282
286
|
return RunResult(
|
|
283
287
|
exit_code=exit_code,
|
|
@@ -293,7 +297,7 @@ def run(
|
|
|
293
297
|
try:
|
|
294
298
|
st = log_path.stat()
|
|
295
299
|
log_size_kb = st.st_size // 1024
|
|
296
|
-
last_write_age_s = max(0, int(
|
|
300
|
+
last_write_age_s = max(0, int(clock.epoch() - st.st_mtime))
|
|
297
301
|
except OSError:
|
|
298
302
|
log_size_kb = 0
|
|
299
303
|
last_write_age_s = 0
|
|
@@ -305,6 +309,6 @@ def run(
|
|
|
305
309
|
}
|
|
306
310
|
)
|
|
307
311
|
last_progress_at = now
|
|
308
|
-
|
|
312
|
+
clock.sleep(0.2)
|
|
309
313
|
finally:
|
|
310
314
|
log_file.close()
|
|
@@ -15,7 +15,6 @@ import shutil
|
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — api uses systemctl + ssh, both subprocess
|
|
17
17
|
import sysconfig
|
|
18
|
-
import time
|
|
19
18
|
from collections.abc import Iterator, Sequence
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
from typing import Any, Literal, TextIO
|
|
@@ -30,6 +29,7 @@ from agent_runner.api_types import (
|
|
|
30
29
|
ServiceStatus,
|
|
31
30
|
select_path,
|
|
32
31
|
)
|
|
32
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
33
33
|
from agent_runner.config import Config, RuntimeConfig, load_config
|
|
34
34
|
from agent_runner.lifecycle import (
|
|
35
35
|
PIDFile,
|
|
@@ -52,6 +52,13 @@ from agent_runner.service_unit import (
|
|
|
52
52
|
# facade without coupling to runner (runner imports api, not the reverse).
|
|
53
53
|
PERMANENT_CONFIG_EXIT = 78
|
|
54
54
|
|
|
55
|
+
# Exit code serve returns when the crash-loop breaker trips (a give-up stop, like
|
|
56
|
+
# config_broken). Distinct from 78 so `systemctl status` / SuccessExitStatus can
|
|
57
|
+
# tell the two "needs intervention" stops apart. 75 = EX_TEMPFAIL (sysexits).
|
|
58
|
+
# The systemd unit lists both in RestartPreventExitStatus so a deliberate stop
|
|
59
|
+
# stays stopped (and visibly failed) while an unexpected supervisor crash restarts.
|
|
60
|
+
CRASH_LOOP_EXIT = 75
|
|
61
|
+
|
|
55
62
|
# Crash-loop circuit breaker (b12). The serve loop escalates the restart delay
|
|
56
63
|
# on consecutive UNKNOWN short crashes (non-zero exit, short duration, no
|
|
57
64
|
# classified transient) and STOPS after CRASH_LOOP_THRESHOLD of them — the Run 6
|
|
@@ -323,13 +330,13 @@ def kill(project: str | Path) -> ServiceStatus:
|
|
|
323
330
|
if pid is None:
|
|
324
331
|
return status(project)
|
|
325
332
|
send_signal_to_pid(pid, signal.SIGTERM)
|
|
326
|
-
deadline =
|
|
333
|
+
deadline = SYSTEM_CLOCK.epoch() + 5
|
|
327
334
|
alive = True
|
|
328
|
-
while
|
|
335
|
+
while SYSTEM_CLOCK.epoch() < deadline:
|
|
329
336
|
alive = pid_alive(pid)
|
|
330
337
|
if not alive:
|
|
331
338
|
break
|
|
332
|
-
|
|
339
|
+
SYSTEM_CLOCK.sleep(0.1)
|
|
333
340
|
if alive:
|
|
334
341
|
send_signal_to_pid(pid, signal.SIGKILL)
|
|
335
342
|
return ServiceStatus(mode=ServiceMode.PID_FILE, active=alive, pid=pid)
|
|
@@ -438,9 +445,16 @@ def peek(
|
|
|
438
445
|
)
|
|
439
446
|
recent_blips = _recent_events_of_kind(parsed_events, AGENT_NETWORK_BLIP, _RECENT_BLIPS_LIMIT)
|
|
440
447
|
|
|
441
|
-
from agent_runner._throttle import _check_throttle_state
|
|
448
|
+
from agent_runner._throttle import _active_throttles, _check_throttle_state
|
|
442
449
|
|
|
443
450
|
throttle = _check_throttle_state(log_dir)
|
|
451
|
+
active = _active_throttles(log_dir)
|
|
452
|
+
if throttle is None and active:
|
|
453
|
+
# The global-latest scalar view can be None while a sibling agent is still
|
|
454
|
+
# throttled (the newest transient event is another agent's recovered). Fall
|
|
455
|
+
# back to the active throttle that clears LAST so the scalar fields stay
|
|
456
|
+
# coherent and the plural throttled_agents view is not dropped to null.
|
|
457
|
+
throttle = max(active.values(), key=lambda s: s.reset_at_epoch)
|
|
444
458
|
rate_limit: RateLimitState | None = None
|
|
445
459
|
if throttle is not None:
|
|
446
460
|
rate_limit = RateLimitState(
|
|
@@ -448,6 +462,8 @@ def peek(
|
|
|
448
462
|
limit_type=throttle.classification,
|
|
449
463
|
agent=throttle.agent,
|
|
450
464
|
since_round=throttle.since_round,
|
|
465
|
+
phase=throttle.phase,
|
|
466
|
+
throttled_agents=tuple(sorted(active)),
|
|
451
467
|
)
|
|
452
468
|
raw_service = status(project if project is not None else work_dir)
|
|
453
469
|
svc = dataclasses.replace(raw_service, rate_limit=rate_limit)
|
|
@@ -567,7 +583,7 @@ def _monitor_loop_iter(
|
|
|
567
583
|
log_dir=cfg.runtime.log_dir,
|
|
568
584
|
allowed_stop_names=cfg.monitor.auto_stop_on,
|
|
569
585
|
)
|
|
570
|
-
|
|
586
|
+
SYSTEM_CLOCK.sleep(interval_s)
|
|
571
587
|
|
|
572
588
|
|
|
573
589
|
def _tail_events_jsonl(
|
|
@@ -586,7 +602,6 @@ def _tail_events_jsonl(
|
|
|
586
602
|
appears, it is picked up from byte 0.
|
|
587
603
|
"""
|
|
588
604
|
import json as _json
|
|
589
|
-
import time as _time
|
|
590
605
|
|
|
591
606
|
seen_positions: dict[Path, int] = {}
|
|
592
607
|
if start_at_now:
|
|
@@ -620,7 +635,7 @@ def _tail_events_jsonl(
|
|
|
620
635
|
any_new = True
|
|
621
636
|
seen_positions[path] = f.tell()
|
|
622
637
|
if not any_new:
|
|
623
|
-
|
|
638
|
+
SYSTEM_CLOCK.sleep(poll_interval_s)
|
|
624
639
|
|
|
625
640
|
|
|
626
641
|
def _primary_prompt_file(cfg: Config) -> Path | None:
|