cli-agent-runner 0.3.4__tar.gz → 0.3.6__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.4 → cli_agent_runner-0.3.6}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CHANGELOG.md +11 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/PKG-INFO +1 -1
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_lifecycle.py +16 -7
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_notify.py +15 -6
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_manifest.py +20 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_sandbox.py +93 -13
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_procwait.py +38 -15
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_serve_policy.py +13 -2
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/agent_runtime.py +27 -9
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_round.py +45 -12
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/common.py +8 -2
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/doctor_cmd.py +15 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/serve_cmd.py +69 -6
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/models.py +27 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/parsers.py +24 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/runner.py +24 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/commands.md +5 -1
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/configuration.md +15 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/memory-efficiency.md +51 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/plugins.md +2 -2
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/runbook.md +4 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_auto_stop_e2e.py +9 -1
- cli_agent_runner-0.3.6/tests/integration/test_cooperative_grace_property.py +142 -0
- cli_agent_runner-0.3.6/tests/integration/test_reap_property.py +314 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_run_one_round_with_fake_agent.py +26 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_wedged.py +14 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_architecture.py +2 -1
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_peek_schema_version.py +1 -1
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_timeout_budget_invariant.py +44 -2
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_grace.py +82 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_reap.py +80 -0
- cli_agent_runner-0.3.6/tests/unit/test_apply_reap_grace_env.py +92 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config.py +44 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_cmd.py +9 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_kill_pgroup_shielding.py +24 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_notify.py +16 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_sandbox_block.py +26 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_manifest.py +44 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_sandbox_wire.py +252 -1
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_procwait.py +14 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner.py +35 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_service_unit.py +4 -3
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_spawn_seam.py +61 -1
- cli_agent_runner-0.3.4/tests/integration/test_reap_property.py +0 -153
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.codecov.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/dependabot.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/.gitignore +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/BACKLOG.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/LICENSE +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/README.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/README.zh.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/SECURITY.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/plugins.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_install.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_observe.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/build.sh +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/README.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/architecture.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/events.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/migrations/0.3.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/container-pi.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/docs/thesis.md +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/pyproject.toml +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/_clock.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/conftest.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_import_footprint.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_asyncio_select.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_procwait_shield.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/linux/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/linux/test_plugin_sandbox_kill.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_container.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_builtin_provenance.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_collapse_spawn_decisions.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_sandbox.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_gate_serve_boot.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_load_fail_closed.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_loader_split.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_manifest_spawn_hooks.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_name_mismatch.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_plugins_config_fields.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_terminate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_wait_exit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_serve_wakeup.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_terminate_round_wait_exit.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.3.4 → cli_agent_runner-0.3.6}/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,17 @@ 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.6] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `agent-runner kill` now reaps a stuck round's detached descendants (a child that `setsid()`'d off its process group) the same way `serve stop` already does, so a forced stop never leaves an orphan behind.
|
|
12
|
+
- A stop (`SIGTERM` / `serve stop`) during a sandboxed third-party spawn hook, or during a plugin-requested `defer`, now takes effect immediately instead of after up to 30 s — and the confined hook's child process is always reaped, never left running. A background process the hook itself forks is not reaped; its abandoned output pipe is dropped after a bounded 2 s drain wait (on a normal hook exit, surfaced as `hook_failed`) rather than stalling the stop.
|
|
13
|
+
|
|
14
|
+
## [0.3.5] - 2026-09-15
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `[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.
|
|
18
|
+
|
|
8
19
|
## [0.3.4] - 2026-09-15
|
|
9
20
|
|
|
10
21
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -181,17 +181,26 @@ def _terminate_round_pid(pid: int) -> None:
|
|
|
181
181
|
"""TERM-first -> grace -> SIGKILL a bare round-child pid.
|
|
182
182
|
|
|
183
183
|
Mirrors ``serve_cmd._terminate_round``'s shape, but this runs from a
|
|
184
|
-
SEPARATE CLI process (``kill()``) that only has the pid
|
|
184
|
+
SEPARATE CLI process (``kill()``) that only has the pid -- not serve's own
|
|
185
185
|
``Popen`` handle. Never ``killpg``: the round is ``start_new_session=True``,
|
|
186
|
-
so
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
so a plain SIGTERM here fires the round's own SIGTERM -> KeyboardInterrupt
|
|
187
|
+
handler (``round_cmd.py``), which walks the link to the agent pgroup and
|
|
188
|
+
reaps it via ``_kill_pgroup`` (itself snapshotting and reaping any stray).
|
|
189
|
+
SIGKILL is only the last-resort escalation for a round that never even runs
|
|
190
|
+
its handler -- and on THAT path the leader's own cooperative reap never
|
|
191
|
+
happened, so we snapshot the leader's descendants up front (while the leader
|
|
192
|
+
subtree is still resolvable) via a bare-pid shim and reap the strays here:
|
|
193
|
+
a descendant that ``setsid()``'d off the leader's pgroup would otherwise
|
|
194
|
+
survive the leader's death, orphaned. Symmetric with ``serve stop``."""
|
|
195
|
+
import types
|
|
196
|
+
|
|
197
|
+
from agent_runner import agent_runtime
|
|
198
|
+
|
|
199
|
+
stray = agent_runtime._snapshot_stray_descendants(types.SimpleNamespace(pid=pid))
|
|
192
200
|
send_signal_to_pid(pid, signal.SIGTERM)
|
|
193
201
|
if _await_pid_exit(pid, _ROUND_TERM_GRACE_S):
|
|
194
202
|
send_signal_to_pid(pid, signal.SIGKILL)
|
|
203
|
+
agent_runtime._kill_stray_descendants(stray)
|
|
195
204
|
|
|
196
205
|
|
|
197
206
|
def stop(project: str | Path) -> ServiceStatus:
|
|
@@ -78,11 +78,14 @@ def drain(fd: int) -> None:
|
|
|
78
78
|
``select()`` on it instantly ready -- an undrained fd busy-spins its
|
|
79
79
|
caller at 100% CPU forever after the first wake, since a level-triggered
|
|
80
80
|
fd stays readable until read. Advisory: the bytes carry no payload and are
|
|
81
|
-
discarded.
|
|
82
|
-
the pause/sleep waits
|
|
83
|
-
|
|
84
|
-
(``
|
|
85
|
-
|
|
81
|
+
discarded. Two callers: :meth:`Listener.wait` self-drains its own fd on the
|
|
82
|
+
wake branch (the pause/sleep waits ``_interruptible_sleep``/``_pause_poll``
|
|
83
|
+
are the consumers of a live ``Listener``); and the sandboxed-hook trampoline
|
|
84
|
+
(``_plugin_sandbox._run_child_process``, P3) drains the CALLER-OWNED wake fd
|
|
85
|
+
it was handed on the ``"woken"`` branch of its exit wait, before re-checking
|
|
86
|
+
``should_stop()`` -- that fd is the serve listener's, watched by
|
|
87
|
+
``wait_exit`` for the duration of the hook, so the trampoline must drain it
|
|
88
|
+
itself rather than leave the next wait instantly ready.
|
|
86
89
|
"""
|
|
87
90
|
while True:
|
|
88
91
|
try:
|
|
@@ -179,7 +182,13 @@ class Listener:
|
|
|
179
182
|
"""
|
|
180
183
|
deadline = clock.monotonic() + timeout_s
|
|
181
184
|
remaining = max(0.0, deadline - clock.monotonic())
|
|
182
|
-
|
|
185
|
+
try:
|
|
186
|
+
ready, _, _ = select.select([self.fd], [], [], remaining)
|
|
187
|
+
except ValueError:
|
|
188
|
+
# fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
|
|
189
|
+
# degrade to a plain timed sleep instead of crashing.
|
|
190
|
+
clock.sleep(remaining)
|
|
191
|
+
return False
|
|
183
192
|
if not ready:
|
|
184
193
|
return False
|
|
185
194
|
|
|
@@ -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
|
*,
|
|
@@ -17,12 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
import json
|
|
18
18
|
import subprocess
|
|
19
19
|
import sys
|
|
20
|
+
from collections.abc import Callable
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Literal
|
|
22
23
|
|
|
23
|
-
from agent_runner import hooks
|
|
24
|
+
from agent_runner import _notify, hooks
|
|
25
|
+
from agent_runner._procwait import wait_exit
|
|
24
26
|
from agent_runner._registry import resolve_entry_target
|
|
25
27
|
from agent_runner.api_types import DirtyOutcome, SpawnDecision
|
|
28
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
26
29
|
|
|
27
30
|
_TRAMPOLINE_TIMEOUT_S = 30.0 # wall-clock; not config-tunable this release
|
|
28
31
|
_MAX_WIRE_BYTES = 64 * 1024
|
|
@@ -30,6 +33,17 @@ _MAX_STDERR_BYTES = 8 * 1024 # hard read cap; a runaway child can't OOM the sup
|
|
|
30
33
|
_REF_CAP = 256
|
|
31
34
|
_STDERR_CAP = 512
|
|
32
35
|
|
|
36
|
+
# Total wall budget for draining a confined child's pipes AFTER it is reaped. A
|
|
37
|
+
# process the hook itself fork()'d inherits the child's stdout/stderr and can hold
|
|
38
|
+
# them open past the child's own death, so the drain join is BOUNDED, not
|
|
39
|
+
# unbounded -- otherwise a single such pipe-holder stalls the stop forever. The
|
|
40
|
+
# pair of joins shares ONE absolute deadline (not _DRAIN_JOIN_S each), and
|
|
41
|
+
# _DRAIN_JOIN_S + 3 <= _lifecycle._PID_SIGNAL_GRACE_S leaves >=3 s of the
|
|
42
|
+
# stop-confirm window for the rest of the unwind (pinned by
|
|
43
|
+
# tests/invariants/test_timeout_budget_invariant.py; a literal mirror -- this
|
|
44
|
+
# module must never import _lifecycle in production).
|
|
45
|
+
_DRAIN_JOIN_S = 2
|
|
46
|
+
|
|
33
47
|
# The child's environment is built default-DENY from this allowlist -- NOT copied
|
|
34
48
|
# from the parent's, which holds the real agent secrets (ANTHROPIC_*, and every
|
|
35
49
|
# *_API_KEY/*_TOKEN/*_SECRET agent_runtime injects). A confined plugin is Python
|
|
@@ -126,6 +140,13 @@ def _ctx_from_wire(data: dict) -> hooks.HookContext:
|
|
|
126
140
|
# ---------------------------------------------------------------------------
|
|
127
141
|
|
|
128
142
|
|
|
143
|
+
class SpawnHookInterrupted(Exception): # noqa: N818 -- a control-flow signal, not an error
|
|
144
|
+
"""Raised by ``_run_child_process`` when ``should_stop()`` fires on a wake
|
|
145
|
+
during a sandboxed spawn hook -- distinct from a plugin failure so the seam
|
|
146
|
+
returns ``None`` WITHOUT emitting ``hook_failed`` (a stop is not the plugin's
|
|
147
|
+
fault). Never raised on the dirty round path, which passes no ``wake_fd``."""
|
|
148
|
+
|
|
149
|
+
|
|
129
150
|
def run_hook_sandboxed(
|
|
130
151
|
hook_kind: Literal["spawn_hook", "dirty_handler"],
|
|
131
152
|
module_path: str,
|
|
@@ -137,6 +158,8 @@ def run_hook_sandboxed(
|
|
|
137
158
|
dirty_files: list[str] | None = None,
|
|
138
159
|
view: hooks.SpawnView | None = None,
|
|
139
160
|
timeout_s: float = _TRAMPOLINE_TIMEOUT_S,
|
|
161
|
+
wake_fd: int | None = None,
|
|
162
|
+
should_stop: Callable[[], bool] | None = None,
|
|
140
163
|
) -> SpawnDecision | DirtyOutcome | None:
|
|
141
164
|
"""Launch the confinement child for one third-party hook call and return its
|
|
142
165
|
validated outcome. ``module_path``/``attr_path`` locate the plugin the child
|
|
@@ -172,6 +195,8 @@ def run_hook_sandboxed(
|
|
|
172
195
|
],
|
|
173
196
|
json.dumps(payload).encode("utf-8"),
|
|
174
197
|
timeout_s,
|
|
198
|
+
wake_fd=wake_fd,
|
|
199
|
+
should_stop=should_stop,
|
|
175
200
|
)
|
|
176
201
|
if returncode < 0:
|
|
177
202
|
from agent_runner.api import emit_plugin_sandbox_kill
|
|
@@ -224,14 +249,50 @@ def _drain_capped(stream, cap: int, out: list[bytes]) -> None:
|
|
|
224
249
|
out.append(bytes(data))
|
|
225
250
|
|
|
226
251
|
|
|
252
|
+
def _join_drain_threads(threads: tuple, budget_s: float) -> bool:
|
|
253
|
+
"""Join the drain threads under ONE absolute deadline -- the whole set is
|
|
254
|
+
bounded by ``budget_s``, NOT ``budget_s`` each -- so a process the hook
|
|
255
|
+
``fork()``'d that still holds the reaped child's output pipe open cannot stall
|
|
256
|
+
the join past ``budget_s``. Returns True iff every thread finished draining
|
|
257
|
+
within the budget (the normal-exit path raises on False; the ``BaseException``
|
|
258
|
+
cleanup path ignores it, since it is already unwinding)."""
|
|
259
|
+
join_deadline = SYSTEM_CLOCK.monotonic() + budget_s
|
|
260
|
+
for t in threads:
|
|
261
|
+
t.join(max(0.0, join_deadline - SYSTEM_CLOCK.monotonic()))
|
|
262
|
+
return not any(t.is_alive() for t in threads)
|
|
263
|
+
|
|
264
|
+
|
|
227
265
|
def _run_child_process(
|
|
228
|
-
argv: list[str],
|
|
266
|
+
argv: list[str],
|
|
267
|
+
stdin_bytes: bytes,
|
|
268
|
+
timeout_s: float,
|
|
269
|
+
*,
|
|
270
|
+
wake_fd: int | None = None,
|
|
271
|
+
should_stop: Callable[[], bool] | None = None,
|
|
229
272
|
) -> tuple[int, bytes, bytes]:
|
|
230
273
|
"""Spawn the confined child with the minimal env and read BOUNDED stdout/stderr
|
|
231
274
|
concurrently (a thread per stream), so a plugin printing without limit within
|
|
232
275
|
the wall-timeout cannot exhaust supervisor memory. Concurrent draining also
|
|
233
|
-
avoids a full-pipe deadlock.
|
|
234
|
-
|
|
276
|
+
avoids a full-pipe deadlock.
|
|
277
|
+
|
|
278
|
+
The exit wait is the fd-driven ``_procwait.wait_exit`` (not a sleep-poll):
|
|
279
|
+
``"exited"`` reaps and returns; ``"timeout"`` kills and raises
|
|
280
|
+
``TimeoutExpired`` (the caller isolates it as ``hook_failed``); ``"woken"``
|
|
281
|
+
(only when ``wake_fd`` is set) drains the advisory byte and, if
|
|
282
|
+
``should_stop()`` is now true, kills the child and raises
|
|
283
|
+
``SpawnHookInterrupted`` -- otherwise a stray ring re-enters the wait on the
|
|
284
|
+
SAME ABSOLUTE deadline so it never shortens the hook's budget. The whole wait
|
|
285
|
+
is wrapped so the confined child NEVER outlives its parent: any BaseException
|
|
286
|
+
(a round ``KeyboardInterrupt``, a timeout, an interrupt) kills and reaps the
|
|
287
|
+
child before propagating.
|
|
288
|
+
|
|
289
|
+
A process the hook itself ``fork()``s is NOT reaped (that child has no process
|
|
290
|
+
group of its own -- see the Option-B follow-up), and it can inherit and hold
|
|
291
|
+
the child's stdout/stderr open past the child's own death; so the drain joins
|
|
292
|
+
are BOUNDED (``_DRAIN_JOIN_S``, one absolute deadline across the pair) at BOTH
|
|
293
|
+
the normal-exit tail and the ``BaseException`` cleanup -- such a pipe-holder can
|
|
294
|
+
never stall the parent. On the normal path a timed-out drain surfaces as a
|
|
295
|
+
``RuntimeError`` the seam isolates as ``hook_failed``."""
|
|
235
296
|
import threading
|
|
236
297
|
|
|
237
298
|
proc = subprocess.Popen(
|
|
@@ -243,9 +304,17 @@ def _run_child_process(
|
|
|
243
304
|
)
|
|
244
305
|
out_box: list[bytes] = []
|
|
245
306
|
err_box: list[bytes] = []
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
307
|
+
# daemon=True is load-bearing, not cosmetic: nothing in agent_runner/ calls
|
|
308
|
+
# os._exit, so a non-daemon drain thread still blocked on a fork()'d
|
|
309
|
+
# pipe-holder's read() would be joined by threading._shutdown() at interpreter
|
|
310
|
+
# exit and re-hang the stop the bounded join below just rescued. Matches the
|
|
311
|
+
# repo's other pump threads (remote_relay._pump, agent_runtime._write_stdin).
|
|
312
|
+
t_out = threading.Thread(
|
|
313
|
+
target=_drain_capped, args=(proc.stdout, _MAX_WIRE_BYTES + 1, out_box), daemon=True
|
|
314
|
+
)
|
|
315
|
+
t_err = threading.Thread(
|
|
316
|
+
target=_drain_capped, args=(proc.stderr, _MAX_STDERR_BYTES, err_box), daemon=True
|
|
317
|
+
)
|
|
249
318
|
t_out.start()
|
|
250
319
|
t_err.start()
|
|
251
320
|
try:
|
|
@@ -255,15 +324,26 @@ def _run_child_process(
|
|
|
255
324
|
pass
|
|
256
325
|
finally:
|
|
257
326
|
proc.stdin.close()
|
|
258
|
-
|
|
259
|
-
|
|
327
|
+
deadline = SYSTEM_CLOCK.monotonic() + timeout_s
|
|
328
|
+
while True:
|
|
329
|
+
outcome = wait_exit(proc, deadline=deadline, wake_fd=wake_fd)
|
|
330
|
+
if outcome == "exited":
|
|
331
|
+
proc.wait()
|
|
332
|
+
break
|
|
333
|
+
if outcome == "timeout":
|
|
334
|
+
raise subprocess.TimeoutExpired(proc.args, timeout_s)
|
|
335
|
+
_notify.drain(wake_fd)
|
|
336
|
+
if should_stop is not None and should_stop():
|
|
337
|
+
raise SpawnHookInterrupted(f"spawn hook interrupted for {argv[0]!r}")
|
|
338
|
+
except BaseException:
|
|
260
339
|
proc.kill()
|
|
261
340
|
proc.wait()
|
|
262
|
-
t_out
|
|
263
|
-
t_err.join()
|
|
341
|
+
_join_drain_threads((t_out, t_err), _DRAIN_JOIN_S)
|
|
264
342
|
raise
|
|
265
|
-
t_out
|
|
266
|
-
|
|
343
|
+
if not _join_drain_threads((t_out, t_err), _DRAIN_JOIN_S):
|
|
344
|
+
raise RuntimeError(
|
|
345
|
+
"drain timed out: a process the hook forked still holds its output pipe (not reaped)"
|
|
346
|
+
)
|
|
267
347
|
return proc.returncode, out_box[0], err_box[0]
|
|
268
348
|
|
|
269
349
|
|
|
@@ -96,12 +96,13 @@ def wait_exit(
|
|
|
96
96
|
*,
|
|
97
97
|
deadline: float,
|
|
98
98
|
clock: Clock = SYSTEM_CLOCK,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
``
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
wake_fd: int | None = None,
|
|
100
|
+
) -> Literal["exited", "timeout", "woken"]:
|
|
101
|
+
"""Block until ``proc`` exits, ``deadline`` (a ``clock.monotonic()``
|
|
102
|
+
timestamp) is reached, or ``wake_fd`` becomes readable -- whichever is
|
|
103
|
+
soonest. Never calls ``os.waitpid``/``proc.wait``/``proc.poll`` on the
|
|
104
|
+
fast path -- a caller that gets back ``"exited"`` must reap ``proc``
|
|
105
|
+
itself right after this returns (see the module docstring).
|
|
105
106
|
|
|
106
107
|
Opens and closes its own ``exit_fd(proc)`` registration internally, once
|
|
107
108
|
per call, in a single ``select.select`` -- callers invoke this at most
|
|
@@ -110,16 +111,27 @@ def wait_exit(
|
|
|
110
111
|
across calls.
|
|
111
112
|
|
|
112
113
|
Returns ``"exited"`` (the caller should immediately reap, e.g.
|
|
113
|
-
``proc.wait()``)
|
|
114
|
-
caller re-evaluates its own logic)
|
|
115
|
-
|
|
116
|
-
``
|
|
117
|
-
|
|
114
|
+
``proc.wait()``), ``"timeout"`` (deadline reached, proc still running,
|
|
115
|
+
caller re-evaluates its own logic), or ``"woken"`` (only possible when
|
|
116
|
+
``wake_fd`` is given: it became readable before either of the other two).
|
|
117
|
+
When ``proc`` has already exited by the time ``deadline`` is reached, the
|
|
118
|
+
exit fd is ready in the SAME ``select`` call that also times out -- the
|
|
119
|
+
exit always wins that tie, so an already-dead proc is never mistakenly
|
|
120
|
+
reported as ``"timeout"``. Likewise, when both the exit fd and
|
|
121
|
+
``wake_fd`` are ready in the same ``select`` call, the exit fd wins --
|
|
122
|
+
an already-dead proc is never mistakenly reported as ``"woken"``.
|
|
123
|
+
``wake_fd`` is caller-owned: this function neither opens, closes, nor
|
|
124
|
+
drains it (the caller drains it after a ``"woken"`` return); ``_procwait``
|
|
125
|
+
never imports ``_notify``.
|
|
118
126
|
|
|
119
127
|
Fallback (``exit_fd(proc)`` is ``None``): polls ``proc.poll()`` at a
|
|
120
128
|
short, fixed cadence via ``clock.sleep``, with the same two return
|
|
121
|
-
meanings
|
|
122
|
-
|
|
129
|
+
meanings (``wake_fd`` is IGNORED on this path -- a documented latency
|
|
130
|
+
degrade, not a "woken" report), so a test driving this path with a fake
|
|
131
|
+
clock still makes progress without blocking on real wall time. The same
|
|
132
|
+
poll fallback is also used when the fast path's ``select.select`` raises
|
|
133
|
+
``ValueError`` (a fd >= ``FD_SETSIZE``, 1024, is unselectable) instead of
|
|
134
|
+
crashing.
|
|
123
135
|
|
|
124
136
|
Short-circuits to ``"exited"`` when ``proc.returncode`` is already set:
|
|
125
137
|
this owner has already reaped it, so its pid is freed (and may be reused).
|
|
@@ -139,8 +151,19 @@ def wait_exit(
|
|
|
139
151
|
|
|
140
152
|
try:
|
|
141
153
|
remaining = max(0.0, deadline - clock.monotonic())
|
|
142
|
-
|
|
143
|
-
|
|
154
|
+
rlist = [fd, wake_fd] if wake_fd is not None else [fd]
|
|
155
|
+
try:
|
|
156
|
+
ready, _, _ = select.select(rlist, [], [], remaining)
|
|
157
|
+
except ValueError:
|
|
158
|
+
# fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
|
|
159
|
+
# degrade to the poll fallback (its documented latency) instead
|
|
160
|
+
# of crashing.
|
|
161
|
+
return _wait_exit_by_polling(proc, deadline=deadline, clock=clock)
|
|
162
|
+
if fd in ready:
|
|
163
|
+
return "exited"
|
|
164
|
+
if ready:
|
|
165
|
+
return "woken"
|
|
166
|
+
return "timeout"
|
|
144
167
|
finally:
|
|
145
168
|
os.close(fd)
|
|
146
169
|
|
|
@@ -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,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.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 6)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -141,7 +141,7 @@ def _snapshot_stray_descendants(proc: subprocess.Popen) -> list[dict]:
|
|
|
141
141
|
while the leader (and hence its subtree) is still alive -- both reap sites
|
|
142
142
|
(``_kill_pgroup`` and ``cli/_serve_round._terminate_round``) open with it, so
|
|
143
143
|
the capture-before-signal ordering lives in ONE place, not copy-pasted."""
|
|
144
|
-
stray, _ignored = _live_children(proc)
|
|
144
|
+
stray, _ignored = _live_children(proc, max_n=None)
|
|
145
145
|
_capture_descendant_pgids(stray)
|
|
146
146
|
return stray
|
|
147
147
|
|
|
@@ -161,7 +161,9 @@ def _wait_exit_shielded(
|
|
|
161
161
|
continue
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
def _kill_pgroup(
|
|
164
|
+
def _kill_pgroup(
|
|
165
|
+
proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK, reap_grace_s: int = REAP_GRACE_S
|
|
166
|
+
) -> None:
|
|
165
167
|
"""SIGTERM the pgroup, grace, then SIGKILL — the reap primitive shared by
|
|
166
168
|
the round-timeout path and ``run``'s BaseException handler (which fires on
|
|
167
169
|
a SIGTERM landing while the round is unwinding from a first one).
|
|
@@ -185,7 +187,7 @@ def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
|
185
187
|
except OSError:
|
|
186
188
|
pass
|
|
187
189
|
deadline = (
|
|
188
|
-
clock.monotonic() +
|
|
190
|
+
clock.monotonic() + reap_grace_s
|
|
189
191
|
) # monotonic: an NTP step must not stretch/skip the reap
|
|
190
192
|
_wait_exit_shielded(proc, deadline=deadline, clock=clock)
|
|
191
193
|
# "exited" or "timeout": either way, fall through to the SIGKILL last-resort
|
|
@@ -204,7 +206,7 @@ def _live_children(
|
|
|
204
206
|
proc: subprocess.Popen,
|
|
205
207
|
*,
|
|
206
208
|
ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
207
|
-
max_n: int = 5,
|
|
209
|
+
max_n: int | None = 5,
|
|
208
210
|
) -> tuple[list[dict], list[dict]]:
|
|
209
211
|
"""Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
|
|
210
212
|
|
|
@@ -215,6 +217,12 @@ def _live_children(
|
|
|
215
217
|
events-*.jsonl. Ignore-pattern MATCHING runs against the full cmdline
|
|
216
218
|
(detection unchanged); only what we STORE is minimized.
|
|
217
219
|
|
|
220
|
+
``max_n`` (default 5) bounds only the event-persisted lists this returns
|
|
221
|
+
to a caller that writes them to events-*.jsonl. The reap snapshot
|
|
222
|
+
(``_snapshot_stray_descendants``) passes ``max_n=None`` — a real subtree
|
|
223
|
+
can exceed 5 live descendants, and undercounting it here would leave the
|
|
224
|
+
excess unreaped, not merely under-logged.
|
|
225
|
+
|
|
218
226
|
The stored name is read from the kernel-reported process name (comm —
|
|
219
227
|
e.g. /proc/<pid>/comm on Linux), not from ``Path(argv[0]).name``: comm is
|
|
220
228
|
derived from the exec()'d binary, not from argv[0], and is itself
|
|
@@ -249,11 +257,11 @@ def _live_children(
|
|
|
249
257
|
matched = p.pattern
|
|
250
258
|
break
|
|
251
259
|
if matched is not None:
|
|
252
|
-
if len(ignored) < max_n:
|
|
260
|
+
if max_n is None or len(ignored) < max_n:
|
|
253
261
|
ignored.append({"name": name, "pid": pid, "matched": matched})
|
|
254
|
-
elif len(live) < max_n:
|
|
262
|
+
elif max_n is None or len(live) < max_n:
|
|
255
263
|
live.append({"name": name, "pid": pid})
|
|
256
|
-
if len(live) >= max_n and len(ignored) >= max_n:
|
|
264
|
+
if max_n is not None and len(live) >= max_n and len(ignored) >= max_n:
|
|
257
265
|
break
|
|
258
266
|
return live, ignored
|
|
259
267
|
|
|
@@ -574,6 +582,7 @@ def _terminate_agent(
|
|
|
574
582
|
container_runtime: str | None,
|
|
575
583
|
container_cidfile: Path | None,
|
|
576
584
|
on_container_orphan_risk: Callable[[str, str | None, bool | None], None] | None,
|
|
585
|
+
reap_grace_s: int = REAP_GRACE_S,
|
|
577
586
|
) -> None:
|
|
578
587
|
"""Reap the agent pgroup (``_kill_pgroup``), then -- for a
|
|
579
588
|
container-launching command only -- make a best-effort container `stop`
|
|
@@ -581,7 +590,7 @@ def _terminate_agent(
|
|
|
581
590
|
hard-kill entry point ``run()`` uses for all three of its termination
|
|
582
591
|
paths (R1128 wall-clock, grace-kill, and the BaseException reap), so the
|
|
583
592
|
container handling isn't duplicated three times."""
|
|
584
|
-
_kill_pgroup(proc, clock)
|
|
593
|
+
_kill_pgroup(proc, clock, reap_grace_s=reap_grace_s)
|
|
585
594
|
if container_runtime is not None:
|
|
586
595
|
cid, stop_ok = _best_effort_container_stop(container_runtime, container_cidfile)
|
|
587
596
|
if on_container_orphan_risk is not None:
|
|
@@ -606,10 +615,16 @@ def run(
|
|
|
606
615
|
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
607
616
|
on_container_orphan_risk: Callable[[str, str | None, bool | None], None] | None = None,
|
|
608
617
|
clock: Clock = SYSTEM_CLOCK,
|
|
618
|
+
reap_grace_s: int = REAP_GRACE_S,
|
|
609
619
|
) -> RunResult:
|
|
610
620
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
611
621
|
|
|
612
|
-
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup →
|
|
622
|
+
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → reap_grace_s → SIGKILL.
|
|
623
|
+
|
|
624
|
+
reap_grace_s: the SIGTERM->SIGKILL grace passed straight through to every
|
|
625
|
+
``_terminate_agent`` call site below (R1128 wall-clock, grace-kill, and
|
|
626
|
+
the BaseException reap) -- one deadline value, threaded end to end.
|
|
627
|
+
Defaults to REAP_GRACE_S so every existing caller is unchanged.
|
|
613
628
|
|
|
614
629
|
work_dir: the agent child's working directory; callers pass the
|
|
615
630
|
already-absolute cfg.runtime.work_dir. CLIs with no --cwd flag of their
|
|
@@ -776,6 +791,7 @@ def run(
|
|
|
776
791
|
container_runtime=container_runtime,
|
|
777
792
|
container_cidfile=container_cidfile,
|
|
778
793
|
on_container_orphan_risk=_report_orphan_once,
|
|
794
|
+
reap_grace_s=reap_grace_s,
|
|
779
795
|
)
|
|
780
796
|
duration = clock.monotonic() - start
|
|
781
797
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
@@ -820,6 +836,7 @@ def run(
|
|
|
820
836
|
container_runtime=container_runtime,
|
|
821
837
|
container_cidfile=container_cidfile,
|
|
822
838
|
on_container_orphan_risk=_report_orphan_once,
|
|
839
|
+
reap_grace_s=reap_grace_s,
|
|
823
840
|
)
|
|
824
841
|
duration = clock.monotonic() - start
|
|
825
842
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
@@ -860,6 +877,7 @@ def run(
|
|
|
860
877
|
container_runtime=container_runtime,
|
|
861
878
|
container_cidfile=container_cidfile,
|
|
862
879
|
on_container_orphan_risk=_report_orphan_once,
|
|
880
|
+
reap_grace_s=reap_grace_s,
|
|
863
881
|
)
|
|
864
882
|
raise
|
|
865
883
|
finally:
|