cli-agent-runner 0.2.5__tar.gz → 0.2.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.2.5 → cli_agent_runner-0.2.6}/.vulture-whitelist.py +5 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CHANGELOG.md +15 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/PKG-INFO +2 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/README.md +1 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_emit.py +37 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/api.py +1 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/serve_cmd.py +21 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/config.py +3 -3
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/defenses.py +11 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/events.py +1 -0
- cli_agent_runner-0.2.6/agent_runner/round_log.py +160 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/runner.py +11 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/architecture.md +4 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/configuration.md +59 -1
- cli_agent_runner-0.2.6/docs/migrations/0.2.6.md +127 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/aider.md +6 -5
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/runbook.md +40 -6
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_architecture.py +1 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config.py +52 -3
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_defenses.py +2 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events.py +28 -0
- cli_agent_runner-0.2.6/tests/unit/test_round_log_helpers.py +269 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_runner.py +67 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_round_log.py +99 -24
- cli_agent_runner-0.2.5/agent_runner/round_log.py +0 -95
- cli_agent_runner-0.2.5/tests/unit/test_round_log_helpers.py +0 -143
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.gitignore +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/LICENSE +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/README.zh.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/build.sh +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/README.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/events.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.0.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.1.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.2.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.3.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.4.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.5.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,6 +5,19 @@ 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.2.6] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Round-log pruning is now opt-in.** `runtime.round_log_retention` defaults to `0` (never prune) and `0` is now a legal value — previously both `0` and `-1` were rejected, so there was no off switch. Retaining too much is defended (`disk_warning` at 90%, `disk_critical` at 95% auto-stops the service); deleting too much was not defended at all. Both round-log families are affected, including the serve-level `round-<N>.log` family that had been capped at 100 for many releases. Set `round_log_retention = 100` for the previous behavior.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- A prune that would delete more files than it keeps is now refused wholesale — nothing is deleted — so opting in on an existing backlog, or lowering the value later, can no longer wipe history in one pass. 0.2.4 and 0.2.5 did exactly that on their first post-upgrade round (one deployment: 12,193 of 12,293 transcripts, ~6.4 GB).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `round_logs_prune_deferred` event — emitted on every prune attempt the bulk guard defers, carrying `directory`, `existing`, `keep`, `would_delete` and a hint naming `runtime.round_log_retention`. Resolve by raising retention above the backlog or deleting the files yourself; the guard never blocks a round. Retention `0` emits nothing.
|
|
18
|
+
|
|
19
|
+
See `docs/migrations/0.2.6.md`.
|
|
20
|
+
|
|
8
21
|
## [0.2.5] - 2026-07-27
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -950,7 +963,8 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
950
963
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
951
964
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
952
965
|
|
|
953
|
-
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.
|
|
966
|
+
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.6...HEAD
|
|
967
|
+
[0.2.6]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...v0.2.6
|
|
954
968
|
[0.2.5]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...v0.2.5
|
|
955
969
|
[0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
|
|
956
970
|
[0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -93,7 +93,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
93
93
|
|
|
94
94
|
## Defenses (built in)
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
13 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
97
97
|
Each carries the historical incident it codifies and the invariant test that
|
|
98
98
|
guards it. Highlights:
|
|
99
99
|
|
|
@@ -57,7 +57,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
57
57
|
|
|
58
58
|
## Defenses (built in)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
13 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
61
61
|
Each carries the historical incident it codifies and the invariant test that
|
|
62
62
|
guards it. Highlights:
|
|
63
63
|
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
"emit_rate_limit_stop",
|
|
23
23
|
"emit_round_grace_extended",
|
|
24
24
|
"emit_round_grace_kill",
|
|
25
|
+
"emit_round_logs_prune_deferred",
|
|
25
26
|
"emit_round_progress",
|
|
26
27
|
"emit_round_substrate_after",
|
|
27
28
|
"emit_round_substrate_before",
|
|
@@ -346,6 +347,42 @@ def emit_round_grace_extended(
|
|
|
346
347
|
)
|
|
347
348
|
|
|
348
349
|
|
|
350
|
+
def emit_round_logs_prune_deferred(
|
|
351
|
+
log_dir: Path,
|
|
352
|
+
*,
|
|
353
|
+
directory: str,
|
|
354
|
+
existing: int,
|
|
355
|
+
keep: int,
|
|
356
|
+
would_delete: int,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Emit when a round-log prune was deferred because it would be *bulk*
|
|
359
|
+
(it would delete more files than it keeps), so nothing was deleted.
|
|
360
|
+
|
|
361
|
+
``directory`` is the directory holding the family — ``{log_dir}/rounds``
|
|
362
|
+
for agent transcripts, ``{log_dir}`` for the serve-level ``round-<N>.log``
|
|
363
|
+
files. Re-emitted on every prune attempt while the condition holds: the
|
|
364
|
+
deferral is permanent until an operator acts, and a one-shot event would
|
|
365
|
+
be missed by anyone who started watching later.
|
|
366
|
+
|
|
367
|
+
The hint is composed here rather than at the call sites so both families
|
|
368
|
+
name the same knob with the same wording.
|
|
369
|
+
"""
|
|
370
|
+
from agent_runner.events import ROUND_LOGS_PRUNE_DEFERRED, emit
|
|
371
|
+
|
|
372
|
+
emit(
|
|
373
|
+
log_dir,
|
|
374
|
+
ROUND_LOGS_PRUNE_DEFERRED,
|
|
375
|
+
directory=directory,
|
|
376
|
+
existing=existing,
|
|
377
|
+
keep=keep,
|
|
378
|
+
would_delete=would_delete,
|
|
379
|
+
hint=(
|
|
380
|
+
f"nothing deleted; raise runtime.round_log_retention to >= {existing} "
|
|
381
|
+
f"to keep this backlog, or delete files in {directory} yourself"
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
349
386
|
def emit_anomaly_repetitive_tool(
|
|
350
387
|
log_dir: Path,
|
|
351
388
|
*,
|
|
@@ -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.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -742,6 +742,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
742
742
|
emit_rate_limit_stop,
|
|
743
743
|
emit_round_grace_extended,
|
|
744
744
|
emit_round_grace_kill,
|
|
745
|
+
emit_round_logs_prune_deferred,
|
|
745
746
|
emit_round_progress,
|
|
746
747
|
emit_round_substrate_after,
|
|
747
748
|
emit_round_substrate_before,
|
|
@@ -15,6 +15,7 @@ import signal
|
|
|
15
15
|
import subprocess # noqa: TID251
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
|
+
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
from agent_runner._substrate import compute_git_head, compute_paths_hash
|
|
20
21
|
from agent_runner._throttle import _check_throttle_state
|
|
@@ -26,6 +27,7 @@ from agent_runner.api import (
|
|
|
26
27
|
emit_fresh_eyes_round_triggered,
|
|
27
28
|
emit_max_rounds_reached,
|
|
28
29
|
emit_rate_limit_stop,
|
|
30
|
+
emit_round_logs_prune_deferred,
|
|
29
31
|
emit_round_substrate_after,
|
|
30
32
|
emit_round_substrate_before,
|
|
31
33
|
emit_stop_file_detected,
|
|
@@ -56,6 +58,24 @@ def _resolve_max_rounds(*, cli_value: int | None, config_value: int | None) -> i
|
|
|
56
58
|
return effective
|
|
57
59
|
|
|
58
60
|
|
|
61
|
+
def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
|
|
62
|
+
"""Prune the serve-level ``round-<N>.log`` family; report a deferred prune.
|
|
63
|
+
|
|
64
|
+
A prune that would delete more files than it keeps is deferred wholesale
|
|
65
|
+
(nothing is deleted) and surfaced as ``round_logs_prune_deferred`` — the
|
|
66
|
+
same contract ``rounds/`` gets at round start, since one knob governs both.
|
|
67
|
+
"""
|
|
68
|
+
outcome = prune_old_round_logs(log_dir, retention)
|
|
69
|
+
if outcome.deferred:
|
|
70
|
+
emit_round_logs_prune_deferred(
|
|
71
|
+
log_dir,
|
|
72
|
+
directory=str(log_dir),
|
|
73
|
+
existing=outcome.existing,
|
|
74
|
+
keep=retention,
|
|
75
|
+
would_delete=outcome.deferred,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
59
79
|
def _is_fresh_eyes_round(*, round_num: int, every_n: int | None) -> bool:
|
|
60
80
|
"""True if this round should signal fresh-eyes mode to the agent subprocess.
|
|
61
81
|
|
|
@@ -106,7 +126,7 @@ def cmd(args) -> int:
|
|
|
106
126
|
|
|
107
127
|
# Pre-loop cleanup: remove stale sentinel, prune old round logs.
|
|
108
128
|
(log_dir / ".agent-done").unlink(missing_ok=True)
|
|
109
|
-
|
|
129
|
+
_prune_serve_round_logs(log_dir, cfg.runtime.round_log_retention)
|
|
110
130
|
|
|
111
131
|
round_env = {**os.environ, "AGENT_RUNNER_LOG_DIR": str(log_dir)}
|
|
112
132
|
|
|
@@ -39,7 +39,7 @@ class RuntimeConfig:
|
|
|
39
39
|
round_timeout_s: int = 1800
|
|
40
40
|
restart_delay_s: int = 3
|
|
41
41
|
disable_pre_round_hooks: bool = False
|
|
42
|
-
round_log_retention: int =
|
|
42
|
+
round_log_retention: int = 0 # 0 = never prune (pruning is opt-in)
|
|
43
43
|
narrative_file: Path | None = None
|
|
44
44
|
transient_error_action: Literal["back_off", "skip", "stop"] = "back_off"
|
|
45
45
|
max_rounds: int | None = None # None = unbounded
|
|
@@ -439,8 +439,8 @@ def load_config(toml_path: Path) -> Config:
|
|
|
439
439
|
runtime_d.get("disable_pre_round_hooks", False),
|
|
440
440
|
field="runtime.disable_pre_round_hooks",
|
|
441
441
|
),
|
|
442
|
-
round_log_retention=
|
|
443
|
-
runtime_d.get("round_log_retention",
|
|
442
|
+
round_log_retention=_require_non_negative_int(
|
|
443
|
+
runtime_d.get("round_log_retention", 0), field="runtime.round_log_retention"
|
|
444
444
|
),
|
|
445
445
|
narrative_file=_expand_and_resolve(str(runtime_d["narrative_file"]), project_name, work_dir)
|
|
446
446
|
if "narrative_file" in runtime_d
|
|
@@ -102,6 +102,17 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
102
102
|
guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
|
|
103
103
|
current_state="active",
|
|
104
104
|
),
|
|
105
|
+
Defense(
|
|
106
|
+
name="bulk_round_log_prune_guard",
|
|
107
|
+
value="a prune deleting more files than it keeps deletes nothing",
|
|
108
|
+
codifies=(
|
|
109
|
+
"0.2.4 — rounds/ pruning shipped against backlogs it never built; "
|
|
110
|
+
"one deployment's first post-upgrade round would have deleted "
|
|
111
|
+
"12,193 of 12,293 transcripts silently"
|
|
112
|
+
),
|
|
113
|
+
guarded_by=Path("tests/unit/test_round_log_helpers.py"),
|
|
114
|
+
current_state="active",
|
|
115
|
+
),
|
|
105
116
|
Defense(
|
|
106
117
|
name="flock_concurrency",
|
|
107
118
|
value="agent-runner.lock",
|
|
@@ -55,6 +55,7 @@ PROMPT_OVERWRITTEN = "prompt_overwritten"
|
|
|
55
55
|
ROUND_END = "round_end"
|
|
56
56
|
ROUND_GRACE_EXTENDED = "round_grace_extended"
|
|
57
57
|
ROUND_GRACE_KILL = "round_grace_kill"
|
|
58
|
+
ROUND_LOGS_PRUNE_DEFERRED = "round_logs_prune_deferred"
|
|
58
59
|
ROUND_PROGRESS = "round_progress"
|
|
59
60
|
ROUND_START = "round_start"
|
|
60
61
|
ROUND_SUBSTRATE_AFTER = "round_substrate_after"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Round-log file operations for serve_cmd.
|
|
2
|
+
|
|
3
|
+
Extracted from serve_cmd to keep that module a thin dispatcher.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from agent_runner.api import read_round_num
|
|
14
|
+
|
|
15
|
+
ROUND_CURRENT_LINK = "round-current.log"
|
|
16
|
+
_AGENT_ROUND_LOG_RE = re.compile(r"^R(\d+)-")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class PruneOutcome:
|
|
21
|
+
"""What one prune pass did to a round-log family.
|
|
22
|
+
|
|
23
|
+
``deleted`` and ``deferred`` are never both nonzero: a pass either prunes
|
|
24
|
+
normally, defers wholesale (see the bulk guard below), or does not run at
|
|
25
|
+
all (pruning disabled). ``existing`` is the total number of files the pass
|
|
26
|
+
considered — carried so a caller reporting a deferral does not have to
|
|
27
|
+
re-derive it from the slice rule.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
deleted: int
|
|
31
|
+
deferred: int
|
|
32
|
+
existing: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pruning_disabled(keep: int) -> bool:
|
|
36
|
+
"""``keep == 0`` disables pruning entirely — the default.
|
|
37
|
+
|
|
38
|
+
Distinct from a deferral, and deliberately reported as neither deleted nor
|
|
39
|
+
deferred: 0 is the operator's expressed intent, not a backlog awaiting a
|
|
40
|
+
decision, so callers emit nothing. Unbounded growth is the accepted trade
|
|
41
|
+
and it is not undefended — ``disk_warning`` (90%) alerts and
|
|
42
|
+
``disk_critical`` (95%) auto-stops the service.
|
|
43
|
+
"""
|
|
44
|
+
return keep == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_bulk(stale: int, keep: int) -> bool:
|
|
48
|
+
"""A prune that would remove more files than it keeps is a *bulk* prune.
|
|
49
|
+
|
|
50
|
+
Bulk prunes delete nothing. In steady state a pass removes ~1 file, so
|
|
51
|
+
this can only trip on the first encounter with a pre-existing backlog or
|
|
52
|
+
after an operator drastically lowers ``runtime.round_log_retention`` —
|
|
53
|
+
both cases where wiping history is far likelier to be an accident than an
|
|
54
|
+
intent. The supervisor never performs a bulk deletion on its own; the
|
|
55
|
+
operator raises retention (the backlog then falls inside the kept window
|
|
56
|
+
and normal pruning resumes) or clears the files themselves.
|
|
57
|
+
"""
|
|
58
|
+
return stale > keep
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def atomic_relink(link: Path, target: Path) -> None:
|
|
62
|
+
"""Atomically replace ``link`` to point at ``target``.
|
|
63
|
+
|
|
64
|
+
Uses ``os.symlink`` + ``os.replace``: create the symlink at a temp path,
|
|
65
|
+
then atomically rename it to the final link name.
|
|
66
|
+
"""
|
|
67
|
+
tmp = link.with_suffix(link.suffix + ".tmp")
|
|
68
|
+
tmp.unlink(missing_ok=True)
|
|
69
|
+
os.symlink(target.name, tmp)
|
|
70
|
+
os.replace(tmp, link)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def prune_old_round_logs(log_dir: Path, retention: int) -> PruneOutcome:
|
|
74
|
+
"""Keep most-recent ``retention`` round-*.log files by mtime; unlink the rest.
|
|
75
|
+
|
|
76
|
+
Excludes the ``round-current.log`` symlink — that's not a regular log file.
|
|
77
|
+
Called once at serve startup (no mid-session pruning, avoid race with
|
|
78
|
+
active writes).
|
|
79
|
+
|
|
80
|
+
``retention == 0`` (the default) never prunes. Otherwise subject to the
|
|
81
|
+
bulk guard (:func:`_is_bulk`): a startup that would delete more than it
|
|
82
|
+
keeps deletes nothing and reports the deferral instead. ``round_log_retention``
|
|
83
|
+
governs both round-log families, so both opting in and lowering the value
|
|
84
|
+
mean the same thing in both.
|
|
85
|
+
"""
|
|
86
|
+
if _pruning_disabled(retention):
|
|
87
|
+
return PruneOutcome(deleted=0, deferred=0, existing=0)
|
|
88
|
+
logs = sorted(
|
|
89
|
+
log_dir.glob("round-*.log"),
|
|
90
|
+
key=lambda p: p.stat().st_mtime,
|
|
91
|
+
reverse=True,
|
|
92
|
+
)
|
|
93
|
+
logs = [p for p in logs if p.name != ROUND_CURRENT_LINK]
|
|
94
|
+
stale = logs[retention:]
|
|
95
|
+
if _is_bulk(len(stale), retention):
|
|
96
|
+
return PruneOutcome(deleted=0, deferred=len(stale), existing=len(logs))
|
|
97
|
+
for old in stale:
|
|
98
|
+
old.unlink(missing_ok=True)
|
|
99
|
+
return PruneOutcome(deleted=len(stale), deferred=0, existing=len(logs))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def prune_rounds_dir(rounds_dir: Path, keep: int) -> PruneOutcome:
|
|
103
|
+
"""Keep the ``keep`` highest-numbered ``R<n>-*.log`` agent logs; unlink the rest.
|
|
104
|
+
|
|
105
|
+
Ordered by the round number parsed out of the filename — not by string sort
|
|
106
|
+
(``R9`` must not outrank ``R10``) and not by mtime (a restore or copy can
|
|
107
|
+
rewrite that; the round number is the semantic order). Names that don't
|
|
108
|
+
match are left alone. Called at round start, before the current round's log
|
|
109
|
+
is minted, so the active log is never a deletion candidate.
|
|
110
|
+
|
|
111
|
+
``keep == 0`` (the default) never prunes: these transcripts are forensic
|
|
112
|
+
material — the record used to reconstruct a round lost to a dropped
|
|
113
|
+
connection, or to verify that a fix actually worked — so retaining them is
|
|
114
|
+
the default and discarding them is opt-in. When pruning IS enabled, the
|
|
115
|
+
bulk guard (:func:`_is_bulk`) still applies: a pass that would delete more
|
|
116
|
+
than it keeps deletes nothing, so opting in (or later lowering the value)
|
|
117
|
+
does not consume a backlog in one shot.
|
|
118
|
+
"""
|
|
119
|
+
if _pruning_disabled(keep):
|
|
120
|
+
return PruneOutcome(deleted=0, deferred=0, existing=0)
|
|
121
|
+
if not rounds_dir.is_dir():
|
|
122
|
+
return PruneOutcome(deleted=0, deferred=0, existing=0)
|
|
123
|
+
numbered = []
|
|
124
|
+
for path in rounds_dir.glob("R*.log"):
|
|
125
|
+
match = _AGENT_ROUND_LOG_RE.match(path.name)
|
|
126
|
+
if match:
|
|
127
|
+
numbered.append((int(match.group(1)), path))
|
|
128
|
+
# Sort on the number alone: the same round can be present twice with
|
|
129
|
+
# different timestamps after a crash-rerun, and tuple comparison would then
|
|
130
|
+
# fall through to comparing Paths.
|
|
131
|
+
numbered.sort(key=lambda item: item[0], reverse=True)
|
|
132
|
+
stale = numbered[keep:]
|
|
133
|
+
if _is_bulk(len(stale), keep):
|
|
134
|
+
return PruneOutcome(deleted=0, deferred=len(stale), existing=len(numbered))
|
|
135
|
+
for _num, old in stale:
|
|
136
|
+
old.unlink(missing_ok=True)
|
|
137
|
+
return PruneOutcome(deleted=len(stale), deferred=0, existing=len(numbered))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def next_round_num(log_dir: Path) -> int:
|
|
141
|
+
"""Return the next round number, avoiding reuse of any existing log file numbers.
|
|
142
|
+
|
|
143
|
+
Takes ``max(read_round_num, max_log_file_num) + 1``. Under normal operation
|
|
144
|
+
these agree. The file-system fallback handles the case where ``status.json``
|
|
145
|
+
has been deleted but old ``round-*.log`` files remain — the counter skips
|
|
146
|
+
forward instead of silently overwriting a numbered log.
|
|
147
|
+
"""
|
|
148
|
+
status_num = read_round_num(log_dir)
|
|
149
|
+
file_nums = []
|
|
150
|
+
for p in log_dir.glob("round-*.log"):
|
|
151
|
+
if p.name == ROUND_CURRENT_LINK:
|
|
152
|
+
continue
|
|
153
|
+
stem_parts = p.stem.split("-", 1)
|
|
154
|
+
if len(stem_parts) == 2:
|
|
155
|
+
try:
|
|
156
|
+
file_nums.append(int(stem_parts[1]))
|
|
157
|
+
except ValueError:
|
|
158
|
+
pass
|
|
159
|
+
max_file_num = max(file_nums, default=0)
|
|
160
|
+
return max(status_num, max_file_num) + 1
|
|
@@ -428,8 +428,17 @@ def _run_one_round_inner(cfg: Config, *, phase_override: str | None = None) -> R
|
|
|
428
428
|
rounds_dir = log_dir / "rounds"
|
|
429
429
|
rounds_dir.mkdir(exist_ok=True)
|
|
430
430
|
# Prune BEFORE minting this round's log: only historical files exist now,
|
|
431
|
-
# so the active round's log can never be a deletion candidate.
|
|
432
|
-
|
|
431
|
+
# so the active round's log can never be a deletion candidate. A bulk
|
|
432
|
+
# prune deletes nothing and is reported instead — never blocks the round.
|
|
433
|
+
pruned = prune_rounds_dir(rounds_dir, cfg.runtime.round_log_retention)
|
|
434
|
+
if pruned.deferred:
|
|
435
|
+
api.emit_round_logs_prune_deferred(
|
|
436
|
+
log_dir,
|
|
437
|
+
directory=str(rounds_dir),
|
|
438
|
+
existing=pruned.existing,
|
|
439
|
+
keep=cfg.runtime.round_log_retention,
|
|
440
|
+
would_delete=pruned.deferred,
|
|
441
|
+
)
|
|
433
442
|
log_path = rounds_dir / f"R{round_num}-{datetime.now(UTC).strftime('%Y%m%dT%H%M%S')}.log"
|
|
434
443
|
|
|
435
444
|
hook_ctx = hooks.HookContext(
|
|
@@ -38,7 +38,7 @@ common to all three. Operator learns one mental model, three lenses.
|
|
|
38
38
|
|
|
39
39
|
## Defenses-as-data
|
|
40
40
|
|
|
41
|
-
`agent_runner.defenses.catalog(cfg)` returns
|
|
41
|
+
`agent_runner.defenses.catalog(cfg)` returns 13 structured `Defense` entries.
|
|
42
42
|
Each entry carries:
|
|
43
43
|
|
|
44
44
|
- `name` — stable identifier
|
|
@@ -65,6 +65,7 @@ surfacing everywhere.
|
|
|
65
65
|
| `critical_envs_injection` | Env injection via [agent.env] block — preset-supplied per CLI (e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates) | `tests/unit/test_agent_runtime.py` |
|
|
66
66
|
| `startup_smoke_check` | R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; now halts serve (config_broken) instead of respawning a broken config | `tests/unit/test_serve_config_broken.py` |
|
|
67
67
|
| `crash_loop_breaker` | Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay | `tests/unit/test_serve_crash_loop.py` |
|
|
68
|
+
| `bulk_round_log_prune_guard` | 0.2.4 — rounds/ pruning shipped against backlogs it never built; one deployment's first post-upgrade round would have deleted 12,193 of 12,293 transcripts silently | `tests/unit/test_round_log_helpers.py` |
|
|
68
69
|
| `flock_concurrency` | Architectural — prevent concurrent supervisors corrupting state | `tests/unit/test_runner.py` |
|
|
69
70
|
| `atomic_state_writes` | Data integrity — crashes never leave half-written state files | `tests/invariants/test_atomic_write_enforced.py` |
|
|
70
71
|
| `event_kind_registry` | Prevent events.emit() typos / unregistered kinds slipping past CI | `tests/invariants/test_event_kind_registry.py` |
|
|
@@ -219,6 +220,7 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
|
219
220
|
- `round_end`
|
|
220
221
|
- `round_grace_extended`
|
|
221
222
|
- `round_grace_kill`
|
|
223
|
+
- `round_logs_prune_deferred`
|
|
222
224
|
- `round_progress`
|
|
223
225
|
- `round_start`
|
|
224
226
|
- `round_substrate_after`
|
|
@@ -240,4 +242,4 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
|
240
242
|
|
|
241
243
|
三层架构:Round(一轮 agent)/ Loop(serve 薄壳)/ Witness(monitor)。
|
|
242
244
|
三视角对称:peek(快照)/ watch(快照循环)/ monitor(异常检测),共用下钻参数。
|
|
243
|
-
防御以结构化目录形式存在(
|
|
245
|
+
防御以结构化目录形式存在(13 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
|
|
@@ -39,7 +39,7 @@ running with newly-set `dirty_action = "auto_commit"` is undefined).
|
|
|
39
39
|
| `round_timeout_s` | `int` | 1800 |
|
|
40
40
|
| `restart_delay_s` | `int` | 3 |
|
|
41
41
|
| `disable_pre_round_hooks` | `bool` | False |
|
|
42
|
-
| `round_log_retention` | `int` |
|
|
42
|
+
| `round_log_retention` | `int` | 0 |
|
|
43
43
|
| `narrative_file` | `Path \| None` | None |
|
|
44
44
|
| `transient_error_action` | `Literal['back_off', 'skip', 'stop']` | 'back_off' |
|
|
45
45
|
| `max_rounds` | `int \| None` | None |
|
|
@@ -119,6 +119,64 @@ commands happen to match a token drawn from its prompt. The `claude` preset
|
|
|
119
119
|
defaults to `"stdin"`; existing configs are unchanged. See
|
|
120
120
|
`docs/migrations/0.2.1.md` to adopt on an existing config.
|
|
121
121
|
|
|
122
|
+
### `runtime.round_log_retention`
|
|
123
|
+
|
|
124
|
+
Type: int (>= 0)
|
|
125
|
+
Default: `0`
|
|
126
|
+
|
|
127
|
+
**Pruning is opt-in. `0` — the default — never prunes anything.** Round logs
|
|
128
|
+
accumulate for as long as the deployment runs, and agent-runner deletes none of
|
|
129
|
+
them unless you ask it to.
|
|
130
|
+
|
|
131
|
+
The knob governs both round-log families:
|
|
132
|
+
|
|
133
|
+
- the serve-level `{log_dir}/round-<N>.log` files, pruned by mtime once at
|
|
134
|
+
serve startup;
|
|
135
|
+
- the agent transcripts in `{log_dir}/rounds/R<N>-<timestamp>.log`, pruned by
|
|
136
|
+
round number at the start of every round.
|
|
137
|
+
|
|
138
|
+
**We are not ignoring disk.** Unbounded growth is watched, and loudly: the
|
|
139
|
+
`disk_warning` detector alerts at 90% used and `disk_critical` — in the default
|
|
140
|
+
`[monitor] auto_stop_on` — stops the service at 95%. Deleting history has no
|
|
141
|
+
equivalent defense; it is discovered on the day you need the file and it is not
|
|
142
|
+
there. Given a risk that already auto-stops the service versus a risk with no
|
|
143
|
+
detector at all, the default belongs on the side that keeps the files.
|
|
144
|
+
|
|
145
|
+
### Enabling pruning
|
|
146
|
+
|
|
147
|
+
Set a positive count — the number of files to keep per family:
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
[runtime]
|
|
151
|
+
round_log_retention = 100 # pre-0.2.6 behavior
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Sizing it: agent transcripts are one file per round, from a few KB to several
|
|
155
|
+
MB each depending on how verbose the agent CLI is. Multiply by your rounds/day
|
|
156
|
+
and check it against free space rather than picking a round number.
|
|
157
|
+
|
|
158
|
+
### Bulk-prune guard (0.2.6+)
|
|
159
|
+
|
|
160
|
+
If you do enable pruning, a prune that would delete *more* files than it keeps
|
|
161
|
+
is a bulk prune, and a bulk prune deletes nothing. It emits
|
|
162
|
+
`round_logs_prune_deferred` (fields: `directory`, `existing`, `keep`,
|
|
163
|
+
`would_delete`, `hint`) and leaves every file in place. In steady state a round
|
|
164
|
+
retires about one file, so the guard only trips when you first opt in on an
|
|
165
|
+
existing backlog, or when you lower the value far below the current file
|
|
166
|
+
count — exactly the moments a mass deletion would otherwise happen silently.
|
|
167
|
+
|
|
168
|
+
The deferral is permanent until you act, and you have two options:
|
|
169
|
+
|
|
170
|
+
- **Raise `round_log_retention`** to at least the reported `existing` count.
|
|
171
|
+
The backlog then fits inside the kept window, nothing is deleted, and normal
|
|
172
|
+
per-round pruning resumes once the count grows past the new value.
|
|
173
|
+
- **Delete files yourself** from the reported `directory`. The next prune sees
|
|
174
|
+
a count the guard no longer calls bulk and resumes.
|
|
175
|
+
|
|
176
|
+
The supervisor never performs a bulk deletion on its own. Note that `0` is not
|
|
177
|
+
a deferral: it emits nothing, because never-prune is a stated intent rather
|
|
178
|
+
than a backlog awaiting a decision.
|
|
179
|
+
|
|
122
180
|
### `vcs.dirty_action`
|
|
123
181
|
|
|
124
182
|
Type: string, one of `"stash"`, `"ignore"`, `"auto_commit"`
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Migration guide: 0.2.6
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
**Round-log pruning is now opt-in.** `runtime.round_log_retention` defaults to
|
|
6
|
+
`0`, which means never prune; both round-log families now grow until you
|
|
7
|
+
configure a cap or clear them yourself. If you want the pre-0.2.6 behavior, set
|
|
8
|
+
`round_log_retention = 100` explicitly. A second change backs it up: when
|
|
9
|
+
pruning *is* enabled, a prune that would delete more files than it keeps is
|
|
10
|
+
refused and reported rather than performed. No public symbols removed, one new
|
|
11
|
+
event kind (`round_logs_prune_deferred`).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why: a release that deleted the evidence for its own fix
|
|
16
|
+
|
|
17
|
+
0.2.4 shipped a `KillMode=mixed` fix for systemd units. A downstream integrator
|
|
18
|
+
was able to confirm that fix actually worked in production — but only because
|
|
19
|
+
they still had the full transcript of round R11498 on disk. Under 0.2.4's own
|
|
20
|
+
default, that file would have been deleted.
|
|
21
|
+
|
|
22
|
+
Their numbers, on an unremarkable long-running deployment: 12,293 transcripts,
|
|
23
|
+
6.5 GB, oldest `R1-20260520T023025.log`, `round_log_retention` never
|
|
24
|
+
configured. The first round after upgrading would have deleted 12,193 files —
|
|
25
|
+
about 6.4 GB, the complete history since R1 — on a host with 856 GB free. That
|
|
26
|
+
history is what they used to reconstruct a round lost to a dropped connection,
|
|
27
|
+
and to find a flaky test's trigger condition.
|
|
28
|
+
|
|
29
|
+
A release that fixes X while silently deleting the evidence needed to verify X
|
|
30
|
+
is the argument for this change.
|
|
31
|
+
|
|
32
|
+
## The default was on the wrong side of an asymmetric risk
|
|
33
|
+
|
|
34
|
+
The two failure modes are not symmetric, and only one of them was defended:
|
|
35
|
+
|
|
36
|
+
| If we | Result | How you find out |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| retain too much | disk fills | **Loud** — `disk_warning` at 90%, `disk_critical` at 95%, which is in the default `[monitor] auto_stop_on` and stops the service |
|
|
39
|
+
| delete too much | history gone | **Silent** — no detector; discovered on the day you need the file |
|
|
40
|
+
|
|
41
|
+
So the old default protected against a risk that already had a louder,
|
|
42
|
+
auto-stopping defense, while creating a risk with no defense at all.
|
|
43
|
+
|
|
44
|
+
Disk was also the *only* remaining justification. The other original reason for
|
|
45
|
+
pruning — the monitor's per-poll cost scaling with file count — was removed
|
|
46
|
+
independently: `load_round_log_tails` now reads only the newest 20 files
|
|
47
|
+
regardless of how many exist.
|
|
48
|
+
|
|
49
|
+
## Changed: `round_log_retention` defaults to `0` = never prune
|
|
50
|
+
|
|
51
|
+
`0` is now a legal, meaningful value (previously the validator rejected both
|
|
52
|
+
`0` and `-1`, so there was no off switch — "set it very large" was a
|
|
53
|
+
workaround, not an answer). Negative values are still rejected at load.
|
|
54
|
+
|
|
55
|
+
Both families are affected:
|
|
56
|
+
|
|
57
|
+
- `{log_dir}/rounds/R<N>-<timestamp>.log` — the agent transcripts. Pruned since
|
|
58
|
+
0.2.4; no longer pruned by default.
|
|
59
|
+
- `{log_dir}/round-<N>.log` — the serve-level wrapper logs. **This one is a
|
|
60
|
+
real behavior change in the safe direction**: that family has been capped at
|
|
61
|
+
100 files at serve startup for many releases, and now it is not capped unless
|
|
62
|
+
you say so. Expect it to start growing; size it deliberately if that matters
|
|
63
|
+
to you.
|
|
64
|
+
|
|
65
|
+
To keep the old behavior, say so explicitly:
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[runtime]
|
|
69
|
+
round_log_retention = 100
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Nothing else in the supervisor reads these files, so retaining them is inert
|
|
73
|
+
apart from disk. To reclaim space without enabling pruning, delete or archive
|
|
74
|
+
old files yourself.
|
|
75
|
+
|
|
76
|
+
## Added: bulk prunes are refused, not performed
|
|
77
|
+
|
|
78
|
+
For anyone who opts in, the second half of this release. A prune that would
|
|
79
|
+
delete **more files than it keeps** is a *bulk* prune, and a bulk prune deletes
|
|
80
|
+
nothing: it emits `round_logs_prune_deferred` and leaves every file in place.
|
|
81
|
+
|
|
82
|
+
In steady state a round retires about one file, so the guard cannot trip during
|
|
83
|
+
normal operation. It trips exactly when a mass deletion would otherwise have
|
|
84
|
+
happened silently — the first prune after you opt in on an existing backlog, or
|
|
85
|
+
after you lower an existing value.
|
|
86
|
+
|
|
87
|
+
| Field | Meaning |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `directory` | the directory whose prune was deferred |
|
|
90
|
+
| `existing` | matching files present |
|
|
91
|
+
| `keep` | `runtime.round_log_retention` as configured |
|
|
92
|
+
| `would_delete` | files a normal prune would have removed |
|
|
93
|
+
| `hint` | one line naming the knob to turn |
|
|
94
|
+
|
|
95
|
+
Re-emitted on every prune attempt while the condition holds — the deferral is
|
|
96
|
+
permanent until an operator acts, and a one-shot event would be invisible to
|
|
97
|
+
anyone who started watching later. `round_log_retention = 0` emits nothing:
|
|
98
|
+
never-prune is a stated intent, not a backlog awaiting a decision.
|
|
99
|
+
|
|
100
|
+
The guard never blocks or fails a round: it defers a deletion, nothing else.
|
|
101
|
+
|
|
102
|
+
When you see it, you have two options — raise `round_log_retention` to at least
|
|
103
|
+
the reported `existing` (the backlog then fits inside the kept window and
|
|
104
|
+
normal pruning resumes as the count grows past it), or delete the files
|
|
105
|
+
yourself from the reported `directory`. There is deliberately no config knob to
|
|
106
|
+
re-enable bulk deletion.
|
|
107
|
+
|
|
108
|
+
## Where you stand, by current version
|
|
109
|
+
|
|
110
|
+
- **On ≤0.2.3** — `rounds/` has never been pruned on your deployment, and after
|
|
111
|
+
upgrading straight to 0.2.6 it still will not be. Your backlog is safe by
|
|
112
|
+
default; you do not need to pre-emptively raise anything before upgrading.
|
|
113
|
+
(Upgrading via 0.2.4 or 0.2.5, even briefly, runs the deletion once.) Your
|
|
114
|
+
`round-<N>.log` family stops being capped at 100 — set `round_log_retention`
|
|
115
|
+
if you want that cap back.
|
|
116
|
+
- **On 0.2.4 or 0.2.5** — the `rounds/` deletion has already happened; those
|
|
117
|
+
files are gone and 0.2.6 cannot bring them back. What it does is guarantee
|
|
118
|
+
there is no second one: pruning stops entirely unless you opt in, and if you
|
|
119
|
+
do, the bulk guard stands between a lowered value and your history.
|
|
120
|
+
- **Anyone who wants the old behavior** — set `round_log_retention = 100`. You
|
|
121
|
+
get pruning back, now with the bulk guard in front of it.
|
|
122
|
+
|
|
123
|
+
## Internal API note
|
|
124
|
+
|
|
125
|
+
`agent_runner.round_log.prune_rounds_dir` and `prune_old_round_logs` return a
|
|
126
|
+
`PruneOutcome` (`deleted`, `deferred`, `existing`) instead of `int` / `None`.
|
|
127
|
+
Both are internal helpers, not part of the pinned public API surface.
|