cli-agent-runner 0.1.37__tar.gz → 0.1.38__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.1.37 → cli_agent_runner-0.1.38}/CHANGELOG.md +10 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_emit.py +35 -6
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/agent_runtime.py +57 -15
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/api.py +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/events.py +1 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/runner.py +10 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/architecture.md +1 -0
- cli_agent_runner-0.1.38/docs/migrations/0.1.38.md +53 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/runbook.md +19 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_grace_kill_emission.py +33 -1
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_catalogs.py +7 -0
- cli_agent_runner-0.1.38/tests/unit/test_agent_runtime_grace.py +159 -0
- cli_agent_runner-0.1.37/tests/unit/test_agent_runtime_grace.py +0 -72
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.gitignore +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/LICENSE +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/README.zh.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/build.sh +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/configuration.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/events.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/plugins.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/unit/test_vcs_state.py +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.38] - 2026-05-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Grace-kill (`max_grace_after_result_s`) no longer reaps a round that emitted `type=result` while a backgrounded child process (e.g. a long build) is still running. It now reaps only when the agent's process group has no live worker processes left (a genuine hang); otherwise it waits for the round to finish or for the `round_timeout_s` ceiling.
|
|
14
|
+
- Corrected `round_grace_kill`'s description: the kill is gated on the process group being idle (no live workers), not on log silence.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- New event `round_grace_extended` — emitted once when grace elapsed after `type=result` but a live worker process kept the round busy; carries the worker cmdlines.
|
|
18
|
+
- `round_grace_kill` now carries `live_children` (cmdlines observed at kill time; empty for a genuine idle hang).
|
|
19
|
+
|
|
10
20
|
## [0.1.37] - 2026-05-22
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.38
|
|
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
|
|
@@ -19,6 +19,7 @@ __all__ = [
|
|
|
19
19
|
"emit_fresh_eyes_round_triggered",
|
|
20
20
|
"emit_max_rounds_reached",
|
|
21
21
|
"emit_rate_limit_stop",
|
|
22
|
+
"emit_round_grace_extended",
|
|
22
23
|
"emit_round_grace_kill",
|
|
23
24
|
"emit_round_progress",
|
|
24
25
|
"emit_round_substrate_after",
|
|
@@ -233,16 +234,44 @@ def emit_round_grace_kill(
|
|
|
233
234
|
*,
|
|
234
235
|
round_num: int,
|
|
235
236
|
grace_s: int,
|
|
237
|
+
live_children: list[str] | None = None,
|
|
236
238
|
) -> None:
|
|
237
|
-
"""Emit when subprocess killed because grace-after-result timer
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(wall-clock exceeded without result event).
|
|
239
|
+
"""Emit when the subprocess was killed because the grace-after-result timer
|
|
240
|
+
expired AND the agent's process group had no live worker processes left
|
|
241
|
+
(a genuine hang). Distinct from round_grace_extended (grace elapsed but a
|
|
242
|
+
worker was still running) and round_timeout_kill (wall-clock exceeded).
|
|
242
243
|
"""
|
|
243
244
|
from agent_runner.events import ROUND_GRACE_KILL, emit
|
|
244
245
|
|
|
245
|
-
emit(
|
|
246
|
+
emit(
|
|
247
|
+
log_dir,
|
|
248
|
+
ROUND_GRACE_KILL,
|
|
249
|
+
round_num=round_num,
|
|
250
|
+
grace_s=grace_s,
|
|
251
|
+
live_children=live_children or [],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def emit_round_grace_extended(
|
|
256
|
+
log_dir: Path,
|
|
257
|
+
*,
|
|
258
|
+
round_num: int,
|
|
259
|
+
grace_s: int,
|
|
260
|
+
live_children: list[str],
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Emit when the grace-after-result timer expired but the agent still had
|
|
263
|
+
live worker processes (e.g. a backgrounded build), so the round was NOT
|
|
264
|
+
killed; it continues until it finishes or hits round_timeout_s.
|
|
265
|
+
"""
|
|
266
|
+
from agent_runner.events import ROUND_GRACE_EXTENDED, emit
|
|
267
|
+
|
|
268
|
+
emit(
|
|
269
|
+
log_dir,
|
|
270
|
+
ROUND_GRACE_EXTENDED,
|
|
271
|
+
round_num=round_num,
|
|
272
|
+
grace_s=grace_s,
|
|
273
|
+
live_children=live_children,
|
|
274
|
+
)
|
|
246
275
|
|
|
247
276
|
|
|
248
277
|
def emit_anomaly_repetitive_tool(
|
|
@@ -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.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.38'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 38)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,9 +15,11 @@ import signal
|
|
|
15
15
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
16
16
|
import time
|
|
17
17
|
from collections.abc import Callable
|
|
18
|
-
from dataclasses import dataclass
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
import psutil
|
|
22
|
+
|
|
21
23
|
REAP_GRACE_S = 5
|
|
22
24
|
|
|
23
25
|
|
|
@@ -28,6 +30,7 @@ class RunResult:
|
|
|
28
30
|
timed_out: bool
|
|
29
31
|
pid: int
|
|
30
32
|
killed_for_grace: bool = False
|
|
33
|
+
grace_kill_children: list[str] = field(default_factory=list)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
|
|
@@ -54,6 +57,31 @@ def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
|
54
57
|
pass
|
|
55
58
|
|
|
56
59
|
|
|
60
|
+
def _live_children(proc: subprocess.Popen, *, max_n: int = 5, max_len: int = 120) -> list[str]:
|
|
61
|
+
"""Cmdlines of live (non-zombie) descendant processes of ``proc``.
|
|
62
|
+
|
|
63
|
+
Empty when ``proc`` has no live workers (a stuck agent that emitted
|
|
64
|
+
type=result then hung). Non-empty when the round backgrounded work (e.g. a
|
|
65
|
+
build) still running. Bounded so the resulting event stays small.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
parent = psutil.Process(proc.pid)
|
|
69
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
70
|
+
return []
|
|
71
|
+
out: list[str] = []
|
|
72
|
+
for child in parent.children(recursive=True):
|
|
73
|
+
try:
|
|
74
|
+
if child.status() == psutil.STATUS_ZOMBIE:
|
|
75
|
+
continue
|
|
76
|
+
line = " ".join(child.cmdline()) or child.name()
|
|
77
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
78
|
+
continue
|
|
79
|
+
out.append(line[:max_len])
|
|
80
|
+
if len(out) >= max_n:
|
|
81
|
+
break
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
57
85
|
# Exact compact bytes — matches claude CLI's no-whitespace JSONL output.
|
|
58
86
|
# A future CLI variant emitting `{"type": "result", ...}` (with space) would
|
|
59
87
|
# bypass this scan; revisit if that happens.
|
|
@@ -71,14 +99,18 @@ def run(
|
|
|
71
99
|
max_grace_after_result_s: int = 0,
|
|
72
100
|
progress_callback: Callable[[dict], None] | None = None,
|
|
73
101
|
progress_interval_s: int = 0,
|
|
102
|
+
on_grace_extended: Callable[[list[str]], None] | None = None,
|
|
74
103
|
) -> RunResult:
|
|
75
104
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
76
105
|
|
|
77
106
|
Wall-clock timeout (R1128). On timeout: SIGTERM pgroup → REAP_GRACE_S → SIGKILL.
|
|
78
107
|
|
|
79
108
|
max_grace_after_result_s: when > 0, start a countdown after the first
|
|
80
|
-
type=result event is detected in the log
|
|
81
|
-
|
|
109
|
+
type=result event is detected in the log. After it elapses, reap the
|
|
110
|
+
process group only if the agent has no live worker processes left (a
|
|
111
|
+
genuine hang). If a worker is still running (e.g. a backgrounded build),
|
|
112
|
+
do not reap — invoke ``on_grace_extended`` once and keep waiting until the
|
|
113
|
+
round finishes or hits the wall-clock ``timeout_s`` ceiling. 0 = disabled.
|
|
82
114
|
|
|
83
115
|
progress_callback: when not None and progress_interval_s > 0, called every
|
|
84
116
|
progress_interval_s seconds with a dict of log stats (log_size_kb,
|
|
@@ -100,6 +132,7 @@ def run(
|
|
|
100
132
|
start_new_session=True,
|
|
101
133
|
)
|
|
102
134
|
result_seen_at: float | None = None
|
|
135
|
+
grace_extended_emitted = False
|
|
103
136
|
try:
|
|
104
137
|
while True:
|
|
105
138
|
ret = proc.poll()
|
|
@@ -114,10 +147,9 @@ def run(
|
|
|
114
147
|
return RunResult(
|
|
115
148
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
116
149
|
)
|
|
117
|
-
# Grace kill: result emitted but subprocess still running
|
|
150
|
+
# Grace kill: result emitted but subprocess still running.
|
|
118
151
|
if max_grace_after_result_s > 0:
|
|
119
152
|
if result_seen_at is None:
|
|
120
|
-
# Cheap check: byte-scan log for marker substring
|
|
121
153
|
try:
|
|
122
154
|
with log_path.open("rb") as f:
|
|
123
155
|
if _RESULT_MARKER in f.read():
|
|
@@ -125,16 +157,26 @@ def run(
|
|
|
125
157
|
except OSError:
|
|
126
158
|
pass # log not flushed yet; check next tick
|
|
127
159
|
if result_seen_at is not None and now - result_seen_at > max_grace_after_result_s:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
children = _live_children(proc)
|
|
161
|
+
if children:
|
|
162
|
+
# Busy: a backgrounded worker is still running. Don't
|
|
163
|
+
# reap — defer to the wall-clock ceiling. Signal once.
|
|
164
|
+
if not grace_extended_emitted:
|
|
165
|
+
if on_grace_extended is not None:
|
|
166
|
+
on_grace_extended(children)
|
|
167
|
+
grace_extended_emitted = True
|
|
168
|
+
else:
|
|
169
|
+
_kill_pgroup(proc)
|
|
170
|
+
duration = time.time() - start
|
|
171
|
+
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
172
|
+
return RunResult(
|
|
173
|
+
exit_code=exit_code,
|
|
174
|
+
duration_s=duration,
|
|
175
|
+
timed_out=True,
|
|
176
|
+
pid=proc.pid,
|
|
177
|
+
killed_for_grace=True,
|
|
178
|
+
grace_kill_children=[],
|
|
179
|
+
)
|
|
138
180
|
# Progress heartbeat: call back if interval elapsed
|
|
139
181
|
if progress_callback is not None and progress_interval_s > 0:
|
|
140
182
|
if now - last_progress_at >= progress_interval_s:
|
|
@@ -733,6 +733,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
733
733
|
emit_fresh_eyes_round_triggered,
|
|
734
734
|
emit_max_rounds_reached,
|
|
735
735
|
emit_rate_limit_stop,
|
|
736
|
+
emit_round_grace_extended,
|
|
736
737
|
emit_round_grace_kill,
|
|
737
738
|
emit_round_progress,
|
|
738
739
|
emit_round_substrate_after,
|
|
@@ -49,6 +49,7 @@ ORPHAN_STASHED = "orphan_stashed"
|
|
|
49
49
|
PACKAGE_UPGRADED = "package_upgraded"
|
|
50
50
|
PROMPT_OVERWRITTEN = "prompt_overwritten"
|
|
51
51
|
ROUND_END = "round_end"
|
|
52
|
+
ROUND_GRACE_EXTENDED = "round_grace_extended"
|
|
52
53
|
ROUND_GRACE_KILL = "round_grace_kill"
|
|
53
54
|
ROUND_PROGRESS = "round_progress"
|
|
54
55
|
ROUND_START = "round_start"
|
|
@@ -466,6 +466,14 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
466
466
|
**stats,
|
|
467
467
|
)
|
|
468
468
|
|
|
469
|
+
def _grace_extended_emit(children: list[str]) -> None:
|
|
470
|
+
api.emit_round_grace_extended(
|
|
471
|
+
log_dir,
|
|
472
|
+
round_num=round_num,
|
|
473
|
+
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
474
|
+
live_children=children,
|
|
475
|
+
)
|
|
476
|
+
|
|
469
477
|
result = agent_runtime.run(
|
|
470
478
|
command=cfg.agent.command,
|
|
471
479
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
@@ -476,6 +484,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
476
484
|
max_grace_after_result_s=cfg.runtime.max_grace_after_result_s,
|
|
477
485
|
progress_callback=_progress_emit,
|
|
478
486
|
progress_interval_s=cfg.monitor.round_progress_interval_s,
|
|
487
|
+
on_grace_extended=_grace_extended_emit,
|
|
479
488
|
)
|
|
480
489
|
events.emit(
|
|
481
490
|
log_dir,
|
|
@@ -549,6 +558,7 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
549
558
|
log_dir,
|
|
550
559
|
round_num=round_num,
|
|
551
560
|
grace_s=cfg.runtime.max_grace_after_result_s,
|
|
561
|
+
live_children=result.grace_kill_children,
|
|
552
562
|
)
|
|
553
563
|
elif result.timed_out:
|
|
554
564
|
events.emit(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Migrating to 0.1.38
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.38
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No action or config change. If you use `max_grace_after_result_s`, grace-kill
|
|
10
|
+
now distinguishes a hung agent from a still-busy one.
|
|
11
|
+
|
|
12
|
+
## What changed
|
|
13
|
+
|
|
14
|
+
Previously, grace-kill reaped the whole process group `max_grace_after_result_s`
|
|
15
|
+
seconds after the agent emitted `type=result`, with no awareness of child
|
|
16
|
+
processes. A round that backgrounded a long build and emitted `type=result`
|
|
17
|
+
("waiting for build…") could have the still-running build reaped.
|
|
18
|
+
|
|
19
|
+
Now, at grace expiry, agent-runner checks for live worker processes in the
|
|
20
|
+
agent's process group:
|
|
21
|
+
|
|
22
|
+
- **No live workers** → genuine hang → reaped (`round_grace_kill`, as before).
|
|
23
|
+
- **A live worker** (e.g. a build) → not reaped; agent-runner emits
|
|
24
|
+
`round_grace_extended` once and waits until the round finishes or hits the
|
|
25
|
+
`round_timeout_s` wall-clock ceiling.
|
|
26
|
+
|
|
27
|
+
The check re-runs each poll tick, so once a backgrounded worker exits, a still-
|
|
28
|
+
stuck agent is reaped promptly rather than waiting out `round_timeout_s`.
|
|
29
|
+
|
|
30
|
+
## Three distinct outcomes
|
|
31
|
+
|
|
32
|
+
- `round_grace_extended` — grace elapsed but a worker is still running (busy).
|
|
33
|
+
- `round_grace_kill` — grace elapsed and the process group is idle (hang).
|
|
34
|
+
- `round_timeout_kill` — wall-clock `round_timeout_s` exceeded (hard ceiling).
|
|
35
|
+
|
|
36
|
+
## Recommended agent contract
|
|
37
|
+
|
|
38
|
+
Treat the safety net as a net, not a crutch: emit `type=result` only when the
|
|
39
|
+
turn is truly done. Run long builds/tests foreground and commit before ending
|
|
40
|
+
the turn, rather than backgrounding work past `type=result`.
|
|
41
|
+
|
|
42
|
+
## Known limitation
|
|
43
|
+
|
|
44
|
+
The "live worker" check treats *any* live non-zombie descendant as busy. If
|
|
45
|
+
your agent keeps persistent helper subprocesses alive past `type=result` (e.g.
|
|
46
|
+
MCP servers), grace-kill will defer hangs to the `round_timeout_s` ceiling. This
|
|
47
|
+
never false-kills; the `round_grace_extended` events make it visible if it
|
|
48
|
+
happens.
|
|
49
|
+
|
|
50
|
+
## What did NOT change
|
|
51
|
+
|
|
52
|
+
- `max_grace_after_result_s` config, default, and `0 = disabled`.
|
|
53
|
+
- `round_timeout_s` wall-clock kill (still the hard ceiling and backstop).
|
|
@@ -565,6 +565,25 @@ agent-runner kill # force terminate
|
|
|
565
565
|
ls -la ~/.agent-runner/<project>/logs/rounds/ # most recent R*.log
|
|
566
566
|
```
|
|
567
567
|
|
|
568
|
+
### Grace-kill and backgrounded work (`max_grace_after_result_s`)
|
|
569
|
+
|
|
570
|
+
Grace-kill is now process-group-liveness-aware. At grace expiry, agent-runner
|
|
571
|
+
inspects the agent's process group for live (non-zombie) worker processes and
|
|
572
|
+
takes one of three paths:
|
|
573
|
+
|
|
574
|
+
- **`round_grace_extended`** — grace elapsed but a live worker is still running
|
|
575
|
+
(e.g. a backgrounded build). Round is NOT killed; agent-runner waits until the
|
|
576
|
+
round finishes or hits the `round_timeout_s` wall-clock ceiling.
|
|
577
|
+
- **`round_grace_kill`** — grace elapsed and the process group is idle (genuine
|
|
578
|
+
hang). Round is reaped, same as pre-0.1.38.
|
|
579
|
+
- **`round_timeout_kill`** — `round_timeout_s` wall-clock exceeded (hard ceiling,
|
|
580
|
+
fires regardless of process-group state).
|
|
581
|
+
|
|
582
|
+
If you see repeated `round_grace_extended` events, the agent is backgrounding
|
|
583
|
+
work past `type=result`. Check the `live_children` field in the event to identify
|
|
584
|
+
the process; consider restructuring the agent to emit `type=result` only when
|
|
585
|
+
truly done.
|
|
586
|
+
|
|
568
587
|
### Disk pressure
|
|
569
588
|
|
|
570
589
|
**Symptom:** `[WARN] disk_warning` at >90%; `[CRIT] disk_critical` at >95% (auto-stops).
|
{cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/tests/integration/test_grace_kill_emission.py
RENAMED
|
@@ -57,7 +57,7 @@ def test_grace_kill_emits_round_grace_kill_event(tmp_path: Path) -> None:
|
|
|
57
57
|
|
|
58
58
|
script = tmp_path / "agent.sh"
|
|
59
59
|
script.write_text(
|
|
60
|
-
'#!/bin/bash\necho \'{"type":"result","is_error":false}\'\
|
|
60
|
+
'#!/bin/bash\necho \'{"type":"result","is_error":false}\'\nexec sleep 10\n',
|
|
61
61
|
encoding="utf-8",
|
|
62
62
|
)
|
|
63
63
|
script.chmod(0o755)
|
|
@@ -77,3 +77,35 @@ def test_grace_kill_emits_round_grace_kill_event(tmp_path: Path) -> None:
|
|
|
77
77
|
# round_timeout_kill must NOT appear (grace kill is distinct)
|
|
78
78
|
timeout_events = [e for e in events if e.get("event") == "round_timeout_kill"]
|
|
79
79
|
assert len(timeout_events) == 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
|
|
83
|
+
"""Full runner flow: subprocess emits result then backgrounds a long child;
|
|
84
|
+
round_grace_extended event fires (not round_grace_kill); wall timeout reaps."""
|
|
85
|
+
_init_git(tmp_path)
|
|
86
|
+
|
|
87
|
+
script = tmp_path / "agent.sh"
|
|
88
|
+
script.write_text(
|
|
89
|
+
'#!/bin/bash\necho \'{"type":"result","is_error":false}\'\nsleep 30 &\nwait\n',
|
|
90
|
+
encoding="utf-8",
|
|
91
|
+
)
|
|
92
|
+
script.chmod(0o755)
|
|
93
|
+
|
|
94
|
+
cfg = _make_grace_config(tmp_path, script, grace_s=1)
|
|
95
|
+
result = run_one_round(cfg)
|
|
96
|
+
|
|
97
|
+
assert result.killed_for_grace is False # spared by liveness
|
|
98
|
+
assert result.timed_out is True # wall-clock ceiling reaped it
|
|
99
|
+
|
|
100
|
+
events = read_events_for_current_month(cfg.runtime.log_dir)
|
|
101
|
+
|
|
102
|
+
# round_grace_extended must appear with live_children populated
|
|
103
|
+
extended_events = [e for e in events if e.get("event") == "round_grace_extended"]
|
|
104
|
+
assert len(extended_events) == 1
|
|
105
|
+
assert extended_events[0]["round_num"] == 1
|
|
106
|
+
assert extended_events[0]["grace_s"] == 1
|
|
107
|
+
assert any("sleep" in c for c in extended_events[0]["live_children"])
|
|
108
|
+
|
|
109
|
+
# round_grace_kill must NOT appear (round was busy, not idle)
|
|
110
|
+
grace_kill_events = [e for e in events if e.get("event") == "round_grace_kill"]
|
|
111
|
+
assert len(grace_kill_events) == 0
|
|
@@ -220,3 +220,10 @@ def test_given_package_upgraded_kind_when_registered_then_in_known_event_kinds()
|
|
|
220
220
|
|
|
221
221
|
assert PACKAGE_UPGRADED == "package_upgraded"
|
|
222
222
|
assert PACKAGE_UPGRADED in _BUILTIN_KINDS
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_given_round_grace_extended_kind_when_registered_then_in_known_event_kinds() -> None:
|
|
226
|
+
from agent_runner.events import _BUILTIN_KINDS, ROUND_GRACE_EXTENDED
|
|
227
|
+
|
|
228
|
+
assert ROUND_GRACE_EXTENDED == "round_grace_extended"
|
|
229
|
+
assert ROUND_GRACE_EXTENDED in _BUILTIN_KINDS
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Tests for max_grace_after_result_s HUNG defense (0.1.31+)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from agent_runner.agent_runtime import run
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _write_fake_script(tmp_path: Path, body: str) -> Path:
|
|
15
|
+
p = tmp_path / "fake.sh"
|
|
16
|
+
p.write_text(f"#!/bin/bash\nset -e\n{body}\n", encoding="utf-8")
|
|
17
|
+
p.chmod(0o755)
|
|
18
|
+
return p
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_grace_kill_fires_when_result_then_idle(tmp_path):
|
|
22
|
+
"""Agent writes type=result then becomes a childless sleeper (exec) -> no
|
|
23
|
+
live workers -> reaped within grace + tick latency."""
|
|
24
|
+
script = _write_fake_script(
|
|
25
|
+
tmp_path,
|
|
26
|
+
'echo \'{"type":"result","is_error":false}\'\nexec sleep 5\n',
|
|
27
|
+
)
|
|
28
|
+
log_path = tmp_path / "round.log"
|
|
29
|
+
result = run(
|
|
30
|
+
command=[str(script)],
|
|
31
|
+
prompt_arg_template=[],
|
|
32
|
+
prompt="x",
|
|
33
|
+
timeout_s=10,
|
|
34
|
+
log_path=log_path,
|
|
35
|
+
env_extra={},
|
|
36
|
+
max_grace_after_result_s=1,
|
|
37
|
+
)
|
|
38
|
+
assert result.killed_for_grace is True
|
|
39
|
+
assert result.grace_kill_children == []
|
|
40
|
+
assert result.duration_s < 4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_no_grace_kill_when_disabled(tmp_path):
|
|
44
|
+
"""max_grace=0 -> grace logic disabled; wall timeout governs."""
|
|
45
|
+
script = _write_fake_script(
|
|
46
|
+
tmp_path,
|
|
47
|
+
'echo \'{"type":"result","is_error":false}\'\nsleep 5\n',
|
|
48
|
+
)
|
|
49
|
+
log_path = tmp_path / "round.log"
|
|
50
|
+
result = run(
|
|
51
|
+
command=[str(script)],
|
|
52
|
+
prompt_arg_template=[],
|
|
53
|
+
prompt="x",
|
|
54
|
+
timeout_s=2, # short wall timeout
|
|
55
|
+
log_path=log_path,
|
|
56
|
+
env_extra={},
|
|
57
|
+
max_grace_after_result_s=0,
|
|
58
|
+
)
|
|
59
|
+
assert result.killed_for_grace is False
|
|
60
|
+
assert result.timed_out is True # killed by wall timeout instead
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_no_grace_kill_when_result_not_emitted(tmp_path):
|
|
64
|
+
"""No result event -> grace countdown never starts."""
|
|
65
|
+
script = _write_fake_script(tmp_path, 'echo "no result here"\nexit 0\n')
|
|
66
|
+
log_path = tmp_path / "round.log"
|
|
67
|
+
result = run(
|
|
68
|
+
command=[str(script)],
|
|
69
|
+
prompt_arg_template=[],
|
|
70
|
+
prompt="x",
|
|
71
|
+
timeout_s=5,
|
|
72
|
+
log_path=log_path,
|
|
73
|
+
env_extra={},
|
|
74
|
+
max_grace_after_result_s=1,
|
|
75
|
+
)
|
|
76
|
+
assert result.killed_for_grace is False
|
|
77
|
+
assert result.timed_out is False
|
|
78
|
+
assert result.exit_code == 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_live_children_empty_when_no_children():
|
|
82
|
+
from agent_runner.agent_runtime import _live_children
|
|
83
|
+
|
|
84
|
+
p = subprocess.Popen(["sleep", "3"], start_new_session=True)
|
|
85
|
+
try:
|
|
86
|
+
assert _live_children(p) == []
|
|
87
|
+
finally:
|
|
88
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
89
|
+
p.wait()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_live_children_lists_backgrounded_child():
|
|
93
|
+
from agent_runner.agent_runtime import _live_children
|
|
94
|
+
|
|
95
|
+
p = subprocess.Popen(["bash", "-c", "sleep 30 & wait"], start_new_session=True)
|
|
96
|
+
try:
|
|
97
|
+
time.sleep(0.5) # let the backgrounded child spawn
|
|
98
|
+
kids = _live_children(p)
|
|
99
|
+
assert any("sleep" in k for k in kids)
|
|
100
|
+
finally:
|
|
101
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
102
|
+
p.wait()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_live_children_empty_when_process_gone():
|
|
106
|
+
from agent_runner.agent_runtime import _live_children
|
|
107
|
+
|
|
108
|
+
p = subprocess.Popen(["true"])
|
|
109
|
+
p.wait()
|
|
110
|
+
assert _live_children(p) == [] # NoSuchProcess swallowed
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_grace_extended_when_result_but_child_running(tmp_path):
|
|
114
|
+
"""Agent emits result then backgrounds a long child -> live worker -> NOT
|
|
115
|
+
grace-killed; round_timeout_s (wall) reaps it instead; extended fired once."""
|
|
116
|
+
script = _write_fake_script(
|
|
117
|
+
tmp_path,
|
|
118
|
+
'echo \'{"type":"result"}\'\nsleep 30 &\nwait\n',
|
|
119
|
+
)
|
|
120
|
+
log_path = tmp_path / "round.log"
|
|
121
|
+
extended = []
|
|
122
|
+
result = run(
|
|
123
|
+
command=[str(script)],
|
|
124
|
+
prompt_arg_template=[],
|
|
125
|
+
prompt="x",
|
|
126
|
+
timeout_s=4,
|
|
127
|
+
log_path=log_path,
|
|
128
|
+
env_extra={},
|
|
129
|
+
max_grace_after_result_s=1,
|
|
130
|
+
on_grace_extended=lambda kids: extended.append(kids),
|
|
131
|
+
)
|
|
132
|
+
assert result.killed_for_grace is False # spared by liveness
|
|
133
|
+
assert result.timed_out is True # round_timeout_s backstop reaped it
|
|
134
|
+
assert len(extended) == 1 # emitted once, not per-tick
|
|
135
|
+
assert any("sleep" in k for k in extended[0])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_grace_kill_after_child_exits_then_idle(tmp_path):
|
|
139
|
+
"""Live child first (extend), child exits, agent becomes childless (exec)
|
|
140
|
+
-> next tick reaps via grace (well before wall timeout)."""
|
|
141
|
+
script = _write_fake_script(
|
|
142
|
+
tmp_path,
|
|
143
|
+
'echo \'{"type":"result"}\'\nsleep 2 &\nwait\nexec sleep 30\n',
|
|
144
|
+
)
|
|
145
|
+
log_path = tmp_path / "round.log"
|
|
146
|
+
extended = []
|
|
147
|
+
result = run(
|
|
148
|
+
command=[str(script)],
|
|
149
|
+
prompt_arg_template=[],
|
|
150
|
+
prompt="x",
|
|
151
|
+
timeout_s=10,
|
|
152
|
+
log_path=log_path,
|
|
153
|
+
env_extra={},
|
|
154
|
+
max_grace_after_result_s=1,
|
|
155
|
+
on_grace_extended=lambda kids: extended.append(kids),
|
|
156
|
+
)
|
|
157
|
+
assert result.killed_for_grace is True # reaped after child exited
|
|
158
|
+
assert result.duration_s < 6 # ~2s child + reap, well under timeout
|
|
159
|
+
assert len(extended) == 1
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"""Tests for max_grace_after_result_s HUNG defense (0.1.31+)."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from agent_runner.agent_runtime import run
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _write_fake_script(tmp_path: Path, body: str) -> Path:
|
|
11
|
-
p = tmp_path / "fake.sh"
|
|
12
|
-
p.write_text(f"#!/bin/bash\nset -e\n{body}\n", encoding="utf-8")
|
|
13
|
-
p.chmod(0o755)
|
|
14
|
-
return p
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_grace_kill_fires_when_result_then_silent(tmp_path):
|
|
18
|
-
"""Fake agent writes type=result then sleeps 5s. max_grace=1s -> kill within 3s."""
|
|
19
|
-
script = _write_fake_script(
|
|
20
|
-
tmp_path,
|
|
21
|
-
'echo \'{"type":"result","is_error":false}\'\nsleep 5\n',
|
|
22
|
-
)
|
|
23
|
-
log_path = tmp_path / "round.log"
|
|
24
|
-
result = run(
|
|
25
|
-
command=[str(script)],
|
|
26
|
-
prompt_arg_template=[],
|
|
27
|
-
prompt="x",
|
|
28
|
-
timeout_s=10,
|
|
29
|
-
log_path=log_path,
|
|
30
|
-
env_extra={},
|
|
31
|
-
max_grace_after_result_s=1,
|
|
32
|
-
)
|
|
33
|
-
assert result.killed_for_grace is True
|
|
34
|
-
assert result.duration_s < 4 # ~1s grace + tick latency + reap grace
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_no_grace_kill_when_disabled(tmp_path):
|
|
38
|
-
"""max_grace=0 -> grace logic disabled; wall timeout governs."""
|
|
39
|
-
script = _write_fake_script(
|
|
40
|
-
tmp_path,
|
|
41
|
-
'echo \'{"type":"result","is_error":false}\'\nsleep 5\n',
|
|
42
|
-
)
|
|
43
|
-
log_path = tmp_path / "round.log"
|
|
44
|
-
result = run(
|
|
45
|
-
command=[str(script)],
|
|
46
|
-
prompt_arg_template=[],
|
|
47
|
-
prompt="x",
|
|
48
|
-
timeout_s=2, # short wall timeout
|
|
49
|
-
log_path=log_path,
|
|
50
|
-
env_extra={},
|
|
51
|
-
max_grace_after_result_s=0,
|
|
52
|
-
)
|
|
53
|
-
assert result.killed_for_grace is False
|
|
54
|
-
assert result.timed_out is True # killed by wall timeout instead
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_no_grace_kill_when_result_not_emitted(tmp_path):
|
|
58
|
-
"""No result event -> grace countdown never starts."""
|
|
59
|
-
script = _write_fake_script(tmp_path, 'echo "no result here"\nexit 0\n')
|
|
60
|
-
log_path = tmp_path / "round.log"
|
|
61
|
-
result = run(
|
|
62
|
-
command=[str(script)],
|
|
63
|
-
prompt_arg_template=[],
|
|
64
|
-
prompt="x",
|
|
65
|
-
timeout_s=5,
|
|
66
|
-
log_path=log_path,
|
|
67
|
-
env_extra={},
|
|
68
|
-
max_grace_after_result_s=1,
|
|
69
|
-
)
|
|
70
|
-
assert result.killed_for_grace is False
|
|
71
|
-
assert result.timed_out is False
|
|
72
|
-
assert result.exit_code == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_agent_runner-0.1.37 → cli_agent_runner-0.1.38}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|