cli-agent-runner 0.2.23__tar.gz → 0.2.24__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.23 → cli_agent_runner-0.2.24}/CHANGELOG.md +9 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/memory.py +7 -3
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_cgroup.py +41 -11
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/doctor_cmd.py +8 -1
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/metrics.py +18 -2
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/memory-efficiency.md +43 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/runbook.md +1 -1
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_serve_loop.py +5 -2
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_grace.py +18 -12
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cgroup_probe.py +191 -7
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_doctor_cmd.py +57 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/dependabot.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.gitignore +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/LICENSE +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/README.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/README.zh.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_serve_policy.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_round.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/models.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/parsers.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/build.sh +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/README.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/configuration.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/events.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/container-pi.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_import_footprint.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_container.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_terminate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,15 @@ 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.24] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `host_cgroup_memory_limit` now advises adding `MemoryHigh` when a cgroup has `memory.max` set without `memory.high` (graceful pre-OOM throttling) — only for a bound on the supervisor's own cgroup, with the PSI-floor caveat when the mid-round floor isn't already deferring.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `doctor`'s phase-plan preview now shows a paused phase's resume time.
|
|
15
|
+
- Trimmed the grace-kill test wall-time (~80s faster suite; behavior-identical).
|
|
16
|
+
|
|
8
17
|
## [0.2.23] - 2026-09-12
|
|
9
18
|
|
|
10
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.24
|
|
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
|
|
@@ -113,7 +113,7 @@ def emit_host_cgroup_memory_limit(
|
|
|
113
113
|
mid-round hard floor can defer to kernel cgroup-OOM -- see
|
|
114
114
|
``emit_mem_pressure_deferred_to_cgroup`` below.
|
|
115
115
|
|
|
116
|
-
This also carries an optional startup
|
|
116
|
+
This also carries an optional startup advisory
|
|
117
117
|
(``swap_total_bytes`` / ``swap_cap_pct`` / ``memory_high`` / ``advisory``)
|
|
118
118
|
as FIELDS on this SAME event -- never a separate event kind, and never an
|
|
119
119
|
auto-change to the operator's cgroup or unit. ``swap_total_bytes`` is the
|
|
@@ -123,8 +123,12 @@ def emit_host_cgroup_memory_limit(
|
|
|
123
123
|
bytes (``metrics.cgroup_memory_high``) -- ``None`` means unset (the
|
|
124
124
|
cgroup read the literal ``"max"``, or no finite value at all), never the
|
|
125
125
|
raw ``"max"`` token, so a caller can't mistake "unset" for a real
|
|
126
|
-
ceiling; ``advisory`` is the human-readable warning text
|
|
127
|
-
|
|
126
|
+
ceiling; ``advisory`` is the human-readable warning text, else ``None`` --
|
|
127
|
+
it covers two independent conditions, joined with ``"; "`` when both
|
|
128
|
+
fire: a ``memory.swap.max`` that looks implausibly tight against host
|
|
129
|
+
swap, and a ``memory.max`` set on this process's OWN cgroup without a
|
|
130
|
+
``memory.high``, which recommends adding one (below ``memory.max``) for
|
|
131
|
+
graceful pre-OOM throttling instead of a hard kill."""
|
|
128
132
|
from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
|
|
129
133
|
|
|
130
134
|
emit(
|
|
@@ -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.24'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 24)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -225,9 +225,18 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
|
|
|
225
225
|
memory.swap.max is bounded but far below the host's own available swap
|
|
226
226
|
(``_SWAP_CAP_ADVISORY_PCT``): the operator capped the cgroup's swap well
|
|
227
227
|
under what the host has, so the mid-round floor may terminate a round
|
|
228
|
-
the kernel would have contained on a wider cap.
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
the kernel would have contained on a wider cap. A second, independent
|
|
229
|
+
advisory recommends ``memory.high`` when ``memory.max`` is set but
|
|
230
|
+
``memory.high`` isn't -- ONLY when the bounding ancestor is the
|
|
231
|
+
operator's OWN leaf (``bounding_cgroup_path == cgroup_path``): since
|
|
232
|
+
``memory.max`` is the MIN across every ancestor, "set" can also mean an
|
|
233
|
+
inherited parent slice or a container root, where "add MemoryHigh" is
|
|
234
|
+
noise the operator can't act on from their own unit. That hint also
|
|
235
|
+
appends a PSI-floor caveat, but only when the floor ISN'T already
|
|
236
|
+
deferring to kernel cgroup-OOM (a deferring host has no floor for the
|
|
237
|
+
throttle's PSI-full rise to trip). Both advisories join with ``"; "``
|
|
238
|
+
and print as one stderr line. The event also carries ``memory_high`` --
|
|
239
|
+
the bounding ancestor's ``memory.high`` soft-throttle threshold
|
|
231
240
|
(``metrics.cgroup_memory_high``), ``None`` when unset -- for the same
|
|
232
241
|
reason: an operator asking "is MemoryHigh even set" is exactly this
|
|
233
242
|
release's field ask. Advisory only -- this never changes the operator's
|
|
@@ -247,13 +256,39 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
|
|
|
247
256
|
swap_cap_pct = (
|
|
248
257
|
round(100.0 * swap_max / swap_total, 1) if swap_max is not None and swap_total > 0 else None
|
|
249
258
|
)
|
|
250
|
-
|
|
259
|
+
advisories: list[str] = []
|
|
251
260
|
if swap_cap_pct is not None and swap_cap_pct < _SWAP_CAP_ADVISORY_PCT:
|
|
252
|
-
|
|
261
|
+
advisories.append(
|
|
253
262
|
"cgroup memory.swap.max is far below host swap; the mid-round floor may "
|
|
254
263
|
"terminate rounds the kernel would have contained -- consider bounding "
|
|
255
264
|
"both memory.max and memory.swap.max, or set in_round_mem_terminate=false"
|
|
256
265
|
)
|
|
266
|
+
defer = (
|
|
267
|
+
limits["memory_max"] is not None
|
|
268
|
+
and swap_max is not None
|
|
269
|
+
and limits["memory_max"] < metrics.mem_total_bytes()
|
|
270
|
+
and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
|
|
271
|
+
)
|
|
272
|
+
own_scope = (
|
|
273
|
+
limits["memory_max"] is not None
|
|
274
|
+
and memory_high is None
|
|
275
|
+
and limits["bounding_cgroup_path"] == limits["cgroup_path"]
|
|
276
|
+
)
|
|
277
|
+
if own_scope:
|
|
278
|
+
hint = (
|
|
279
|
+
"memory.max is set on this cgroup without memory.high -- add "
|
|
280
|
+
"memory.high (below memory.max) for graceful throttling before an "
|
|
281
|
+
"OOM kill"
|
|
282
|
+
)
|
|
283
|
+
if not defer:
|
|
284
|
+
hint += (
|
|
285
|
+
"; bound memory.swap.max (at or below host swap) too, or set "
|
|
286
|
+
"in_round_mem_terminate=false, so the throttle's PSI-full rise "
|
|
287
|
+
"doesn't trip the mid-round floor"
|
|
288
|
+
)
|
|
289
|
+
advisories.append(hint)
|
|
290
|
+
advisory = "; ".join(advisories) or None
|
|
291
|
+
if advisory is not None:
|
|
257
292
|
print(f"agent-runner: {advisory}", file=sys.stderr)
|
|
258
293
|
emit_host_cgroup_memory_limit(
|
|
259
294
|
log_dir,
|
|
@@ -265,9 +300,4 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
|
|
|
265
300
|
memory_high=memory_high,
|
|
266
301
|
advisory=advisory,
|
|
267
302
|
)
|
|
268
|
-
return
|
|
269
|
-
limits["memory_max"] is not None
|
|
270
|
-
and swap_max is not None
|
|
271
|
-
and limits["memory_max"] < metrics.mem_total_bytes()
|
|
272
|
-
and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
|
|
273
|
-
)
|
|
303
|
+
return defer
|
|
@@ -33,6 +33,8 @@ def _plan(cfg, rounds: int) -> list[dict]:
|
|
|
33
33
|
"phase": sel.phase,
|
|
34
34
|
"paused": sel.paused,
|
|
35
35
|
"resume_phase": sel.resume_phase,
|
|
36
|
+
"resume_at": sel.resume_at.isoformat() if sel.resume_at else None,
|
|
37
|
+
"resume_timezone": sel.resume_timezone,
|
|
36
38
|
"skipped": sel.skipped,
|
|
37
39
|
"active_window": sel.active_window,
|
|
38
40
|
}
|
|
@@ -72,6 +74,11 @@ def _format(report: DoctorReport) -> str:
|
|
|
72
74
|
lines.append(f" {o.phase_a} ({o.window_a}) vs {o.phase_b} ({o.window_b})")
|
|
73
75
|
lines.append("phase plan:")
|
|
74
76
|
for step in report.plan:
|
|
75
|
-
|
|
77
|
+
if step["paused"]:
|
|
78
|
+
where = "paused"
|
|
79
|
+
if step["resume_at"] is not None:
|
|
80
|
+
where += f" -> resumes {step['resume_phase']} at {step['resume_at']}"
|
|
81
|
+
else:
|
|
82
|
+
where = step["phase"] or "base"
|
|
76
83
|
lines.append(f" round {step['round']}: {where}")
|
|
77
84
|
return "\n".join(lines)
|
|
@@ -297,6 +297,12 @@ def cgroup_memory_limits(
|
|
|
297
297
|
host, or any host missing that file, has no reliable fixed-path budget
|
|
298
298
|
file to read, so this returns all-``None`` rather than guessing.
|
|
299
299
|
|
|
300
|
+
``cgroup_path`` is always the caller's OWN leaf; ``bounding_cgroup_path``
|
|
301
|
+
is the ancestor (possibly the leaf itself) that actually owns the
|
|
302
|
+
winning ``memory.max`` (see :func:`_bounding_ancestor_path`) -- callers
|
|
303
|
+
that need to tell "my own limit" from "an inherited parent/container
|
|
304
|
+
bound" compare the two.
|
|
305
|
+
|
|
300
306
|
``root`` and ``proc_self_cgroup`` default to the real paths and are
|
|
301
307
|
injectable so tests can point at a fake ``/sys/fs/cgroup`` tree;
|
|
302
308
|
``self_cgroup`` lets a caller supply the resolved cgroup path directly,
|
|
@@ -307,12 +313,22 @@ def cgroup_memory_limits(
|
|
|
307
313
|
"""
|
|
308
314
|
resolved = _resolve_cgroup(root, proc_self_cgroup, self_cgroup)
|
|
309
315
|
if resolved is None:
|
|
310
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
"memory_max": None,
|
|
318
|
+
"memory_swap_max": None,
|
|
319
|
+
"cgroup_path": None,
|
|
320
|
+
"bounding_cgroup_path": None,
|
|
321
|
+
}
|
|
311
322
|
cgroup_path, ancestors = resolved
|
|
323
|
+
# One ancestor walk for memory.max, shared for both its value and its
|
|
324
|
+
# owning path (the "SAME winning ancestor" _min_ancestor_candidate's
|
|
325
|
+
# docstring promises) -- memory.swap.max is a distinct file, its own walk.
|
|
326
|
+
max_candidate = _min_ancestor_candidate(root, ancestors, "memory.max")
|
|
312
327
|
return {
|
|
313
|
-
"memory_max":
|
|
328
|
+
"memory_max": max_candidate[0] if max_candidate else None,
|
|
314
329
|
"memory_swap_max": _min_ancestor_limit(root, ancestors, "memory.swap.max"),
|
|
315
330
|
"cgroup_path": cgroup_path,
|
|
331
|
+
"bounding_cgroup_path": max_candidate[1] if max_candidate else None,
|
|
316
332
|
}
|
|
317
333
|
|
|
318
334
|
|
|
@@ -175,6 +175,49 @@ reads/round) weren't re-measured this release since neither's code path
|
|
|
175
175
|
moved — re-running the full harness would report the same number as
|
|
176
176
|
0.2.19 for a code path this release didn't touch, not new signal.
|
|
177
177
|
|
|
178
|
+
## 0.2.23 → 0.2.24
|
|
179
|
+
|
|
180
|
+
Measured the same way as the table above (see Methodology), same machine,
|
|
181
|
+
comparing **v0.2.23**'s base — `60c666a` (one commit past the `v0.2.23` tag:
|
|
182
|
+
a ruff-format-only fix, so equivalent to the shipped release for this
|
|
183
|
+
purpose) — against **0.2.24**, the tip of this branch at measurement time.
|
|
184
|
+
|
|
185
|
+
### Import/startup RSS
|
|
186
|
+
|
|
187
|
+
| | v0.2.23 | 0.2.24 | delta |
|
|
188
|
+
|---|---|---|---|
|
|
189
|
+
| RSS (avg of 5 runs) | 24.0 MB | 24.1 MB | +0.03 MB (flat, within noise) |
|
|
190
|
+
| RSS range | 23.9–24.1 MB | 24.0–24.3 MB | |
|
|
191
|
+
| `sys.modules` count | 229 | 229 | +0 |
|
|
192
|
+
|
|
193
|
+
### Module count, source LOC, largest module
|
|
194
|
+
|
|
195
|
+
Tracked source only (`git ls-tree`, same convention as the row above).
|
|
196
|
+
|
|
197
|
+
| | v0.2.23 | 0.2.24 | delta |
|
|
198
|
+
|---|---|---|---|
|
|
199
|
+
| `.py` files | 75 | 75 | +0 |
|
|
200
|
+
| total LOC | 18,276 | 18,325 | +49 |
|
|
201
|
+
| largest module | `api.py`, 990 | `api.py`, 990 | +0 |
|
|
202
|
+
|
|
203
|
+
### Why it's flat
|
|
204
|
+
|
|
205
|
+
This release's only startup-path-adjacent change is the second
|
|
206
|
+
`host_cgroup_memory_limit` advisory (recommending `memory.high` when
|
|
207
|
+
`memory.max` is set on the supervisor's own cgroup): it lives inside the
|
|
208
|
+
probe function already called from the already-loaded `cli/_serve_cgroup`
|
|
209
|
+
module, adds no new import, and only walks its extra branch when a real
|
|
210
|
+
cgroup bound is present at startup. The `doctor` phase-plan resume-time
|
|
211
|
+
addition and the grace-kill test wall-time trim touch neither the startup
|
|
212
|
+
import graph nor any per-round allocation path.
|
|
213
|
+
`tests/invariants/test_import_footprint.py` and
|
|
214
|
+
`tests/invariants/test_round_alloc_growth.py` both stay green unmodified,
|
|
215
|
+
confirming no new eager import and no new per-round leak.
|
|
216
|
+
|
|
217
|
+
**Suite wall-time**: trimming the grace-kill negative test's sleep cuts
|
|
218
|
+
roughly 80s off the test suite's wall-clock time — a test-only change, not
|
|
219
|
+
reflected in any of the runtime numbers above.
|
|
220
|
+
|
|
178
221
|
## Methodology
|
|
179
222
|
|
|
180
223
|
Machine: macOS 26.6.2, arm64, 16 KB pages. Python 3.11.3 (CPython, pyenv),
|
|
@@ -748,7 +748,7 @@ host-safety needs are already in-core. Detector rows require a running
|
|
|
748
748
|
| Runaway round | `[runtime] round_timeout_s` (default 1800) <!-- authored: default round_timeout_s; SSOT agent_runner/config/models.py --> | Wall-clock kill, emits `round_timeout_kill`. For a CLI with no self-timeout (pi has no turn cap, runtime timeout, or token budget) this is the **only** brake. Per-phase override: `[phases.<name>] round_timeout_s` |
|
|
749
749
|
| Cap the agent process itself | `[agent.env]` | Passed verbatim into the round subprocess env and takes precedence over the inherited `os.environ`, so e.g. `NODE_OPTIONS = "--max-old-space-size=384"` reaches a Node-based CLI |
|
|
750
750
|
| Host memory pressure | `mem_pressure` detector + serve-loop admission gate | `host_health`'s signal ladder (PSI → swap-out rate → combined MemFree+MemAvailable low → unavailable) reports `warning` or `critical` — a bare `mem_available_mb` gate is unreliable on cache-poor small-memory hosts. PSI's critical bar is `[monitor.host_health] psi_full_avg10_critical` (default 60.0, i.e. sustained ~60% avg10, not a single 1% blip); `psi_some_avg10_warning` (default 5.0) gates the warning tier. `mem_pressure_gate_inert` fires once if a cache-poor-valid signal shows real pressure while the configured `mem_avail_min_mb` (default 200) gate stays green; `mem_signal_unavailable` fires once when no tier is readable. The detector itself is notify-only (`auto_action="none"`), but `serve` separately **defers the next round** while `host_health` reports **`critical`** pressure only (`round_deferred`/`round_resumed` — a mere `warning` no longer delays a round) and, mid-round, **terminates a ballooning round** once pressure reads `critical` for `mem_critical_consecutive_samples` (default 3) consecutive ~10s ticks in a row (`round_mem_terminated`, `round_mem_critical_sample` per tick, capped at `2 × mem_critical_consecutive_samples` consecutive ticks — 1..6 at the default — so a sustained-critical deferred/off run doesn't write one event per tick for the whole round; any non-critical tick resets both the streak and the cap). `in_round_mem_terminate = false` keeps sampling and counting that streak without ever terminating a round. If this process's cgroup has both `memory.max` and `memory.swap.max` set (`host_cgroup_memory_limit` at startup), the mid-round floor defers to kernel cgroup-OOM instead (`mem_pressure_deferred_to_cgroup`) — the kernel will already contain the agent within that budget. Whole-host figures, not the agent's own share <!-- authored: default mem_avail_min_mb / psi_full_avg10_critical / psi_some_avg10_warning / mem_critical_consecutive_samples; SSOT agent_runner/config/models.py --> |
|
|
751
|
-
| Cgroup swap cap set far below what the host actually has | `host_cgroup_memory_limit` startup event (advisory fields) + one stderr line | Computed once at serve startup, alongside the existing `memory_max`/`memory_swap_max`/`cgroup_path` fields on this SAME event (no new event kind): `swap_total_bytes` (the host's total swap), `swap_cap_pct` (`memory_swap_max` as a percentage of it, `null` when either side is unknown), `memory_high` (the cgroup's `memory.high` soft-throttle threshold in bytes, `null` when unset — the cgroup read the literal `"max"`, or the file/cgroup v2 itself is unavailable), and `advisory` (
|
|
751
|
+
| Cgroup memory bound missing graceful throttling, or a swap cap set far below what the host actually has | `host_cgroup_memory_limit` startup event (advisory fields) + one stderr line | Computed once at serve startup, alongside the existing `memory_max`/`memory_swap_max`/`cgroup_path` fields on this SAME event (no new event kind): `swap_total_bytes` (the host's total swap), `swap_cap_pct` (`memory_swap_max` as a percentage of it, `null` when either side is unknown), `memory_high` (the cgroup's `memory.high` soft-throttle threshold in bytes, `null` when unset — the cgroup read the literal `"max"`, or the file/cgroup v2 itself is unavailable), and `advisory` (one or two warning strings joined with `"; "`, else `null`). One advisory fires when a finite `memory.swap.max` sits under 25% of host swap — this is the blind spot where the mid-round floor can terminate a round the kernel's own cgroup-OOM would have contained on a wider cap. A second, independent advisory fires when `memory.max` is set on the supervisor's OWN cgroup (not an inherited parent slice or container root, where the operator couldn't act on the hint from their own unit anyway) without `memory.high`: it recommends adding `memory.high` below `memory.max` for graceful pre-OOM throttling, plus — only when the mid-round floor ISN'T already deferring to kernel cgroup-OOM — a caveat to also bound `memory.swap.max` or set `in_round_mem_terminate=false`, so the throttle's own PSI-full rise doesn't trip the floor mid-round. Either advisory prints its own `agent-runner: ...` line to stderr. Advisory only: nothing here ever edits the operator's cgroup or systemd unit. The auto-defer decision above also requires `memory_swap_max` to be at most `swap_total_bytes` — a cap far ABOVE host swap can't bind before host-wide swap exhaustion either, so it can't disarm the floor |
|
|
752
752
|
| See cgroup memory pressure BUILDING (not just the terminate/defer verdict) | `round_cgroup_memory` event, once per round | Emitted once per round when this process's cgroup has a finite `memory.max` (see `host_cgroup_memory_limit` above): the round's peak `memory.current`/`memory.swap.current` (the max seen across the existing ~10s mid-round ticks, not the cumulative-since-startup `memory.peak`) and the `memory.events` counter DELTAS over the round (`events_high_delta`/`events_max_delta`/`events_oom_delta`/`events_oom_kill_delta`) — pressure an operator can watch build toward an OOM without SSH-ing in. Deltas, not absolutes: a nonzero `events_oom_kill_delta` means the kernel OOM-killed something inside this round specifically. `bounding_cgroup_path` names the ancestor these figures are read at (deliberately distinct from `host_cgroup_memory_limit`'s `cgroup_path`, which is this process's own leaf cgroup — the two can differ). No-op (nothing emitted) on a host with no finite cgroup bound, or if that bounding cgroup is no longer readable at round end |
|
|
753
753
|
| A round dies to the kernel's own cgroup-OOM, not just a silent 137 | `round_oom_killed` event, folded from the same `round_cgroup_memory` scan | Emitted when `memory.events.oom_kill` rose over the round (the same delta `round_cgroup_memory` already computed — no second read). Pointer-only: `log_path`, `log_bytes`, and `oom_kill_delta`, never the round-log/transcript content itself. `partial_log` reports whether the supervisor appended its own truncation trailer to the round log (a residue marker, not content) so a later reader can tell the tail is supervisor-added. The round still exits 137 and counts toward the crash streak exactly as today — this event only makes the kill legible, it never changes the give-up decision |
|
|
754
754
|
| Disk filling up | `disk_warning` at ≥90%, `disk_critical` at ≥95% | Sampled on `log_dir`'s partition. `disk_critical` auto-stops the service by default; `disk_warning` only alerts. Thresholds under `[monitor.host_health]` <!-- authored: disk_critical ships in the default auto_stop_on set; SSOT agent_runner/config/models.py --> |
|
|
@@ -17,7 +17,8 @@ def _write_toml(
|
|
|
17
17
|
prompt = tmp_git_repo / "p.md"
|
|
18
18
|
prompt.write_text("Body content for serve loop test. " * 50)
|
|
19
19
|
log_dir = tmp_git_repo / "logs"
|
|
20
|
-
toml.write_text(
|
|
20
|
+
toml.write_text(
|
|
21
|
+
f"""
|
|
21
22
|
[agent]
|
|
22
23
|
command = ["{fake_agent}"]
|
|
23
24
|
prompt_arg_template = ["{{prompt}}"]
|
|
@@ -28,7 +29,9 @@ round_timeout_s = {round_timeout}
|
|
|
28
29
|
restart_delay_s = 1
|
|
29
30
|
[prompt]
|
|
30
31
|
file = "{prompt}"
|
|
31
|
-
"""
|
|
32
|
+
"""
|
|
33
|
+
+ phases_block
|
|
34
|
+
)
|
|
32
35
|
(tmp_git_repo / ".gitignore").write_text("logs/\n")
|
|
33
36
|
subprocess.run(["git", "add", "."], cwd=tmp_git_repo, check=True)
|
|
34
37
|
subprocess.run(
|
|
@@ -165,15 +165,18 @@ def test_run_should_wait_for_ceiling_when_marker_never_matches(tmp_path, monkeyp
|
|
|
165
165
|
starts a countdown, so the round rides out to the wall-clock ceiling exactly
|
|
166
166
|
like a round that emitted nothing at all.
|
|
167
167
|
|
|
168
|
-
Childless (exec) + a
|
|
168
|
+
Childless (exec) + a 5s wall + the 0.1s patched scan interval, all
|
|
169
169
|
mirroring test_run_should_start_grace_countdown_when_configured_marker_appears:
|
|
170
170
|
if the match were ever broken (e.g. treating any marker as a hit), this round
|
|
171
|
-
would be reaped for grace at ~1s -- well before the
|
|
171
|
+
would be reaped for grace at ~1s -- well before the 5s wall -- so the
|
|
172
172
|
`killed_for_grace is False` below actually exercises the match, it doesn't
|
|
173
173
|
just win a timing race against a 2s wall (see the T3 review finding this test
|
|
174
|
-
used to be vacuous under). The exec'd sleep
|
|
175
|
-
--
|
|
176
|
-
|
|
174
|
+
used to be vacuous under). The exec'd sleep is kept at a long 60s (must
|
|
175
|
+
OUTLAST the wall) -- a short sleep would let the child exit on its own
|
|
176
|
+
before timeout_s ever fires (a risk under `-n auto` contention, see
|
|
177
|
+
test_run_should_kill_for_grace_when_agent_has_no_live_children_after_result's
|
|
178
|
+
docstring above), making `timed_out is True` false-fail regardless of the
|
|
179
|
+
marker logic; only the wall (timeout_s) shrinks."""
|
|
177
180
|
monkeypatch.setattr(agent_runtime, "_RESULT_SCAN_INTERVAL_S", 0.1)
|
|
178
181
|
script = _write_fake_script(
|
|
179
182
|
tmp_path,
|
|
@@ -186,7 +189,7 @@ def test_run_should_wait_for_ceiling_when_marker_never_matches(tmp_path, monkeyp
|
|
|
186
189
|
command=[str(script)],
|
|
187
190
|
prompt_arg_template=[],
|
|
188
191
|
prompt="x",
|
|
189
|
-
timeout_s=
|
|
192
|
+
timeout_s=5,
|
|
190
193
|
log_path=log_path,
|
|
191
194
|
env_extra={},
|
|
192
195
|
max_grace_after_result_s=1,
|
|
@@ -203,16 +206,19 @@ def test_run_should_disable_marker_grace_when_terminal_marker_empty(tmp_path, mo
|
|
|
203
206
|
never runs (guarded by `and marker_bytes`) -- no empty-substring-matches-
|
|
204
207
|
everything trap. The round rides out to the wall-clock ceiling.
|
|
205
208
|
|
|
206
|
-
Childless (exec) + a
|
|
209
|
+
Childless (exec) + a 5s wall + the 0.1s patched scan interval, all
|
|
207
210
|
mirroring test_run_should_start_grace_countdown_when_configured_marker_appears:
|
|
208
211
|
if the `and marker_bytes` guard were ever dropped, an empty marker_bytes is
|
|
209
212
|
a substring of everything, so this round would be reaped for grace at ~1s --
|
|
210
|
-
well before the
|
|
213
|
+
well before the 5s wall -- so `killed_for_grace is False` below actually
|
|
211
214
|
exercises the guard, it doesn't just win a timing race against a 2s wall (see
|
|
212
215
|
the T3 review finding this test used to be vacuous under). The exec'd sleep
|
|
213
|
-
must OUTLAST the wall
|
|
214
|
-
its own before timeout_s ever fires
|
|
215
|
-
|
|
216
|
+
is kept at a long 60s (must OUTLAST the wall) -- a short sleep would let the
|
|
217
|
+
child exit on its own before timeout_s ever fires (a risk under `-n auto`
|
|
218
|
+
contention, see
|
|
219
|
+
test_run_should_kill_for_grace_when_agent_has_no_live_children_after_result's
|
|
220
|
+
docstring above), making `timed_out is True` false-fail regardless of the
|
|
221
|
+
marker logic; only the wall (timeout_s) shrinks."""
|
|
216
222
|
monkeypatch.setattr(agent_runtime, "_RESULT_SCAN_INTERVAL_S", 0.1)
|
|
217
223
|
script = _write_fake_script(
|
|
218
224
|
tmp_path,
|
|
@@ -225,7 +231,7 @@ def test_run_should_disable_marker_grace_when_terminal_marker_empty(tmp_path, mo
|
|
|
225
231
|
command=[str(script)],
|
|
226
232
|
prompt_arg_template=[],
|
|
227
233
|
prompt="x",
|
|
228
|
-
timeout_s=
|
|
234
|
+
timeout_s=5,
|
|
229
235
|
log_path=log_path,
|
|
230
236
|
env_extra={},
|
|
231
237
|
max_grace_after_result_s=1,
|
|
@@ -97,6 +97,7 @@ def test_cgroup_memory_limits_should_return_both_limits_when_both_finite(
|
|
|
97
97
|
"memory_max": 335544320,
|
|
98
98
|
"memory_swap_max": 167772160,
|
|
99
99
|
"cgroup_path": _LEAF,
|
|
100
|
+
"bounding_cgroup_path": _LEAF,
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
|
|
@@ -141,6 +142,33 @@ def test_cgroup_memory_limits_should_pick_lower_of_leaf_and_ancestor_when_both_f
|
|
|
141
142
|
assert lim["memory_max"] == 335544320
|
|
142
143
|
|
|
143
144
|
|
|
145
|
+
def test_cgroup_memory_limits_should_report_bounding_path_as_the_min_max_owner_when_resolved(
|
|
146
|
+
fake_cgroup: _FakeCgroup,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""The own leaf sets memory.max itself, so it's also the bounding
|
|
149
|
+
ancestor -- ``bounding_cgroup_path`` equals ``cgroup_path``."""
|
|
150
|
+
fake_cgroup(memory_max="335544320", memory_swap_max="167772160")
|
|
151
|
+
|
|
152
|
+
lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
|
|
153
|
+
|
|
154
|
+
assert lim["bounding_cgroup_path"] == lim["cgroup_path"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_cgroup_memory_limits_should_report_bounding_path_as_the_ancestor_when_leaf_unlimited(
|
|
158
|
+
fake_cgroup: _FakeCgroup,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""The leaf itself is unlimited; a parent slice owns the real budget --
|
|
161
|
+
``bounding_cgroup_path`` names that ancestor, NOT the (looser) leaf, so a
|
|
162
|
+
caller can tell an inherited bound from an own-scope one."""
|
|
163
|
+
fake_cgroup(memory_max="max", memory_swap_max="max")
|
|
164
|
+
fake_cgroup.set_limit("/system.slice", memory_max="335544320", memory_swap_max="167772160")
|
|
165
|
+
|
|
166
|
+
lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
|
|
167
|
+
|
|
168
|
+
assert lim["bounding_cgroup_path"] == "/system.slice"
|
|
169
|
+
assert lim["bounding_cgroup_path"] != lim["cgroup_path"]
|
|
170
|
+
|
|
171
|
+
|
|
144
172
|
def test_cgroup_memory_limits_should_return_all_none_when_cgroup_v1_or_missing(
|
|
145
173
|
fake_cgroup: _FakeCgroup,
|
|
146
174
|
) -> None:
|
|
@@ -151,7 +179,12 @@ def test_cgroup_memory_limits_should_return_all_none_when_cgroup_v1_or_missing(
|
|
|
151
179
|
|
|
152
180
|
lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
|
|
153
181
|
|
|
154
|
-
assert lim == {
|
|
182
|
+
assert lim == {
|
|
183
|
+
"memory_max": None,
|
|
184
|
+
"memory_swap_max": None,
|
|
185
|
+
"cgroup_path": None,
|
|
186
|
+
"bounding_cgroup_path": None,
|
|
187
|
+
}
|
|
155
188
|
|
|
156
189
|
|
|
157
190
|
def test_cgroup_memory_limits_should_parse_self_cgroup_from_proc_file(tmp_path: Path) -> None:
|
|
@@ -176,6 +209,7 @@ def test_cgroup_memory_limits_should_parse_self_cgroup_from_proc_file(tmp_path:
|
|
|
176
209
|
"memory_max": 335544320,
|
|
177
210
|
"memory_swap_max": 167772160,
|
|
178
211
|
"cgroup_path": "/system.slice/example.service",
|
|
212
|
+
"bounding_cgroup_path": "/system.slice/example.service",
|
|
179
213
|
}
|
|
180
214
|
|
|
181
215
|
|
|
@@ -190,7 +224,12 @@ def test_cgroup_memory_limits_should_return_all_none_when_proc_self_cgroup_missi
|
|
|
190
224
|
|
|
191
225
|
lim = metrics.cgroup_memory_limits(root=root, proc_self_cgroup=tmp_path / "does-not-exist")
|
|
192
226
|
|
|
193
|
-
assert lim == {
|
|
227
|
+
assert lim == {
|
|
228
|
+
"memory_max": None,
|
|
229
|
+
"memory_swap_max": None,
|
|
230
|
+
"cgroup_path": None,
|
|
231
|
+
"bounding_cgroup_path": None,
|
|
232
|
+
}
|
|
194
233
|
|
|
195
234
|
|
|
196
235
|
# --- 0.2.18 T1c fix round 1: cgroup_memory_high, a REAL memory.high read ---
|
|
@@ -277,15 +316,22 @@ def _patch_probe(
|
|
|
277
316
|
monkeypatch,
|
|
278
317
|
*,
|
|
279
318
|
memory_max: int | None,
|
|
280
|
-
memory_swap_max: int,
|
|
319
|
+
memory_swap_max: int | None,
|
|
281
320
|
mem_total: int,
|
|
282
321
|
swap_total: int,
|
|
283
322
|
memory_high: int | None = None,
|
|
323
|
+
cgroup_path: str | None = "/x",
|
|
324
|
+
bounding_cgroup_path: str | None = "/x",
|
|
284
325
|
):
|
|
285
326
|
monkeypatch.setattr(
|
|
286
327
|
metrics,
|
|
287
328
|
"cgroup_memory_limits",
|
|
288
|
-
lambda: {
|
|
329
|
+
lambda: {
|
|
330
|
+
"memory_max": memory_max,
|
|
331
|
+
"memory_swap_max": memory_swap_max,
|
|
332
|
+
"cgroup_path": cgroup_path,
|
|
333
|
+
"bounding_cgroup_path": bounding_cgroup_path,
|
|
334
|
+
},
|
|
289
335
|
)
|
|
290
336
|
monkeypatch.setattr(metrics, "mem_total_bytes", lambda: mem_total)
|
|
291
337
|
monkeypatch.setattr(metrics, "swap_total_bytes", lambda: swap_total)
|
|
@@ -369,7 +415,9 @@ def test_probe_and_emit_cgroup_defer_should_gate_on_limit_plausibility(
|
|
|
369
415
|
pytest.param(
|
|
370
416
|
200_000_000, 12.5, True, "swap.max is far below host swap", id="far_below_host"
|
|
371
417
|
),
|
|
372
|
-
pytest.param(
|
|
418
|
+
pytest.param(
|
|
419
|
+
1_280_000_000, 80.0, True, "memory.high", id="within_host_swap_but_memory_high_unset"
|
|
420
|
+
),
|
|
373
421
|
],
|
|
374
422
|
)
|
|
375
423
|
def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
|
|
@@ -383,8 +431,10 @@ def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
|
|
|
383
431
|
) -> None:
|
|
384
432
|
"""swap.max below the 25% advisory floor (here 12.5% of host swap) rides
|
|
385
433
|
as fields on the EXISTING host_cgroup_memory_limit event -- not a new
|
|
386
|
-
kind -- plus one stderr line; a plausible cap (80%) gets
|
|
387
|
-
|
|
434
|
+
kind -- plus one stderr line; a plausible cap (80%) gets no swap-cap
|
|
435
|
+
advisory, but BOTH cases still leave memory.high unset on the own leaf,
|
|
436
|
+
so both also carry the memory.high hint (joined onto the swap-cap one
|
|
437
|
+
for the far-below-host case). Never changes the cgroup/unit either way."""
|
|
388
438
|
from agent_runner.cli._serve_cgroup import _probe_and_emit_cgroup_defer
|
|
389
439
|
|
|
390
440
|
_patch_probe(
|
|
@@ -409,6 +459,7 @@ def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
|
|
|
409
459
|
assert ev["swap_cap_pct"] == expected_swap_cap_pct
|
|
410
460
|
assert ev["memory_high"] is None
|
|
411
461
|
assert (ev["advisory"] is not None) is expect_advisory
|
|
462
|
+
assert "memory.high" in ev["advisory"]
|
|
412
463
|
|
|
413
464
|
captured = capsys.readouterr()
|
|
414
465
|
if expected_stderr_substring is None:
|
|
@@ -446,3 +497,136 @@ def test_probe_and_emit_cgroup_defer_should_emit_real_memory_high_when_set(
|
|
|
446
497
|
for line in f.read_text().splitlines()
|
|
447
498
|
]
|
|
448
499
|
assert ev["memory_high"] == 192_000_000
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# --- 0.2.24 T1: the memory.high advisory, own-scope-gated + defer-aware ---
|
|
503
|
+
#
|
|
504
|
+
# memory.max set (anywhere on the ancestor chain) without memory.high means
|
|
505
|
+
# the operator has a hard kill ceiling but no soft pre-OOM throttle -- worth
|
|
506
|
+
# flagging. But cgroup_memory_limits reports memory.max as the MIN across
|
|
507
|
+
# ALL ancestors, so "set" can also mean an inherited parent slice or a
|
|
508
|
+
# container root, where "add MemoryHigh" is noise the operator can't act on
|
|
509
|
+
# from their own unit. The hint fires ONLY when the bounding ancestor IS the
|
|
510
|
+
# operator's own leaf (bounding_cgroup_path == cgroup_path), and only
|
|
511
|
+
# appends the PSI-floor swap caveat when the mid-round floor isn't already
|
|
512
|
+
# deferring to kernel cgroup-OOM. Advisory only -- never writes a cgroup or
|
|
513
|
+
# unit file.
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _run_probe(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, **kwargs) -> None:
|
|
517
|
+
from agent_runner.cli._serve_cgroup import _probe_and_emit_cgroup_defer
|
|
518
|
+
|
|
519
|
+
_patch_probe(monkeypatch, **kwargs)
|
|
520
|
+
log_dir = tmp_path / "logs"
|
|
521
|
+
log_dir.mkdir()
|
|
522
|
+
_probe_and_emit_cgroup_defer(log_dir)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _only_event(tmp_path: Path) -> dict:
|
|
526
|
+
log_dir = tmp_path / "logs"
|
|
527
|
+
[ev] = [
|
|
528
|
+
json.loads(line)
|
|
529
|
+
for f in sorted(log_dir.glob("events-*.jsonl"))
|
|
530
|
+
for line in f.read_text().splitlines()
|
|
531
|
+
]
|
|
532
|
+
return ev
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_advisory_should_not_hint_memory_high_when_bound_is_an_inherited_ancestor(
|
|
536
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
537
|
+
) -> None:
|
|
538
|
+
# memory.max finite but owned by a PARENT slice, not the operator's leaf -> no hint
|
|
539
|
+
|
|
540
|
+
_run_probe(
|
|
541
|
+
monkeypatch,
|
|
542
|
+
tmp_path,
|
|
543
|
+
memory_max=256_000_000,
|
|
544
|
+
memory_swap_max=1_280_000_000,
|
|
545
|
+
mem_total=462_000_000,
|
|
546
|
+
swap_total=1_600_000_000,
|
|
547
|
+
memory_high=None,
|
|
548
|
+
bounding_cgroup_path="/system.slice",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
ev = _only_event(tmp_path)
|
|
552
|
+
assert ev["advisory"] is None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_advisory_should_omit_memory_high_hint_when_high_already_set(
|
|
556
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
557
|
+
) -> None:
|
|
558
|
+
# high already set -> no hint (and no swap-cap advisory here either)
|
|
559
|
+
|
|
560
|
+
_run_probe(
|
|
561
|
+
monkeypatch,
|
|
562
|
+
tmp_path,
|
|
563
|
+
memory_max=256_000_000,
|
|
564
|
+
memory_swap_max=1_280_000_000,
|
|
565
|
+
mem_total=462_000_000,
|
|
566
|
+
swap_total=1_600_000_000,
|
|
567
|
+
memory_high=224_000_000,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
ev = _only_event(tmp_path)
|
|
571
|
+
assert ev["advisory"] is None
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_advisory_memory_high_hint_should_omit_swap_caveat_when_already_deferring(
|
|
575
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
576
|
+
) -> None:
|
|
577
|
+
# both-finite + plausible -> floor defers -> hint fires WITHOUT the "bound swap" caveat
|
|
578
|
+
|
|
579
|
+
_run_probe(
|
|
580
|
+
monkeypatch,
|
|
581
|
+
tmp_path,
|
|
582
|
+
memory_max=256_000_000,
|
|
583
|
+
memory_swap_max=1_280_000_000,
|
|
584
|
+
mem_total=462_000_000,
|
|
585
|
+
swap_total=1_600_000_000,
|
|
586
|
+
memory_high=None,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
ev = _only_event(tmp_path)
|
|
590
|
+
assert "memory.high" in ev["advisory"]
|
|
591
|
+
assert "swap" not in ev["advisory"].lower()
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def test_advisory_memory_high_hint_should_include_swap_caveat_when_not_deferring(
|
|
595
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
596
|
+
) -> None:
|
|
597
|
+
# memory.max bounded but memory.swap.max UNBOUNDED -> floor stays armed (no defer) -> caveat
|
|
598
|
+
|
|
599
|
+
_run_probe(
|
|
600
|
+
monkeypatch,
|
|
601
|
+
tmp_path,
|
|
602
|
+
memory_max=256_000_000,
|
|
603
|
+
memory_swap_max=None,
|
|
604
|
+
mem_total=462_000_000,
|
|
605
|
+
swap_total=1_600_000_000,
|
|
606
|
+
memory_high=None,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
ev = _only_event(tmp_path)
|
|
610
|
+
assert "memory.high" in ev["advisory"]
|
|
611
|
+
assert "swap" in ev["advisory"].lower()
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_advisory_should_be_none_when_cgroup_v2_unavailable(
|
|
615
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
616
|
+
) -> None:
|
|
617
|
+
# no cgroup v2 -> memory_max/cgroup_path/bounding_cgroup_path all None -> own_scope stays inert
|
|
618
|
+
|
|
619
|
+
_run_probe(
|
|
620
|
+
monkeypatch,
|
|
621
|
+
tmp_path,
|
|
622
|
+
memory_max=None,
|
|
623
|
+
memory_swap_max=None,
|
|
624
|
+
mem_total=462_000_000,
|
|
625
|
+
swap_total=1_600_000_000,
|
|
626
|
+
memory_high=None,
|
|
627
|
+
cgroup_path=None,
|
|
628
|
+
bounding_cgroup_path=None,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
ev = _only_event(tmp_path)
|
|
632
|
+
assert ev["advisory"] is None
|