cli-agent-runner 0.2.10__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.10 → cli_agent_runner-0.2.11}/.vulture-whitelist.py +1 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/CHANGELOG.md +18 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/PKG-INFO +1 -1
- cli_agent_runner-0.2.11/agent_runner/_throttle.py +372 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/agent_runtime.py +9 -7
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/api.py +16 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/api_types.py +3 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/common.py +1 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/round_cmd.py +7 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/serve_cmd.py +162 -58
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/config.py +9 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/defenses.py +3 -2
- cli_agent_runner-0.2.11/agent_runner/lifecycle.py +101 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/monitor.py +3 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/runner.py +14 -64
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/service_unit.py +9 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/configuration.md +8 -5
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/migrations/0.2.md +29 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/plugins.md +2 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/runbook.md +30 -18
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/_clock.py +5 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_fresh_eyes_signal.py +3 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_substrate_fingerprint.py +4 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_transient_error_backoff.py +3 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_architecture.py +14 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_atomic_write_enforced.py +3 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_test_pointers_exist.py +14 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_module_boundaries.py +48 -37
- cli_agent_runner-0.2.11/tests/invariants/test_no_vacuous_invariants.py +41 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_peek_schema_version.py +7 -2
- cli_agent_runner-0.2.11/tests/invariants/test_stash_uses_sha_not_index.py +54 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_grace.py +33 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_observation.py +54 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_events.py +1 -1
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_exit_cause.py +10 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_lifecycle.py +23 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detectors.py +19 -0
- cli_agent_runner-0.2.11/tests/unit/test_round_cmd.py +52 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_runner_throttle.py +206 -15
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_config_broken.py +3 -3
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_crash_loop.py +7 -2
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_phase_schedule.py +125 -26
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_service_unit.py +11 -1
- cli_agent_runner-0.2.10/agent_runner/_throttle.py +0 -220
- cli_agent_runner-0.2.10/agent_runner/lifecycle.py +0 -67
- cli_agent_runner-0.2.10/tests/invariants/test_stash_uses_sha_not_index.py +0 -21
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/.gitignore +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/LICENSE +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/README.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/README.zh.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_emit.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/build.sh +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/README.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/events.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.10 → 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.10 → 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.10 → 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.10 → cli_agent_runner-0.2.11}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.10 → cli_agent_runner-0.2.11}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,24 @@ 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
|
+
|
|
8
26
|
## [0.2.10] - 2026-08-29
|
|
9
27
|
|
|
10
28
|
### 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
|
|
@@ -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
|
|
@@ -75,8 +75,10 @@ def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
|
75
75
|
os.killpg(pgid, signal.SIGTERM)
|
|
76
76
|
except OSError:
|
|
77
77
|
pass
|
|
78
|
-
deadline =
|
|
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:
|
|
80
82
|
clock.sleep(0.1)
|
|
81
83
|
try:
|
|
82
84
|
os.killpg(pgid, signal.SIGKILL)
|
|
@@ -215,7 +217,7 @@ def run(
|
|
|
215
217
|
env = {**os.environ, **env_extra, "PWD": str(work_dir)}
|
|
216
218
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
219
|
log_file = log_path.open("w", encoding="utf-8")
|
|
218
|
-
start = clock.
|
|
220
|
+
start = clock.monotonic() # all round-duration/deadline math is monotonic (NTP-safe)
|
|
219
221
|
last_progress_at = start
|
|
220
222
|
proc = subprocess.Popen(
|
|
221
223
|
argv,
|
|
@@ -248,13 +250,13 @@ def run(
|
|
|
248
250
|
try:
|
|
249
251
|
while True:
|
|
250
252
|
ret = proc.poll()
|
|
251
|
-
now = clock.
|
|
253
|
+
now = clock.monotonic() # duration / R1128 hard-wall / grace / interval — all monotonic
|
|
252
254
|
if ret is not None:
|
|
253
255
|
duration = now - start
|
|
254
256
|
return RunResult(exit_code=ret, duration_s=duration, timed_out=False, pid=proc.pid)
|
|
255
257
|
if now - start > timeout_s:
|
|
256
258
|
_kill_pgroup(proc, clock)
|
|
257
|
-
duration = clock.
|
|
259
|
+
duration = clock.monotonic() - start
|
|
258
260
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
259
261
|
return RunResult(
|
|
260
262
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
@@ -279,7 +281,7 @@ def run(
|
|
|
279
281
|
grace_extended_emitted = True
|
|
280
282
|
else:
|
|
281
283
|
_kill_pgroup(proc, clock)
|
|
282
|
-
duration = clock.
|
|
284
|
+
duration = clock.monotonic() - start
|
|
283
285
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
284
286
|
return RunResult(
|
|
285
287
|
exit_code=exit_code,
|
|
@@ -295,7 +297,7 @@ def run(
|
|
|
295
297
|
try:
|
|
296
298
|
st = log_path.stat()
|
|
297
299
|
log_size_kb = st.st_size // 1024
|
|
298
|
-
last_write_age_s = max(0, int(
|
|
300
|
+
last_write_age_s = max(0, int(clock.epoch() - st.st_mtime))
|
|
299
301
|
except OSError:
|
|
300
302
|
log_size_kb = 0
|
|
301
303
|
last_write_age_s = 0
|
|
@@ -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
|
|
@@ -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(
|
|
@@ -449,6 +463,7 @@ def peek(
|
|
|
449
463
|
agent=throttle.agent,
|
|
450
464
|
since_round=throttle.since_round,
|
|
451
465
|
phase=throttle.phase,
|
|
466
|
+
throttled_agents=tuple(sorted(active)),
|
|
452
467
|
)
|
|
453
468
|
raw_service = status(project if project is not None else work_dir)
|
|
454
469
|
svc = dataclasses.replace(raw_service, rate_limit=rate_limit)
|
|
@@ -49,6 +49,9 @@ class RateLimitState:
|
|
|
49
49
|
agent: str
|
|
50
50
|
since_round: int
|
|
51
51
|
phase: str = "" # which rotation phase is throttled ("" = no [phases])
|
|
52
|
+
# All agents (binary basenames) currently throttled — plural view for multi-provider
|
|
53
|
+
# rotations. The scalar fields above stay the "latest" throttle for back-compat.
|
|
54
|
+
throttled_agents: tuple[str, ...] = ()
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
@dataclass(frozen=True)
|
|
@@ -16,7 +16,7 @@ from agent_runner.hooks import plugin_context_enrichers, post_round_hooks, pre_r
|
|
|
16
16
|
from agent_runner.monitor import plugin_detectors
|
|
17
17
|
from agent_runner.vcs_state import plugin_owned_paths
|
|
18
18
|
|
|
19
|
-
PEEK_SCHEMA_VERSION = "1.
|
|
19
|
+
PEEK_SCHEMA_VERSION = "1.10"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def cfg_from_args(args) -> Config:
|
|
@@ -21,5 +21,10 @@ def add_parser(sub, parent) -> None:
|
|
|
21
21
|
|
|
22
22
|
def cmd(args) -> int:
|
|
23
23
|
cfg = cfg_from_args(args)
|
|
24
|
-
run_one_round(cfg, phase_override=args.phase)
|
|
25
|
-
|
|
24
|
+
result = run_one_round(cfg, phase_override=args.phase)
|
|
25
|
+
# Surface a real agent crash as a non-zero exit so serve's crash-loop breaker
|
|
26
|
+
# (which keys on this subprocess's returncode) can count it. A grace-kill (agent
|
|
27
|
+
# produced a result then lingered) or a timeout (long, not a short crash) is NOT
|
|
28
|
+
# a crash → 0. Never returns the serve-reserved 78 / 75.
|
|
29
|
+
crashed = result.exit_code != 0 and not result.killed_for_grace and not result.timed_out
|
|
30
|
+
return 1 if crashed else 0
|