cli-agent-runner 0.2.14__tar.gz → 0.2.15__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.15}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CHANGELOG.md +14 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/PKG-INFO +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/README.md +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_emit.py +37 -7
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_serve_policy.py +30 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_throttle.py +8 -9
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/api.py +4 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/serve_cmd.py +53 -38
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/models.py +2 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/parsers.py +8 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/defenses.py +12 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/events.py +1 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/host_health.py +19 -17
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/monitor.py +4 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/service_unit.py +2 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/architecture.md +4 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/commands.md +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/configuration.md +5 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/migrations/0.2.md +34 -3
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/runbook.md +18 -6
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/contract/test_public_api_surface.py +3 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_owned_paths.py +3 -3
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_architecture.py +8 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_claims_match_ssot.py +11 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_kinds_ssot.py +5 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_module_boundaries.py +2 -2
- cli_agent_runner-0.2.15/tests/invariants/test_no_internal_refs.py +39 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_peek_schema_version.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_repo_constants_patched_in_tests.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_service.py +30 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_claude_error_detector.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config.py +90 -6
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_defenses.py +2 -2
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_gemini_plugin.py +1 -1
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_host_health.py +34 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_crash_loop.py +35 -7
- cli_agent_runner-0.2.15/tests/unit/test_serve_policy.py +68 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_round_log.py +39 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_vcs_state.py +2 -2
- 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.15}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.gitignore +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/LICENSE +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/README.zh.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/build.sh +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/README.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/events.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/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.15}/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.15}/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.15}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_upgrade_migrate.py +0 -0
|
@@ -5,6 +5,20 @@ 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.15] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[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.
|
|
12
|
+
- `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).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
See `docs/migrations/0.2.md`.
|
|
21
|
+
|
|
8
22
|
## [0.2.14] - 2026-09-03
|
|
9
23
|
|
|
10
24
|
### 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.15
|
|
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
|
+
14 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
|
+
14 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
|
|
|
@@ -20,6 +20,7 @@ __all__ = [
|
|
|
20
20
|
"emit_config_migrated",
|
|
21
21
|
"emit_fresh_eyes_round_triggered",
|
|
22
22
|
"emit_max_rounds_reached",
|
|
23
|
+
"emit_mem_loop",
|
|
23
24
|
"emit_rate_limit_stop",
|
|
24
25
|
"emit_round_deferred",
|
|
25
26
|
"emit_round_grace_extended",
|
|
@@ -64,20 +65,49 @@ def emit_config_broken(log_dir: Path, *, reason: str) -> None:
|
|
|
64
65
|
emit(log_dir, CONFIG_BROKEN, reason=reason)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
def _emit_giveup(
|
|
69
|
+
log_dir: Path, kind: str, *, consecutive: int, exit_code: int, log_path: Path
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Shared body for the give-up events (serve stopped/broke after consecutive
|
|
72
|
+
failures of one kind): capture a redacted tail of the round log as ``reason``
|
|
73
|
+
so the recurring failure can be inspected (or later classified into a
|
|
74
|
+
transient bucket), and emit ``kind`` with the standard give-up payload.
|
|
72
75
|
"""
|
|
73
76
|
from agent_runner._redact import redact_secrets
|
|
74
|
-
from agent_runner.events import
|
|
77
|
+
from agent_runner.events import emit
|
|
75
78
|
|
|
76
79
|
try:
|
|
77
80
|
reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
|
|
78
81
|
except OSError:
|
|
79
82
|
reason = ""
|
|
80
|
-
emit(log_dir,
|
|
83
|
+
emit(log_dir, kind, consecutive=consecutive, exit_code=exit_code, reason=reason)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
|
|
87
|
+
"""Emit crash_loop (serve stopped after consecutive unknown short crashes).
|
|
88
|
+
|
|
89
|
+
Captures the failure reason — a redacted tail of the round log — so a
|
|
90
|
+
recurring unknown crash can later be classified into a transient bucket.
|
|
91
|
+
"""
|
|
92
|
+
from agent_runner.events import CRASH_LOOP
|
|
93
|
+
|
|
94
|
+
_emit_giveup(
|
|
95
|
+
log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def emit_mem_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
|
|
100
|
+
"""Emit mem_loop (serve gave up after consecutive mid-round memory-pressure
|
|
101
|
+
terminations — the 0.2.15 coma-preventer's give-up cap). Distinct from
|
|
102
|
+
crash_loop: this is a break-then-restart, not a deliberate stop, so it is
|
|
103
|
+
deliberately absent from the unit's RestartPreventExitStatus.
|
|
104
|
+
|
|
105
|
+
Captures the failure reason — a redacted tail of the round log — same as
|
|
106
|
+
crash_loop, so an operator can see what the round was doing when the host
|
|
107
|
+
ran out of memory."""
|
|
108
|
+
from agent_runner.events import MEM_LOOP
|
|
109
|
+
|
|
110
|
+
_emit_giveup(log_dir, MEM_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path)
|
|
81
111
|
|
|
82
112
|
|
|
83
113
|
def emit_config_migrated(
|
|
@@ -43,6 +43,17 @@ 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
|
+
|
|
46
57
|
|
|
47
58
|
class EnvironmentalError(Exception):
|
|
48
59
|
"""Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
|
|
@@ -106,6 +117,25 @@ def post_round_decision(
|
|
|
106
117
|
return ("continue", delay, 0)
|
|
107
118
|
|
|
108
119
|
|
|
120
|
+
def _mem_loop_decision(
|
|
121
|
+
*, mem_terminated: bool, consecutive: int
|
|
122
|
+
) -> tuple[Literal["mem_loop", "continue"], int]:
|
|
123
|
+
"""Mem-loop give-up cap (0.2.15 Task 4) — a NEW, SEPARATE, private counter
|
|
124
|
+
from ``post_round_decision``'s crash-loop breaker, not a widening of its
|
|
125
|
+
3-tuple contract. A mem-terminated round already sets ``throttle_active``
|
|
126
|
+
for ``post_round_decision`` (never counted as a crash), so without this
|
|
127
|
+
cap a sustained memory-pressure host would retry forever instead of
|
|
128
|
+
eventually giving up. Returns ``("mem_loop", consecutive)`` once
|
|
129
|
+
``consecutive`` reaches ``MEM_LOOP_THRESHOLD``; a non-mem-terminated round
|
|
130
|
+
resets the counter to 0, same reset shape as the crash-loop breaker."""
|
|
131
|
+
if not mem_terminated:
|
|
132
|
+
return ("continue", 0)
|
|
133
|
+
consecutive += 1
|
|
134
|
+
if consecutive >= MEM_LOOP_THRESHOLD:
|
|
135
|
+
return ("mem_loop", consecutive)
|
|
136
|
+
return ("continue", consecutive)
|
|
137
|
+
|
|
138
|
+
|
|
109
139
|
def classify_round_exit(exc: BaseException) -> int:
|
|
110
140
|
"""The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
|
|
111
141
|
identity, decides serve's response. Replaces the old exception *whitelist*
|
|
@@ -277,21 +277,20 @@ def _active_throttles(
|
|
|
277
277
|
def round_was_mem_terminated(log_dir: Path) -> bool:
|
|
278
278
|
"""True iff the round that JUST ran was killed by ``_spawn_round``'s
|
|
279
279
|
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.
|
|
280
|
+
than a genuine crash — so ``serve_cmd.cmd`` can excuse it from the
|
|
281
|
+
crash-loop breaker exactly like an active throttle (flat back-off,
|
|
282
|
+
``consecutive`` reset to 0), the same treatment ``ENV_BATTERY_EXIT``
|
|
283
|
+
already gets. A mem-terminated round can be killed within the first ~10s
|
|
284
|
+
(the mid-round check's own cadence) — far under the crash-loop breaker's
|
|
285
|
+
60s "short crash" window — so, unlike a wall-clock-ceiling wedge (whose
|
|
286
|
+
long duration alone dodges the breaker), this needs an explicit signal.
|
|
288
287
|
|
|
289
288
|
Scoped to "this round" (not some earlier one) by comparing the newest
|
|
290
289
|
``round_mem_terminated`` event's timestamp against the newest
|
|
291
290
|
``round_substrate_before``'s: ``round_substrate_before(N)`` always
|
|
292
291
|
precedes round N's own attempt, and no later round's
|
|
293
292
|
``round_substrate_before(N+1)`` has been emitted yet at the point
|
|
294
|
-
``
|
|
293
|
+
``cmd()`` runs this check (right after ``_spawn_round`` returns) —
|
|
295
294
|
``round_substrate_after``, also emitted before this check runs, cannot
|
|
296
295
|
serve as that boundary, since it always comes AFTER any
|
|
297
296
|
``round_mem_terminated`` within the very round it is scoping. ``>=``, not
|
|
@@ -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.15'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 15)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -28,6 +28,7 @@ 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,
|
|
31
32
|
PERMANENT_CONFIG_EXIT,
|
|
32
33
|
post_round_decision,
|
|
33
34
|
)
|
|
@@ -629,6 +630,8 @@ def _poll_once(
|
|
|
629
630
|
mem_avail_min_mb=cfg.monitor.host_health.mem_avail_min_mb,
|
|
630
631
|
disk_warning_pct=cfg.monitor.host_health.disk_warning_pct,
|
|
631
632
|
disk_critical_pct=cfg.monitor.host_health.disk_critical_pct,
|
|
633
|
+
swap_sout_noise_floor_mb=cfg.monitor.host_health.swap_sout_noise_floor_mb,
|
|
634
|
+
mem_free_low_mb=cfg.monitor.host_health.mem_free_low_mb,
|
|
632
635
|
log_dir=cfg.runtime.log_dir,
|
|
633
636
|
)
|
|
634
637
|
if not monitor._PLUGIN_DETECTORS:
|
|
@@ -784,6 +787,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
784
787
|
emit_crash_loop,
|
|
785
788
|
emit_fresh_eyes_round_triggered,
|
|
786
789
|
emit_max_rounds_reached,
|
|
790
|
+
emit_mem_loop,
|
|
787
791
|
emit_rate_limit_stop,
|
|
788
792
|
emit_round_deferred,
|
|
789
793
|
emit_round_grace_extended,
|
|
@@ -22,7 +22,9 @@ from typing import Literal
|
|
|
22
22
|
from agent_runner import host_health, metrics, phase_select, schedule
|
|
23
23
|
from agent_runner._serve_policy import (
|
|
24
24
|
CRASH_LOOP_EXIT,
|
|
25
|
+
MEM_LOOP_EXIT,
|
|
25
26
|
PERMANENT_CONFIG_EXIT,
|
|
27
|
+
_mem_loop_decision,
|
|
26
28
|
post_round_decision,
|
|
27
29
|
)
|
|
28
30
|
from agent_runner._substrate import compute_git_head, compute_paths_hash
|
|
@@ -40,6 +42,7 @@ from agent_runner.api import (
|
|
|
40
42
|
emit_crash_loop,
|
|
41
43
|
emit_fresh_eyes_round_triggered,
|
|
42
44
|
emit_max_rounds_reached,
|
|
45
|
+
emit_mem_loop,
|
|
43
46
|
emit_rate_limit_stop,
|
|
44
47
|
emit_round_deferred,
|
|
45
48
|
emit_round_logs_prune_deferred,
|
|
@@ -432,22 +435,18 @@ def _throttle_skip_context(cfg, log_dir) -> tuple[frozenset[str], int | None]:
|
|
|
432
435
|
|
|
433
436
|
|
|
434
437
|
def _ran_agent_throttled(cfg, phase_arg, log_dir) -> bool:
|
|
435
|
-
"""Was the round that JUST ran throttled
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
wedge kill, whose long duration alone dodges the breaker — it needs this
|
|
442
|
-
explicit exemption.
|
|
438
|
+
"""Was the round that JUST ran throttled? — so the crash-loop breaker
|
|
439
|
+
excuses a fast throttle-induced exit rather than counting it as a crash.
|
|
440
|
+
A mem-terminated round (round_mem_terminated — a coma-preventer kill, not
|
|
441
|
+
a crash) is excused the same way, but that check is computed ONCE in
|
|
442
|
+
cmd() (``round_was_mem_terminated``) and OR'd into ``throttle_active``
|
|
443
|
+
there, not repeated here.
|
|
443
444
|
|
|
444
445
|
When serve chose the phase (``phase_arg`` set) we check that exact agent. When the
|
|
445
446
|
round self-rotated (``phase_arg`` None: no ``[phases]``, or ``--ignore-schedule``),
|
|
446
447
|
serve does NOT know which agent ran, so fall back to "any agent throttled" — the
|
|
447
448
|
pre-0.2.11 agent-agnostic check. Erring toward excusing keeps a real throttle from
|
|
448
449
|
being misread as a crash (a false ``crash_loop`` permanent stop)."""
|
|
449
|
-
if round_was_mem_terminated(log_dir):
|
|
450
|
-
return True
|
|
451
450
|
active = _active_throttles(log_dir)
|
|
452
451
|
if phase_arg is None:
|
|
453
452
|
return bool(active)
|
|
@@ -587,6 +586,14 @@ def _apply_round_num_env(round_env: dict, round_num: int) -> None:
|
|
|
587
586
|
round_env["AGENT_RUNNER_ROUND_NUM"] = str(round_num)
|
|
588
587
|
|
|
589
588
|
|
|
589
|
+
def _capture_substrate(work_dir, cfg, log_dir, round_num, *, when):
|
|
590
|
+
"""Snapshot git-head + paths-hash and emit the round-substrate event for `when`."""
|
|
591
|
+
git_head = compute_git_head(work_dir)
|
|
592
|
+
paths_hash = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
|
|
593
|
+
emit = emit_round_substrate_before if when == "before" else emit_round_substrate_after
|
|
594
|
+
emit(log_dir, round_num=round_num, git_head=git_head, paths_hash=paths_hash)
|
|
595
|
+
|
|
596
|
+
|
|
590
597
|
# Grace after TERMing a wedged round before killpg: the round's own SIGTERM handler
|
|
591
598
|
# reaps its agent pgroup (agent_runtime.REAP_GRACE_S) then exits, so allow that plus
|
|
592
599
|
# margin. Kept local to honor serve_cmd's import allowlist; test_spawn_round_wedged
|
|
@@ -645,9 +652,10 @@ def _spawn_round(
|
|
|
645
652
|
(``prev`` passed to ``host_health.memory_pressure``), so the delta grows
|
|
646
653
|
across the whole round rather than resetting each ~10s interval. A slow
|
|
647
654
|
SD/USB-backed swap device pages only a few MB per interval — below the
|
|
648
|
-
32 MiB
|
|
649
|
-
stay inert on exactly that host. PSI-full
|
|
650
|
-
critical path (it reads the current sample
|
|
655
|
+
configured swap-out floor (32 MiB by default) — but crosses it over a long
|
|
656
|
+
round; a per-interval delta would stay inert on exactly that host. PSI-full
|
|
657
|
+
stays an independent immediate critical path (it reads the current sample
|
|
658
|
+
alone, no baseline needed)."""
|
|
651
659
|
log_dir = round_log_path.parent
|
|
652
660
|
with round_log_path.open("w") as f:
|
|
653
661
|
proc = subprocess.Popen(
|
|
@@ -665,10 +673,11 @@ def _spawn_round(
|
|
|
665
673
|
# Baseline pinned at the first mid-round sample (round-start), held
|
|
666
674
|
# fixed so the swap-out delta the floor tests is CUMULATIVE SINCE
|
|
667
675
|
# ROUND-START, not per-interval: a slow SD/USB swap device trickles
|
|
668
|
-
# only a few MB per ~10s tick (below the
|
|
669
|
-
# it over a long round, so a
|
|
670
|
-
#
|
|
671
|
-
#
|
|
676
|
+
# only a few MB per ~10s tick (below the configured swap-out floor,
|
|
677
|
+
# 32 MiB by default) yet crosses it over a long round, so a
|
|
678
|
+
# per-interval comparison stayed inert on exactly the field host
|
|
679
|
+
# this floor exists for. PSI-full stays an independent immediate
|
|
680
|
+
# path (it reads the current sample alone).
|
|
672
681
|
round_start_sample: dict | None = None
|
|
673
682
|
while True:
|
|
674
683
|
try:
|
|
@@ -776,6 +785,7 @@ def cmd(args) -> int:
|
|
|
776
785
|
work_dir = cfg.runtime.work_dir
|
|
777
786
|
rounds_completed = 0
|
|
778
787
|
consecutive_crashes = 0 # b12: consecutive UNKNOWN short crashes (crash-loop breaker)
|
|
788
|
+
consecutive_mem_terminations = 0 # 0.2.15: consecutive mem-terminated rounds (mem-loop cap)
|
|
779
789
|
# Give-up stops (config_broken/crash_loop) return a distinct non-zero code the
|
|
780
790
|
# systemd unit lists in RestartPreventExitStatus so they stay stopped; every
|
|
781
791
|
# other stop (sentinel/stop_file/max_rounds/SIGTERM/once) is a clean exit 0.
|
|
@@ -823,16 +833,7 @@ def cmd(args) -> int:
|
|
|
823
833
|
continue
|
|
824
834
|
if stop["requested"]:
|
|
825
835
|
break # SIGTERM landed during selection/back-off — don't spawn a round
|
|
826
|
-
|
|
827
|
-
paths_hash_before = compute_paths_hash(
|
|
828
|
-
work_dir, cfg.runtime.substrate_fingerprint_paths
|
|
829
|
-
)
|
|
830
|
-
emit_round_substrate_before(
|
|
831
|
-
log_dir,
|
|
832
|
-
round_num=round_num,
|
|
833
|
-
git_head=git_head_before,
|
|
834
|
-
paths_hash=paths_hash_before,
|
|
835
|
-
)
|
|
836
|
+
_capture_substrate(work_dir, cfg, log_dir, round_num, when="before")
|
|
836
837
|
_apply_fresh_eyes(cfg, log_dir, round_num, round_env)
|
|
837
838
|
_apply_round_num_env(round_env, round_num)
|
|
838
839
|
round_log_path = log_dir / f"round-{round_num}.log"
|
|
@@ -856,27 +857,28 @@ def cmd(args) -> int:
|
|
|
856
857
|
)
|
|
857
858
|
round_duration_s = SYSTEM_CLOCK.monotonic() - round_started
|
|
858
859
|
atomic_relink(log_dir / ROUND_CURRENT_LINK, round_log_path)
|
|
859
|
-
|
|
860
|
-
paths_hash_after = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
|
|
861
|
-
emit_round_substrate_after(
|
|
862
|
-
log_dir,
|
|
863
|
-
round_num=round_num,
|
|
864
|
-
git_head=git_head_after,
|
|
865
|
-
paths_hash=paths_hash_after,
|
|
866
|
-
)
|
|
860
|
+
_capture_substrate(work_dir, cfg, log_dir, round_num, when="after")
|
|
867
861
|
rounds_completed += 1
|
|
868
862
|
# Restart policy (config_broken / crash_loop / continue) lives in the
|
|
869
863
|
# tested api.post_round_decision helper so this loop stays thin. Those
|
|
870
864
|
# strings are that enum, not events.py kinds — do not normalize them.
|
|
865
|
+
# mem_terminated computed ONCE here and OR'd into throttle_active so
|
|
866
|
+
# crash_loop never double-counts a mem-terminated round; _mem_loop_decision
|
|
867
|
+
# (a separate, private cap) tracks it on its own counter below.
|
|
868
|
+
mem_terminated = round_was_mem_terminated(log_dir)
|
|
871
869
|
action, delay, consecutive_crashes = post_round_decision(
|
|
872
870
|
returncode=r_returncode,
|
|
873
871
|
duration_s=round_duration_s,
|
|
874
|
-
# Crash-loop breaker excuses a fast exit
|
|
875
|
-
#
|
|
876
|
-
|
|
872
|
+
# Crash-loop breaker excuses a fast exit when the agent that just ran is
|
|
873
|
+
# throttled (agent-agnostic when the round self-rotated the phase) or
|
|
874
|
+
# mem-terminated.
|
|
875
|
+
throttle_active=mem_terminated or _ran_agent_throttled(cfg, phase_arg, log_dir),
|
|
877
876
|
consecutive=consecutive_crashes,
|
|
878
877
|
restart_delay_s=cfg.runtime.restart_delay_s,
|
|
879
878
|
)
|
|
879
|
+
mem_action, consecutive_mem_terminations = _mem_loop_decision(
|
|
880
|
+
mem_terminated=mem_terminated, consecutive=consecutive_mem_terminations
|
|
881
|
+
)
|
|
880
882
|
if action == "config_broken":
|
|
881
883
|
# classify_round_exit maps ANY ConfigError to this exit code (Group
|
|
882
884
|
# A) — not only a startup-battery check failure (e.g. _phase_for's
|
|
@@ -888,6 +890,19 @@ def cmd(args) -> int:
|
|
|
888
890
|
)
|
|
889
891
|
exit_code = PERMANENT_CONFIG_EXIT
|
|
890
892
|
break
|
|
893
|
+
if mem_action == "mem_loop":
|
|
894
|
+
# Checked before crash_loop: a mem-terminated round always has
|
|
895
|
+
# throttle_active=True above, so post_round_decision never returns
|
|
896
|
+
# crash_loop for it anyway — this ordering just makes the precedence
|
|
897
|
+
# explicit (config_broken > mem_loop > crash_loop).
|
|
898
|
+
emit_mem_loop(
|
|
899
|
+
log_dir,
|
|
900
|
+
consecutive=consecutive_mem_terminations,
|
|
901
|
+
exit_code=r_returncode,
|
|
902
|
+
log_path=round_log_path,
|
|
903
|
+
)
|
|
904
|
+
exit_code = MEM_LOOP_EXIT
|
|
905
|
+
break
|
|
891
906
|
if action == "crash_loop":
|
|
892
907
|
emit_crash_loop(
|
|
893
908
|
log_dir,
|
|
@@ -160,6 +160,8 @@ class MonitorHostHealthConfig:
|
|
|
160
160
|
mem_avail_min_mb: int = 200 # mem_pressure fires when mem_available_mb < this
|
|
161
161
|
disk_warning_pct: float = 90.0 # disk_warning fires when disk_used_pct >= this
|
|
162
162
|
disk_critical_pct: float = 95.0 # disk_critical fires when disk_used_pct >= this
|
|
163
|
+
swap_sout_noise_floor_mb: int = 32 # tier-2 swap-out noise floor (MiB); compare uses *1024*1024
|
|
164
|
+
mem_free_low_mb: int = 16 # tier-3 MemFree floor (MB)
|
|
163
165
|
|
|
164
166
|
|
|
165
167
|
@dataclass(frozen=True)
|
|
@@ -462,6 +462,14 @@ def _parse_monitor(monitor_d: dict) -> MonitorConfig:
|
|
|
462
462
|
hh_d.get("disk_critical_pct", 95.0),
|
|
463
463
|
field="monitor.host_health.disk_critical_pct",
|
|
464
464
|
),
|
|
465
|
+
swap_sout_noise_floor_mb=_require_positive_int(
|
|
466
|
+
hh_d.get("swap_sout_noise_floor_mb", 32),
|
|
467
|
+
field="monitor.host_health.swap_sout_noise_floor_mb",
|
|
468
|
+
),
|
|
469
|
+
mem_free_low_mb=_require_positive_int(
|
|
470
|
+
hh_d.get("mem_free_low_mb", 16),
|
|
471
|
+
field="monitor.host_health.mem_free_low_mb",
|
|
472
|
+
),
|
|
465
473
|
)
|
|
466
474
|
monitor = MonitorConfig(
|
|
467
475
|
auth_fail_patterns=_validate_regex_list(
|
|
@@ -103,6 +103,18 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
103
103
|
guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
|
|
104
104
|
current_state="active",
|
|
105
105
|
),
|
|
106
|
+
Defense(
|
|
107
|
+
name="mem_loop_breaker",
|
|
108
|
+
value="stop after 5 consecutive mid-round memory-terminated rounds (exit 71)",
|
|
109
|
+
codifies=(
|
|
110
|
+
"0.2.15 — a host stuck under sustained memory pressure could mem-terminate "
|
|
111
|
+
"every round forever with no give-up; break-then-restart cap added (exit 71 "
|
|
112
|
+
"stays outside RestartPreventExitStatus, so systemd restarts serve, which may "
|
|
113
|
+
"find the pressure has cleared)"
|
|
114
|
+
),
|
|
115
|
+
guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
|
|
116
|
+
current_state="active",
|
|
117
|
+
),
|
|
106
118
|
Defense(
|
|
107
119
|
name="bulk_round_log_prune_guard",
|
|
108
120
|
value="a prune deleting more files than it keeps deletes nothing",
|
|
@@ -46,6 +46,7 @@ DIRTY_DETECTED = "dirty_detected"
|
|
|
46
46
|
FRESH_EYES_ROUND_TRIGGERED = "fresh_eyes_round_triggered"
|
|
47
47
|
HOOK_FAILED = "hook_failed"
|
|
48
48
|
MAX_ROUNDS_REACHED = "max_rounds_reached"
|
|
49
|
+
MEM_LOOP = "mem_loop"
|
|
49
50
|
MONITOR_ALERT_EMITTED = "monitor_alert_emitted"
|
|
50
51
|
MONITOR_AUTO_STOP_FAILED = "monitor_auto_stop_failed"
|
|
51
52
|
MONITOR_AUTO_STOP_TRIGGERED = "monitor_auto_stop_triggered"
|
|
@@ -11,8 +11,9 @@ defined signal ladder with graceful degrade:
|
|
|
11
11
|
1. **PSI** (``psi_some_avg10``/``psi_full_avg10``) if readable — the kernel's
|
|
12
12
|
own "reclaim is slowing progress" measure, immune to cache inflation.
|
|
13
13
|
2. else **swap-out rate** — the delta of ``swap_sout`` (cumulative bytes) over
|
|
14
|
-
the ``prev_sample`` the caller passes; more than
|
|
15
|
-
|
|
14
|
+
the ``prev_sample`` the caller passes; more than the configured noise floor
|
|
15
|
+
(tens of MB by default) is flagged as pressure — above the noise floor, not
|
|
16
|
+
benign startup/idle churn. Escalates to
|
|
16
17
|
**critical** when ``mem_free_mb`` is ALSO critically low (the same absolute
|
|
17
18
|
floor tier 3 uses below) WHILE swap-out is positive — the unambiguous
|
|
18
19
|
"actively dying" state. This is gated on MemFree, not on the swap-out
|
|
@@ -46,21 +47,22 @@ from typing import Any
|
|
|
46
47
|
_PSI_SOME_AVG10_WARNING = 5.0
|
|
47
48
|
_PSI_FULL_AVG10_CRITICAL = 1.0
|
|
48
49
|
|
|
49
|
-
# Tier 2 -- swap-out delta (bytes) between two samples. Below
|
|
50
|
-
# tens of MB is ordinary startup/idle
|
|
51
|
-
# boundaries, or the mid-round loop's
|
|
52
|
-
#
|
|
53
|
-
# sensitive and made the pre-round gate
|
|
54
|
-
# swap movement every round.
|
|
55
|
-
|
|
50
|
+
# Tier 2 -- swap-out delta (bytes) between two samples. Below
|
|
51
|
+
# cfg.swap_sout_noise_floor_mb is noise: tens of MB is ordinary startup/idle
|
|
52
|
+
# churn between successive samples (round boundaries, or the mid-round loop's
|
|
53
|
+
# own ~10s ticks) and is NOT, on its own, a real paging signal -- a single
|
|
54
|
+
# page (4096B, the old floor) is far too sensitive and made the pre-round gate
|
|
55
|
+
# defer/resume on a few idle KB of swap movement every round. Config-tunable
|
|
56
|
+
# (``MonitorHostHealthConfig.swap_sout_noise_floor_mb``, default 32 MiB) so a
|
|
57
|
+
# tiny host can lower it and a large host can raise it.
|
|
56
58
|
|
|
57
59
|
# Tier 3 -- combined-low. MemFree alone is always low on a cache-heavy host
|
|
58
60
|
# (the kernel prefers to keep it near-zero and use spare RAM for cache), so
|
|
59
61
|
# it must never gate alone -- only together with a low MemAvailable (tier 3)
|
|
60
62
|
# or an actual measured, above-floor swap-out delta (tier 2's critical
|
|
61
63
|
# escalation, below). Also doubles as the critical-escalation MemFree floor:
|
|
62
|
-
# "critically low" is the same absolute bar either way.
|
|
63
|
-
|
|
64
|
+
# "critically low" is the same absolute bar either way. Config-tunable
|
|
65
|
+
# (``MonitorHostHealthConfig.mem_free_low_mb``, default 16 MiB).
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
@dataclass(frozen=True)
|
|
@@ -122,20 +124,20 @@ def memory_pressure(
|
|
|
122
124
|
return None # PSI is readable and says healthy -- trust it, no fall-through.
|
|
123
125
|
|
|
124
126
|
delta = _swap_sout_delta(sample, prev_sample)
|
|
125
|
-
if delta is not None and delta >
|
|
127
|
+
if delta is not None and delta > cfg.swap_sout_noise_floor_mb * 1024 * 1024:
|
|
126
128
|
mem_free = sample.get("mem_free_mb")
|
|
127
|
-
if mem_free is not None and mem_free <
|
|
129
|
+
if mem_free is not None and mem_free < cfg.mem_free_low_mb:
|
|
128
130
|
return Pressure(
|
|
129
131
|
"critical",
|
|
130
132
|
"swap_out_rate",
|
|
131
133
|
f"swap sout +{delta}B since last sample while mem_free_mb {mem_free} "
|
|
132
|
-
f"< {
|
|
134
|
+
f"< {cfg.mem_free_low_mb} (actively dying, independent of swap-device speed)",
|
|
133
135
|
{"swap_sout_delta": delta, "mem_free_mb": mem_free},
|
|
134
136
|
)
|
|
135
137
|
return Pressure(
|
|
136
138
|
"warning",
|
|
137
139
|
"swap_out_rate",
|
|
138
|
-
f"swap sout +{delta}B since last sample (
|
|
140
|
+
f"swap sout +{delta}B since last sample (above the noise floor)",
|
|
139
141
|
{"swap_sout_delta": delta},
|
|
140
142
|
)
|
|
141
143
|
# Swap tier gave no evidence either way (no data, or below the noise
|
|
@@ -146,11 +148,11 @@ def memory_pressure(
|
|
|
146
148
|
mem_free = sample.get("mem_free_mb")
|
|
147
149
|
mem_avail = sample.get("mem_available_mb")
|
|
148
150
|
if mem_free is not None and mem_avail is not None:
|
|
149
|
-
if mem_free <
|
|
151
|
+
if mem_free < cfg.mem_free_low_mb and mem_avail < cfg.mem_avail_min_mb:
|
|
150
152
|
return Pressure(
|
|
151
153
|
"warning",
|
|
152
154
|
"combined_low",
|
|
153
|
-
f"mem_free_mb {mem_free} < {
|
|
155
|
+
f"mem_free_mb {mem_free} < {cfg.mem_free_low_mb} and "
|
|
154
156
|
f"mem_available_mb {mem_avail} < {cfg.mem_avail_min_mb}",
|
|
155
157
|
{"mem_free_mb": mem_free, "mem_available_mb": mem_avail},
|
|
156
158
|
)
|
|
@@ -91,6 +91,8 @@ def run_all_detectors(
|
|
|
91
91
|
mem_avail_min_mb: int = 200,
|
|
92
92
|
disk_warning_pct: float = 90.0,
|
|
93
93
|
disk_critical_pct: float = 95.0,
|
|
94
|
+
swap_sout_noise_floor_mb: int = 32,
|
|
95
|
+
mem_free_low_mb: int = 16,
|
|
94
96
|
log_dir: Path | None = None,
|
|
95
97
|
) -> list[Alert]:
|
|
96
98
|
"""Run all 13 detectors; returns alerts (empty = healthy).
|
|
@@ -112,6 +114,8 @@ def run_all_detectors(
|
|
|
112
114
|
mem_avail_min_mb=mem_avail_min_mb,
|
|
113
115
|
disk_warning_pct=disk_warning_pct,
|
|
114
116
|
disk_critical_pct=disk_critical_pct,
|
|
117
|
+
swap_sout_noise_floor_mb=swap_sout_noise_floor_mb,
|
|
118
|
+
mem_free_low_mb=mem_free_low_mb,
|
|
115
119
|
)
|
|
116
120
|
detectors: list[tuple[str, Callable[[], Alert | None]]] = [
|
|
117
121
|
("timeout_rate", lambda: detect_timeout_rate(events)),
|
|
@@ -103,6 +103,8 @@ def render_serve_unit(
|
|
|
103
103
|
f"Restart=on-failure\n"
|
|
104
104
|
# 78 = api.PERMANENT_CONFIG_EXIT, 75 = api.CRASH_LOOP_EXIT (literal here to
|
|
105
105
|
# avoid an api→service_unit→api import cycle; pinned by test_service_unit).
|
|
106
|
+
# NOTE: mem_loop (71) is deliberately absent here — it must restart
|
|
107
|
+
# (break-then-restart, not a deliberate stop; see api.MEM_LOOP_EXIT).
|
|
106
108
|
f"RestartPreventExitStatus=78 75\n"
|
|
107
109
|
f"RestartSec=3\n"
|
|
108
110
|
f"KillMode=mixed\n"
|
|
@@ -38,7 +38,7 @@ common to all three. Operator learns one mental model, three lenses.
|
|
|
38
38
|
|
|
39
39
|
## Defenses-as-data
|
|
40
40
|
|
|
41
|
-
`agent_runner.defenses.catalog(cfg)` returns
|
|
41
|
+
`agent_runner.defenses.catalog(cfg)` returns 14 structured `Defense` entries.
|
|
42
42
|
Each entry carries:
|
|
43
43
|
|
|
44
44
|
- `name` — stable identifier
|
|
@@ -66,6 +66,7 @@ surfacing everywhere.
|
|
|
66
66
|
| `critical_envs_injection` | Env injection via [agent.env] block — preset-supplied per CLI (e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates) | `tests/unit/test_agent_runtime.py` |
|
|
67
67
|
| `startup_smoke_check` | R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; now halts serve (config_broken) instead of respawning a broken config | `tests/unit/test_serve_config_broken.py` |
|
|
68
68
|
| `crash_loop_breaker` | Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay | `tests/unit/test_serve_crash_loop.py` |
|
|
69
|
+
| `mem_loop_breaker` | 0.2.15 — a host stuck under sustained memory pressure could mem-terminate every round forever with no give-up; break-then-restart cap added (exit 71 stays outside RestartPreventExitStatus, so systemd restarts serve, which may find the pressure has cleared) | `tests/unit/test_serve_crash_loop.py` |
|
|
69
70
|
| `bulk_round_log_prune_guard` | 0.2.4 — rounds/ pruning shipped against backlogs it never built; one deployment's first post-upgrade round would have deleted 12,193 of 12,293 transcripts silently | `tests/unit/test_round_log_helpers.py` |
|
|
70
71
|
| `flock_concurrency` | Architectural — prevent concurrent supervisors corrupting state | `tests/unit/test_runner.py` |
|
|
71
72
|
| `atomic_state_writes` | Data integrity — crashes never leave half-written state files | `tests/invariants/test_atomic_write_enforced.py` |
|
|
@@ -212,6 +213,7 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
|
212
213
|
- `fresh_eyes_round_triggered`
|
|
213
214
|
- `hook_failed`
|
|
214
215
|
- `max_rounds_reached`
|
|
216
|
+
- `mem_loop`
|
|
215
217
|
- `monitor_alert_emitted`
|
|
216
218
|
- `monitor_auto_stop_failed`
|
|
217
219
|
- `monitor_auto_stop_triggered`
|
|
@@ -257,4 +259,4 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
|
257
259
|
|
|
258
260
|
三层架构:Round(一轮 agent)/ Loop(serve 薄壳)/ Witness(monitor)。
|
|
259
261
|
三视角对称:peek(快照)/ watch(快照循环)/ monitor(异常检测),共用下钻参数。
|
|
260
|
-
防御以结构化目录形式存在(
|
|
262
|
+
防御以结构化目录形式存在(14 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
|
|
@@ -283,7 +283,7 @@ The transforms `migrate` applies (generated from the registry):
|
|
|
283
283
|
- unknown [phases.<name>.prompt] key(s) rejected in 0.2.13; delete them (allowed: ['files'])
|
|
284
284
|
- [agent] prompt_arg_template has no {prompt} placeholder; the prompt is never delivered to the agent — add {prompt} to one of the argv tokens
|
|
285
285
|
- a [phases.<name>.agent] prompt_arg_template has no {prompt} placeholder; add one, or set that phase's prompt.files = [] if it truly sends no prompt
|
|
286
|
-
- unknown [monitor.host_health] key(s) rejected in 0.2.14; delete them (allowed: ['disk_critical_pct', 'disk_warning_pct', 'mem_avail_min_mb'])
|
|
286
|
+
- unknown [monitor.host_health] key(s) rejected in 0.2.14; delete them (allowed: ['disk_critical_pct', 'disk_warning_pct', 'mem_avail_min_mb', 'mem_free_low_mb', 'swap_sout_noise_floor_mb'])
|
|
287
287
|
<!-- /gen:migrate-transforms -->
|
|
288
288
|
|
|
289
289
|
## 中文摘要
|