cli-agent-runner 0.3.3__tar.gz → 0.3.4__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.4}/CHANGELOG.md +9 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/PKG-INFO +1 -1
- cli_agent_runner-0.3.4/agent_runner/_notify.py +245 -0
- cli_agent_runner-0.3.4/agent_runner/_procwait.py +165 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_throttle.py +49 -6
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/agent_runtime.py +26 -15
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/_serve_round.py +84 -41
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/events_cmd.py +29 -24
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/serve_cmd.py +46 -7
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/events.py +2 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/memory-efficiency.md +72 -3
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/pyproject.toml +2 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/conftest.py +24 -0
- cli_agent_runner-0.3.4/tests/integration/test_reap_property.py +153 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_spawn_round_mem_floor.py +35 -10
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_architecture.py +6 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_import_footprint.py +11 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_layer_2_loop_size.py +4 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_module_boundaries.py +1 -0
- cli_agent_runner-0.3.4/tests/invariants/test_no_asyncio_select.py +253 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_vacuous_invariants.py +2 -0
- cli_agent_runner-0.3.4/tests/invariants/test_procwait_shield.py +92 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_grace.py +9 -50
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd.py +32 -19
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd_rollover.py +21 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd_tail.py +31 -9
- cli_agent_runner-0.3.4/tests/unit/test_kill_pgroup_shielding.py +110 -0
- cli_agent_runner-0.3.4/tests/unit/test_notify.py +174 -0
- cli_agent_runner-0.3.4/tests/unit/test_procwait.py +113 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner_throttle.py +7 -2
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_phase_schedule.py +8 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_cgroup_spine.py +24 -1
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_terminate.py +19 -2
- cli_agent_runner-0.3.4/tests/unit/test_serve_round_wait_exit.py +81 -0
- cli_agent_runner-0.3.4/tests/unit/test_serve_wakeup.py +169 -0
- cli_agent_runner-0.3.4/tests/unit/test_terminate_round_wait_exit.py +96 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.codecov.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/dependabot.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.gitignore +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/BACKLOG.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/LICENSE +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/README.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/README.zh.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/SECURITY.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/plugins.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_observe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_manifest.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_sandbox.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_serve_policy.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/doctor_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/models.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/parsers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/build.sh +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/README.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/architecture.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/commands.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/configuration.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/events.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/migrations/0.3.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/plugins.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/container-pi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/runbook.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/thesis.md +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/_clock.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/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.4}/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.4}/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.4}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/linux/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/linux/test_plugin_sandbox_kill.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_container.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_builtin_provenance.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_collapse_spawn_decisions.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_doctor_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_doctor_sandbox.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_gate_serve_boot.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_load_fail_closed.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_loader_split.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_manifest_spawn_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_sandbox_block.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_manifest.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_name_mismatch.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_sandbox_wire.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugins_config_fields.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_spawn_seam.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/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.3.4] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- 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.
|
|
12
|
+
- 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).
|
|
13
|
+
- `agent-runner events --tail` shows a new matching event within a drain cycle of its being written, instead of on a ~1 s poll.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
8
17
|
## [0.3.3] - 2026-09-14
|
|
9
18
|
|
|
10
19
|
### 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.4
|
|
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
|
|
@@ -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))
|
|
@@ -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.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -19,9 +19,11 @@ import threading
|
|
|
19
19
|
from collections.abc import Callable
|
|
20
20
|
from dataclasses import dataclass
|
|
21
21
|
from pathlib import Path
|
|
22
|
+
from typing import Literal
|
|
22
23
|
|
|
23
24
|
import psutil
|
|
24
25
|
|
|
26
|
+
from agent_runner._procwait import wait_exit
|
|
25
27
|
from agent_runner.api_types import _round_ok
|
|
26
28
|
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
27
29
|
|
|
@@ -144,14 +146,30 @@ def _snapshot_stray_descendants(proc: subprocess.Popen) -> list[dict]:
|
|
|
144
146
|
return stray
|
|
145
147
|
|
|
146
148
|
|
|
149
|
+
def _wait_exit_shielded(
|
|
150
|
+
proc: subprocess.Popen, *, deadline: float, clock: Clock
|
|
151
|
+
) -> Literal["exited", "timeout"]:
|
|
152
|
+
"""``wait_exit`` retried against the SAME absolute ``deadline`` on a
|
|
153
|
+
re-entrant ``KeyboardInterrupt`` (round_cmd's process-wide SIGTERM handler
|
|
154
|
+
raises one through the blocking wait) -- so an impatient double-kill can
|
|
155
|
+
never inflate the grace window or skip the SIGKILL escalation. The deadline
|
|
156
|
+
is absolute, so a retry never extends it."""
|
|
157
|
+
while True:
|
|
158
|
+
try:
|
|
159
|
+
return wait_exit(proc, deadline=deadline, clock=clock)
|
|
160
|
+
except KeyboardInterrupt:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
|
|
147
164
|
def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
148
165
|
"""SIGTERM the pgroup, grace, then SIGKILL — the reap primitive shared by
|
|
149
166
|
the round-timeout path and ``run``'s BaseException handler (which fires on
|
|
150
167
|
a SIGTERM landing while the round is unwinding from a first one).
|
|
151
168
|
``round_cmd``'s SIGTERM handler stays installed for the whole process life
|
|
152
169
|
(it converts every SIGTERM into a fresh ``KeyboardInterrupt``, not just the
|
|
153
|
-
first), so a re-entrant SIGTERM during the grace
|
|
154
|
-
shielded (caught and retried
|
|
170
|
+
first), so a re-entrant SIGTERM during the grace wait below raises HERE —
|
|
171
|
+
shielded (caught and retried against the SAME absolute deadline, so it
|
|
172
|
+
cannot inflate the grace window) so the SIGKILL escalation always runs. An
|
|
155
173
|
operator's impatient double-kill must not leave the agent outliving the
|
|
156
174
|
grace period unreaped.
|
|
157
175
|
|
|
@@ -169,24 +187,17 @@ def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
|
169
187
|
deadline = (
|
|
170
188
|
clock.monotonic() + REAP_GRACE_S
|
|
171
189
|
) # monotonic: an NTP step must not stretch/skip the reap
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
clock.sleep(0.1)
|
|
175
|
-
except KeyboardInterrupt:
|
|
176
|
-
pass # shielded: keep waiting out the grace window, never skip SIGKILL
|
|
190
|
+
_wait_exit_shielded(proc, deadline=deadline, clock=clock)
|
|
191
|
+
# "exited" or "timeout": either way, fall through to the SIGKILL last-resort
|
|
177
192
|
try:
|
|
178
193
|
os.killpg(pgid, signal.SIGKILL)
|
|
179
194
|
except OSError:
|
|
180
195
|
pass
|
|
181
196
|
_kill_stray_descendants(stray)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
pass # reaped via the SIGKILL above regardless; nothing more to wait for
|
|
187
|
-
except KeyboardInterrupt:
|
|
188
|
-
continue # shielded: still must reap before returning
|
|
189
|
-
return
|
|
197
|
+
final_deadline = clock.monotonic() + 10
|
|
198
|
+
if _wait_exit_shielded(proc, deadline=final_deadline, clock=clock) == "exited":
|
|
199
|
+
proc.wait() # reap the zombie the fast path left; idempotent if already reaped
|
|
200
|
+
return
|
|
190
201
|
|
|
191
202
|
|
|
192
203
|
def _live_children(
|