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.
Files changed (274) hide show
  1. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.vulture-whitelist.py +5 -0
  2. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CHANGELOG.md +15 -1
  3. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/PKG-INFO +2 -2
  4. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/README.md +1 -1
  5. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_emit.py +37 -0
  6. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/api.py +1 -0
  8. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/serve_cmd.py +21 -1
  9. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/config.py +3 -3
  10. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/defenses.py +11 -0
  11. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/events.py +1 -0
  12. cli_agent_runner-0.2.6/agent_runner/round_log.py +160 -0
  13. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/runner.py +11 -2
  14. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/architecture.md +4 -2
  15. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/configuration.md +59 -1
  16. cli_agent_runner-0.2.6/docs/migrations/0.2.6.md +127 -0
  17. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/aider.md +6 -5
  18. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/runbook.md +40 -6
  19. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_architecture.py +1 -0
  20. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config.py +52 -3
  21. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_defenses.py +2 -2
  22. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events.py +28 -0
  23. cli_agent_runner-0.2.6/tests/unit/test_round_log_helpers.py +269 -0
  24. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_runner.py +67 -0
  25. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_round_log.py +99 -24
  26. cli_agent_runner-0.2.5/agent_runner/round_log.py +0 -95
  27. cli_agent_runner-0.2.5/tests/unit/test_round_log_helpers.py +0 -143
  28. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.codecov.yml +0 -0
  29. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  31. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/workflows/ci.yml +0 -0
  34. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.github/workflows/release.yml +0 -0
  35. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/.gitignore +0 -0
  36. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/BACKLOG.md +0 -0
  37. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CODE_OF_CONDUCT.md +0 -0
  38. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/CONTRIBUTING.md +0 -0
  39. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/LICENSE +0 -0
  40. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/README.zh.md +0 -0
  41. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/SECURITY.md +0 -0
  42. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/__init__.py +0 -0
  43. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_docgen.py +0 -0
  44. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_redact.py +0 -0
  45. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_registry.py +0 -0
  46. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_substrate.py +0 -0
  47. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/_throttle.py +0 -0
  48. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/agent_runtime.py +0 -0
  49. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/api_types.py +0 -0
  50. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/__init__.py +0 -0
  51. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/_constants.py +0 -0
  52. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  53. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/codewhale.py +0 -0
  54. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  55. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/gemini.py +0 -0
  56. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/kimi.py +0 -0
  57. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/builtin_plugins/pi.py +0 -0
  58. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/__init__.py +0 -0
  59. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/__main__.py +0 -0
  60. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/common.py +0 -0
  61. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/events_cmd.py +0 -0
  62. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/init_cmd.py +0 -0
  63. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/install_cmd.py +0 -0
  64. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/monitor_cmd.py +0 -0
  65. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/peek_cmd.py +0 -0
  66. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/round_cmd.py +0 -0
  67. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/service_cmd.py +0 -0
  68. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/cli/upgrade_cmd.py +0 -0
  69. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/context_store.py +0 -0
  70. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/hooks.py +0 -0
  71. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/http_progress.py +0 -0
  72. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/lifecycle.py +0 -0
  73. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/metrics.py +0 -0
  74. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/monitor.py +0 -0
  75. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/__init__.py +0 -0
  76. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/aider.toml +0 -0
  77. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/claude.toml +0 -0
  78. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/codewhale.toml +0 -0
  79. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/gemini.toml +0 -0
  80. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/kimi.toml +0 -0
  81. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/presets/pi.toml +0 -0
  82. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/prompt_loader.py +0 -0
  83. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/remote_relay.py +0 -0
  84. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/round_view.py +0 -0
  85. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/scaffold.py +0 -0
  86. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/service_unit.py +0 -0
  87. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/startup_check.py +0 -0
  88. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/agent_runner/vcs_state.py +0 -0
  89. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/build.sh +0 -0
  90. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/README.md +0 -0
  91. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/commands.md +0 -0
  92. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/events.md +0 -0
  93. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/long-running-agents.md +0 -0
  94. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.16.md +0 -0
  95. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.17.md +0 -0
  96. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.19.md +0 -0
  97. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.20.md +0 -0
  98. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.21.md +0 -0
  99. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.22.md +0 -0
  100. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.23.md +0 -0
  101. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.24.md +0 -0
  102. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.25.md +0 -0
  103. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.26.md +0 -0
  104. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.27.md +0 -0
  105. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.28.md +0 -0
  106. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.29.md +0 -0
  107. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.30.md +0 -0
  108. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.31.md +0 -0
  109. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.32.md +0 -0
  110. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.33.md +0 -0
  111. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.34.md +0 -0
  112. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.35.md +0 -0
  113. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.36.md +0 -0
  114. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.37.md +0 -0
  115. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.38.md +0 -0
  116. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.39.md +0 -0
  117. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.40.md +0 -0
  118. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.1.42.md +0 -0
  119. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.0.md +0 -0
  120. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.1.md +0 -0
  121. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.2.md +0 -0
  122. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.3.md +0 -0
  123. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.4.md +0 -0
  124. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/migrations/0.2.5.md +0 -0
  125. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/plugins.md +0 -0
  126. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/quickstart.md +0 -0
  127. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/codewhale.md +0 -0
  128. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/kimi.md +0 -0
  129. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/recipes/pi.md +0 -0
  130. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/docs/thesis.md +0 -0
  131. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/pyproject.toml +0 -0
  132. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/__init__.py +0 -0
  133. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/_test_helpers.py +0 -0
  134. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/conftest.py +0 -0
  135. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/contract/__init__.py +0 -0
  136. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/contract/test_public_api_surface.py +0 -0
  137. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/__init__.py +0 -0
  138. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/conftest.py +0 -0
  139. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  140. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_install_systemd.py +0 -0
  141. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  142. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  143. {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
  144. {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
  145. {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
  146. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/generate_vulture_whitelist.py +0 -0
  147. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/__init__.py +0 -0
  148. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_bounded_run.py +0 -0
  149. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_context_enricher_namespacing.py +0 -0
  150. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_dirty_handler_seam.py +0 -0
  151. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_fresh_eyes_signal.py +0 -0
  152. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_grace_kill_emission.py +0 -0
  153. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_install_dry_run.py +0 -0
  154. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_monitor_seeded.py +0 -0
  155. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_detector_loaded.py +0 -0
  156. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_owned_paths.py +0 -0
  157. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_plugin_real_flow.py +0 -0
  158. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  159. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  160. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_scaffold_presets.py +0 -0
  161. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_serve_loop.py +0 -0
  162. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_substrate_fingerprint.py +0 -0
  163. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/integration/test_transient_error_backoff.py +0 -0
  164. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/__init__.py +0 -0
  165. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/_event_scan.py +0 -0
  166. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_atomic_write_enforced.py +0 -0
  167. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  168. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_catalogs.py +0 -0
  169. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_classification_ssot.py +0 -0
  170. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_config_error_consistency.py +0 -0
  171. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_config_value_set_ssot.py +0 -0
  172. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  173. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  174. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_cli_claims.py +0 -0
  175. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_operator_surface.py +0 -0
  176. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  177. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_generated.py +0 -0
  178. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_docs_index_complete.py +0 -0
  179. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_entry_points_resolve.py +0 -0
  180. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kind_registry.py +0 -0
  181. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_event_kinds_ssot.py +0 -0
  182. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_events_doc_contract.py +0 -0
  183. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_hook_contract_docs.py +0 -0
  184. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_layer_2_loop_size.py +0 -0
  185. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_module_boundaries.py +0 -0
  186. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_module_sizes.py +0 -0
  187. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_no_ai_signatures.py +0 -0
  188. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  189. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_peek_schema_version.py +0 -0
  190. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_plugins_config_stable.py +0 -0
  191. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  192. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  193. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_round_result_stable.py +0 -0
  194. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  195. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  196. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_upstream_schema_canary.py +0 -0
  197. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  198. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/__init__.py +0 -0
  199. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/parser.py +0 -0
  200. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/test_parser.py +0 -0
  201. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/literate/test_quickstart.py +0 -0
  202. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/__init__.py +0 -0
  203. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime.py +0 -0
  204. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_grace.py +0 -0
  205. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_progress.py +0 -0
  206. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  207. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_assemble_prompt.py +0 -0
  208. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_events_stream.py +0 -0
  209. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_install.py +0 -0
  210. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_observation.py +0 -0
  211. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_read_round_num.py +0 -0
  212. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_resolve_phase.py +0 -0
  213. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_service.py +0 -0
  214. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_api_types.py +0 -0
  215. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_auto_stop_gating.py +0 -0
  216. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_catalogs.py +0 -0
  217. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_claude_error_detector.py +0 -0
  218. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli.py +0 -0
  219. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_common.py +0 -0
  220. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_init_install.py +0 -0
  221. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_monitor_http.py +0 -0
  222. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  223. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_cli_upgrade.py +0 -0
  224. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_codewhale_plugin.py +0 -0
  225. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_fresh_eyes.py +0 -0
  226. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_max_rounds.py +0 -0
  227. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_prompt_delivery.py +0 -0
  228. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_stop_file.py +0 -0
  229. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  230. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_config_transient_error_action.py +0 -0
  231. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_context_store.py +0 -0
  232. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_default_dirty_handler.py +0 -0
  233. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_detector_protocol.py +0 -0
  234. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_dirty_handlers.py +0 -0
  235. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_docgen.py +0 -0
  236. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events_cmd.py +0 -0
  237. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_events_cmd_tail.py +0 -0
  238. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_exit_cause.py +0 -0
  239. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  240. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_gemini_plugin.py +0 -0
  241. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_hook_failure_isolation.py +0 -0
  242. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_hooks.py +0 -0
  243. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_http_progress.py +0 -0
  244. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_init_entry_points.py +0 -0
  245. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_kimi_plugin.py +0 -0
  246. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_lifecycle.py +0 -0
  247. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_metrics.py +0 -0
  248. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_assembly.py +0 -0
  249. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  250. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  251. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  252. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_detectors.py +0 -0
  253. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_monitor_remote.py +0 -0
  254. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_network_blip_signal_guard.py +0 -0
  255. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_peek_argparse.py +0 -0
  256. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_peek_select.py +0 -0
  257. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_pi_plugin.py +0 -0
  258. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_plugin_constants.py +0 -0
  259. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_presets.py +0 -0
  260. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_prompt_loader.py +0 -0
  261. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_redact.py +0 -0
  262. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_round_view.py +0 -0
  263. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_runner_throttle.py +0 -0
  264. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_scaffold.py +0 -0
  265. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_cmd_bounded.py +0 -0
  266. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_config_broken.py +0 -0
  267. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_crash_loop.py +0 -0
  268. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_sentinel.py +0 -0
  269. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_serve_startup_hooks.py +0 -0
  270. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_service_unit.py +0 -0
  271. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_startup_check.py +0 -0
  272. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_substrate.py +0 -0
  273. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_throttle_plugin_classification.py +0 -0
  274. {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.6}/tests/unit/test_vcs_state.py +0 -0
@@ -228,6 +228,11 @@ path
228
228
  # agent_runner.monitor.LocalSource
229
229
  log_dir
230
230
 
231
+ # agent_runner.round_log.PruneOutcome
232
+ deleted
233
+ deferred
234
+ existing
235
+
231
236
  # agent_runner.startup_check.CheckResult
232
237
  name
233
238
  ok
@@ -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.5...HEAD
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.5
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
- 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
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
- 12 named defenses, structured as data — see `agent-runner peek --select defenses`.
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.5'
22
- __version_tuple__ = version_tuple = (0, 2, 5)
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
- prune_old_round_logs(log_dir, cfg.runtime.round_log_retention)
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 = 100
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=_require_positive_int(
443
- runtime_d.get("round_log_retention", 100), field="runtime.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
- prune_rounds_dir(rounds_dir, cfg.runtime.round_log_retention)
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 12 structured `Defense` entries.
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
- 防御以结构化目录形式存在(12 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
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` | 100 |
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.