cli-agent-runner 0.3.3__tar.gz → 0.3.5__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.3.3 → cli_agent_runner-0.3.5}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CHANGELOG.md +14 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/PKG-INFO +1 -1
- cli_agent_runner-0.3.5/agent_runner/_notify.py +245 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_manifest.py +20 -0
- cli_agent_runner-0.3.5/agent_runner/_procwait.py +165 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_serve_policy.py +13 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_throttle.py +49 -6
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/agent_runtime.py +42 -19
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/_serve_round.py +90 -45
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/common.py +8 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/doctor_cmd.py +15 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/events_cmd.py +29 -24
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/serve_cmd.py +95 -7
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/models.py +26 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/parsers.py +24 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/events.py +2 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/runner.py +24 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/commands.md +1 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/configuration.md +15 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/memory-efficiency.md +95 -3
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/plugins.md +2 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/pyproject.toml +2 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/conftest.py +24 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_auto_stop_e2e.py +9 -1
- cli_agent_runner-0.3.5/tests/integration/test_cooperative_grace_property.py +142 -0
- cli_agent_runner-0.3.5/tests/integration/test_reap_property.py +153 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_run_one_round_with_fake_agent.py +26 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_spawn_round_mem_floor.py +35 -10
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_spawn_round_wedged.py +14 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_architecture.py +7 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_import_footprint.py +11 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_layer_2_loop_size.py +4 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_module_boundaries.py +1 -0
- cli_agent_runner-0.3.5/tests/invariants/test_no_asyncio_select.py +253 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_vacuous_invariants.py +2 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_peek_schema_version.py +1 -1
- cli_agent_runner-0.3.5/tests/invariants/test_procwait_shield.py +92 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_timeout_budget_invariant.py +32 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_grace.py +91 -50
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_reap.py +53 -0
- cli_agent_runner-0.3.5/tests/unit/test_apply_reap_grace_env.py +92 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config.py +44 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_doctor_cmd.py +9 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd.py +32 -19
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd_rollover.py +21 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd_tail.py +31 -9
- cli_agent_runner-0.3.5/tests/unit/test_kill_pgroup_shielding.py +134 -0
- cli_agent_runner-0.3.5/tests/unit/test_notify.py +174 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_sandbox_block.py +26 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_manifest.py +44 -0
- cli_agent_runner-0.3.5/tests/unit/test_procwait.py +113 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner.py +35 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner_throttle.py +7 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_phase_schedule.py +8 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_cgroup_spine.py +24 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_terminate.py +19 -2
- cli_agent_runner-0.3.5/tests/unit/test_serve_round_wait_exit.py +81 -0
- cli_agent_runner-0.3.5/tests/unit/test_serve_wakeup.py +169 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_service_unit.py +4 -3
- cli_agent_runner-0.3.5/tests/unit/test_terminate_round_wait_exit.py +96 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.codecov.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/dependabot.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.gitignore +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/BACKLOG.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/LICENSE +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/README.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/README.zh.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/SECURITY.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/plugins.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_observe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_sandbox.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/build.sh +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/README.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/architecture.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/events.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/migrations/0.3.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/container-pi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/runbook.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/thesis.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/_clock.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/linux/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/linux/test_plugin_sandbox_kill.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_container.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_builtin_provenance.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_collapse_spawn_decisions.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_doctor_sandbox.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_gate_serve_boot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_load_fail_closed.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_loader_split.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_manifest_spawn_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_name_mismatch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_sandbox_wire.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugins_config_fields.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_spawn_seam.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_vcs_state.py +0 -0
|
@@ -158,6 +158,7 @@ overlaps
|
|
|
158
158
|
plan
|
|
159
159
|
sandbox
|
|
160
160
|
cgroup
|
|
161
|
+
sigterm_grace
|
|
161
162
|
|
|
162
163
|
# agent_runner.config.models.AgentConfig
|
|
163
164
|
command
|
|
@@ -167,6 +168,7 @@ env
|
|
|
167
168
|
prompt_delivery
|
|
168
169
|
exec_prefix
|
|
169
170
|
terminal_marker
|
|
171
|
+
sigterm_grace_s
|
|
170
172
|
|
|
171
173
|
# agent_runner.config.models.Config
|
|
172
174
|
agent
|
|
@@ -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.3.5] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[agent] sigterm_grace_s` (default 10): the SIGTERM→SIGKILL grace a round gives its agent when the agent's preset declares itself cooperative (the built-in `gemini` preset does), so an agent that catches SIGTERM to flush or commit its work gets time to finish. A non-cooperative agent (e.g. `claude`) keeps the previous 5 s. Capped at 12 s, inside the supervisor's 15 s wait for the round; `peek --json` (schema `2.4`) and `doctor` report the resolved grace per agent.
|
|
12
|
+
|
|
13
|
+
## [0.3.4] - 2026-09-15
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- While a round runs, the supervisor now blocks on the round process's exit instead of polling about once a second — it uses no CPU while waiting and notices a round's exit immediately.
|
|
17
|
+
- A stop (`SIGTERM` / `agent-runner serve stop`) during a schedule, memory, or phase pause, or during the inter-round restart delay, now takes effect near-instantly instead of after the current pause chunk (up to 30 s).
|
|
18
|
+
- `agent-runner events --tail` shows a new matching event within a drain cycle of its being written, instead of on a ~1 s poll.
|
|
19
|
+
|
|
20
|
+
No change to the rules for when a round is deferred, terminated, or reaped. The mid-round memory check and the round budget now take effect on schedule instead of up to ~1 s late, so within that window the sustained-pressure floor or the budget cutoff can act on a round the old ~1 s poll would have seen finish first.
|
|
21
|
+
|
|
8
22
|
## [0.3.3] - 2026-09-14
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""A broker-less FIFO doorbell -- wake a waiting consumer within milliseconds
|
|
2
|
+
of a durable event write, instead of a busy-poll loop checking every second.
|
|
3
|
+
|
|
4
|
+
Per-listener FIFO under ``log_dir/.notify/``: one named pipe per live
|
|
5
|
+
``Listener``, path-addressed so the producer (``events.emit``) needs no
|
|
6
|
+
registry, no socket, and no persistent connection -- it just fans a single
|
|
7
|
+
byte out to every ``*.fifo`` file it finds. Zero cost when nobody is
|
|
8
|
+
listening (the directory is empty, ``ring`` is a fast no-op scan).
|
|
9
|
+
Self-healing on a stale entry left behind by a crashed listener: opening a
|
|
10
|
+
FIFO with no reader raises ``ENXIO``, which ``ring`` treats as "nobody's
|
|
11
|
+
there" and cleans up.
|
|
12
|
+
|
|
13
|
+
Stdlib only (``os``, ``select``, ``pathlib``) -- no watchfiles/inotify,
|
|
14
|
+
no socketpair/AF_UNIX. A FIFO is opened ``O_RDWR`` (the self-pipe trick):
|
|
15
|
+
holding the write side open ourselves means the read side never sees the
|
|
16
|
+
"all writers closed" EOF a plain read-only open would eventually hit.
|
|
17
|
+
|
|
18
|
+
Fail-open throughout: any ``OSError`` from ``mkfifo``/``open`` degrades a
|
|
19
|
+
would-be ``Listener`` to a ``NullListener`` that just sleeps out its
|
|
20
|
+
timeout, so a filesystem without FIFO support (or without write
|
|
21
|
+
permission under ``log_dir``) never breaks the caller -- it only falls
|
|
22
|
+
back to the busy-poll cadence this module was built to avoid.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import errno
|
|
28
|
+
import os
|
|
29
|
+
import select
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
33
|
+
|
|
34
|
+
_NOTIFY_DIRNAME = ".notify"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ring(log_dir: Path) -> None:
|
|
38
|
+
"""Best-effort wake for every live ``Listener`` under ``log_dir/.notify/``.
|
|
39
|
+
|
|
40
|
+
For each ``*.fifo``: open ``O_WRONLY | O_NONBLOCK``, write one byte,
|
|
41
|
+
close. Every ``OSError`` is swallowed -- ``EAGAIN`` (the listener's
|
|
42
|
+
kernel pipe buffer is full) is harmless, the wake already landed;
|
|
43
|
+
``ENXIO`` (no reader -- a stale FIFO left by a listener that crashed
|
|
44
|
+
without unlinking) means the entry is dead, so it is unlinked. Never
|
|
45
|
+
raises into the caller's durable-write path.
|
|
46
|
+
"""
|
|
47
|
+
notify_dir = log_dir / _NOTIFY_DIRNAME
|
|
48
|
+
try:
|
|
49
|
+
with os.scandir(notify_dir) as it:
|
|
50
|
+
paths = [e.path for e in it if e.name.endswith(".fifo")]
|
|
51
|
+
except OSError:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
for path in paths:
|
|
55
|
+
try:
|
|
56
|
+
fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
|
|
57
|
+
except OSError as e:
|
|
58
|
+
if e.errno == errno.ENXIO:
|
|
59
|
+
try:
|
|
60
|
+
os.unlink(path)
|
|
61
|
+
except OSError:
|
|
62
|
+
pass # a permission-denied .notify/ can't be cleaned up -- never mind
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
os.write(fd, b"\x00")
|
|
66
|
+
except OSError:
|
|
67
|
+
pass # EAGAIN: listener's kernel buffer is full -- the wake already landed
|
|
68
|
+
finally:
|
|
69
|
+
try:
|
|
70
|
+
os.close(fd)
|
|
71
|
+
except OSError:
|
|
72
|
+
pass # EINTR or similar -- fail-open, never let a close raise into emit()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def drain(fd: int) -> None:
|
|
76
|
+
"""Read a level-triggered wakeup fd until ``EAGAIN`` so a delivered byte
|
|
77
|
+
(a ``ring()`` or a ``signal.set_wakeup_fd`` signal) doesn't leave the next
|
|
78
|
+
``select()`` on it instantly ready -- an undrained fd busy-spins its
|
|
79
|
+
caller at 100% CPU forever after the first wake, since a level-triggered
|
|
80
|
+
fd stays readable until read. Advisory: the bytes carry no payload and are
|
|
81
|
+
discarded. Used only by :meth:`Listener.wait` (its own timeout branch) --
|
|
82
|
+
the pause/sleep waits (``_interruptible_sleep``/``_pause_poll``) are the
|
|
83
|
+
sole consumers of a live ``Listener``. Nothing watches this fd mid-round
|
|
84
|
+
(``_spawn_round`` only ever waits on the round leader's own exit fd), so
|
|
85
|
+
there is no other caller that must drain it itself.
|
|
86
|
+
"""
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
if not os.read(fd, 4096):
|
|
90
|
+
break
|
|
91
|
+
except OSError:
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Listener:
|
|
96
|
+
"""Context manager owning one FIFO under ``log_dir/.notify/``.
|
|
97
|
+
|
|
98
|
+
``__enter__`` creates ``log_dir/.notify/<pid>-<id(self)>.fifo`` and
|
|
99
|
+
opens it ``O_RDWR | O_NONBLOCK`` -- holding the write side ourselves
|
|
100
|
+
is what keeps the read side from ever seeing a persistent EOF. It
|
|
101
|
+
retries up to 3 times if a concurrent ``ring()`` wins the race between
|
|
102
|
+
``mkfifo`` and ``open`` and unlinks the readerless FIFO out from under
|
|
103
|
+
us (see ``__enter__``'s own comment).
|
|
104
|
+
``__exit__`` unlinks the FIFO unconditionally (including when an
|
|
105
|
+
exception, e.g. ``KeyboardInterrupt``, propagates through the
|
|
106
|
+
``with`` block).
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, log_dir: Path) -> None:
|
|
110
|
+
self._notify_dir = log_dir / _NOTIFY_DIRNAME
|
|
111
|
+
self._path = self._notify_dir / f"{os.getpid()}-{id(self)}.fifo"
|
|
112
|
+
self._fd: int | None = None
|
|
113
|
+
|
|
114
|
+
def __enter__(self) -> Listener:
|
|
115
|
+
# Idempotent: open_listener() pre-enters a Listener to detect OSError
|
|
116
|
+
# and degrade to a NullListener, then hands the already-entered
|
|
117
|
+
# instance to the caller's own `with` statement, which enters it a
|
|
118
|
+
# second time -- that second entry must be a no-op, not a re-mkfifo.
|
|
119
|
+
if self._fd is not None:
|
|
120
|
+
return self
|
|
121
|
+
self._notify_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
# Bounded retry: mkfifo and open are two separate syscalls, so there is a
|
|
123
|
+
# window between them where the FIFO exists but has no reader yet. A
|
|
124
|
+
# concurrent ring() from another process sharing this log_dir can open
|
|
125
|
+
# our fresh, readerless FIFO O_WRONLY|O_NONBLOCK, get ENXIO, and (by
|
|
126
|
+
# design -- see ring()'s docstring) unlink it as "stale". Our own
|
|
127
|
+
# os.open then loses the file underneath us with FileNotFoundError.
|
|
128
|
+
# Retrying re-creates the FIFO from scratch; three tries makes losing
|
|
129
|
+
# the race every single time astronomically unlikely.
|
|
130
|
+
last_exc: OSError | None = None
|
|
131
|
+
for _ in range(3):
|
|
132
|
+
try:
|
|
133
|
+
os.mkfifo(self._path)
|
|
134
|
+
except FileExistsError:
|
|
135
|
+
pass # a prior retry (or the same path) already made it -- try to open it
|
|
136
|
+
try:
|
|
137
|
+
self._fd = os.open(self._path, os.O_RDWR | os.O_NONBLOCK)
|
|
138
|
+
return self
|
|
139
|
+
except FileNotFoundError as e:
|
|
140
|
+
last_exc = e # a concurrent ring() unlinked our readerless FIFO -- retry
|
|
141
|
+
raise last_exc if last_exc is not None else OSError("Listener.__enter__ exhausted retries")
|
|
142
|
+
|
|
143
|
+
def __exit__(
|
|
144
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: object
|
|
145
|
+
) -> None:
|
|
146
|
+
if self._fd is not None:
|
|
147
|
+
try:
|
|
148
|
+
os.close(self._fd)
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
self._fd = None
|
|
152
|
+
try:
|
|
153
|
+
self._path.unlink(missing_ok=True)
|
|
154
|
+
except OSError:
|
|
155
|
+
pass # e.g. a permission-denied .notify/ -- fail-open, never crash teardown
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def fd(self) -> int:
|
|
159
|
+
"""The FIFO's read/write fd -- selectable, and the value handed to
|
|
160
|
+
``signal.set_wakeup_fd``."""
|
|
161
|
+
assert self._fd is not None, "Listener.fd read outside its `with` block"
|
|
162
|
+
return self._fd
|
|
163
|
+
|
|
164
|
+
def wait(self, timeout_s: float, *, clock: Clock = SYSTEM_CLOCK) -> bool:
|
|
165
|
+
"""Block until a ring wakes this listener or ``timeout_s`` elapses.
|
|
166
|
+
|
|
167
|
+
``select.select`` against a ``clock.monotonic()`` deadline so a
|
|
168
|
+
real timeout still elapses under real time even though the wake
|
|
169
|
+
itself is an fd event no fake clock can simulate. On wake, drains
|
|
170
|
+
the fd (reads until ``EAGAIN``) so a burst of rings before the
|
|
171
|
+
next ``wait`` call coalesces into a single wake instead of one
|
|
172
|
+
per byte.
|
|
173
|
+
|
|
174
|
+
Returns ``True`` iff woken before the timeout -- a ring, a signal
|
|
175
|
+
delivered via ``signal.set_wakeup_fd`` on this same fd, or a
|
|
176
|
+
spurious wake all look identical here; callers never distinguish
|
|
177
|
+
them because the wake is purely advisory -- either way the caller
|
|
178
|
+
re-reads its own state after ``wait`` returns.
|
|
179
|
+
"""
|
|
180
|
+
deadline = clock.monotonic() + timeout_s
|
|
181
|
+
remaining = max(0.0, deadline - clock.monotonic())
|
|
182
|
+
ready, _, _ = select.select([self.fd], [], [], remaining)
|
|
183
|
+
if not ready:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
drain(self.fd)
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class NullListener:
|
|
191
|
+
"""Fallback when ``mkfifo``/``open`` fails (no filesystem permission,
|
|
192
|
+
FIFOs unsupported). ``fd`` is ``None`` -- there is nothing to select
|
|
193
|
+
on. A no-op context manager so ``with open_listener(...) as listener``
|
|
194
|
+
works identically whether or not the FIFO could be created.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def fd(self) -> None:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def __enter__(self) -> NullListener:
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def __exit__(
|
|
205
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: object
|
|
206
|
+
) -> None:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def wait(self, timeout_s: float, *, clock: Clock = SYSTEM_CLOCK) -> bool:
|
|
210
|
+
"""Degrades to a plain timed sleep -- preserves every ``FakeClock``
|
|
211
|
+
test's semantics unchanged: waiting still advances virtual time by
|
|
212
|
+
exactly ``timeout_s``, same as a bare ``clock.sleep(timeout_s)``."""
|
|
213
|
+
clock.sleep(timeout_s)
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Stateless (fd=None, wait==clock.sleep, no-op ctx mgr), so one module-level
|
|
218
|
+
# instance is a safe default arg -- the stable-singleton default (mirrors
|
|
219
|
+
# SYSTEM_CLOCK) every non-serve caller of a listener-aware function keeps by
|
|
220
|
+
# not passing one, staying byte-identical to its pre-doorbell behavior.
|
|
221
|
+
NULL_LISTENER = NullListener()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def open_listener(log_dir: Path) -> Listener | NullListener:
|
|
225
|
+
"""Try to construct a live ``Listener``; degrade to a ``NullListener``
|
|
226
|
+
on any ``OSError`` from ``mkfifo``/``open`` (e.g. a read-only
|
|
227
|
+
filesystem or a platform without FIFO support) so the consumer never
|
|
228
|
+
has to branch on which one it got.
|
|
229
|
+
|
|
230
|
+
Pre-enters the ``Listener`` here (rather than leaving that to the
|
|
231
|
+
caller's own ``with``) so the failure can be caught and swapped for a
|
|
232
|
+
``NullListener``; ``Listener.__enter__`` is idempotent, so the
|
|
233
|
+
caller's subsequent ``with open_listener(log_dir) as listener:`` is
|
|
234
|
+
safe to call it a second time.
|
|
235
|
+
"""
|
|
236
|
+
listener = Listener(log_dir)
|
|
237
|
+
try:
|
|
238
|
+
listener.__enter__()
|
|
239
|
+
except OSError:
|
|
240
|
+
try:
|
|
241
|
+
listener._path.unlink(missing_ok=True)
|
|
242
|
+
except OSError:
|
|
243
|
+
pass # cleanup best-effort -- must not crash the caller's degrade-to-Null path
|
|
244
|
+
return NullListener()
|
|
245
|
+
return listener
|
|
@@ -62,6 +62,26 @@ def cooperative_manifest_names() -> list[str]:
|
|
|
62
62
|
return [m.name for m in _LOADED_MANIFESTS if m.sigterm_cooperative]
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def is_cooperative_agent(agent_binary: str | None) -> bool:
|
|
66
|
+
"""Whether the agent's preset declares itself SIGTERM-cooperative.
|
|
67
|
+
`None in [...]` is safely False, so no None-guard is needed."""
|
|
68
|
+
return agent_binary in cooperative_manifest_names()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_sigterm_grace_s(agent_binary: str | None, sigterm_grace_s: int) -> int:
|
|
72
|
+
"""The SIGTERM->SIGKILL grace this round's agent actually gets:
|
|
73
|
+
`sigterm_grace_s` (config's [agent] sigterm_grace_s, boot-capped at
|
|
74
|
+
_ROUND_TERM_GRACE_S) when `agent_binary` names a manifest declaring
|
|
75
|
+
sigterm_cooperative=True, else agent_runtime.REAP_GRACE_S (5s)
|
|
76
|
+
unchanged. One function, two callers: cli.serve_cmd._apply_reap_grace_env
|
|
77
|
+
(what the agent actually gets) and doctor/peek (what an operator sees)."""
|
|
78
|
+
from agent_runner.agent_runtime import REAP_GRACE_S
|
|
79
|
+
|
|
80
|
+
if is_cooperative_agent(agent_binary):
|
|
81
|
+
return sigterm_grace_s
|
|
82
|
+
return REAP_GRACE_S
|
|
83
|
+
|
|
84
|
+
|
|
65
85
|
def register_manifest(
|
|
66
86
|
manifest: PluginManifest,
|
|
67
87
|
*,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Process-exit waiting on a readable fd, not a busy-poll loop.
|
|
2
|
+
|
|
3
|
+
``exit_fd`` opens a throwaway fd that becomes readable once a subprocess has
|
|
4
|
+
exited (``os.pidfd_open`` on Linux, a kqueue ``NOTE_EXIT`` registration on
|
|
5
|
+
macOS/BSD, ``None`` on any other platform or any setup failure). ``wait_exit``
|
|
6
|
+
blocks in one ``select.select`` on that fd until the process exits or a real
|
|
7
|
+
deadline passes.
|
|
8
|
+
|
|
9
|
+
Neither function ever reaps the process on this fast path -- a pidfd or a
|
|
10
|
+
kqueue registration only observes the kernel's process table, it never
|
|
11
|
+
consumes the zombie slot. There is exactly one path from "notices exit" to
|
|
12
|
+
"reaps": the caller, in the same thread, right after ``wait_exit`` returns
|
|
13
|
+
``"exited"``. No background watcher thread ever touches the process table,
|
|
14
|
+
which is what keeps this immune to CPython's pidfd/child-watcher race
|
|
15
|
+
(https://github.com/python/cpython/issues/127049): a background reaper can
|
|
16
|
+
consume a zombie's pid slot between the moment some other code decides to
|
|
17
|
+
signal that pid and the moment it actually does, letting a freshly spawned,
|
|
18
|
+
unrelated process reuse the pid and get signalled by mistake. With no
|
|
19
|
+
background reaper at all, that window doesn't exist.
|
|
20
|
+
|
|
21
|
+
The one exception is the poll fallback used when ``exit_fd`` returns
|
|
22
|
+
``None`` (no pidfd/kqueue support, or a kernel/pid-race failure on setup):
|
|
23
|
+
it calls ``proc.poll()`` on a short cadence, exactly like the busy-poll loop
|
|
24
|
+
this module replaces. That does reap on exit, but synchronously, inside the
|
|
25
|
+
one call, in the caller's own thread -- there is still no separate
|
|
26
|
+
notice-then-reap window and no background thread, so the safety argument
|
|
27
|
+
above still holds; only the fast path is structurally two separate steps
|
|
28
|
+
(notice, then a later caller-owned reap).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import select
|
|
35
|
+
import subprocess # noqa: TID251 -- exit_fd/wait_exit observe Popen.pid; see pyproject.toml
|
|
36
|
+
import sys
|
|
37
|
+
from typing import Literal
|
|
38
|
+
|
|
39
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
40
|
+
|
|
41
|
+
# Fallback poll cadence when exit_fd() is unsupported -- matches the
|
|
42
|
+
# real-second value of the busy-poll tick this module's callers used to run
|
|
43
|
+
# themselves before switching to wait_exit (e.g. _spawn_round's old
|
|
44
|
+
# proc.wait(timeout=1)). Kept as its own constant, not imported by/from a
|
|
45
|
+
# caller, so this module has no dependency on any of them.
|
|
46
|
+
_POLL_TICK_S = 1.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def exit_fd(proc: subprocess.Popen) -> int | None:
|
|
50
|
+
"""A fd that becomes readable once ``proc``'s pid has exited (zombie, not
|
|
51
|
+
yet reaped) -- this never itself waits on or reaps it.
|
|
52
|
+
|
|
53
|
+
Linux: ``os.pidfd_open(proc.pid, 0)``. macOS/BSD: a kqueue registered
|
|
54
|
+
with ``EVFILT_PROC``/``NOTE_EXIT`` on ``proc.pid``, whose underlying
|
|
55
|
+
kernel object is kept alive by handing back a ``os.dup``'d descriptor
|
|
56
|
+
rather than the kqueue object's own fileno -- the ``select.kqueue``
|
|
57
|
+
Python object is closed before this function returns (its ``__del__``
|
|
58
|
+
would otherwise race a caller's pending ``select`` and close the fd out
|
|
59
|
+
from under it), while the duplicated fd keeps the same underlying kqueue
|
|
60
|
+
alive until the caller closes it. Any other platform, or any ``OSError``
|
|
61
|
+
from either path (kernel too old, permission denied, or the pid already
|
|
62
|
+
reaped out from under us): ``None`` -- always fail-open, never fatal.
|
|
63
|
+
|
|
64
|
+
The caller owns the returned fd for exactly the lifetime of the one
|
|
65
|
+
``wait_exit`` call that opened it; see that function's docstring for the
|
|
66
|
+
close-after-use discipline this enables.
|
|
67
|
+
"""
|
|
68
|
+
if sys.platform.startswith("linux") and hasattr(os, "pidfd_open"):
|
|
69
|
+
try:
|
|
70
|
+
return os.pidfd_open(proc.pid, 0)
|
|
71
|
+
except OSError:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
if hasattr(select, "kqueue"):
|
|
75
|
+
try:
|
|
76
|
+
kq = select.kqueue()
|
|
77
|
+
try:
|
|
78
|
+
event = select.kevent(
|
|
79
|
+
proc.pid,
|
|
80
|
+
filter=select.KQ_FILTER_PROC,
|
|
81
|
+
flags=select.KQ_EV_ADD,
|
|
82
|
+
fflags=select.KQ_NOTE_EXIT,
|
|
83
|
+
)
|
|
84
|
+
kq.control([event], 0, 0)
|
|
85
|
+
return os.dup(kq.fileno())
|
|
86
|
+
finally:
|
|
87
|
+
kq.close()
|
|
88
|
+
except OSError:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def wait_exit(
|
|
95
|
+
proc: subprocess.Popen,
|
|
96
|
+
*,
|
|
97
|
+
deadline: float,
|
|
98
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
99
|
+
) -> Literal["exited", "timeout"]:
|
|
100
|
+
"""Block until ``proc`` exits or ``deadline`` (a ``clock.monotonic()``
|
|
101
|
+
timestamp) is reached, whichever is soonest. Never calls
|
|
102
|
+
``os.waitpid``/``proc.wait``/``proc.poll`` on the fast path -- a caller
|
|
103
|
+
that gets back ``"exited"`` must reap ``proc`` itself right after this
|
|
104
|
+
returns (see the module docstring).
|
|
105
|
+
|
|
106
|
+
Opens and closes its own ``exit_fd(proc)`` registration internally, once
|
|
107
|
+
per call, in a single ``select.select`` -- callers invoke this at most
|
|
108
|
+
every few seconds (a round's mem-check interval) or once per grace
|
|
109
|
+
window, so the per-call setup cost is negligible and no fd is held open
|
|
110
|
+
across calls.
|
|
111
|
+
|
|
112
|
+
Returns ``"exited"`` (the caller should immediately reap, e.g.
|
|
113
|
+
``proc.wait()``) or ``"timeout"`` (deadline reached, proc still running,
|
|
114
|
+
caller re-evaluates its own logic). When ``proc`` has already exited by
|
|
115
|
+
the time ``deadline`` is reached, the exit fd is ready in the SAME
|
|
116
|
+
``select`` call that also times out -- the exit always wins that tie, so
|
|
117
|
+
an already-dead proc is never mistakenly reported as ``"timeout"``.
|
|
118
|
+
|
|
119
|
+
Fallback (``exit_fd(proc)`` is ``None``): polls ``proc.poll()`` at a
|
|
120
|
+
short, fixed cadence via ``clock.sleep``, with the same two return
|
|
121
|
+
meanings, so a test driving this path with a fake clock still makes
|
|
122
|
+
progress without blocking on real wall time.
|
|
123
|
+
|
|
124
|
+
Short-circuits to ``"exited"`` when ``proc.returncode`` is already set:
|
|
125
|
+
this owner has already reaped it, so its pid is freed (and may be reused).
|
|
126
|
+
Re-opening ``exit_fd(proc.pid)`` on a freed pid could bind pidfd/kqueue to
|
|
127
|
+
an UNRELATED process and ride out the whole deadline as a false
|
|
128
|
+
``"timeout"``. The most common way this happens is ``Popen.terminate()``,
|
|
129
|
+
whose ``send_signal`` calls ``proc.poll()`` first and reaps a leader that
|
|
130
|
+
exited in the microseconds before the signal.
|
|
131
|
+
"""
|
|
132
|
+
if proc.returncode is not None:
|
|
133
|
+
return "exited" # already reaped by this owner -- never re-open a pid we no longer hold
|
|
134
|
+
|
|
135
|
+
fd = exit_fd(proc)
|
|
136
|
+
|
|
137
|
+
if fd is None:
|
|
138
|
+
return _wait_exit_by_polling(proc, deadline=deadline, clock=clock)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
remaining = max(0.0, deadline - clock.monotonic())
|
|
142
|
+
ready, _, _ = select.select([fd], [], [], remaining)
|
|
143
|
+
return "exited" if ready else "timeout"
|
|
144
|
+
finally:
|
|
145
|
+
os.close(fd)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _wait_exit_by_polling(
|
|
149
|
+
proc: subprocess.Popen,
|
|
150
|
+
*,
|
|
151
|
+
deadline: float,
|
|
152
|
+
clock: Clock,
|
|
153
|
+
) -> Literal["exited", "timeout"]:
|
|
154
|
+
# Not clock.wait_until: keep this poll loop in _procwait so
|
|
155
|
+
# test_procwait_shield.py's _procwait-only scan guards its KeyboardInterrupt
|
|
156
|
+
# propagation (the grace-kill shield depends on it).
|
|
157
|
+
while True:
|
|
158
|
+
if proc.poll() is not None:
|
|
159
|
+
return "exited"
|
|
160
|
+
|
|
161
|
+
remaining = deadline - clock.monotonic()
|
|
162
|
+
if remaining <= 0:
|
|
163
|
+
return "timeout"
|
|
164
|
+
|
|
165
|
+
clock.sleep(min(_POLL_TICK_S, remaining))
|
|
@@ -312,13 +312,24 @@ def timeout_budget(round_budget_s: int) -> tuple[int, int]:
|
|
|
312
312
|
round subprocess (``api.outer_round_ceiling_s``): ``round_budget_s``
|
|
313
313
|
plus reap grace + git-commit ceiling + hook allowance, so it only trips
|
|
314
314
|
when the round supervisor itself is wedged, never during its own
|
|
315
|
-
bounded post-round cleanup.
|
|
315
|
+
bounded post-round cleanup. The reap margin must cover the widest grace
|
|
316
|
+
ANY round can be configured with -- a cooperative agent's
|
|
317
|
+
``sigterm_grace_s`` is boot-capped at ``_ROUND_TERM_GRACE_S``
|
|
318
|
+
(``config/models.py``'s mirror of this module's single source), not
|
|
319
|
+
the non-cooperative default ``_REAP_GRACE_S``, so the margin is
|
|
320
|
+
``max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)`` -- else a long cooperative
|
|
321
|
+
grace could trip the outer wall-clock ceiling mid-grace.
|
|
316
322
|
- ``timeout_stop_sec`` — systemd's ``TimeoutStopSec``
|
|
317
323
|
(``service_unit.py``): must clear ``outer_ceiling_s`` by enough for a
|
|
318
324
|
SIGTERM to reach and drain the round (``_ROUND_TERM_GRACE_S``) plus a
|
|
319
325
|
stop-request overhead pad, so `systemctl stop` never SIGKILLs a round
|
|
320
326
|
that is draining normally.
|
|
321
327
|
"""
|
|
322
|
-
outer_ceiling_s =
|
|
328
|
+
outer_ceiling_s = (
|
|
329
|
+
round_budget_s
|
|
330
|
+
+ max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)
|
|
331
|
+
+ _GIT_COMMIT_TIMEOUT_S
|
|
332
|
+
+ _HOOK_ALLOWANCE_S
|
|
333
|
+
)
|
|
323
334
|
timeout_stop_sec = outer_ceiling_s + _ROUND_TERM_GRACE_S + _STOP_GRACE_MARGIN_S
|
|
324
335
|
return timeout_stop_sec, outer_ceiling_s
|
|
@@ -18,6 +18,7 @@ from datetime import UTC, datetime
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
+
from agent_runner._notify import NULL_LISTENER, Listener, NullListener
|
|
21
22
|
from agent_runner.api_types import TransientErrorState
|
|
22
23
|
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
23
24
|
from agent_runner.events import (
|
|
@@ -594,6 +595,7 @@ def _interruptible_sleep(
|
|
|
594
595
|
chunk_s: int = 30,
|
|
595
596
|
should_stop: Callable[[], bool] | None = None,
|
|
596
597
|
deadline_epoch: float | None = None,
|
|
598
|
+
listener: Listener | NullListener = NULL_LISTENER,
|
|
597
599
|
) -> bool:
|
|
598
600
|
"""Sleep ``total_s`` in ``<= chunk_s`` slices, re-checking ``stop`` (and, if given,
|
|
599
601
|
``should_stop()``) at each boundary; return True iff ``stop["requested"]`` OR
|
|
@@ -601,14 +603,45 @@ def _interruptible_sleep(
|
|
|
601
603
|
:func:`_apply_back_off` so a SIGTERM or a stop_file lands within one chunk instead
|
|
602
604
|
of after the full sleep (e.g. the 8h back-off cap).
|
|
603
605
|
|
|
606
|
+
``listener`` (default :data:`agent_runner._notify.NULL_LISTENER`) is the single
|
|
607
|
+
wakeup mechanism for the per-chunk nap below: a real ``Listener`` wakes early on a
|
|
608
|
+
SIGTERM/SIGINT (via ``signal.set_wakeup_fd``) or a cross-process ``ring()``,
|
|
609
|
+
dropping the wait to milliseconds instead of riding out the full ``chunk_s``; the
|
|
610
|
+
default ``NullListener`` degrades ``listener.wait(nap, clock=clock)`` to a plain
|
|
611
|
+
``clock.sleep(nap)`` on the SAME clock, so every existing caller (none of which
|
|
612
|
+
pass ``listener``) stays byte-identical.
|
|
613
|
+
|
|
604
614
|
``should_stop`` matches :func:`_pause_poll`'s contract (cli/_serve_round.py): a
|
|
605
615
|
zero-arg predicate the caller closes over its own stop_file check with, so this
|
|
606
616
|
module never needs to know what "should stop" means beyond calling it.
|
|
607
617
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
618
|
+
A live ``Listener`` can return from ``wait`` early on an advisory event byte that
|
|
619
|
+
is not a stop (e.g. a doorbell already rung by a concurrent ``events.emit``
|
|
620
|
+
before this call even started, or another emit landing mid-sleep) — crediting
|
|
621
|
+
the full nap for a wait that actually returned in microseconds would collapse
|
|
622
|
+
the whole sleep to ~0 on the first stray byte, defeating callers like the serve
|
|
623
|
+
restart/crash back-off delay that depend on this function to ride out its full
|
|
624
|
+
duration. So a live listener is credited its *actual* ``clock.monotonic()``
|
|
625
|
+
elapsed per slice, not the intended nap.
|
|
626
|
+
|
|
627
|
+
``NullListener.wait``, by contrast, can never return early — there is no fd to
|
|
628
|
+
wake on, it always just calls ``clock.sleep(nap)`` and returns — so for it the
|
|
629
|
+
intended nap IS the correct credit, and this function keeps using it directly
|
|
630
|
+
(``remaining -= nap``) rather than measuring elapsed around it. This is
|
|
631
|
+
deliberate, not an optimization: a monotonic-elapsed measurement would only be
|
|
632
|
+
byte-identical to the old intended-nap count if the injected clock's
|
|
633
|
+
``monotonic()`` genuinely tracked its own ``sleep()`` — true for ``FakeClock``,
|
|
634
|
+
but NOT for a test that monkeypatches the stdlib ``time.sleep`` (what
|
|
635
|
+
``RealClock.sleep`` calls) to skip real waiting without touching
|
|
636
|
+
``time.monotonic()`` (a real, used-elsewhere-in-this-suite pattern, e.g.
|
|
637
|
+
``test_serve_crash_loop.py``): that combination would leave elapsed pinned
|
|
638
|
+
near 0 forever and busy-spin the loop for the full ``chunk_s``/``total_s``
|
|
639
|
+
instead of terminating. Branching on the listener type sidesteps that clock
|
|
640
|
+
fragility entirely and keeps every existing NULL_LISTENER caller provably
|
|
641
|
+
byte-identical to the pre-fix accounting. Still NTP-step immune either way:
|
|
642
|
+
``clock.monotonic()`` is never stepped by an NTP correction (only
|
|
643
|
+
``clock.epoch()`` is, which the ``deadline_epoch`` branch below handles
|
|
644
|
+
separately).
|
|
612
645
|
|
|
613
646
|
``deadline_epoch``, when given, additionally re-checks ``clock.epoch() >=
|
|
614
647
|
deadline_epoch`` at each chunk boundary and returns False (completed, NOT
|
|
@@ -627,8 +660,18 @@ def _interruptible_sleep(
|
|
|
627
660
|
if deadline_epoch is not None and clock.epoch() >= deadline_epoch:
|
|
628
661
|
return False
|
|
629
662
|
nap = min(float(chunk_s), remaining)
|
|
630
|
-
|
|
631
|
-
|
|
663
|
+
if isinstance(listener, NullListener):
|
|
664
|
+
# Never returns early (no fd to wake on) -- the intended nap is exactly
|
|
665
|
+
# what it consumed, so crediting it directly is provably byte-identical
|
|
666
|
+
# to the pre-fix accounting for every existing (non-serve) caller.
|
|
667
|
+
listener.wait(nap, clock=clock)
|
|
668
|
+
remaining -= nap
|
|
669
|
+
else:
|
|
670
|
+
# May return early on a spurious/stale ring -- credit only what the
|
|
671
|
+
# wait actually consumed, or a stray event byte collapses the sleep.
|
|
672
|
+
started = clock.monotonic()
|
|
673
|
+
listener.wait(nap, clock=clock)
|
|
674
|
+
remaining -= max(0.0, clock.monotonic() - started)
|
|
632
675
|
return False
|
|
633
676
|
|
|
634
677
|
|
|
@@ -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.3.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 3,
|
|
21
|
+
__version__ = version = '0.3.5'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 5)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|