cli-agent-runner 0.2.11__tar.gz → 0.2.13__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.11 → cli_agent_runner-0.2.13}/.vulture-whitelist.py +7 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/CHANGELOG.md +37 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_docgen.py +9 -4
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_emit.py +31 -4
- cli_agent_runner-0.2.13/agent_runner/_resolve.py +120 -0
- cli_agent_runner-0.2.13/agent_runner/_serve_policy.py +189 -0
- cli_agent_runner-0.2.13/agent_runner/_throttle.py +623 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/agent_runtime.py +87 -50
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/api.py +281 -155
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/api_types.py +18 -6
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/_constants.py +9 -4
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/claude_rate_limit.py +68 -19
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/codewhale.py +9 -9
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/gemini.py +53 -20
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/pi.py +3 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/__init__.py +24 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/common.py +29 -5
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/events_cmd.py +24 -29
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/install_cmd.py +15 -5
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/migrate_cmd.py +3 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/monitor_cmd.py +3 -1
- cli_agent_runner-0.2.13/agent_runner/cli/round_cmd.py +86 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/serve_cmd.py +175 -36
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/service_cmd.py +6 -5
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/upgrade_cmd.py +83 -9
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/clock.py +20 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/config.py +218 -34
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/context_store.py +25 -7
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/events.py +54 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/hooks.py +26 -15
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/http_progress.py +14 -6
- cli_agent_runner-0.2.13/agent_runner/lifecycle.py +192 -0
- cli_agent_runner-0.2.13/agent_runner/migrations.py +629 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/monitor.py +241 -98
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/phase_select.py +8 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/round_log.py +54 -6
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/round_view.py +3 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/runner.py +85 -17
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/scaffold.py +7 -3
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/service_unit.py +53 -19
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/startup_check.py +50 -9
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/vcs_state.py +92 -15
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/architecture.md +5 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/commands.md +36 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/configuration.md +4 -3
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/migrations/0.2.md +98 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/plugins.md +8 -5
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/quickstart.md +7 -8
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/runbook.md +14 -9
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/pyproject.toml +2 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/_test_helpers.py +27 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/contract/test_public_api_surface.py +137 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/conftest.py +1 -1
- cli_agent_runner-0.2.13/tests/integration/test_auto_stop_e2e.py +188 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_fresh_eyes_signal.py +10 -12
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_real_flow.py +1 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_serve_loop.py +1 -1
- cli_agent_runner-0.2.13/tests/integration/test_spawn_round_wedged.py +92 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/_event_scan.py +64 -16
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_architecture.py +10 -5
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_authored_facts_declared.py +3 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_catalogs.py +3 -0
- cli_agent_runner-0.2.13/tests/invariants/test_config_error_consistency.py +135 -0
- cli_agent_runner-0.2.13/tests/invariants/test_dataclass_fields_have_producers.py +210 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_cli_claims.py +13 -3
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_docs_index_complete.py +3 -1
- cli_agent_runner-0.2.13/tests/invariants/test_event_int_coercion.py +56 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_event_kind_registry.py +3 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_event_kinds_ssot.py +4 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_hook_contract_docs.py +3 -1
- cli_agent_runner-0.2.13/tests/invariants/test_migrations_registry_ssot.py +22 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_module_boundaries.py +1 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_module_sizes.py +3 -0
- cli_agent_runner-0.2.13/tests/invariants/test_monitor_self_exclusion.py +22 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_no_ai_signatures.py +9 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +3 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_no_raw_time.py +28 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_no_vacuous_invariants.py +18 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_repo_constants_patched_in_tests.py +3 -0
- cli_agent_runner-0.2.13/tests/invariants/test_round_log_decode_boundary.py +45 -0
- cli_agent_runner-0.2.13/tests/invariants/test_round_num_single_source.py +43 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_set_diff_for_auto_tool_classification.py +3 -1
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_stash_uses_sha_not_index.py +25 -1
- cli_agent_runner-0.2.13/tests/invariants/test_timeout_budget_invariant.py +47 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/literate/parser.py +6 -3
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/literate/test_parser.py +8 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/literate/test_quickstart.py +10 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime.py +27 -11
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_grace.py +57 -5
- cli_agent_runner-0.2.13/tests/unit/test_agent_runtime_marker_scan.py +84 -0
- cli_agent_runner-0.2.13/tests/unit/test_agent_runtime_reap.py +114 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_install.py +75 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_observation.py +42 -0
- cli_agent_runner-0.2.13/tests/unit/test_api_service.py +583 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_types.py +25 -0
- cli_agent_runner-0.2.13/tests/unit/test_auto_stop_gating.py +157 -0
- cli_agent_runner-0.2.13/tests/unit/test_claude_foreign_schema.py +133 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli.py +1 -1
- cli_agent_runner-0.2.13/tests/unit/test_cli_main_boundary.py +43 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli_service_peek_monitor.py +29 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli_upgrade.py +220 -60
- cli_agent_runner-0.2.13/tests/unit/test_codewhale_code_precedence.py +17 -0
- cli_agent_runner-0.2.13/tests/unit/test_coerce_int_poison.py +94 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config.py +187 -0
- cli_agent_runner-0.2.13/tests/unit/test_config_strictness.py +96 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_context_store.py +11 -0
- cli_agent_runner-0.2.13/tests/unit/test_control_char_config.py +129 -0
- cli_agent_runner-0.2.13/tests/unit/test_detect_hung_latch.py +69 -0
- cli_agent_runner-0.2.13/tests/unit/test_detector_isolation.py +25 -0
- cli_agent_runner-0.2.13/tests/unit/test_effective_throttle_view.py +68 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_events_cmd.py +47 -0
- cli_agent_runner-0.2.13/tests/unit/test_events_cmd_rollover.py +42 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_events_cmd_tail.py +38 -1
- cli_agent_runner-0.2.13/tests/unit/test_events_jsonl_decode.py +126 -0
- cli_agent_runner-0.2.13/tests/unit/test_exit_classifier.py +89 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_gemini_plugin.py +44 -0
- cli_agent_runner-0.2.13/tests/unit/test_git_timeout.py +139 -0
- cli_agent_runner-0.2.13/tests/unit/test_hook_redact_order.py +33 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_http_progress.py +48 -0
- cli_agent_runner-0.2.13/tests/unit/test_interruptible_sleep_should_stop.py +47 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_lifecycle.py +30 -0
- cli_agent_runner-0.2.13/tests/unit/test_migrate_cmd.py +186 -0
- cli_agent_runner-0.2.13/tests/unit/test_migrations.py +359 -0
- cli_agent_runner-0.2.13/tests/unit/test_monitor_cmd_severity.py +19 -0
- cli_agent_runner-0.2.13/tests/unit/test_monitor_dedup_identity.py +125 -0
- cli_agent_runner-0.2.13/tests/unit/test_monitor_dedup_rearm.py +42 -0
- cli_agent_runner-0.2.13/tests/unit/test_monitor_detect_rate_limit.py +87 -0
- cli_agent_runner-0.2.13/tests/unit/test_monitor_event_tail.py +20 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_remote.py +59 -0
- cli_agent_runner-0.2.13/tests/unit/test_open_round_log.py +24 -0
- cli_agent_runner-0.2.13/tests/unit/test_orphan_chain_single_pass.py +26 -0
- cli_agent_runner-0.2.13/tests/unit/test_outer_round_ceiling.py +43 -0
- cli_agent_runner-0.2.13/tests/unit/test_peek_project_isolation.py +38 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_phase_select.py +19 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_plugin_constants.py +20 -0
- cli_agent_runner-0.2.13/tests/unit/test_primary_prompt_file_home.py +23 -0
- cli_agent_runner-0.2.13/tests/unit/test_read_json_hardening.py +85 -0
- cli_agent_runner-0.2.13/tests/unit/test_resolve.py +125 -0
- cli_agent_runner-0.2.13/tests/unit/test_round_cmd.py +124 -0
- cli_agent_runner-0.2.13/tests/unit/test_round_cmd_sigterm.py +33 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_round_log_helpers.py +88 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_runner.py +61 -0
- cli_agent_runner-0.2.13/tests/unit/test_runner_sidecar_ordering.py +34 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_runner_throttle.py +193 -67
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_scaffold.py +29 -0
- cli_agent_runner-0.2.13/tests/unit/test_serve_config_broken.py +75 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_crash_loop.py +34 -17
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_phase_schedule.py +7 -8
- cli_agent_runner-0.2.13/tests/unit/test_serve_policy.py +29 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_round_log.py +15 -24
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_schedule_gate.py +7 -4
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_sentinel.py +30 -30
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_startup_hooks.py +11 -12
- cli_agent_runner-0.2.13/tests/unit/test_service_mode.py +103 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_service_unit.py +88 -14
- cli_agent_runner-0.2.13/tests/unit/test_startup_battery_exit.py +92 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_startup_check.py +38 -0
- cli_agent_runner-0.2.13/tests/unit/test_supervisor_stale_self_blind.py +31 -0
- cli_agent_runner-0.2.13/tests/unit/test_throttle_back_off_ladder.py +95 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_upgrade_migrate.py +26 -0
- cli_agent_runner-0.2.11/agent_runner/_throttle.py +0 -372
- cli_agent_runner-0.2.11/agent_runner/cli/round_cmd.py +0 -30
- cli_agent_runner-0.2.11/agent_runner/lifecycle.py +0 -101
- cli_agent_runner-0.2.11/agent_runner/migrations.py +0 -115
- cli_agent_runner-0.2.11/tests/invariants/test_config_error_consistency.py +0 -74
- cli_agent_runner-0.2.11/tests/unit/test_api_service.py +0 -214
- cli_agent_runner-0.2.11/tests/unit/test_auto_stop_gating.py +0 -87
- cli_agent_runner-0.2.11/tests/unit/test_migrate_cmd.py +0 -85
- cli_agent_runner-0.2.11/tests/unit/test_migrations.py +0 -83
- cli_agent_runner-0.2.11/tests/unit/test_monitor_detect_rate_limit.py +0 -47
- cli_agent_runner-0.2.11/tests/unit/test_round_cmd.py +0 -52
- cli_agent_runner-0.2.11/tests/unit/test_serve_config_broken.py +0 -33
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/.gitignore +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/LICENSE +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/README.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/README.zh.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/build.sh +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/README.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/events.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.11 → cli_agent_runner-0.2.13}/tests/unit/test_vcs_state.py +0 -0
|
@@ -12,7 +12,6 @@ duration_s
|
|
|
12
12
|
timed_out
|
|
13
13
|
pid
|
|
14
14
|
killed_for_grace
|
|
15
|
-
grace_kill_children
|
|
16
15
|
|
|
17
16
|
# agent_runner.api_types.Alert
|
|
18
17
|
severity
|
|
@@ -248,15 +247,21 @@ path
|
|
|
248
247
|
detect
|
|
249
248
|
apply
|
|
250
249
|
describe
|
|
250
|
+
advisory
|
|
251
251
|
|
|
252
252
|
# agent_runner.migrations.MigrationResult
|
|
253
253
|
new_text
|
|
254
254
|
applied
|
|
255
255
|
manual
|
|
256
|
+
advisory
|
|
256
257
|
|
|
257
258
|
# agent_runner.monitor.LocalSource
|
|
258
259
|
log_dir
|
|
259
260
|
|
|
261
|
+
# agent_runner.monitor._EventTail
|
|
262
|
+
offsets
|
|
263
|
+
buffer
|
|
264
|
+
|
|
260
265
|
# agent_runner.phase_select.Selection
|
|
261
266
|
phase
|
|
262
267
|
paused
|
|
@@ -287,6 +292,7 @@ name
|
|
|
287
292
|
ok
|
|
288
293
|
reason
|
|
289
294
|
how_to_fix
|
|
295
|
+
permanent
|
|
290
296
|
|
|
291
297
|
# agent_runner.vcs_state.StashRef
|
|
292
298
|
sha
|
|
@@ -5,6 +5,43 @@ 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.13] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Breaking
|
|
11
|
+
- Config validation now rejects unknown keys in base `[agent]`/`[runtime]`/`[vcs]`/`[monitor]`, a table given as a scalar, unknown per-phase `[phases.<name>.prompt]` keys, and an argv `prompt_arg_template` missing `{prompt}`. **Run `agent-runner migrate` before `upgrade`**, as in 0.2.12.
|
|
12
|
+
- systemd `serve` units grow `TimeoutStopSec` (now `round_timeout_s + 210`, up from `+60`) — re-render after upgrading.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Round-child exit codes are classified by permanence, not a type whitelist: a config break stays 78, a self-healing environmental failure (lock contention, a git-status timeout, an unwritable log dir) now reliably reaches retryable 76, and everything else still falls to 1 so the crash-loop breaker bounds a genuine bug; serve itself still never exits 76.
|
|
16
|
+
- `kill` reaps the whole round-and-agent process group; `stop`/`restart`/`uninstall` are drain-aware so a mid-round stop no longer leaves a systemd serve stopped or raises; service-mode detection checks systemd `is-active` instead of guessing; and `monitor_auto_stop_triggered` fires only after a confirmed stop.
|
|
17
|
+
- `install --system` rejects control characters in `work_dir`/`log_dir`/config path, closing a root-injection path through the rendered unit.
|
|
18
|
+
- `read_json`/orphan-state reads survive a non-UTF-8 byte or a directory instead of crashing serve; a git-status timeout mid-round no longer drops the round's own bookkeeping (`dirty_check_failed`).
|
|
19
|
+
- Throttle readers (serve's loop-top gate, the skip path, `peek`, monitor) converge on one reset source; the back-off sleep now wakes early on an NTP clock jump.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
- New event kind: `dirty_check_failed`. `peek --json` schema is unchanged this release.
|
|
23
|
+
|
|
24
|
+
See `docs/migrations/0.2.md`.
|
|
25
|
+
|
|
26
|
+
## [0.2.12] - 2026-08-31
|
|
27
|
+
|
|
28
|
+
### Breaking
|
|
29
|
+
- Config validation now hard-rejects footguns that used to load silently: a scalar where a list is required (`phases.list`, `[agent] command`, `prompt_arg_template`, `[prompt] files`), an empty `[agent] command` / empty top-level `[prompt] files`, unknown keys in `[schedule]`/`[prompt]`, and `threshold > window`. **Run `agent-runner migrate` BEFORE `upgrade`** — the orchestrated upgrade runs the old binary's migrate, which cannot self-cross this break. Every new error names `agent-runner migrate`.
|
|
30
|
+
- systemd `serve` unit adds `StartLimitIntervalSec`/`StartLimitBurst`; re-render or hand-edit existing units.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- The monitor no longer dies on a non-UTF-8 byte or an odd plugin severity; each builtin detector is isolated (`detector_error` event) and a hung round is TERM→grace→killpg'd (`round_supervisor_wedged`) instead of orphaning the agent or latching `hung`.
|
|
34
|
+
- Environmental startup failures (ENOSPC, mount hiccup, unclassified) exit 76 and keep serve retrying at a fixed back-off until they heal, instead of tripping the crash-loop breaker; deterministic config failures give up by name (exit 78).
|
|
35
|
+
- A git-commit timeout leaves no surviving `.git/index.lock` (self-caused only; `stale_index_lock_cleared`); a stop-file lands within one back-off chunk.
|
|
36
|
+
- `status`/`kill`/`restart` report and act on real service state: liveness falls back to `serve.pid` when `systemctl` is absent, and `restart` refuses on non-service modes before stopping.
|
|
37
|
+
- `upgrade` runs the new binary's `migrate` before the smoke check, restores the config on rollback, and emits `upgrade_start_failed` with a remedy when the new version installs but won't start.
|
|
38
|
+
- `json_tail` no longer lets a blank-line flood evict the terminal round-log record; `init` commits only its scaffold files.
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
- New event kinds: `detector_error`, `round_supervisor_wedged`, `stale_index_lock_cleared`, `upgrade_start_failed`. `RestartPreventExitStatus` stays `78 75` — environmental failures (round exit 76) are retried inside serve rather than stopping it.
|
|
42
|
+
|
|
43
|
+
See `docs/migrations/0.2.md`.
|
|
44
|
+
|
|
8
45
|
## [0.2.11] - 2026-08-29
|
|
9
46
|
|
|
10
47
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
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
|
|
@@ -157,10 +157,15 @@ def render_event_kinds_list() -> str:
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def _render_migrate_transforms() -> str:
|
|
160
|
-
"""Bullet list of the transforms `agent-runner migrate` applies, from the registry.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
"""Bullet list of the transforms `agent-runner migrate` applies, from the registry.
|
|
161
|
+
A few entries carry a `parsed -> str` describe (so the manual report can name the
|
|
162
|
+
exact offending key found in a real config); rendered here with `{}`, which degrades
|
|
163
|
+
to the same generic instruction since there's no config in hand."""
|
|
164
|
+
from agent_runner.migrations import MIGRATIONS, _describe
|
|
165
|
+
|
|
166
|
+
# No config in hand here, so pass an empty parsed dict: a `parsed -> str`
|
|
167
|
+
# describe degrades to its generic instruction, same rule as the manual report.
|
|
168
|
+
return "\n".join(f"- {_describe(m, {})}" for m in MIGRATIONS)
|
|
164
169
|
|
|
165
170
|
|
|
166
171
|
def _subparser_action() -> argparse.Action:
|
|
@@ -27,9 +27,11 @@ __all__ = [
|
|
|
27
27
|
"emit_round_progress",
|
|
28
28
|
"emit_round_substrate_after",
|
|
29
29
|
"emit_round_substrate_before",
|
|
30
|
+
"emit_round_supervisor_wedged",
|
|
30
31
|
"emit_schedule_paused",
|
|
31
32
|
"emit_schedule_phase_skipped",
|
|
32
33
|
"emit_schedule_resumed",
|
|
34
|
+
"emit_stale_index_lock_cleared",
|
|
33
35
|
"emit_stop_file_detected",
|
|
34
36
|
"emit_transient_error_backoff_capped",
|
|
35
37
|
"emit_transient_error_detected",
|
|
@@ -52,7 +54,8 @@ def emit_max_rounds_reached(log_dir: Path, *, rounds_completed: int, max_rounds:
|
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
def emit_config_broken(log_dir: Path, *, reason: str) -> None:
|
|
55
|
-
"""Emit config_broken (serve
|
|
57
|
+
"""Emit config_broken (serve gave up on a permanent, non-self-healing failure —
|
|
58
|
+
a startup-battery check, or any other ConfigError-classified round exit)."""
|
|
56
59
|
from agent_runner.events import CONFIG_BROKEN, emit
|
|
57
60
|
|
|
58
61
|
emit(log_dir, CONFIG_BROKEN, reason=reason)
|
|
@@ -68,7 +71,7 @@ def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path
|
|
|
68
71
|
from agent_runner.events import CRASH_LOOP, emit
|
|
69
72
|
|
|
70
73
|
try:
|
|
71
|
-
reason = redact_secrets(log_path.read_text(errors="replace")[-2000:])
|
|
74
|
+
reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
|
|
72
75
|
except OSError:
|
|
73
76
|
reason = ""
|
|
74
77
|
emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
|
|
@@ -98,6 +101,15 @@ def emit_stop_file_detected(
|
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
|
|
104
|
+
def emit_stale_index_lock_cleared(log_dir: Path, *, lock_path: str, round_num: int) -> None:
|
|
105
|
+
"""Emit when serve removed a .git/index.lock that its own timed-out+killed git
|
|
106
|
+
call orphaned. Single-writer: serve holds the round lock, so any lock surviving
|
|
107
|
+
our kill is ours to clear."""
|
|
108
|
+
from agent_runner.events import STALE_INDEX_LOCK_CLEARED, emit
|
|
109
|
+
|
|
110
|
+
emit(log_dir, STALE_INDEX_LOCK_CLEARED, lock_path=lock_path, round_num=round_num)
|
|
111
|
+
|
|
112
|
+
|
|
101
113
|
def emit_schedule_paused(
|
|
102
114
|
log_dir: Path, *, active_window: str, resume_at: str, timezone: str, phase: str = ""
|
|
103
115
|
) -> None:
|
|
@@ -168,6 +180,18 @@ def emit_round_substrate_after(
|
|
|
168
180
|
)
|
|
169
181
|
|
|
170
182
|
|
|
183
|
+
def emit_round_supervisor_wedged(
|
|
184
|
+
log_dir: Path, *, pid: int, timeout_s: int, log_path: Path
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Emit when the round subprocess blew past the outer ceiling and serve had to
|
|
187
|
+
TERM/kill it (the round supervisor was wedged — not doing its own bounded
|
|
188
|
+
post-round cleanup). Distinct from round_timeout_kill (the AGENT hit the inner
|
|
189
|
+
wall inside a healthy round)."""
|
|
190
|
+
from agent_runner.events import ROUND_SUPERVISOR_WEDGED, emit
|
|
191
|
+
|
|
192
|
+
emit(log_dir, ROUND_SUPERVISOR_WEDGED, pid=pid, timeout_s=timeout_s, log_path=str(log_path))
|
|
193
|
+
|
|
194
|
+
|
|
171
195
|
def emit_fresh_eyes_round_triggered(log_dir: Path, *, round_num: int, every_n: int) -> None:
|
|
172
196
|
"""Emit fresh-eyes signal trigger event (only on triggered rounds)."""
|
|
173
197
|
from agent_runner.events import FRESH_EYES_ROUND_TRIGGERED, emit
|
|
@@ -292,8 +316,11 @@ def emit_agent_usage_recorded(
|
|
|
292
316
|
- ``models_breakdown``: only populated when a round used multiple models
|
|
293
317
|
(gemini multi-model rounds). None for claude (always single-model).
|
|
294
318
|
- ``cache_creation_tokens``: claude only — ``usage.cache_creation_input_tokens``,
|
|
295
|
-
independent count from ``cached_tokens`` (cache_read).
|
|
296
|
-
|
|
319
|
+
independent count from ``cached_tokens`` (cache_read). Priced differently
|
|
320
|
+
from fresh input by the provider; the framework records the count only and
|
|
321
|
+
never interprets cost (no price tables here — see ``cost_usd`` above for the
|
|
322
|
+
one cost figure the framework passes through verbatim). Gemini has no
|
|
323
|
+
equivalent → 0.
|
|
297
324
|
- ``tool_call_count``: number of tool invocations the agent made in the round.
|
|
298
325
|
Claude: count of ``tool_use`` content blocks across all assistant events.
|
|
299
326
|
Gemini: ``stats.tool_calls``.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Internal identity/location resolver (Group C, 0.2.13 "close the seams").
|
|
2
|
+
|
|
3
|
+
Project name, config path, unit filename and log_dir were each derived
|
|
4
|
+
independently at several call sites and could disagree when ``work_dir`` and
|
|
5
|
+
the TOML's own directory differ. This module is the single source for all
|
|
6
|
+
four; the divergent call sites route through it instead of re-deriving. It
|
|
7
|
+
also carries ``guard_against_clobber``, ``api.install``'s same-basename
|
|
8
|
+
sibling-unit guard — a natural fit since it works entirely off unit-file
|
|
9
|
+
identity (``WorkingDirectory=``) rather than any install-specific state.
|
|
10
|
+
|
|
11
|
+
INTERNAL ONLY -- not a public/documented contract. Exposing a resolver as a
|
|
12
|
+
public contract is deferred to 0.3.
|
|
13
|
+
|
|
14
|
+
Leaf module: imports only ``agent_runner.config`` and
|
|
15
|
+
``agent_runner.service_unit`` (both leaves themselves), so anything in the
|
|
16
|
+
package can import this without risking a cycle.
|
|
17
|
+
|
|
18
|
+
Lenient/strict split (spec-review correction): validating the project
|
|
19
|
+
name on the serve/round/init path would newly reject any ``work_dir`` whose
|
|
20
|
+
basename has a space or CJK characters -- common on macOS dev boxes -- an
|
|
21
|
+
unflagged BREAK, since only lifecycle/observe verbs validate today. So
|
|
22
|
+
``strict=True`` applies ONLY where the name is interpolated into a unit
|
|
23
|
+
filename / ssh / systemd identity (api.py's lifecycle verbs, install,
|
|
24
|
+
lifecycle.py, install_cmd.py); ``strict=False`` for descriptive uses
|
|
25
|
+
(``hook_ctx.project`` in runner.py, the scaffolded toml's ``{project}`` in
|
|
26
|
+
scaffold.py) that must keep accepting any basename.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import re
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from agent_runner.config import load_config
|
|
35
|
+
from agent_runner.service_unit import serve_unit_filename
|
|
36
|
+
|
|
37
|
+
_PROJECT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def project_name(work_dir: Path, *, strict: bool) -> str:
|
|
41
|
+
"""Derive the project name from ``work_dir``'s basename.
|
|
42
|
+
|
|
43
|
+
strict=True rejects anything but ``[A-Za-z0-9._-]+`` -- required wherever
|
|
44
|
+
the name is interpolated into a unit filename / ssh / systemd identity.
|
|
45
|
+
strict=False accepts any non-empty basename (spaces, CJK, etc.) -- for
|
|
46
|
+
descriptive uses where the name is never shelled out or used as a
|
|
47
|
+
filesystem/systemd token by itself.
|
|
48
|
+
"""
|
|
49
|
+
name = work_dir.resolve().name or "default"
|
|
50
|
+
if strict and not _PROJECT_NAME_RE.match(name):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"invalid project name {name!r}: must match [A-Za-z0-9._-]+. "
|
|
53
|
+
"The project name is the basename of work_dir and is interpolated into "
|
|
54
|
+
"ssh remote commands and systemd unit filenames; shell metacharacters "
|
|
55
|
+
"and path separators are rejected."
|
|
56
|
+
)
|
|
57
|
+
return name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def config_path(args) -> Path:
|
|
61
|
+
"""Resolve the config TOML path CLI ``args`` refer to (``--config``,
|
|
62
|
+
default ``./agent-runner.toml``).
|
|
63
|
+
|
|
64
|
+
Single source for "given argparse args, which agent-runner.toml" so every
|
|
65
|
+
CLI bootstrap step (which runs before a ``Config`` exists to read
|
|
66
|
+
``runtime.work_dir`` back from) agrees on the same file, instead of each
|
|
67
|
+
reconstructing its own getattr/default dance.
|
|
68
|
+
"""
|
|
69
|
+
cfg = getattr(args, "config", None)
|
|
70
|
+
return Path(cfg).resolve() if cfg is not None else Path("agent-runner.toml").resolve()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def unit_filename(project: str) -> str:
|
|
74
|
+
"""Thin wrap of the existing ``service_unit.serve_unit_filename`` -- the
|
|
75
|
+
single name every unit-filename call site should go through."""
|
|
76
|
+
return serve_unit_filename(project)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def log_dir(work_dir: Path) -> Path:
|
|
80
|
+
"""Return the configured log_dir from ``<work_dir>/agent-runner.toml``.
|
|
81
|
+
|
|
82
|
+
Falls back to the conventional ``~/.agent-runner/<project>/logs`` only
|
|
83
|
+
when the toml is missing there, keeping lifecycle/observe verbs aligned
|
|
84
|
+
with where ``serve_cmd.py`` actually writes ``serve.pid``.
|
|
85
|
+
"""
|
|
86
|
+
cfg_path = work_dir / "agent-runner.toml"
|
|
87
|
+
if cfg_path.exists():
|
|
88
|
+
return load_config(cfg_path).runtime.log_dir
|
|
89
|
+
return Path.home() / ".agent-runner" / project_name(work_dir, strict=True) / "logs"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _unit_owner(serve_path: Path) -> Path | None:
|
|
93
|
+
"""Parse an existing unit's ``WorkingDirectory=`` line, or None if the file
|
|
94
|
+
doesn't exist / can't be read / has no such line."""
|
|
95
|
+
try:
|
|
96
|
+
text = serve_path.read_text()
|
|
97
|
+
except OSError:
|
|
98
|
+
return None
|
|
99
|
+
for line in text.splitlines():
|
|
100
|
+
if line.startswith("WorkingDirectory="):
|
|
101
|
+
return Path(line.removeprefix("WorkingDirectory="))
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def guard_against_clobber(serve_path: Path, work_dir: Path, *, force: bool) -> None:
|
|
106
|
+
"""Refuse to overwrite a same-basename sibling project's unit.
|
|
107
|
+
|
|
108
|
+
The unit filename is derived from the project name (work_dir's basename)
|
|
109
|
+
alone, so two unrelated projects sharing a basename — or the same project
|
|
110
|
+
moved to a new path — would silently clobber each other's install.
|
|
111
|
+
force=True is the explicit override for the moved-repo case (``api.install``).
|
|
112
|
+
"""
|
|
113
|
+
owner = _unit_owner(serve_path)
|
|
114
|
+
if owner is None or owner == work_dir or force:
|
|
115
|
+
return
|
|
116
|
+
raise FileExistsError(
|
|
117
|
+
f"{serve_path} already manages a different project at {owner} "
|
|
118
|
+
f"(this is {work_dir}); pass force=True to overwrite "
|
|
119
|
+
"(e.g. after moving/renaming that project's directory)."
|
|
120
|
+
)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Pure serve restart policy — no I/O, no clock, no events read.
|
|
2
|
+
|
|
3
|
+
Extracted from api.py (0.2.12 Group G): serve_cmd imports the decision + exit
|
|
4
|
+
codes from here directly (see the architecture allowlist); api.py re-exports
|
|
5
|
+
them for external back-compat. Kept dependency-free so the serve loop stays a
|
|
6
|
+
thin dispatcher and the policy is unit-testable in isolation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from agent_runner.config import ConfigError
|
|
14
|
+
|
|
15
|
+
# Exit code for a permanent (no-retry) startup-battery failure. A broken config
|
|
16
|
+
# does not self-heal between rounds, so serve STOPS rather than respawning it
|
|
17
|
+
# forever. 78 = EX_CONFIG (sysexits) — avoids argparse's 2 and the generic 1.
|
|
18
|
+
PERMANENT_CONFIG_EXIT = 78
|
|
19
|
+
|
|
20
|
+
# Exit code serve returns when the crash-loop breaker trips (a give-up stop, like
|
|
21
|
+
# config_broken). Distinct from 78 so `systemctl status` / SuccessExitStatus can
|
|
22
|
+
# tell the two "needs intervention" stops apart. 75 = EX_TEMPFAIL (sysexits).
|
|
23
|
+
# The systemd unit lists both in RestartPreventExitStatus so a deliberate stop
|
|
24
|
+
# stays stopped (and visibly failed) while an unexpected supervisor crash restarts.
|
|
25
|
+
CRASH_LOOP_EXIT = 75
|
|
26
|
+
|
|
27
|
+
# Exit code for an ENVIRONMENTAL startup-battery failure (ENOSPC, mount hiccup,
|
|
28
|
+
# an unclassified check): recoverable, unlike a permanent config break. NOT in
|
|
29
|
+
# the unit's RestartPreventExitStatus (stays "78 75") so 76 restarts; treated
|
|
30
|
+
# exactly like an active throttle (post_round_decision), so serve keeps
|
|
31
|
+
# retrying at a fixed doubled delay with the crash-loop breaker disarmed,
|
|
32
|
+
# staying alive until the environment heals. StartLimit bounds only repeated
|
|
33
|
+
# serve-PROCESS death, not this round-level path. 76 = EX_PROTOCOL (sysexits).
|
|
34
|
+
ENV_BATTERY_EXIT = 76
|
|
35
|
+
|
|
36
|
+
# Crash-loop circuit breaker (b12). The serve loop escalates the restart delay
|
|
37
|
+
# on consecutive UNKNOWN short crashes (non-zero exit, short duration, no
|
|
38
|
+
# classified transient) and STOPS after CRASH_LOOP_THRESHOLD of them — the Run 6
|
|
39
|
+
# ~100-empty-rounds scar. Recoverable-slow failures (rate limit / 5h quota / 5xx
|
|
40
|
+
# / timeout) are already handled by the transient-error throttle and never reach
|
|
41
|
+
# this path. A clean (exit 0), long, or classified-transient round resets the run.
|
|
42
|
+
CRASH_LOOP_THRESHOLD = 5
|
|
43
|
+
CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
|
|
44
|
+
CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EnvironmentalError(Exception):
|
|
48
|
+
"""Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
|
|
49
|
+
permanent config break — classify_round_exit maps it to ENV_BATTERY_EXIT
|
|
50
|
+
(76), same treatment as an active throttle: serve retries at a flat back-off
|
|
51
|
+
with the crash-loop breaker disarmed.
|
|
52
|
+
|
|
53
|
+
A raise site elsewhere in the package (runner.LockHeldError,
|
|
54
|
+
vcs_state.GitTimeout) mixes this in alongside its own base class instead of
|
|
55
|
+
_serve_policy importing every module that raises one — this leaf module
|
|
56
|
+
sits BELOW runner.py/vcs_state.py in the import graph (both reach here via
|
|
57
|
+
api.py already), so importing upward would cycle. classify_round_exit
|
|
58
|
+
matches this marker, not the concrete class or its OTHER base.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# The action strings below are the restart-action enum, not events.py kinds:
|
|
63
|
+
# "continue" has no constant, and a constant cannot sit inside Literal[...].
|
|
64
|
+
def post_round_decision(
|
|
65
|
+
*,
|
|
66
|
+
returncode: int,
|
|
67
|
+
duration_s: float,
|
|
68
|
+
throttle_active: bool,
|
|
69
|
+
consecutive: int,
|
|
70
|
+
restart_delay_s: int,
|
|
71
|
+
) -> tuple[Literal["config_broken", "crash_loop", "continue"], int, int]:
|
|
72
|
+
"""Restart policy after one round — keeps the serve loop a thin dispatcher.
|
|
73
|
+
|
|
74
|
+
Returns ``(action, delay_s, consecutive)`` where action is:
|
|
75
|
+
- ``"config_broken"`` — permanent startup failure (b18): stop.
|
|
76
|
+
- ``"crash_loop"`` — CRASH_LOOP_THRESHOLD consecutive unknown short crashes
|
|
77
|
+
(b12): stop. An unknown short crash is a non-zero, fast exit with no
|
|
78
|
+
classified transient (rate-limit/5xx/timeout are handled by the throttle).
|
|
79
|
+
- ``"continue"`` — sleep ``delay_s`` then run the next round.
|
|
80
|
+
|
|
81
|
+
A clean (exit 0), long, or transient round resets ``consecutive`` to 0; an
|
|
82
|
+
unknown short crash escalates the delay (restart × 2ⁿ, capped) until the stop.
|
|
83
|
+
An ``ENV_BATTERY_EXIT`` (76, environmental startup-battery failure) is treated
|
|
84
|
+
exactly like an active throttle: it takes the flat ``restart_delay_s * 2``
|
|
85
|
+
delay below (never escalating) and resets ``consecutive`` to 0, so it never
|
|
86
|
+
counts toward the crash-loop breaker — a short environmental outage (ENOSPC,
|
|
87
|
+
mount hiccup) never trips ``crash_loop``. This always returns ``"continue"``,
|
|
88
|
+
so serve itself never exits 76 and systemd's StartLimit window never engages
|
|
89
|
+
for this path; a *persistent* environmental failure keeps serve alive
|
|
90
|
+
retrying at the doubled delay until the environment heals. StartLimit bounds
|
|
91
|
+
only repeated serve-PROCESS death, independent of this round-level path.
|
|
92
|
+
"""
|
|
93
|
+
if returncode == PERMANENT_CONFIG_EXIT:
|
|
94
|
+
return ("config_broken", 0, consecutive)
|
|
95
|
+
throttle_active = throttle_active or returncode == ENV_BATTERY_EXIT
|
|
96
|
+
unknown_short_crash = (
|
|
97
|
+
returncode != 0 and duration_s < CRASH_LOOP_SHORT_EXIT_S and not throttle_active
|
|
98
|
+
)
|
|
99
|
+
if unknown_short_crash:
|
|
100
|
+
consecutive += 1
|
|
101
|
+
if consecutive >= CRASH_LOOP_THRESHOLD:
|
|
102
|
+
return ("crash_loop", 0, consecutive)
|
|
103
|
+
delay = min(restart_delay_s * 2**consecutive, CRASH_LOOP_MAX_DELAY_S)
|
|
104
|
+
return ("continue", delay, consecutive)
|
|
105
|
+
delay = restart_delay_s if returncode == 0 else restart_delay_s * 2
|
|
106
|
+
return ("continue", delay, 0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def classify_round_exit(exc: BaseException) -> int:
|
|
110
|
+
"""The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
|
|
111
|
+
identity, decides serve's response. Replaces the old exception *whitelist*
|
|
112
|
+
(``cli/__init__.py`` mapped only ``ConfigError``->78, ``round_cmd.py`` only
|
|
113
|
+
``KeyboardInterrupt``->130; ~10 other classes fell through to Python's own
|
|
114
|
+
uncaught-exception exit 1 — neither retry-76 nor stay-stopped-78/75).
|
|
115
|
+
|
|
116
|
+
- ``ConfigError`` (won't self-heal) -> ``PERMANENT_CONFIG_EXIT`` (78).
|
|
117
|
+
- ``EnvironmentalError``-marked (self-heals) -> ``ENV_BATTERY_EXIT`` (76).
|
|
118
|
+
- ``KeyboardInterrupt`` (SIGTERM/SIGINT) -> 130, the shell's convention.
|
|
119
|
+
- ``SystemExit`` passes its own code through unchanged (an int code; a
|
|
120
|
+
non-int/absent code — argparse-style ``sys.exit("msg")`` or a bare
|
|
121
|
+
``sys.exit()`` — falls to 1, same as unclassified).
|
|
122
|
+
- Anything else, INCLUDING a totally unclassified traceback, falls to 1 so
|
|
123
|
+
the crash-loop breaker still bounds a genuine supervisor bug. Do NOT
|
|
124
|
+
default unclassified -> 76: ``post_round_decision`` resets its counter on
|
|
125
|
+
76, so an arbitrary bug would loop forever at the doubled delay with no
|
|
126
|
+
breaker and no detector. The battery's own "unclassified -> environmental"
|
|
127
|
+
rule (``startup_check.py``) governs *authored probes*, not arbitrary round
|
|
128
|
+
tracebacks — a different trust boundary.
|
|
129
|
+
|
|
130
|
+
Never returns ``CRASH_LOOP_EXIT`` (75) — that verdict belongs exclusively to
|
|
131
|
+
serve's own ``post_round_decision``, counting consecutive unknown short
|
|
132
|
+
crashes; a round child never claims it directly.
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(exc, SystemExit):
|
|
135
|
+
return exc.code if isinstance(exc.code, int) else 1
|
|
136
|
+
if isinstance(exc, KeyboardInterrupt):
|
|
137
|
+
return 130
|
|
138
|
+
if isinstance(exc, ConfigError):
|
|
139
|
+
return PERMANENT_CONFIG_EXIT
|
|
140
|
+
if isinstance(exc, EnvironmentalError):
|
|
141
|
+
return ENV_BATTERY_EXIT
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Round-timeout budget (Group C, seam 3 — 0.2.13). TimeoutStopSec
|
|
147
|
+
# (service_unit.py, systemd's SIGKILL deadline after `systemctl stop`) and the
|
|
148
|
+
# outer round-wall ceiling (api.outer_round_ceiling_s, the in-process watchdog
|
|
149
|
+
# around the round subprocess) were derived independently and could drift —
|
|
150
|
+
# TimeoutStopSec only added a flat 60s to the inner round timeout, with no
|
|
151
|
+
# margin for the outer ceiling's own reap/git-commit/hook slack, let alone the
|
|
152
|
+
# SIGTERM-to-round-child grace on top of that. One function computes both so
|
|
153
|
+
# they cannot disagree.
|
|
154
|
+
#
|
|
155
|
+
# service_unit.py must not import api.py (cycle: api.py already imports
|
|
156
|
+
# service_unit.render_serve_unit), so this lives here — a dependency-free leaf
|
|
157
|
+
# both service_unit.py and api.py can import. Its margin constants are
|
|
158
|
+
# LITERAL mirrors of the real sources of truth (agent_runtime.REAP_GRACE_S,
|
|
159
|
+
# vcs_state.GIT_COMMIT_TIMEOUT_S, api._ROUND_TERM_GRACE_S) rather than
|
|
160
|
+
# imports, same as service_unit.py's literal "78 75" RestartPreventExitStatus
|
|
161
|
+
# mirroring PERMANENT_CONFIG_EXIT/CRASH_LOOP_EXIT — pinned by
|
|
162
|
+
# test_timeout_budget_invariant.py so a change to any of the three can't
|
|
163
|
+
# silently drift this budget out of sync.
|
|
164
|
+
_REAP_GRACE_S = 5
|
|
165
|
+
_GIT_COMMIT_TIMEOUT_S = 120
|
|
166
|
+
_HOOK_ALLOWANCE_S = 60 # slack for post-round hooks between the inner wall and the ceiling
|
|
167
|
+
_ROUND_TERM_GRACE_S = 15 # mirrors api._ROUND_TERM_GRACE_S / serve_cmd._ROUND_TERM_GRACE_S
|
|
168
|
+
_STOP_GRACE_MARGIN_S = 10 # pad above _ROUND_TERM_GRACE_S for systemd stop-request overhead
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def timeout_budget(round_timeout_s: int) -> tuple[int, int]:
|
|
172
|
+
"""Single source for the round-timeout safety budget.
|
|
173
|
+
|
|
174
|
+
Returns ``(timeout_stop_sec, outer_ceiling_s)``:
|
|
175
|
+
|
|
176
|
+
- ``outer_ceiling_s`` — the in-process outer wall-clock ceiling for the
|
|
177
|
+
round subprocess (``api.outer_round_ceiling_s``): ``round_timeout_s``
|
|
178
|
+
plus reap grace + git-commit ceiling + hook allowance, so it only trips
|
|
179
|
+
when the round supervisor itself is wedged, never during its own
|
|
180
|
+
bounded post-round cleanup.
|
|
181
|
+
- ``timeout_stop_sec`` — systemd's ``TimeoutStopSec``
|
|
182
|
+
(``service_unit.py``): must clear ``outer_ceiling_s`` by enough for a
|
|
183
|
+
SIGTERM to reach and drain the round (``_ROUND_TERM_GRACE_S``) plus a
|
|
184
|
+
stop-request overhead pad, so `systemctl stop` never SIGKILLs a round
|
|
185
|
+
that is draining normally.
|
|
186
|
+
"""
|
|
187
|
+
outer_ceiling_s = round_timeout_s + _REAP_GRACE_S + _GIT_COMMIT_TIMEOUT_S + _HOOK_ALLOWANCE_S
|
|
188
|
+
timeout_stop_sec = outer_ceiling_s + _ROUND_TERM_GRACE_S + _STOP_GRACE_MARGIN_S
|
|
189
|
+
return timeout_stop_sec, outer_ceiling_s
|