cli-agent-runner 0.3.5__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.5 → cli_agent_runner-0.3.6}/CHANGELOG.md +6 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/PKG-INFO +1 -1
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_lifecycle.py +16 -7
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_notify.py +15 -6
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_sandbox.py +93 -13
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_procwait.py +38 -15
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/agent_runtime.py +11 -5
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_round.py +39 -8
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/serve_cmd.py +20 -6
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/models.py +8 -7
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/commands.md +4 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/memory-efficiency.md +28 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/runbook.md +4 -0
- cli_agent_runner-0.3.6/tests/integration/test_reap_property.py +314 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_architecture.py +1 -1
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_timeout_budget_invariant.py +14 -3
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_reap.py +27 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_notify.py +16 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_sandbox_wire.py +252 -1
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_procwait.py +14 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_spawn_seam.py +61 -1
- cli_agent_runner-0.3.5/tests/integration/test_reap_property.py +0 -153
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.codecov.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/dependabot.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.gitignore +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/BACKLOG.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/LICENSE +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/README.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/README.zh.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/SECURITY.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/plugins.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_install.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_observe.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_manifest.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_serve_policy.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/doctor_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/parsers.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/build.sh +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/README.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/architecture.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/configuration.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/events.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/migrations/0.3.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/plugins.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/container-pi.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/docs/thesis.md +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/pyproject.toml +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/_clock.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/conftest.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.3.5 → 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.5 → 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.5 → 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.5 → cli_agent_runner-0.3.6}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_cooperative_grace_property.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_import_footprint.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_asyncio_select.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_procwait_shield.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/linux/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/linux/test_plugin_sandbox_kill.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_container.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_apply_reap_grace_env.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_builtin_provenance.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_collapse_spawn_decisions.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_doctor_sandbox.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_gate_serve_boot.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_kill_pgroup_shielding.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_load_fail_closed.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_loader_split.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_manifest_spawn_hooks.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_sandbox_block.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_checksum.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_manifest.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_name_mismatch.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_plugins_config_fields.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_sandbox_probe.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_terminate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_round_wait_exit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_serve_wakeup.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_terminate_round_wait_exit.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.6}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,12 @@ 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
|
+
|
|
8
14
|
## [0.3.5] - 2026-09-15
|
|
9
15
|
|
|
10
16
|
### 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.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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -206,7 +206,7 @@ def _live_children(
|
|
|
206
206
|
proc: subprocess.Popen,
|
|
207
207
|
*,
|
|
208
208
|
ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
209
|
-
max_n: int = 5,
|
|
209
|
+
max_n: int | None = 5,
|
|
210
210
|
) -> tuple[list[dict], list[dict]]:
|
|
211
211
|
"""Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
|
|
212
212
|
|
|
@@ -217,6 +217,12 @@ def _live_children(
|
|
|
217
217
|
events-*.jsonl. Ignore-pattern MATCHING runs against the full cmdline
|
|
218
218
|
(detection unchanged); only what we STORE is minimized.
|
|
219
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
|
+
|
|
220
226
|
The stored name is read from the kernel-reported process name (comm —
|
|
221
227
|
e.g. /proc/<pid>/comm on Linux), not from ``Path(argv[0]).name``: comm is
|
|
222
228
|
derived from the exec()'d binary, not from argv[0], and is itself
|
|
@@ -251,11 +257,11 @@ def _live_children(
|
|
|
251
257
|
matched = p.pattern
|
|
252
258
|
break
|
|
253
259
|
if matched is not None:
|
|
254
|
-
if len(ignored) < max_n:
|
|
260
|
+
if max_n is None or len(ignored) < max_n:
|
|
255
261
|
ignored.append({"name": name, "pid": pid, "matched": matched})
|
|
256
|
-
elif len(live) < max_n:
|
|
262
|
+
elif max_n is None or len(live) < max_n:
|
|
257
263
|
live.append({"name": name, "pid": pid})
|
|
258
|
-
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:
|
|
259
265
|
break
|
|
260
266
|
return live, ignored
|
|
261
267
|
|
|
@@ -28,7 +28,7 @@ from typing import Literal
|
|
|
28
28
|
|
|
29
29
|
from agent_runner import _resolve, events, hooks, host_health, metrics
|
|
30
30
|
from agent_runner._notify import NULL_LISTENER, Listener, NullListener
|
|
31
|
-
from agent_runner._plugin_sandbox import run_hook_sandboxed
|
|
31
|
+
from agent_runner._plugin_sandbox import SpawnHookInterrupted, run_hook_sandboxed
|
|
32
32
|
from agent_runner._procwait import wait_exit
|
|
33
33
|
from agent_runner._serve_policy import (
|
|
34
34
|
_MEM_LOOP_PERSIST_THRESHOLD,
|
|
@@ -251,7 +251,9 @@ def _spawn_view(work_dir, profile) -> hooks.SpawnView:
|
|
|
251
251
|
)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
-
def _run_one_spawn_hook(
|
|
254
|
+
def _run_one_spawn_hook(
|
|
255
|
+
h, ctx, log_dir, view, *, sandbox, engaged, listener=NULL_LISTENER, stop
|
|
256
|
+
) -> SpawnDecision | None:
|
|
255
257
|
"""Run one spawn hook, isolating any failure exactly as ``dispatch_dirty``
|
|
256
258
|
isolates a raising dirty handler: emit ``hook_failed`` and return None (the
|
|
257
259
|
caller omits None from the collapse — treated as proceed).
|
|
@@ -265,7 +267,9 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
|
|
|
265
267
|
in-process (unconfined — announced once at serve boot) and ``require`` refuses
|
|
266
268
|
it (belt-and-braces; ``gate_serve_boot`` already aborts serve under
|
|
267
269
|
require+can't-engage). The same ``view`` is passed on every path, so ``env``
|
|
268
|
-
stays names-only (no secret value).
|
|
270
|
+
stays names-only (no secret value). A sandboxed hook interrupted by a stop
|
|
271
|
+
(``should_stop`` fires on the serve doorbell) returns None WITHOUT emitting
|
|
272
|
+
``hook_failed`` -- a stop is not a plugin failure."""
|
|
269
273
|
third_party = not hooks._SPAWN_HOOK_BUILTIN.get(id(h), False)
|
|
270
274
|
try:
|
|
271
275
|
from agent_runner._sandbox_probe import hook_route
|
|
@@ -279,9 +283,19 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
|
|
|
279
283
|
if route == "trampoline":
|
|
280
284
|
module_path, attr_path = hooks._SPAWN_HOOK_MODULE.get(id(h), ("", ""))
|
|
281
285
|
return run_hook_sandboxed(
|
|
282
|
-
"spawn_hook",
|
|
286
|
+
"spawn_hook",
|
|
287
|
+
module_path,
|
|
288
|
+
attr_path,
|
|
289
|
+
h.name,
|
|
290
|
+
ctx,
|
|
291
|
+
log_dir=log_dir,
|
|
292
|
+
view=view,
|
|
293
|
+
wake_fd=listener.fd,
|
|
294
|
+
should_stop=lambda: stop["requested"],
|
|
283
295
|
)
|
|
284
296
|
return h.before_spawn(ctx, view)
|
|
297
|
+
except SpawnHookInterrupted:
|
|
298
|
+
return None
|
|
285
299
|
except Exception as exc: # noqa: BLE001 — isolate; omit from collapse (== proceed)
|
|
286
300
|
events.emit(
|
|
287
301
|
log_dir,
|
|
@@ -294,7 +308,15 @@ def _run_one_spawn_hook(h, ctx, log_dir, view, *, sandbox, engaged) -> SpawnDeci
|
|
|
294
308
|
|
|
295
309
|
|
|
296
310
|
def _maybe_defer_for_spawn_hooks(
|
|
297
|
-
cfg,
|
|
311
|
+
cfg,
|
|
312
|
+
log_dir,
|
|
313
|
+
stop,
|
|
314
|
+
*,
|
|
315
|
+
phase,
|
|
316
|
+
work_dir,
|
|
317
|
+
engaged: bool,
|
|
318
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
319
|
+
listener: Listener | NullListener = NULL_LISTENER,
|
|
298
320
|
) -> bool:
|
|
299
321
|
"""The LAST serve-admission gate: run every registered SpawnHook over a
|
|
300
322
|
read-only view of the resolved spawn and collapse their verdicts
|
|
@@ -321,8 +343,17 @@ def _maybe_defer_for_spawn_hooks(
|
|
|
321
343
|
named: list[tuple[str, SpawnDecision]] = []
|
|
322
344
|
for h in registered:
|
|
323
345
|
decision = _run_one_spawn_hook(
|
|
324
|
-
h,
|
|
346
|
+
h,
|
|
347
|
+
ctx,
|
|
348
|
+
log_dir,
|
|
349
|
+
view,
|
|
350
|
+
sandbox=cfg.plugins.sandbox,
|
|
351
|
+
engaged=engaged,
|
|
352
|
+
listener=listener,
|
|
353
|
+
stop=stop,
|
|
325
354
|
)
|
|
355
|
+
if stop["requested"]:
|
|
356
|
+
return False # a stop landed during a hook -- serve_cmd's stop-check refuses the spawn
|
|
326
357
|
if decision is None:
|
|
327
358
|
continue # hook_failed already emitted; omit from collapse (== proceed)
|
|
328
359
|
if decision.action != "proceed" and h.name not in allow:
|
|
@@ -351,13 +382,13 @@ def _maybe_defer_for_spawn_hooks(
|
|
|
351
382
|
defer_s=collapsed.defer_s,
|
|
352
383
|
reason=collapsed.reason,
|
|
353
384
|
)
|
|
354
|
-
if not _interruptible_sleep(collapsed.defer_s, stop, clock=clock):
|
|
385
|
+
if not _interruptible_sleep(collapsed.defer_s, stop, clock=clock, listener=listener):
|
|
355
386
|
emit_round_resumed(log_dir, deferred_for_s=int(clock.monotonic() - started))
|
|
356
387
|
return True
|
|
357
388
|
emit_plugin_spawn_decision(
|
|
358
389
|
log_dir, hook=winner, action="skip", defer_s=0, reason=collapsed.reason
|
|
359
390
|
)
|
|
360
|
-
_interruptible_sleep(_SPAWN_SKIP_REPOLL_S, stop, clock=clock)
|
|
391
|
+
_interruptible_sleep(_SPAWN_SKIP_REPOLL_S, stop, clock=clock, listener=listener)
|
|
361
392
|
return True
|
|
362
393
|
|
|
363
394
|
|
|
@@ -456,7 +456,7 @@ def _round_throttle_gate(cfg, args, log_dir, stop) -> tuple[frozenset[str], int
|
|
|
456
456
|
return frozenset(), None
|
|
457
457
|
|
|
458
458
|
|
|
459
|
-
def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool):
|
|
459
|
+
def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool, listener=NULL_LISTENER):
|
|
460
460
|
"""The final admission gate: run the pre-spawn hooks for the resolved
|
|
461
461
|
``phase`` and translate a defer/skip into the ``_PAUSED_CONTINUE`` sentinel
|
|
462
462
|
(caller re-admits next iteration); a proceed returns ``phase`` unchanged.
|
|
@@ -465,9 +465,17 @@ def _spawn_gate(cfg, log_dir, stop, phase, *, sandbox_engaged: bool):
|
|
|
465
465
|
already paused, and it is the LAST gate (after memory / schedule / throttle
|
|
466
466
|
have all resolved a concrete phase to spawn). ``sandbox_engaged`` is the
|
|
467
467
|
boot probe verdict threaded from ``cmd()`` so the seam routes without
|
|
468
|
-
re-probing per round.
|
|
468
|
+
re-probing per round. ``listener`` forwards into the sandboxed spawn-hook
|
|
469
|
+
wait and any defer/skip sleep, so a stop lands immediately instead of
|
|
470
|
+
riding out the trampoline's 30s wall-timeout."""
|
|
469
471
|
if _maybe_defer_for_spawn_hooks(
|
|
470
|
-
cfg,
|
|
472
|
+
cfg,
|
|
473
|
+
log_dir,
|
|
474
|
+
stop,
|
|
475
|
+
phase=phase,
|
|
476
|
+
work_dir=cfg.runtime.work_dir,
|
|
477
|
+
engaged=sandbox_engaged,
|
|
478
|
+
listener=listener,
|
|
471
479
|
):
|
|
472
480
|
return _PAUSED_CONTINUE
|
|
473
481
|
return phase
|
|
@@ -505,11 +513,15 @@ def _select_and_gate(
|
|
|
505
513
|
if args.ignore_schedule:
|
|
506
514
|
# rotation self-resolves in the round; no --phase, but the pre-spawn gate
|
|
507
515
|
# still runs on the concrete (None) outcome.
|
|
508
|
-
return _spawn_gate(
|
|
516
|
+
return _spawn_gate(
|
|
517
|
+
cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged, listener=listener
|
|
518
|
+
)
|
|
509
519
|
if not _phase_aware(cfg):
|
|
510
520
|
if _maybe_pause_for_schedule(cfg, log_dir, stop, listener=listener):
|
|
511
521
|
return _PAUSED_CONTINUE
|
|
512
|
-
return _spawn_gate(
|
|
522
|
+
return _spawn_gate(
|
|
523
|
+
cfg, log_dir, stop, None, sandbox_engaged=sandbox_engaged, listener=listener
|
|
524
|
+
)
|
|
513
525
|
# Pass the clock explicitly (call-time lookup) so tests can monkeypatch
|
|
514
526
|
# schedule.now_in_zone; a default arg would capture the original at import.
|
|
515
527
|
sel = phase_select.select_phase(
|
|
@@ -548,7 +560,9 @@ def _select_and_gate(
|
|
|
548
560
|
chosen=sel.phase,
|
|
549
561
|
active_window=sel.active_window or "",
|
|
550
562
|
)
|
|
551
|
-
return _spawn_gate(
|
|
563
|
+
return _spawn_gate(
|
|
564
|
+
cfg, log_dir, stop, sel.phase, sandbox_engaged=sandbox_engaged, listener=listener
|
|
565
|
+
)
|
|
552
566
|
|
|
553
567
|
|
|
554
568
|
def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
|
|
@@ -34,13 +34,14 @@ DEFAULT_SIGTERM_GRACE_S = 10
|
|
|
34
34
|
|
|
35
35
|
# Ceiling for [agent] sigterm_grace_s: sits a STRICT ~3s BELOW _serve_policy's
|
|
36
36
|
# _ROUND_TERM_GRACE_S (15, the supervisor's own wait for the round leader) --
|
|
37
|
-
# NOT equal to it. The margin
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
37
|
+
# NOT equal to it. The margin buys the round LEADER's own cooperative
|
|
38
|
+
# _kill_pgroup (SIGTERM -> grace -> killpg of its agent) room to finish before
|
|
39
|
+
# any outside SIGKILL lands: on the out-of-process `agent-runner kill` path
|
|
40
|
+
# (_lifecycle._terminate_round_pid) the leader is SIGTERM'd first and only
|
|
41
|
+
# SIGKILLed after this same grace, so a flushing agent isn't cut off an instant
|
|
42
|
+
# before the leader's own escalation. config/ cannot import _serve_policy (a
|
|
43
|
+
# cycle: _serve_policy already imports agent_runner.config for ConfigError), so
|
|
44
|
+
# this is a LITERAL mirror-with-margin, pinned by
|
|
44
45
|
# tests/invariants/test_timeout_budget_invariant.py against drift, the same
|
|
45
46
|
# pattern _serve_policy uses for its own agent_runtime.REAP_GRACE_S /
|
|
46
47
|
# vcs_state.GIT_COMMIT_TIMEOUT_S mirrors.
|
|
@@ -89,6 +89,10 @@ there:
|
|
|
89
89
|
- `start` is idempotent.
|
|
90
90
|
- `stop` waits up to `round_budget_s` for the current round.
|
|
91
91
|
- `kill` (and `restart --force`) is for a stuck round only — 5s grace then SIGKILL.
|
|
92
|
+
- `agent-runner kill` does NOT honor `[agent] sigterm_grace_s`: under a systemd
|
|
93
|
+
`--user` unit it `systemctl kill`s the whole cgroup after a fixed 5 s, so a
|
|
94
|
+
cooperative agent (e.g. `gemini`) that needs its full grace to flush should be
|
|
95
|
+
stopped with `agent-runner serve stop` (drain-aware) rather than `kill`.
|
|
92
96
|
- `restart` against a `--system`-installed unit refuses, printing the
|
|
93
97
|
`sudo systemctl restart ...` command to run instead (it manages user-scope
|
|
94
98
|
units only). `status` still reports it correctly — a `--system` install
|
|
@@ -496,6 +496,34 @@ No new internal wait — the grace window rides the v0.3.4 `wait_exit` deadline
|
|
|
496
496
|
|
|
497
497
|
Dev-host-relative (macOS). The grace's real value shows on the constrained fleet (a `gemini` round flushing under memory pressure before an early-SIGTERM nudge) — that nudge is the 462 MB-constrained-host-gated v0.3.7 work; this release ships only the correct, configurable grace it will use. No new constrained-host claim.
|
|
498
498
|
|
|
499
|
+
## 0.3.5 → 0.3.6 (2026-09-15)
|
|
500
|
+
|
|
501
|
+
Two closed lifecycle gaps, not a new subsystem: `kill` now reaps a stuck round's detached descendants the way `serve stop` already did, and the sandboxed spawn-hook trampoline wait now joins the same `wait_exit` fd primitive as the rest of `serve`, so a stop during a confined third-party hook (or a plugin `defer`) reacts immediately instead of riding out the old poll. Same macOS harness (§ methodology above).
|
|
502
|
+
|
|
503
|
+
### 1. Import/startup RSS
|
|
504
|
+
|
|
505
|
+
| | 0.3.5 | 0.3.6 | Δ |
|
|
506
|
+
|---|---|---|---|
|
|
507
|
+
| RSS (avg of 5 cold runs) | 24.12 MB | 24.18 MB | +0.06 MB (flat, within noise) |
|
|
508
|
+
| RSS range | 24.03–24.19 MB* | 24.00–24.34 MB | |
|
|
509
|
+
|
|
510
|
+
\* one v0.3.5 run came in at 27.17 MB — the same first-invocation cold-page-cache outlier this page excludes elsewhere (see Caveats); dropped from the average/range as noise.
|
|
511
|
+
|
|
512
|
+
Flat, and no new import edge. `_plugin_sandbox`/`_procwait`/`_notify` are NOT on the bare `import agent_runner` graph, but they ARE on the `import agent_runner.cli` (serve/round) startup graph — frozen in `test_import_footprint.py`'s `EXPECTED_STARTUP_PKG_MODULES` allowlist since the v0.3.2 SpawnHook seam (`_serve_round` imports `run_hook_sandboxed` at module level) and the v0.3.4 doorbell. v0.3.6 adds no new module to that graph — only new NAMES within those already-imported modules (`wake_fd`/`should_stop`/`SpawnHookInterrupted`/`_DRAIN_JOIN_S`/`_join_drain_threads`) — so the allowlist is unchanged and `test_import_footprint.py` + `test_round_alloc_growth.py` are green. Base dependency stays `psutil>=5.9`. (The raw `sys.modules` count shows +1, `agent_runner._version` — a generated file a fresh throwaway worktree lacks, a measurement artifact, not a code change.)
|
|
513
|
+
|
|
514
|
+
### 2. Efficiency
|
|
515
|
+
|
|
516
|
+
No headline RSS number here — the win is in wakeups and reaction latency, and it's conditional on workload. For the first-party plugin fleet, which runs no third-party spawn hooks, this release is flat, same as 0.3.5. The number that moves is scoped to whoever *does* run one:
|
|
517
|
+
|
|
518
|
+
- **Per engaged third-party spawn hook, the trampoline wait drops from ~20 wakeups/s to exactly 1.** The old confinement trampoline joined a stdlib `subprocess.Popen.wait()`, which sleep-polls internally (~20 Hz); the rewrite blocks the trampoline on the same `wait_exit` fd primitive the rest of `serve` already uses, so it wakes exactly once, on the hook's actual exit — asserted by `test_plugin_sandbox_wire.py`'s `calls["n"] == 1` property test.
|
|
519
|
+
- **A stop during a confined hook or a plugin `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 — closing the two lifecycle gaps this release targets. Behavioral, not an RSS change.
|
|
520
|
+
|
|
521
|
+
The trampoline bounds its post-reap pipe drain to `_DRAIN_JOIN_S` (`agent_runner/_plugin_sandbox.py`): if a process the hook itself `fork()`'d keeps the reaped child's output pipe open, the drain thread stays blocked in `read()` and is abandoned (a daemon thread) rather than stalling the stop. That leaks at most one drain-thread pair per such invocation, bounded by the plugin's own process leak and negligible in RSS (an untouched thread stack); the thread dies when the fork()'d holder exits or when `serve` itself does. Reaping the hook's own `fork()`'d descendants is a recorded internal follow-up.
|
|
522
|
+
|
|
523
|
+
### 3. Constrained-host (honesty)
|
|
524
|
+
|
|
525
|
+
Dev-host-relative (macOS). Neither closed gap is constrained-host-specific — both are plain process-lifecycle correctness fixes exercised on CI (Linux, where the sandbox trampoline actually engages) and dev. No new constrained-host claim; the 462 MB `memory.high` write stays 0.3.7, Pi-gated.
|
|
526
|
+
|
|
499
527
|
## Enforcement
|
|
500
528
|
|
|
501
529
|
Four invariant tests keep these numbers from drifting silently:
|