cli-agent-runner 0.2.14__tar.gz → 0.2.16__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.14 → cli_agent_runner-0.2.16}/.vulture-whitelist.py +6 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CHANGELOG.md +33 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/PKG-INFO +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/README.md +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_emit.py +180 -10
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_serve_policy.py +92 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_throttle.py +113 -9
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/api.py +10 -0
- cli_agent_runner-0.2.16/agent_runner/cli/_serve_round.py +442 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/serve_cmd.py +79 -270
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/models.py +17 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/parsers.py +25 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/validators.py +12 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/defenses.py +33 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/events.py +6 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/host_health.py +34 -27
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/metrics.py +108 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/monitor.py +4 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/round_log.py +10 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/service_unit.py +9 -3
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/architecture.md +16 -6
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/commands.md +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/configuration.md +9 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/migrations/0.2.md +110 -3
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/runbook.md +60 -16
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/contract/test_public_api_surface.py +11 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_owned_paths.py +3 -3
- cli_agent_runner-0.2.16/tests/integration/test_spawn_round_mem_floor.py +441 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_architecture.py +112 -21
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_claims_match_ssot.py +11 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_kinds_ssot.py +12 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_module_boundaries.py +3 -3
- cli_agent_runner-0.2.16/tests/invariants/test_no_internal_refs.py +39 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_peek_schema_version.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_repo_constants_patched_in_tests.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_service.py +30 -0
- cli_agent_runner-0.2.16/tests/unit/test_cgroup_probe.py +233 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_claude_error_detector.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config.py +165 -6
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_defenses.py +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_gemini_plugin.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_host_health.py +47 -0
- cli_agent_runner-0.2.16/tests/unit/test_serve_crash_loop.py +640 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_mem_gate.py +25 -1
- cli_agent_runner-0.2.16/tests/unit/test_serve_policy.py +110 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_round_log.py +39 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_service_unit.py +27 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_vcs_state.py +2 -2
- cli_agent_runner-0.2.14/tests/integration/test_spawn_round_mem_floor.py +0 -224
- cli_agent_runner-0.2.14/tests/unit/test_serve_crash_loop.py +0 -198
- cli_agent_runner-0.2.14/tests/unit/test_serve_policy.py +0 -29
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.gitignore +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/LICENSE +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/README.zh.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/build.sh +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/README.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/events.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_upgrade_migrate.py +0 -0
|
@@ -148,6 +148,12 @@ supervisor_stale_threshold_s
|
|
|
148
148
|
mem_avail_min_mb
|
|
149
149
|
disk_warning_pct
|
|
150
150
|
disk_critical_pct
|
|
151
|
+
swap_sout_noise_floor_mb
|
|
152
|
+
mem_free_low_mb
|
|
153
|
+
psi_full_avg10_critical
|
|
154
|
+
psi_some_avg10_warning
|
|
155
|
+
mem_critical_consecutive_samples
|
|
156
|
+
in_round_mem_terminate
|
|
151
157
|
|
|
152
158
|
# agent_runner.config.models.PhaseOverride
|
|
153
159
|
round_timeout_s
|
|
@@ -5,6 +5,39 @@ 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.16] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[monitor.host_health]` gains PSI knobs — `psi_full_avg10_critical` (default 60.0) and `psi_some_avg10_warning` (default 5.0) — plus `mem_critical_consecutive_samples` (default 3) and an `in_round_mem_terminate` off switch for the mid-round memory floor.
|
|
12
|
+
- `serve` probes this process's cgroup v2 memory budget at startup (`host_cgroup_memory_limit`); when `memory.max` and `memory.swap.max` are both finite, the mid-round floor defers to kernel cgroup-OOM (`mem_pressure_deferred_to_cgroup`) instead of terminating the round itself.
|
|
13
|
+
- `mem_loop` now escalates across restarts: recurring 3 times inside a rolling 2h window stops the service for real (`mem_loop_persistent`, exit `70`) instead of respawning into the same loop forever.
|
|
14
|
+
- New exit-0 no-progress breaker: 5 consecutive fast, clean rounds with no recorded usage (a CLI that exits 0 without ever reaching the model) now stop `serve` (`stalled_no_progress`, exit `75`) instead of spinning invisibly.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- PSI-full critical was a single ≥1% momentary sample — a hiccup, not a coma — on every PSI-capable host. It now requires a sustained ≥60% avg10 held for `mem_critical_consecutive_samples` (default 3, ~30s) consecutive checks.
|
|
18
|
+
- The mid-round swap-out check is now per-tick instead of cumulative since round-start, so a host that recovers mid-round is no longer permanently flagged for the rest of the round.
|
|
19
|
+
- The pre-round admission gate now defers only on `critical` pressure, not on any pressure (a mere `warning`, e.g. ordinary swap churn, no longer delays the next round).
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
- New events: `host_cgroup_memory_limit`, `mem_pressure_deferred_to_cgroup`, `mem_loop_persistent`, `stalled_no_progress`, `round_mem_critical_sample`; `round_mem_terminated` gained `consecutive`/`context` fields. Not breaking: no `agent-runner migrate` needed, `peek --json` schema is unchanged.
|
|
23
|
+
- Re-render the systemd unit (`agent-runner install`) after upgrading: the new unit adds exit `70` to `RestartPreventExitStatus` so `mem_loop_persistent` stops the service instead of restarting into the same loop.
|
|
24
|
+
|
|
25
|
+
See `docs/migrations/0.2.md`.
|
|
26
|
+
|
|
27
|
+
## [0.2.15] - 2026-09-04
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `[monitor.host_health]` gains two config floors — `swap_sout_noise_floor_mb` (default 32) and `mem_free_low_mb` (default 16) — making the 0.2.14 mem-pressure ladder's thresholds operator-tunable; defaults are byte-identical to the prior hardcoded behavior.
|
|
31
|
+
- `serve` now gives up after `MEM_LOOP_THRESHOLD` (5) consecutive mem-terminated rounds instead of retrying forever: emits a new `mem_loop` event and exits `71` — deliberately excluded from the systemd unit's `RestartPreventExitStatus` so the service restarts fresh (break-then-restart).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- The mem-pressure warning no longer claims "active paging" it can't confirm; it now says only that swap-out crossed the configured noise floor.
|
|
35
|
+
|
|
36
|
+
### Notes
|
|
37
|
+
- New event kind: `mem_loop`. Not breaking: no `agent-runner migrate` needed this release — nothing that used to load is rejected. `peek --json` schema is unchanged.
|
|
38
|
+
|
|
39
|
+
See `docs/migrations/0.2.md`.
|
|
40
|
+
|
|
8
41
|
## [0.2.14] - 2026-09-03
|
|
9
42
|
|
|
10
43
|
### Breaking
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
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
|
|
@@ -94,7 +94,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
94
94
|
|
|
95
95
|
## Defenses (built in)
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
15 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
98
98
|
Each carries the historical incident it codifies and the invariant test that
|
|
99
99
|
guards it. Highlights:
|
|
100
100
|
|
|
@@ -58,7 +58,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
58
58
|
|
|
59
59
|
## Defenses (built in)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
15 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
62
62
|
Each carries the historical incident it codifies and the invariant test that
|
|
63
63
|
guards it. Highlights:
|
|
64
64
|
|
|
@@ -19,12 +19,17 @@ __all__ = [
|
|
|
19
19
|
"emit_anomaly_repetitive_tool",
|
|
20
20
|
"emit_config_migrated",
|
|
21
21
|
"emit_fresh_eyes_round_triggered",
|
|
22
|
+
"emit_host_cgroup_memory_limit",
|
|
22
23
|
"emit_max_rounds_reached",
|
|
24
|
+
"emit_mem_loop",
|
|
25
|
+
"emit_mem_loop_persistent",
|
|
26
|
+
"emit_mem_pressure_deferred_to_cgroup",
|
|
23
27
|
"emit_rate_limit_stop",
|
|
24
28
|
"emit_round_deferred",
|
|
25
29
|
"emit_round_grace_extended",
|
|
26
30
|
"emit_round_grace_kill",
|
|
27
31
|
"emit_round_logs_prune_deferred",
|
|
32
|
+
"emit_round_mem_critical_sample",
|
|
28
33
|
"emit_round_mem_terminated",
|
|
29
34
|
"emit_round_progress",
|
|
30
35
|
"emit_round_resumed",
|
|
@@ -35,6 +40,7 @@ __all__ = [
|
|
|
35
40
|
"emit_schedule_phase_skipped",
|
|
36
41
|
"emit_schedule_resumed",
|
|
37
42
|
"emit_stale_index_lock_cleared",
|
|
43
|
+
"emit_stalled_no_progress",
|
|
38
44
|
"emit_stop_file_detected",
|
|
39
45
|
"emit_transient_error_backoff_capped",
|
|
40
46
|
"emit_transient_error_detected",
|
|
@@ -64,20 +70,103 @@ def emit_config_broken(log_dir: Path, *, reason: str) -> None:
|
|
|
64
70
|
emit(log_dir, CONFIG_BROKEN, reason=reason)
|
|
65
71
|
|
|
66
72
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
def _emit_giveup(
|
|
74
|
+
log_dir: Path, kind: str, *, consecutive: int, exit_code: int, log_path: Path
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Shared body for the give-up events (serve stopped/broke after consecutive
|
|
77
|
+
failures of one kind): capture a redacted tail of the round log as ``reason``
|
|
78
|
+
so the recurring failure can be inspected (or later classified into a
|
|
79
|
+
transient bucket), and emit ``kind`` with the standard give-up payload.
|
|
72
80
|
"""
|
|
73
81
|
from agent_runner._redact import redact_secrets
|
|
74
|
-
from agent_runner.events import
|
|
82
|
+
from agent_runner.events import emit
|
|
75
83
|
|
|
76
84
|
try:
|
|
77
85
|
reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
|
|
78
86
|
except OSError:
|
|
79
87
|
reason = ""
|
|
80
|
-
emit(log_dir,
|
|
88
|
+
emit(log_dir, kind, consecutive=consecutive, exit_code=exit_code, reason=reason)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
|
|
92
|
+
"""Emit crash_loop (serve stopped after consecutive unknown short crashes).
|
|
93
|
+
|
|
94
|
+
Captures the failure reason — a redacted tail of the round log — so a
|
|
95
|
+
recurring unknown crash can later be classified into a transient bucket.
|
|
96
|
+
"""
|
|
97
|
+
from agent_runner.events import CRASH_LOOP
|
|
98
|
+
|
|
99
|
+
_emit_giveup(
|
|
100
|
+
log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def emit_mem_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
|
|
105
|
+
"""Emit mem_loop (serve gave up after consecutive mid-round memory-pressure
|
|
106
|
+
terminations — the 0.2.15 coma-preventer's give-up cap). Distinct from
|
|
107
|
+
crash_loop: this is a break-then-restart, not a deliberate stop, so it is
|
|
108
|
+
deliberately absent from the unit's RestartPreventExitStatus.
|
|
109
|
+
|
|
110
|
+
Captures the failure reason — a redacted tail of the round log — same as
|
|
111
|
+
crash_loop, so an operator can see what the round was doing when the host
|
|
112
|
+
ran out of memory."""
|
|
113
|
+
from agent_runner.events import MEM_LOOP
|
|
114
|
+
|
|
115
|
+
_emit_giveup(log_dir, MEM_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def emit_mem_loop_persistent(
|
|
119
|
+
log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Emit mem_loop_persistent (serve STOPS for real — 0.2.16 Task 5 cross-restart
|
|
122
|
+
convergence: mem_loop itself kept recurring across restarts within the
|
|
123
|
+
escalation window, so systemd is told to stop rather than respawn into the
|
|
124
|
+
identical loop forever). Distinct from mem_loop: this IS a deliberate stop,
|
|
125
|
+
like crash_loop/config_broken, so it belongs in the unit's
|
|
126
|
+
RestartPreventExitStatus.
|
|
127
|
+
|
|
128
|
+
``consecutive`` counts mem_loop EPISODES within the persistence window
|
|
129
|
+
(up to ``_serve_policy._MEM_LOOP_PERSIST_THRESHOLD``) — NOT mid-round
|
|
130
|
+
terminations within one episode, which is mem_loop's own ``consecutive``
|
|
131
|
+
(up to ``MEM_LOOP_THRESHOLD``). Same redacted-log-tail reason capture as
|
|
132
|
+
crash_loop/mem_loop."""
|
|
133
|
+
from agent_runner.events import MEM_LOOP_PERSISTENT
|
|
134
|
+
|
|
135
|
+
_emit_giveup(
|
|
136
|
+
log_dir,
|
|
137
|
+
MEM_LOOP_PERSISTENT,
|
|
138
|
+
consecutive=consecutive,
|
|
139
|
+
exit_code=exit_code,
|
|
140
|
+
log_path=log_path,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def emit_stalled_no_progress(
|
|
145
|
+
log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Emit stalled_no_progress (serve gave up after consecutive clean-but-
|
|
148
|
+
no-progress rounds -- 0.2.16 Task 6). A round that exits 0 fast with no
|
|
149
|
+
``agent_usage_recorded`` never reached the model (pi, and CLIs like it,
|
|
150
|
+
exit 0 on a provider failure) -- ``_round_ok = exit_code == 0``
|
|
151
|
+
(api_types.py) reads that as clean, so without this breaker it is a fast,
|
|
152
|
+
invisible, unclassified-failure spin, no different in kind from an
|
|
153
|
+
unknown short crash except for the exit code it hides behind.
|
|
154
|
+
|
|
155
|
+
Deliberately reuses ``CRASH_LOOP_EXIT`` (75), not a new exit code: this is
|
|
156
|
+
the SAME give-up verdict as crash_loop ("an unknown failure kept
|
|
157
|
+
recurring, stop for real") reached via a different signal (no usage
|
|
158
|
+
instead of a non-zero exit) -- not a new failure class, so it needs no new
|
|
159
|
+
entry in the systemd unit's ``RestartPreventExitStatus``. Same redacted-
|
|
160
|
+
log-tail reason capture as crash_loop/mem_loop."""
|
|
161
|
+
from agent_runner.events import STALLED_NO_PROGRESS
|
|
162
|
+
|
|
163
|
+
_emit_giveup(
|
|
164
|
+
log_dir,
|
|
165
|
+
STALLED_NO_PROGRESS,
|
|
166
|
+
consecutive=consecutive,
|
|
167
|
+
exit_code=exit_code,
|
|
168
|
+
log_path=log_path,
|
|
169
|
+
)
|
|
81
170
|
|
|
82
171
|
|
|
83
172
|
def emit_config_migrated(
|
|
@@ -172,15 +261,96 @@ def emit_round_resumed(log_dir: Path, *, deferred_for_s: int) -> None:
|
|
|
172
261
|
|
|
173
262
|
|
|
174
263
|
def emit_round_mem_terminated(
|
|
175
|
-
log_dir: Path,
|
|
264
|
+
log_dir: Path,
|
|
265
|
+
*,
|
|
266
|
+
pid: int,
|
|
267
|
+
severity: str,
|
|
268
|
+
signal: str,
|
|
269
|
+
message: str,
|
|
270
|
+
consecutive: int,
|
|
271
|
+
context: dict,
|
|
176
272
|
) -> None:
|
|
177
273
|
"""Emit when _spawn_round's mid-round hard floor terminated a ballooning
|
|
178
274
|
round on critical host_health pressure -- the actual coma-preventer (a
|
|
179
275
|
pre-round-only gate can't stop a single round mid-flight). Distinct from
|
|
180
|
-
round_supervisor_wedged (a wall-clock ceiling breach, unrelated cause).
|
|
276
|
+
round_supervisor_wedged (a wall-clock ceiling breach, unrelated cause).
|
|
277
|
+
|
|
278
|
+
0.2.16: ``consecutive`` (the critical_streak that crossed the threshold)
|
|
279
|
+
and ``context`` (Pressure.context -- the actual psi/mem numbers, e.g.
|
|
280
|
+
psi_full_avg10) make the kill legible from the event stream alone, so an
|
|
281
|
+
operator can retune host_health thresholds without SSH."""
|
|
181
282
|
from agent_runner.events import ROUND_MEM_TERMINATED, emit
|
|
182
283
|
|
|
183
|
-
emit(
|
|
284
|
+
emit(
|
|
285
|
+
log_dir,
|
|
286
|
+
ROUND_MEM_TERMINATED,
|
|
287
|
+
pid=pid,
|
|
288
|
+
severity=severity,
|
|
289
|
+
signal=signal,
|
|
290
|
+
message=message,
|
|
291
|
+
consecutive=consecutive,
|
|
292
|
+
context=context,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def emit_round_mem_critical_sample(
|
|
297
|
+
log_dir: Path, *, round_num: int, consecutive: int, context: dict
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Emit on EACH critical host_health sample inside _spawn_round's mid-round
|
|
300
|
+
hard floor -- deliberately NOT deduped (unlike round_mem_terminated): the
|
|
301
|
+
point is calibration visibility into near-misses, so an operator watching
|
|
302
|
+
the event stream sees the critical_streak build (1, 2, ...) even on ticks
|
|
303
|
+
that never reach the terminate threshold (a healthy tick resets it before
|
|
304
|
+
3-in-a-row). ``context`` carries the same Pressure.context numbers as
|
|
305
|
+
round_mem_terminated. Fires only during critical pressure -- never during
|
|
306
|
+
warning/healthy -- so it adds no normal-operation noise, including while
|
|
307
|
+
``defer_to_cgroup`` steps back from terminating (the streak/context is
|
|
308
|
+
still useful calibration there; this event stays scoped to the
|
|
309
|
+
sample-level signal, distinct from mem_pressure_deferred_to_cgroup's
|
|
310
|
+
once-per-episode terminate-vs-defer notice)."""
|
|
311
|
+
from agent_runner.events import ROUND_MEM_CRITICAL_SAMPLE, emit
|
|
312
|
+
|
|
313
|
+
emit(
|
|
314
|
+
log_dir,
|
|
315
|
+
ROUND_MEM_CRITICAL_SAMPLE,
|
|
316
|
+
round_num=round_num,
|
|
317
|
+
consecutive=consecutive,
|
|
318
|
+
context=context,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def emit_host_cgroup_memory_limit(
|
|
323
|
+
log_dir: Path, *, memory_max: int | None, memory_swap_max: int | None, cgroup_path: str | None
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Emit once at serve startup: this process's cgroup v2 memory budget
|
|
326
|
+
(``metrics.cgroup_memory_limits``). ``None`` fields mean unlimited (or
|
|
327
|
+
cgroup v2 unavailable). Serve uses this once to decide whether the
|
|
328
|
+
mid-round hard floor can defer to kernel cgroup-OOM -- see
|
|
329
|
+
``emit_mem_pressure_deferred_to_cgroup`` below."""
|
|
330
|
+
from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
|
|
331
|
+
|
|
332
|
+
emit(
|
|
333
|
+
log_dir,
|
|
334
|
+
HOST_CGROUP_MEMORY_LIMIT,
|
|
335
|
+
memory_max=memory_max,
|
|
336
|
+
memory_swap_max=memory_swap_max,
|
|
337
|
+
cgroup_path=cgroup_path,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def emit_mem_pressure_deferred_to_cgroup(
|
|
342
|
+
log_dir: Path, *, pid: int, signal: str, message: str
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Emit when _spawn_round's mid-round hard floor hits sustained critical
|
|
345
|
+
pressure but this cgroup's (mem+swap) budget is bounded end to end
|
|
346
|
+
(both memory.max and memory.swap.max finite) -- kernel cgroup-OOM will
|
|
347
|
+
contain the agent and keep the host responsive on its own, so the
|
|
348
|
+
cruder host-wide round-kill steps back instead of firing
|
|
349
|
+
round_mem_terminated. This OVERRIDES in_round_mem_terminate=True: a
|
|
350
|
+
bounded cgroup makes the host floor strictly worse, not just redundant."""
|
|
351
|
+
from agent_runner.events import MEM_PRESSURE_DEFERRED_TO_CGROUP, emit
|
|
352
|
+
|
|
353
|
+
emit(log_dir, MEM_PRESSURE_DEFERRED_TO_CGROUP, pid=pid, signal=signal, message=message)
|
|
184
354
|
|
|
185
355
|
|
|
186
356
|
def emit_round_substrate_before(
|
|
@@ -43,6 +43,57 @@ CRASH_LOOP_THRESHOLD = 5
|
|
|
43
43
|
CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
|
|
44
44
|
CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
|
|
45
45
|
|
|
46
|
+
# Exit code for the mem-loop give-up cap (0.2.15 coma-preventer, Task 4): serve
|
|
47
|
+
# gives up after MEM_LOOP_THRESHOLD consecutive mid-round memory-terminated
|
|
48
|
+
# rounds rather than retrying forever. Distinct from CRASH_LOOP_EXIT (75) on
|
|
49
|
+
# purpose: this is a "break-then-restart", NOT a deliberate stop — the host
|
|
50
|
+
# memory condition may have cleared by the time systemd respawns a fresh serve
|
|
51
|
+
# process, so unlike config_broken/crash_loop this code is deliberately absent
|
|
52
|
+
# from the unit's RestartPreventExitStatus. 71 = EX_OSERR (sysexits).
|
|
53
|
+
MEM_LOOP_EXIT = 71
|
|
54
|
+
# Consecutive mem-terminated rounds before serve gives up (break-then-restart).
|
|
55
|
+
MEM_LOOP_THRESHOLD = 5
|
|
56
|
+
|
|
57
|
+
# Exit code for mem_loop's cross-restart escalation (0.2.16 Task 5 — field
|
|
58
|
+
# confirmed: NRestarts climbs, never converges). MEM_LOOP_EXIT (71) alone
|
|
59
|
+
# resets on every process restart, so a host stuck in sustained pressure
|
|
60
|
+
# respawns into the identical break-then-restart loop forever. Once mem_loop
|
|
61
|
+
# itself keeps recurring across restarts (see _MEM_LOOP_PERSIST_THRESHOLD),
|
|
62
|
+
# this is a DELIBERATE stop instead — "gave up for real, needs a human" — so,
|
|
63
|
+
# unlike 71, it IS listed in the unit's RestartPreventExitStatus. 70 =
|
|
64
|
+
# EX_SOFTWARE (sysexits); verified free (grep -rnE "= 70\b|code=70\b").
|
|
65
|
+
MEM_LOOP_PERSISTENT_EXIT = 70
|
|
66
|
+
|
|
67
|
+
# Escalation window/threshold for the cross-restart convergence above: prior
|
|
68
|
+
# mem_loop events are counted only within the last _MEM_LOOP_PERSIST_WINDOW_S
|
|
69
|
+
# seconds (2h) of "now" — old episodes age out on their own, so a host that
|
|
70
|
+
# stays healthy for a sustained window earns a fresh restartable 71 again
|
|
71
|
+
# instead of ratcheting toward the persistent stop forever (no state file:
|
|
72
|
+
# purely events-tail derived, restart-safe by construction). Once this run's
|
|
73
|
+
# mem_loop would be the _MEM_LOOP_PERSIST_THRESHOLD-th (3rd) occurrence inside
|
|
74
|
+
# the window (~2 prior + this one, ⇒ ~15 mem-terminated kills total), serve
|
|
75
|
+
# escalates to MEM_LOOP_PERSISTENT_EXIT instead of the usual 71.
|
|
76
|
+
_MEM_LOOP_PERSIST_WINDOW_S = 7200
|
|
77
|
+
_MEM_LOOP_PERSIST_THRESHOLD = 3
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Exit-0 no-progress breaker (0.2.16 Task 6). Some CLIs (pi -- see
|
|
81
|
+
# builtin_plugins/pi.py's "pi exits 0 on provider failure") exit 0 on a
|
|
82
|
+
# provider failure that never reaches the model: `_round_ok = exit_code == 0`
|
|
83
|
+
# (api_types.py) reads that as a clean round, so without this breaker an
|
|
84
|
+
# invalid credential (or an exhausted-retries outage) spins as a fast,
|
|
85
|
+
# invisible "success" loop with no breaker, no back-off, and no alert -- on a
|
|
86
|
+
# constrained host, that tight loop is itself a memory-pressure generator.
|
|
87
|
+
# "No progress" = exit 0, a SHORT round (< _NO_PROGRESS_SHORT_S), and no
|
|
88
|
+
# agent_usage_recorded for it (see _throttle.round_had_no_progress for the
|
|
89
|
+
# events-derived scoping). Deliberately reuses CRASH_LOOP_THRESHOLD /
|
|
90
|
+
# CRASH_LOOP_EXIT rather than minting new ones: this is the SAME give-up
|
|
91
|
+
# verdict as the crash-loop breaker ("an unknown failure kept recurring, stop
|
|
92
|
+
# for real"), reached via a different signal (no usage instead of a non-zero
|
|
93
|
+
# exit) -- not a new failure class needing its own systemd
|
|
94
|
+
# RestartPreventExitStatus entry.
|
|
95
|
+
_NO_PROGRESS_SHORT_S = 30
|
|
96
|
+
|
|
46
97
|
|
|
47
98
|
class EnvironmentalError(Exception):
|
|
48
99
|
"""Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
|
|
@@ -106,6 +157,47 @@ def post_round_decision(
|
|
|
106
157
|
return ("continue", delay, 0)
|
|
107
158
|
|
|
108
159
|
|
|
160
|
+
def _mem_loop_decision(
|
|
161
|
+
*, mem_terminated: bool, consecutive: int
|
|
162
|
+
) -> tuple[Literal["mem_loop", "continue"], int]:
|
|
163
|
+
"""Mem-loop give-up cap (0.2.15 Task 4) — a NEW, SEPARATE, private counter
|
|
164
|
+
from ``post_round_decision``'s crash-loop breaker, not a widening of its
|
|
165
|
+
3-tuple contract. A mem-terminated round already sets ``throttle_active``
|
|
166
|
+
for ``post_round_decision`` (never counted as a crash), so without this
|
|
167
|
+
cap a sustained memory-pressure host would retry forever instead of
|
|
168
|
+
eventually giving up. Returns ``("mem_loop", consecutive)`` once
|
|
169
|
+
``consecutive`` reaches ``MEM_LOOP_THRESHOLD``; a non-mem-terminated round
|
|
170
|
+
resets the counter to 0, same reset shape as the crash-loop breaker."""
|
|
171
|
+
if not mem_terminated:
|
|
172
|
+
return ("continue", 0)
|
|
173
|
+
consecutive += 1
|
|
174
|
+
if consecutive >= MEM_LOOP_THRESHOLD:
|
|
175
|
+
return ("mem_loop", consecutive)
|
|
176
|
+
return ("continue", consecutive)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _no_progress_decision(
|
|
180
|
+
*, no_progress: bool, consecutive: int
|
|
181
|
+
) -> tuple[Literal["stalled_no_progress", "continue"], int]:
|
|
182
|
+
"""Exit-0 no-progress give-up cap (0.2.16 Task 6) -- a NEW, SEPARATE,
|
|
183
|
+
private counter from ``post_round_decision``'s crash-loop breaker (that one
|
|
184
|
+
keys on ``returncode != 0``; here returncode IS 0, so a round that never
|
|
185
|
+
reached the model would otherwise sail through with no breaker at all).
|
|
186
|
+
Same shape as :func:`_mem_loop_decision`: returns ``("stalled_no_progress",
|
|
187
|
+
consecutive)`` once ``consecutive`` reaches ``CRASH_LOOP_THRESHOLD`` --
|
|
188
|
+
reusing that threshold (and, at the call site, ``CRASH_LOOP_EXIT``) rather
|
|
189
|
+
than minting new ones, since this is the identical give-up verdict reached
|
|
190
|
+
via a different signal. A round WITH progress (usage recorded, non-zero
|
|
191
|
+
exit, or slow) resets the counter to 0, same reset shape as
|
|
192
|
+
crash-loop/mem-loop."""
|
|
193
|
+
if not no_progress:
|
|
194
|
+
return ("continue", 0)
|
|
195
|
+
consecutive += 1
|
|
196
|
+
if consecutive >= CRASH_LOOP_THRESHOLD:
|
|
197
|
+
return ("stalled_no_progress", consecutive)
|
|
198
|
+
return ("continue", consecutive)
|
|
199
|
+
|
|
200
|
+
|
|
109
201
|
def classify_round_exit(exc: BaseException) -> int:
|
|
110
202
|
"""The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
|
|
111
203
|
identity, decides serve's response. Replaces the old exception *whitelist*
|
|
@@ -22,6 +22,7 @@ from agent_runner.api_types import TransientErrorState
|
|
|
22
22
|
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
23
23
|
from agent_runner.events import (
|
|
24
24
|
AGENT_USAGE_RECORDED,
|
|
25
|
+
MEM_LOOP,
|
|
25
26
|
ROUND_MEM_TERMINATED,
|
|
26
27
|
ROUND_SUBSTRATE_BEFORE,
|
|
27
28
|
TRANSIENT_ERROR_DETECTED,
|
|
@@ -37,7 +38,9 @@ __all__ = [
|
|
|
37
38
|
"_interruptible_sleep",
|
|
38
39
|
"compute_adjusted_reset_at",
|
|
39
40
|
"effective_throttle_view",
|
|
41
|
+
"mem_loop_events_in_window",
|
|
40
42
|
"pending_recovered",
|
|
43
|
+
"round_had_no_progress",
|
|
41
44
|
"round_was_mem_terminated",
|
|
42
45
|
]
|
|
43
46
|
|
|
@@ -277,21 +280,20 @@ def _active_throttles(
|
|
|
277
280
|
def round_was_mem_terminated(log_dir: Path) -> bool:
|
|
278
281
|
"""True iff the round that JUST ran was killed by ``_spawn_round``'s
|
|
279
282
|
mid-round memory-pressure hard floor (``round_mem_terminated``) rather
|
|
280
|
-
than a genuine crash — so ``serve_cmd.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
an explicit signal.
|
|
283
|
+
than a genuine crash — so ``serve_cmd.cmd`` can excuse it from the
|
|
284
|
+
crash-loop breaker exactly like an active throttle (flat back-off,
|
|
285
|
+
``consecutive`` reset to 0), the same treatment ``ENV_BATTERY_EXIT``
|
|
286
|
+
already gets. A mem-terminated round can be killed within the first ~10s
|
|
287
|
+
(the mid-round check's own cadence) — far under the crash-loop breaker's
|
|
288
|
+
60s "short crash" window — so, unlike a wall-clock-ceiling wedge (whose
|
|
289
|
+
long duration alone dodges the breaker), this needs an explicit signal.
|
|
288
290
|
|
|
289
291
|
Scoped to "this round" (not some earlier one) by comparing the newest
|
|
290
292
|
``round_mem_terminated`` event's timestamp against the newest
|
|
291
293
|
``round_substrate_before``'s: ``round_substrate_before(N)`` always
|
|
292
294
|
precedes round N's own attempt, and no later round's
|
|
293
295
|
``round_substrate_before(N+1)`` has been emitted yet at the point
|
|
294
|
-
``
|
|
296
|
+
``cmd()`` runs this check (right after ``_spawn_round`` returns) —
|
|
295
297
|
``round_substrate_after``, also emitted before this check runs, cannot
|
|
296
298
|
serve as that boundary, since it always comes AFTER any
|
|
297
299
|
``round_mem_terminated`` within the very round it is scoping. ``>=``, not
|
|
@@ -315,6 +317,108 @@ def round_was_mem_terminated(log_dir: Path) -> bool:
|
|
|
315
317
|
return parse_iso_ms(newest_terminated_ts) >= parse_iso_ms(newest_before_ts)
|
|
316
318
|
|
|
317
319
|
|
|
320
|
+
def round_had_no_progress(
|
|
321
|
+
log_dir: Path,
|
|
322
|
+
*,
|
|
323
|
+
returncode: int,
|
|
324
|
+
duration_s: float,
|
|
325
|
+
threshold_s: float,
|
|
326
|
+
throttle_active: bool = False,
|
|
327
|
+
) -> bool:
|
|
328
|
+
"""True iff the round that JUST ran exited 0, finished fast, but never
|
|
329
|
+
reached the model -- pi (and CLIs like it: see builtin_plugins/pi.py's
|
|
330
|
+
"pi exits 0 on provider failure") can exit 0 on an auth failure or an
|
|
331
|
+
exhausted-retries outage that ``_round_ok = exit_code == 0`` (api_types.py)
|
|
332
|
+
reads as clean, so this needs its own events-derived signal (feeding
|
|
333
|
+
``_serve_policy._no_progress_decision``'s "clean-but-no-progress" streak)
|
|
334
|
+
exactly parallel to :func:`round_was_mem_terminated`'s mem-terminated one.
|
|
335
|
+
|
|
336
|
+
TWO gates guard the verdict against over-firing on healthy deployments
|
|
337
|
+
(0.2.16 fix-wave CRITICAL #1):
|
|
338
|
+
|
|
339
|
+
1. ``throttle_active`` (the SAME value ``cmd()`` already computed for
|
|
340
|
+
``post_round_decision`` -- caller threads it through, never
|
|
341
|
+
recomputed here): a provider outage that exhausts retries and exits 0
|
|
342
|
+
(429/503) is ``transient_error_detected`` and excused from the
|
|
343
|
+
crash-loop breaker; without this gate it would be double-counted here
|
|
344
|
+
as "no progress" and stop the loop instead of riding the back-off.
|
|
345
|
+
2. Usage-capability: armed ONLY when at least one ``agent_usage_recorded``
|
|
346
|
+
event exists anywhere in the scanned tail. Some CLIs (kimi -- see
|
|
347
|
+
builtin_plugins/kimi.py; aider; any custom ``[agent] command`` with no
|
|
348
|
+
usage-emitting plugin) never emit usage BY DESIGN -- for those, "no
|
|
349
|
+
usage this round" is indistinguishable from "normal for this CLI", so
|
|
350
|
+
arming would stop a perfectly healthy deployment after 5 fast clean
|
|
351
|
+
rounds. If the CLI's plugin stack ever emits usage (pi does on a good
|
|
352
|
+
round), a round with none is genuine no-progress and still trips --
|
|
353
|
+
this makes the breaker CLI-adaptive with no config descriptor.
|
|
354
|
+
|
|
355
|
+
Short-circuits on ``returncode != 0`` or ``duration_s >= threshold_s``
|
|
356
|
+
without touching the events tail: a non-zero exit already has its own
|
|
357
|
+
crash-loop signal, and a slow round (even with no usage) is not a TIGHT
|
|
358
|
+
loop -- a wedged/hung round already has its own signal
|
|
359
|
+
(``round_supervisor_wedged``), so this floor is specifically the fast spin.
|
|
360
|
+
|
|
361
|
+
"Never reached the model" = no ``agent_usage_recorded`` event stamped at
|
|
362
|
+
or after this round's ``round_substrate_before`` -- the CLI plugins only
|
|
363
|
+
emit usage when the round actually consumed tokens (e.g.
|
|
364
|
+
``builtin_plugins/pi.py``'s ``_aggregate_usage`` returns ``None`` on
|
|
365
|
+
all-zero usage), so an auth failure or exhausted retries leaves no such
|
|
366
|
+
event. Same round-scoping shape as :func:`round_was_mem_terminated`
|
|
367
|
+
(newest-event-timestamp comparison against the newest
|
|
368
|
+
``round_substrate_before``), with the comparison inverted: no progress
|
|
369
|
+
means the newest usage event is EITHER absent OR older than this round's
|
|
370
|
+
own start."""
|
|
371
|
+
if throttle_active or returncode != 0 or duration_s >= threshold_s:
|
|
372
|
+
return False
|
|
373
|
+
newest_before_ts: str | None = None
|
|
374
|
+
newest_usage_ts: str | None = None
|
|
375
|
+
usage_capable = False
|
|
376
|
+
for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
|
|
377
|
+
for ev in _iter_events(path):
|
|
378
|
+
kind = ev.get("event")
|
|
379
|
+
if kind == AGENT_USAGE_RECORDED:
|
|
380
|
+
usage_capable = True
|
|
381
|
+
ts = ev.get("ts")
|
|
382
|
+
if not ts:
|
|
383
|
+
continue
|
|
384
|
+
if kind == ROUND_SUBSTRATE_BEFORE:
|
|
385
|
+
newest_before_ts = ts
|
|
386
|
+
elif kind == AGENT_USAGE_RECORDED:
|
|
387
|
+
newest_usage_ts = ts
|
|
388
|
+
if not usage_capable:
|
|
389
|
+
return False # this CLI/plugin stack has never emitted usage -- can't arm
|
|
390
|
+
if newest_before_ts is None:
|
|
391
|
+
return False
|
|
392
|
+
if newest_usage_ts is None:
|
|
393
|
+
return True
|
|
394
|
+
return parse_iso_ms(newest_usage_ts) < parse_iso_ms(newest_before_ts)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def mem_loop_events_in_window(log_dir: Path, clock: Clock, window_s: int) -> int:
|
|
398
|
+
"""Count ``mem_loop`` events in the events tail stamped within the last
|
|
399
|
+
``window_s`` seconds of ``clock.epoch()`` — events-derived, no state file,
|
|
400
|
+
same tail-reconstruction shape as :func:`round_was_mem_terminated` above.
|
|
401
|
+
|
|
402
|
+
Feeds ``_serve_round.round_outcome_exit_code``'s cross-restart escalation
|
|
403
|
+
(0.2.16 Task 5): ``MEM_LOOP_EXIT`` (71) alone resets on every serve
|
|
404
|
+
process restart, so a host stuck in sustained pressure respawns into the
|
|
405
|
+
identical loop forever (field-confirmed: NRestarts climbs, never
|
|
406
|
+
converges). Counting PRIOR occurrences in a bounded window — rather than
|
|
407
|
+
an all-time total, or a restart-local counter — gives "ages out on its
|
|
408
|
+
own after a sustained-healthy stretch" for free: an old mem_loop episode
|
|
409
|
+
outside the window simply stops counting, no explicit reset needed."""
|
|
410
|
+
cutoff = clock.epoch() - window_s
|
|
411
|
+
count = 0
|
|
412
|
+
for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
|
|
413
|
+
for ev in _iter_events(path):
|
|
414
|
+
if ev.get("event") != MEM_LOOP:
|
|
415
|
+
continue
|
|
416
|
+
ts = ev.get("ts")
|
|
417
|
+
if ts and parse_iso_ms(ts).timestamp() >= cutoff:
|
|
418
|
+
count += 1
|
|
419
|
+
return count
|
|
420
|
+
|
|
421
|
+
|
|
318
422
|
def effective_throttle_view(
|
|
319
423
|
log_dir: Path, *, clock: Clock = SYSTEM_CLOCK
|
|
320
424
|
) -> tuple[TransientErrorState | None, dict[str, TransientErrorState]]:
|
|
@@ -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.16'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 16)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -28,6 +28,8 @@ from agent_runner._serve_policy import ( # noqa: F401 — public re-export
|
|
|
28
28
|
CRASH_LOOP_SHORT_EXIT_S,
|
|
29
29
|
CRASH_LOOP_THRESHOLD,
|
|
30
30
|
ENV_BATTERY_EXIT,
|
|
31
|
+
MEM_LOOP_EXIT,
|
|
32
|
+
MEM_LOOP_PERSISTENT_EXIT,
|
|
31
33
|
PERMANENT_CONFIG_EXIT,
|
|
32
34
|
post_round_decision,
|
|
33
35
|
)
|
|
@@ -629,6 +631,8 @@ def _poll_once(
|
|
|
629
631
|
mem_avail_min_mb=cfg.monitor.host_health.mem_avail_min_mb,
|
|
630
632
|
disk_warning_pct=cfg.monitor.host_health.disk_warning_pct,
|
|
631
633
|
disk_critical_pct=cfg.monitor.host_health.disk_critical_pct,
|
|
634
|
+
swap_sout_noise_floor_mb=cfg.monitor.host_health.swap_sout_noise_floor_mb,
|
|
635
|
+
mem_free_low_mb=cfg.monitor.host_health.mem_free_low_mb,
|
|
632
636
|
log_dir=cfg.runtime.log_dir,
|
|
633
637
|
)
|
|
634
638
|
if not monitor._PLUGIN_DETECTORS:
|
|
@@ -783,12 +787,17 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
783
787
|
emit_config_migrated,
|
|
784
788
|
emit_crash_loop,
|
|
785
789
|
emit_fresh_eyes_round_triggered,
|
|
790
|
+
emit_host_cgroup_memory_limit,
|
|
786
791
|
emit_max_rounds_reached,
|
|
792
|
+
emit_mem_loop,
|
|
793
|
+
emit_mem_loop_persistent,
|
|
794
|
+
emit_mem_pressure_deferred_to_cgroup,
|
|
787
795
|
emit_rate_limit_stop,
|
|
788
796
|
emit_round_deferred,
|
|
789
797
|
emit_round_grace_extended,
|
|
790
798
|
emit_round_grace_kill,
|
|
791
799
|
emit_round_logs_prune_deferred,
|
|
800
|
+
emit_round_mem_critical_sample,
|
|
792
801
|
emit_round_mem_terminated,
|
|
793
802
|
emit_round_progress,
|
|
794
803
|
emit_round_resumed,
|
|
@@ -799,6 +808,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
799
808
|
emit_schedule_phase_skipped,
|
|
800
809
|
emit_schedule_resumed,
|
|
801
810
|
emit_stale_index_lock_cleared,
|
|
811
|
+
emit_stalled_no_progress,
|
|
802
812
|
emit_stop_file_detected,
|
|
803
813
|
emit_transient_error_backoff_capped,
|
|
804
814
|
emit_transient_error_detected,
|