cli-agent-runner 0.2.16__tar.gz → 0.2.17__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 (340) hide show
  1. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.vulture-whitelist.py +7 -0
  2. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/CHANGELOG.md +12 -0
  3. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/PKG-INFO +1 -1
  4. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_emit.py +17 -7
  5. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_serve_policy.py +32 -13
  6. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_throttle.py +161 -72
  7. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_version.py +2 -2
  8. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/api.py +20 -3
  9. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/_serve_round.py +160 -68
  10. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/serve_cmd.py +65 -72
  11. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/metrics.py +18 -13
  12. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/monitor.py +62 -16
  13. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/round_log.py +0 -10
  14. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/runbook.md +1 -1
  15. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_spawn_round_mem_floor.py +110 -3
  16. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_spawn_round_wedged.py +5 -4
  17. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_architecture.py +12 -21
  18. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_event_kinds_ssot.py +4 -2
  19. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_timeout_budget_invariant.py +1 -1
  20. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_service.py +3 -3
  21. cli_agent_runner-0.2.17/tests/unit/test_monitor_dedup_rearm.py +104 -0
  22. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_remote.py +37 -0
  23. cli_agent_runner-0.2.17/tests/unit/test_round_outcome.py +319 -0
  24. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_policy.py +25 -0
  25. cli_agent_runner-0.2.17/tests/unit/test_serve_round.py +34 -0
  26. cli_agent_runner-0.2.16/tests/unit/test_monitor_dedup_rearm.py +0 -42
  27. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.codecov.yml +0 -0
  28. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  30. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/workflows/ci.yml +0 -0
  33. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.github/workflows/release.yml +0 -0
  34. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/.gitignore +0 -0
  35. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/BACKLOG.md +0 -0
  36. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/CODE_OF_CONDUCT.md +0 -0
  37. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/CONTRIBUTING.md +0 -0
  38. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/LICENSE +0 -0
  39. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/README.md +0 -0
  40. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/README.zh.md +0 -0
  41. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/SECURITY.md +0 -0
  42. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/__init__.py +0 -0
  43. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_docgen.py +0 -0
  44. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_monitor_detectors.py +0 -0
  45. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_monitor_registry.py +0 -0
  46. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_monitor_state.py +0 -0
  47. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_redact.py +0 -0
  48. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_registry.py +0 -0
  49. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_resolve.py +0 -0
  50. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_round_support.py +0 -0
  51. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/_substrate.py +0 -0
  52. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/agent_runtime.py +0 -0
  53. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/api_types.py +0 -0
  54. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/__init__.py +0 -0
  55. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/_constants.py +0 -0
  56. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  57. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/codewhale.py +0 -0
  58. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  59. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/gemini.py +0 -0
  60. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/kimi.py +0 -0
  61. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/builtin_plugins/pi.py +0 -0
  62. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/__init__.py +0 -0
  63. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/__main__.py +0 -0
  64. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/common.py +0 -0
  65. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/events_cmd.py +0 -0
  66. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/init_cmd.py +0 -0
  67. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/install_cmd.py +0 -0
  68. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/migrate_cmd.py +0 -0
  69. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/monitor_cmd.py +0 -0
  70. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/peek_cmd.py +0 -0
  71. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/round_cmd.py +0 -0
  72. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/service_cmd.py +0 -0
  73. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/cli/upgrade_cmd.py +0 -0
  74. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/clock.py +0 -0
  75. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/__init__.py +0 -0
  76. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/errors.py +0 -0
  77. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/loader.py +0 -0
  78. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/models.py +0 -0
  79. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/parsers.py +0 -0
  80. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/config/validators.py +0 -0
  81. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/context_store.py +0 -0
  82. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/defenses.py +0 -0
  83. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/events.py +0 -0
  84. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/hooks.py +0 -0
  85. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/host_health.py +0 -0
  86. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/http_progress.py +0 -0
  87. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/lifecycle.py +0 -0
  88. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/migrations.py +0 -0
  89. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/phase_select.py +0 -0
  90. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/__init__.py +0 -0
  91. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/aider.toml +0 -0
  92. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/claude.toml +0 -0
  93. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/codewhale.toml +0 -0
  94. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/gemini.toml +0 -0
  95. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/kimi.toml +0 -0
  96. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/presets/pi.toml +0 -0
  97. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/prompt_loader.py +0 -0
  98. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/remote_relay.py +0 -0
  99. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/round_view.py +0 -0
  100. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/runner.py +0 -0
  101. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/scaffold.py +0 -0
  102. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/schedule.py +0 -0
  103. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/service_unit.py +0 -0
  104. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/startup_check.py +0 -0
  105. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/agent_runner/vcs_state.py +0 -0
  106. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/build.sh +0 -0
  107. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/README.md +0 -0
  108. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/architecture.md +0 -0
  109. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/commands.md +0 -0
  110. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/configuration.md +0 -0
  111. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/events.md +0 -0
  112. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/long-running-agents.md +0 -0
  113. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/migrations/0.2.md +0 -0
  114. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/plugins.md +0 -0
  115. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/quickstart.md +0 -0
  116. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/recipes/aider.md +0 -0
  117. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/recipes/codewhale.md +0 -0
  118. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/recipes/kimi.md +0 -0
  119. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/recipes/pi.md +0 -0
  120. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/docs/thesis.md +0 -0
  121. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/pyproject.toml +0 -0
  122. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/__init__.py +0 -0
  123. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/_clock.py +0 -0
  124. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/_test_helpers.py +0 -0
  125. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/conftest.py +0 -0
  126. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/contract/__init__.py +0 -0
  127. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/contract/test_public_api_surface.py +0 -0
  128. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/__init__.py +0 -0
  129. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/conftest.py +0 -0
  130. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  131. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/test_e2e_install_systemd.py +0 -0
  132. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  133. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  134. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  135. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  136. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  137. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/generate_vulture_whitelist.py +0 -0
  138. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/__init__.py +0 -0
  139. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_auto_stop_e2e.py +0 -0
  140. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_bounded_run.py +0 -0
  141. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_context_enricher_namespacing.py +0 -0
  142. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_dirty_handler_seam.py +0 -0
  143. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_fresh_eyes_signal.py +0 -0
  144. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_grace_kill_emission.py +0 -0
  145. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_install_dry_run.py +0 -0
  146. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_monitor_seeded.py +0 -0
  147. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_plugin_detector_loaded.py +0 -0
  148. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_plugin_owned_paths.py +0 -0
  149. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_plugin_real_flow.py +0 -0
  150. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  151. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  152. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_scaffold_presets.py +0 -0
  153. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_serve_loop.py +0 -0
  154. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_substrate_fingerprint.py +0 -0
  155. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/integration/test_transient_error_backoff.py +0 -0
  156. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/__init__.py +0 -0
  157. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/_docs.py +0 -0
  158. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/_event_scan.py +0 -0
  159. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_atomic_write_enforced.py +0 -0
  160. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_authored_facts_declared.py +0 -0
  161. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  162. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_catalogs.py +0 -0
  163. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_classification_ssot.py +0 -0
  164. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_config_error_consistency.py +0 -0
  165. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_config_value_set_ssot.py +0 -0
  166. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  167. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  168. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  169. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_cli_claims.py +0 -0
  170. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_operator_surface.py +0 -0
  171. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  172. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  173. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  174. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_docs_generated.py +0 -0
  175. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_docs_index_complete.py +0 -0
  176. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_entry_points_resolve.py +0 -0
  177. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_event_int_coercion.py +0 -0
  178. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_event_kind_registry.py +0 -0
  179. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_events_doc_contract.py +0 -0
  180. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  181. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_hook_contract_docs.py +0 -0
  182. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_layer_2_loop_size.py +0 -0
  183. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  184. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_module_boundaries.py +0 -0
  185. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_module_sizes.py +0 -0
  186. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  187. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_no_ai_signatures.py +0 -0
  188. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_no_internal_refs.py +0 -0
  189. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  190. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_no_raw_time.py +0 -0
  191. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  192. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_peek_schema_version.py +0 -0
  193. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_phase_select_stateless.py +0 -0
  194. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_plugins_config_stable.py +0 -0
  195. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  196. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  197. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  198. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_round_num_single_source.py +0 -0
  199. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_round_result_stable.py +0 -0
  200. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  201. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  202. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_upstream_schema_canary.py +0 -0
  203. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  204. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/literate/__init__.py +0 -0
  205. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/literate/parser.py +0 -0
  206. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/literate/test_parser.py +0 -0
  207. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/literate/test_quickstart.py +0 -0
  208. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/__init__.py +0 -0
  209. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime.py +0 -0
  210. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime_grace.py +0 -0
  211. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  212. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime_progress.py +0 -0
  213. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime_reap.py +0 -0
  214. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  215. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_assemble_prompt.py +0 -0
  216. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_events_stream.py +0 -0
  217. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_install.py +0 -0
  218. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_observation.py +0 -0
  219. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_read_round_num.py +0 -0
  220. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_resolve_phase.py +0 -0
  221. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_api_types.py +0 -0
  222. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_auto_stop_gating.py +0 -0
  223. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_catalogs.py +0 -0
  224. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cgroup_probe.py +0 -0
  225. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_claude_error_detector.py +0 -0
  226. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_claude_foreign_schema.py +0 -0
  227. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli.py +0 -0
  228. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_common.py +0 -0
  229. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_init_install.py +0 -0
  230. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_main_boundary.py +0 -0
  231. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_monitor_http.py +0 -0
  232. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  233. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_cli_upgrade.py +0 -0
  234. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_codewhale_code_precedence.py +0 -0
  235. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_codewhale_plugin.py +0 -0
  236. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_coerce_int_poison.py +0 -0
  237. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config.py +0 -0
  238. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_fresh_eyes.py +0 -0
  239. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_max_rounds.py +0 -0
  240. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_nested_strictness.py +0 -0
  241. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_prompt_delivery.py +0 -0
  242. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_schedule.py +0 -0
  243. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_stop_file.py +0 -0
  244. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_strictness.py +0 -0
  245. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  246. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_config_transient_error_action.py +0 -0
  247. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_context_store.py +0 -0
  248. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_control_char_config.py +0 -0
  249. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_default_dirty_handler.py +0 -0
  250. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_defenses.py +0 -0
  251. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_detect_hung_latch.py +0 -0
  252. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_detector_isolation.py +0 -0
  253. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_detector_protocol.py +0 -0
  254. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_dirty_handlers.py +0 -0
  255. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_docgen.py +0 -0
  256. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_effective_throttle_view.py +0 -0
  257. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_emit_config_migrated.py +0 -0
  258. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_emit_schedule.py +0 -0
  259. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  260. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_events.py +0 -0
  261. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_events_cmd.py +0 -0
  262. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_events_cmd_rollover.py +0 -0
  263. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_events_cmd_tail.py +0 -0
  264. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_events_jsonl_decode.py +0 -0
  265. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_example_dirty_handler.py +0 -0
  266. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_example_plugin.py +0 -0
  267. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_exit_cause.py +0 -0
  268. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_exit_classifier.py +0 -0
  269. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  270. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_gemini_plugin.py +0 -0
  271. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_git_timeout.py +0 -0
  272. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_hook_failure_isolation.py +0 -0
  273. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_hook_redact_order.py +0 -0
  274. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_hooks.py +0 -0
  275. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_host_health.py +0 -0
  276. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_http_progress.py +0 -0
  277. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_init_entry_points.py +0 -0
  278. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  279. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_kimi_plugin.py +0 -0
  280. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_lifecycle.py +0 -0
  281. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_metrics.py +0 -0
  282. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_migrate_cmd.py +0 -0
  283. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_migrations.py +0 -0
  284. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_assembly.py +0 -0
  285. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_cmd_severity.py +0 -0
  286. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_dedup_identity.py +0 -0
  287. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  288. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  289. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  290. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_detectors.py +0 -0
  291. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_event_tail.py +0 -0
  292. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  293. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_monitor_schedule_pause.py +0 -0
  294. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_network_blip_signal_guard.py +0 -0
  295. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_open_round_log.py +0 -0
  296. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  297. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_outer_round_ceiling.py +0 -0
  298. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_peek_argparse.py +0 -0
  299. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_peek_project_isolation.py +0 -0
  300. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_peek_schedule.py +0 -0
  301. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_peek_select.py +0 -0
  302. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_phase_agent_launch.py +0 -0
  303. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_phase_profiles.py +0 -0
  304. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_phase_select.py +0 -0
  305. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_pi_plugin.py +0 -0
  306. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_plugin_constants.py +0 -0
  307. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_presets.py +0 -0
  308. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_primary_prompt_file_home.py +0 -0
  309. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_prompt_loader.py +0 -0
  310. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_read_json_hardening.py +0 -0
  311. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_redact.py +0 -0
  312. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_resolve.py +0 -0
  313. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_round_cmd.py +0 -0
  314. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_round_cmd_sigterm.py +0 -0
  315. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_round_log_helpers.py +0 -0
  316. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_round_view.py +0 -0
  317. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_runner.py +0 -0
  318. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  319. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_runner_throttle.py +0 -0
  320. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_scaffold.py +0 -0
  321. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_schedule.py +0 -0
  322. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_cmd_bounded.py +0 -0
  323. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_config_broken.py +0 -0
  324. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_crash_loop.py +0 -0
  325. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_mem_gate.py +0 -0
  326. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_phase_schedule.py +0 -0
  327. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_round_log.py +0 -0
  328. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_schedule_gate.py +0 -0
  329. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_sentinel.py +0 -0
  330. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_serve_startup_hooks.py +0 -0
  331. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_service_mode.py +0 -0
  332. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_service_unit.py +0 -0
  333. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_startup_battery_exit.py +0 -0
  334. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_startup_check.py +0 -0
  335. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_substrate.py +0 -0
  336. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  337. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  338. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_throttle_plugin_classification.py +0 -0
  339. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_upgrade_migrate.py +0 -0
  340. {cli_agent_runner-0.2.16 → cli_agent_runner-0.2.17}/tests/unit/test_vcs_state.py +0 -0
@@ -13,6 +13,13 @@ log_dir
13
13
  offsets
14
14
  buffer
15
15
 
16
+ # agent_runner._throttle.RoundOutcome
17
+ mem_terminated
18
+ usage_capable
19
+ newest_usage_ts
20
+ newest_substrate_before_ts
21
+ latest_transient_per_agent
22
+
16
23
  # agent_runner.agent_runtime.RunResult
17
24
  exit_code
18
25
  duration_s
@@ -5,6 +5,18 @@ 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.17] - 2026-09-05
9
+
10
+ ### Changed
11
+ - Internal refactor ("round-outcome unification"): per-round classification now does one events-tail scan per round (was up to three), the give-up decision is unified behind a single `post_round_verdicts` (precedence and exit codes `78`/`70`/`71`/`75` unchanged), and the `_serve_round`↔`serve_cmd` import cycle is removed. Behavior-preserving.
12
+ - `round_mem_critical_sample` is now capped at `2 × mem_critical_consecutive_samples` emits per critical episode (any non-critical tick resets it), so a host stuck under sustained pressure no longer writes that calibration event unbounded for a whole round.
13
+
14
+ ### Fixed
15
+ - Auto-stop no longer records `monitor_auto_stop_failed` for a stop that actually succeeds: under load a round can outlast the stop-confirmation window while draining, which was mis-reported as a failure. The monitor now recognizes a still-draining round and records the real outcome (triggered or failed) once it resolves on the next poll, instead of a false failure.
16
+
17
+ ### Notes
18
+ - Not breaking: no `agent-runner migrate`, no systemd unit change, no new event kinds, `peek --json` schema unchanged. The only non-fix behavior change is the bounded `round_mem_critical_sample` above.
19
+
8
20
  ## [0.2.16] - 2026-09-05
9
21
 
10
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.16
3
+ Version: 0.2.17
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
@@ -296,18 +296,28 @@ def emit_round_mem_terminated(
296
296
  def emit_round_mem_critical_sample(
297
297
  log_dir: Path, *, round_num: int, consecutive: int, context: dict
298
298
  ) -> None:
299
- """Emit on EACH critical host_health sample inside _spawn_round's mid-round
300
- hard floor -- deliberately NOT deduped (unlike round_mem_terminated): the
301
- point is calibration visibility into near-misses, so an operator watching
302
- the event stream sees the critical_streak build (1, 2, ...) even on ticks
303
- that never reach the terminate threshold (a healthy tick resets it before
304
- 3-in-a-row). ``context`` carries the same Pressure.context numbers as
299
+ """Emit on each critical host_health sample inside _spawn_round's mid-round
300
+ hard floor, up to the per-episode cap (0.2.17, below) -- unlike
301
+ round_mem_terminated (deduped to once-per-episode), this fires on every
302
+ critical tick within that cap: the point is calibration visibility into
303
+ near-misses, so an operator watching the event stream sees the
304
+ critical_streak build (1, 2, ...) even on ticks that never reach the
305
+ terminate threshold (a healthy tick resets it before 3-in-a-row).
306
+ ``context`` carries the same Pressure.context numbers as
305
307
  round_mem_terminated. Fires only during critical pressure -- never during
306
308
  warning/healthy -- so it adds no normal-operation noise, including while
307
309
  ``defer_to_cgroup`` steps back from terminating (the streak/context is
308
310
  still useful calibration there; this event stays scoped to the
309
311
  sample-level signal, distinct from mem_pressure_deferred_to_cgroup's
310
- once-per-episode terminate-vs-defer notice)."""
312
+ once-per-episode terminate-vs-defer notice).
313
+
314
+ 0.2.17: the caller caps this at ``2 * mem_critical_consecutive_samples``
315
+ consecutive ticks (1..6 at the default 3) -- a sustained-critical
316
+ don't-terminate run (cgroup-defer, or the off switch) would otherwise
317
+ write one event per ~10s tick for up to a whole ``round_timeout_s`` on a
318
+ permanently-deferred/off host. The cap is per streak-episode, not a
319
+ lifetime limit: any non-critical tick still resets the streak to 0, and
320
+ sampling resumes from 1 the next time critical pressure recurs."""
311
321
  from agent_runner.events import ROUND_MEM_CRITICAL_SAMPLE, emit
312
322
 
313
323
  emit(
@@ -157,6 +157,25 @@ def post_round_decision(
157
157
  return ("continue", delay, 0)
158
158
 
159
159
 
160
+ def _consecutive_streak_decision(
161
+ *, triggered: bool, consecutive: int, threshold: int, verdict: str
162
+ ) -> tuple[str, int]:
163
+ """The generic give-up-cap shape both :func:`_mem_loop_decision` and
164
+ :func:`_no_progress_decision` delegate to (0.2.17 Task 2 — de-duplicated
165
+ out of the two, which were byte-identical apart from their reset
166
+ condition, threshold constant, and verdict string). A NEW, SEPARATE,
167
+ private counter from ``post_round_decision``'s crash-loop breaker, not a
168
+ widening of its 3-tuple contract. Returns ``(verdict, consecutive)`` once
169
+ ``consecutive`` reaches ``threshold``; a non-``triggered`` round resets the
170
+ counter to 0."""
171
+ if not triggered:
172
+ return ("continue", 0)
173
+ consecutive += 1
174
+ if consecutive >= threshold:
175
+ return (verdict, consecutive)
176
+ return ("continue", consecutive)
177
+
178
+
160
179
  def _mem_loop_decision(
161
180
  *, mem_terminated: bool, consecutive: int
162
181
  ) -> tuple[Literal["mem_loop", "continue"], int]:
@@ -168,12 +187,12 @@ def _mem_loop_decision(
168
187
  eventually giving up. Returns ``("mem_loop", consecutive)`` once
169
188
  ``consecutive`` reaches ``MEM_LOOP_THRESHOLD``; a non-mem-terminated round
170
189
  resets the counter to 0, same reset shape as the crash-loop breaker."""
171
- if not mem_terminated:
172
- return ("continue", 0)
173
- consecutive += 1
174
- if consecutive >= MEM_LOOP_THRESHOLD:
175
- return ("mem_loop", consecutive)
176
- return ("continue", consecutive)
190
+ return _consecutive_streak_decision(
191
+ triggered=mem_terminated,
192
+ consecutive=consecutive,
193
+ threshold=MEM_LOOP_THRESHOLD,
194
+ verdict="mem_loop",
195
+ )
177
196
 
178
197
 
179
198
  def _no_progress_decision(
@@ -190,12 +209,12 @@ def _no_progress_decision(
190
209
  via a different signal. A round WITH progress (usage recorded, non-zero
191
210
  exit, or slow) resets the counter to 0, same reset shape as
192
211
  crash-loop/mem-loop."""
193
- if not no_progress:
194
- return ("continue", 0)
195
- consecutive += 1
196
- if consecutive >= CRASH_LOOP_THRESHOLD:
197
- return ("stalled_no_progress", consecutive)
198
- return ("continue", consecutive)
212
+ return _consecutive_streak_decision(
213
+ triggered=no_progress,
214
+ consecutive=consecutive,
215
+ threshold=CRASH_LOOP_THRESHOLD,
216
+ verdict="stalled_no_progress",
217
+ )
199
218
 
200
219
 
201
220
  def classify_round_exit(exc: BaseException) -> int:
@@ -256,7 +275,7 @@ def classify_round_exit(exc: BaseException) -> int:
256
275
  _REAP_GRACE_S = 5
257
276
  _GIT_COMMIT_TIMEOUT_S = 120
258
277
  _HOOK_ALLOWANCE_S = 60 # slack for post-round hooks between the inner wall and the ceiling
259
- _ROUND_TERM_GRACE_S = 15 # mirrors api._ROUND_TERM_GRACE_S / serve_cmd._ROUND_TERM_GRACE_S
278
+ _ROUND_TERM_GRACE_S = 15 # mirrors api._ROUND_TERM_GRACE_S / cli._serve_round._ROUND_TERM_GRACE_S
260
279
  _STOP_GRACE_MARGIN_S = 10 # pad above _ROUND_TERM_GRACE_S for systemd stop-request overhead
261
280
 
262
281
 
@@ -13,7 +13,8 @@ from __future__ import annotations
13
13
  import math
14
14
  import random
15
15
  import warnings
16
- from collections.abc import Callable
16
+ from collections.abc import Callable, Iterator
17
+ from dataclasses import dataclass
17
18
  from datetime import UTC
18
19
  from pathlib import Path
19
20
  from typing import Any
@@ -41,7 +42,9 @@ __all__ = [
41
42
  "mem_loop_events_in_window",
42
43
  "pending_recovered",
43
44
  "round_had_no_progress",
45
+ "round_outcome",
44
46
  "round_was_mem_terminated",
47
+ "RoundOutcome", # trailing: class, not function -- kept with its round_outcome() sibling above
45
48
  ]
46
49
 
47
50
  # _scan_events_for_transient sentinel: file held no transient event at all
@@ -107,6 +110,21 @@ def _iter_events(path: Path):
107
110
  return iter_event_dicts(path)
108
111
 
109
112
 
113
+ def _tail_events(log_dir: Path) -> Iterator[dict[str, Any]]:
114
+ """The ONE events-tail scan every per-round / per-agent / global-latest
115
+ reader below builds on: the newest TWO monthly ``events-*.jsonl`` files
116
+ (oldest-first), each read forward. 0.2.17 Task 1 consolidated 5 copies of
117
+ this exact ``sorted(log_dir.glob(...))[-2:]`` loop into this single
118
+ generator so a refactor of the tail-selection logic (e.g. widening the
119
+ window) touches one place, not five.
120
+
121
+ Forward, OLD-then-NEW order matters (INVARIANT 2 for callers that merge a
122
+ per-key "latest wins" map: iterating oldest file first means a later event
123
+ for the same key naturally overwrites an earlier one)."""
124
+ for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
125
+ yield from _iter_events(path)
126
+
127
+
110
128
  def _scan_events_for_transient(path: Path):
111
129
  """The file's LAST transient event: the detected dict, ``None`` (latest was a
112
130
  recovered), or ``_NO_TRANSIENT`` (no transient event in the file).
@@ -152,13 +170,15 @@ def _latest_transient_per_agent(log_dir: Path) -> dict[str, Any]:
152
170
  agent A in the old file must not be masked by agent B's event in the newer one.
153
171
  Iterates old-then-new so the newest event per agent wins."""
154
172
  latest: dict[str, Any] = {}
155
- for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
156
- for ev in _iter_events(path):
157
- kind = ev.get("event")
158
- if kind == TRANSIENT_ERROR_DETECTED:
159
- latest[str(ev.get("agent", "unknown"))] = ev
160
- elif kind == TRANSIENT_ERROR_RECOVERED:
161
- latest[str(ev.get("agent", "unknown"))] = None
173
+ for ev in _tail_events(log_dir):
174
+ kind = ev.get("event")
175
+ # INVARIANT 2: keyed by str(agent), forward old->new merge (_tail_events
176
+ # yields the newest-two-files tail oldest-first) — newest event per agent
177
+ # wins, matching every other latest_transient_per_agent copy pre-0.2.17.
178
+ if kind == TRANSIENT_ERROR_DETECTED:
179
+ latest[str(ev.get("agent", "unknown"))] = ev
180
+ elif kind == TRANSIENT_ERROR_RECOVERED:
181
+ latest[str(ev.get("agent", "unknown"))] = None
162
182
  return latest
163
183
 
164
184
 
@@ -174,15 +194,14 @@ def _backoff_exponent(log_dir: Path, agent: str) -> int:
174
194
  deliberately NOT a reset point — the throttle-aware-skip path emits one every
175
195
  cycle, which would pin the exponent at <=1 and flatten the ladder."""
176
196
  count = 0
177
- for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
178
- for ev in _iter_events(path):
179
- if str(ev.get("agent", "")) != agent:
180
- continue
181
- kind = ev.get("event")
182
- if kind == TRANSIENT_ERROR_DETECTED:
183
- count += 1
184
- elif kind == AGENT_USAGE_RECORDED and ev.get("success"):
185
- count = 0
197
+ for ev in _tail_events(log_dir):
198
+ if str(ev.get("agent", "")) != agent:
199
+ continue
200
+ kind = ev.get("event")
201
+ if kind == TRANSIENT_ERROR_DETECTED:
202
+ count += 1
203
+ elif kind == AGENT_USAGE_RECORDED and ev.get("success"):
204
+ count = 0
186
205
  return max(0, count - 1)
187
206
 
188
207
 
@@ -240,7 +259,11 @@ def _events_derived_reset(
240
259
 
241
260
 
242
261
  def _active_throttles(
243
- log_dir: Path, *, clock: Clock = SYSTEM_CLOCK, _exponent_cache: dict[str, int] | None = None
262
+ log_dir: Path,
263
+ *,
264
+ clock: Clock = SYSTEM_CLOCK,
265
+ _exponent_cache: dict[str, int] | None = None,
266
+ _latest: dict[str, Any] | None = None,
244
267
  ) -> dict[str, TransientErrorState]:
245
268
  """Currently-active throttles keyed by AGENT label (binary basename), one entry
246
269
  per agent whose latest transient event is an unrecovered ``detected`` with its
@@ -251,10 +274,16 @@ def _active_throttles(
251
274
  healthy agent's phase keeps running. Restart-safe (events-derived). The effective
252
275
  reset applies the events-derived exp-backoff ladder (:func:`_events_derived_reset`)
253
276
  so a permanently-failing agent escalates here too, not just on the wait/back_off
254
- path — ONE ladder, shared by both regimes."""
277
+ path — ONE ladder, shared by both regimes.
278
+
279
+ ``_latest``, when given, is an already-computed :func:`_latest_transient_per_agent`
280
+ map (e.g. :attr:`RoundOutcome.latest_transient_per_agent`) — skips a fresh
281
+ events-tail scan here, the same memoization idea as ``_exponent_cache``. Omitted,
282
+ it is computed fresh (unchanged pre-0.2.17 behavior)."""
255
283
  now = clock.epoch()
256
284
  active: dict[str, TransientErrorState] = {}
257
- for agent, detected in _latest_transient_per_agent(log_dir).items():
285
+ latest = _latest if _latest is not None else _latest_transient_per_agent(log_dir)
286
+ for agent, detected in latest.items():
258
287
  if detected is None:
259
288
  continue
260
289
  classification = str(detected.get("classification", "rate_limit_account"))
@@ -277,7 +306,79 @@ def _active_throttles(
277
306
  return active
278
307
 
279
308
 
280
- def round_was_mem_terminated(log_dir: Path) -> bool:
309
+ @dataclass(frozen=True)
310
+ class RoundOutcome:
311
+ """Every PER-ROUND-scoped verdict :func:`round_was_mem_terminated` and
312
+ :func:`round_had_no_progress` need, folded from ONE :func:`_tail_events`
313
+ scan (0.2.17 Task 1) instead of the pre-0.2.17 three separate ones (those
314
+ two functions' own scans, plus the throttle check's
315
+ :func:`_latest_transient_per_agent` scan serve_cmd ran back-to-back with
316
+ them every round).
317
+
318
+ Deliberately does NOT include :func:`mem_loop_events_in_window` (give-up-only,
319
+ needs its own clock cutoff — a different axis than "this round") or
320
+ :func:`_backoff_exponent` (agent-scoped, keyed by a caller-supplied agent, not
321
+ "the round that just ran") — both stay on :func:`_tail_events` directly."""
322
+
323
+ mem_terminated: bool
324
+ usage_capable: bool
325
+ newest_usage_ts: str | None
326
+ newest_substrate_before_ts: str | None
327
+ latest_transient_per_agent: dict[str, Any]
328
+
329
+
330
+ def round_outcome(log_dir: Path) -> RoundOutcome:
331
+ """Single-pass fold over :func:`_tail_events` computing :class:`RoundOutcome`.
332
+ Callers: :func:`round_was_mem_terminated`, :func:`round_had_no_progress` (each
333
+ accepts a precomputed ``outcome=`` to skip re-scanning), and
334
+ ``serve_cmd.cmd()``'s post-round block, which computes this ONCE per round
335
+ (INVARIANT 3) and reuses it for the mem-terminated / throttle / no-progress
336
+ checks that used to scan separately."""
337
+ newest_before_ts: str | None = None
338
+ newest_terminated_ts: str | None = None
339
+ newest_usage_ts: str | None = None
340
+ usage_capable = False
341
+ transient: dict[str, Any] = {}
342
+ for ev in _tail_events(log_dir):
343
+ kind = ev.get("event")
344
+ # INVARIANT 2: latest_transient_per_agent keys by str(agent), no ts guard,
345
+ # forward old->new merge (_tail_events yields oldest-file-first) — identical
346
+ # to the pre-0.2.17 _latest_transient_per_agent copy this replaces.
347
+ if kind == TRANSIENT_ERROR_DETECTED:
348
+ transient[str(ev.get("agent", "unknown"))] = ev
349
+ elif kind == TRANSIENT_ERROR_RECOVERED:
350
+ transient[str(ev.get("agent", "unknown"))] = None
351
+ # INVARIANT 1: usage_capable set UNCONDITIONALLY here, on the raw kind check,
352
+ # BEFORE the `ts` guard below — matches the pre-refactor round_had_no_progress
353
+ # (usage_capable must arm even off a ts-less agent_usage_recorded event).
354
+ if kind == AGENT_USAGE_RECORDED:
355
+ usage_capable = True
356
+ ts = ev.get("ts")
357
+ if not ts:
358
+ continue
359
+ if kind == ROUND_SUBSTRATE_BEFORE:
360
+ newest_before_ts = ts
361
+ elif kind == ROUND_MEM_TERMINATED:
362
+ newest_terminated_ts = ts
363
+ elif kind == AGENT_USAGE_RECORDED:
364
+ newest_usage_ts = ts
365
+ # round_was_mem_terminated's own scoping logic (see its docstring): >=, not >,
366
+ # since a fast loop can legitimately stamp both events in the same millisecond.
367
+ mem_terminated = (
368
+ newest_before_ts is not None
369
+ and newest_terminated_ts is not None
370
+ and parse_iso_ms(newest_terminated_ts) >= parse_iso_ms(newest_before_ts)
371
+ )
372
+ return RoundOutcome(
373
+ mem_terminated=mem_terminated,
374
+ usage_capable=usage_capable,
375
+ newest_usage_ts=newest_usage_ts,
376
+ newest_substrate_before_ts=newest_before_ts,
377
+ latest_transient_per_agent=transient,
378
+ )
379
+
380
+
381
+ def round_was_mem_terminated(log_dir: Path, *, outcome: RoundOutcome | None = None) -> bool:
281
382
  """True iff the round that JUST ran was killed by ``_spawn_round``'s
282
383
  mid-round memory-pressure hard floor (``round_mem_terminated``) rather
283
384
  than a genuine crash — so ``serve_cmd.cmd`` can excuse it from the
@@ -299,22 +400,15 @@ def round_was_mem_terminated(log_dir: Path) -> bool:
299
400
  ``round_mem_terminated`` within the very round it is scoping. ``>=``, not
300
401
  ``>``: a fast loop (or millisecond-resolution ties) can legitimately stamp
301
402
  both events in the same millisecond; erring toward "this round's" on a tie
302
- only risks over-excusing, never mistaking a genuine crash for a rescue."""
303
- newest_before_ts: str | None = None
304
- newest_terminated_ts: str | None = None
305
- for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
306
- for ev in _iter_events(path):
307
- ts = ev.get("ts")
308
- if not ts:
309
- continue
310
- kind = ev.get("event")
311
- if kind == ROUND_SUBSTRATE_BEFORE:
312
- newest_before_ts = ts
313
- elif kind == ROUND_MEM_TERMINATED:
314
- newest_terminated_ts = ts
315
- if newest_before_ts is None or newest_terminated_ts is None:
316
- return False
317
- return parse_iso_ms(newest_terminated_ts) >= parse_iso_ms(newest_before_ts)
403
+ only risks over-excusing, never mistaking a genuine crash for a rescue.
404
+
405
+ ``outcome``, when given (the serve post-round block's one-scan path — see
406
+ :class:`RoundOutcome`), is used verbatim instead of triggering a fresh
407
+ :func:`round_outcome` scan; every existing caller/test omits it and gets the
408
+ pre-0.2.17 from-scratch-scan behavior unchanged."""
409
+ if outcome is None:
410
+ outcome = round_outcome(log_dir)
411
+ return outcome.mem_terminated
318
412
 
319
413
 
320
414
  def round_had_no_progress(
@@ -324,6 +418,7 @@ def round_had_no_progress(
324
418
  duration_s: float,
325
419
  threshold_s: float,
326
420
  throttle_active: bool = False,
421
+ outcome: RoundOutcome | None = None,
327
422
  ) -> bool:
328
423
  """True iff the round that JUST ran exited 0, finished fast, but never
329
424
  reached the model -- pi (and CLIs like it: see builtin_plugins/pi.py's
@@ -352,9 +447,11 @@ def round_had_no_progress(
352
447
  round), a round with none is genuine no-progress and still trips --
353
448
  this makes the breaker CLI-adaptive with no config descriptor.
354
449
 
355
- Short-circuits on ``returncode != 0`` or ``duration_s >= threshold_s``
356
- without touching the events tail: a non-zero exit already has its own
357
- crash-loop signal, and a slow round (even with no usage) is not a TIGHT
450
+ INVARIANT 4: short-circuits on ``returncode != 0`` or ``duration_s >=
451
+ threshold_s`` (or ``throttle_active``) BEFORE touching ``outcome`` at all --
452
+ still ahead of any events-tail scan / ``newest_usage_ts`` comparison, exactly
453
+ as the pre-0.2.17 single-scan version did: a non-zero exit already has its
454
+ own crash-loop signal, and a slow round (even with no usage) is not a TIGHT
358
455
  loop -- a wedged/hung round already has its own signal
359
456
  (``round_supervisor_wedged``), so this floor is specifically the fast spin.
360
457
 
@@ -367,31 +464,22 @@ def round_had_no_progress(
367
464
  (newest-event-timestamp comparison against the newest
368
465
  ``round_substrate_before``), with the comparison inverted: no progress
369
466
  means the newest usage event is EITHER absent OR older than this round's
370
- own start."""
467
+ own start.
468
+
469
+ ``outcome``, when given (the serve post-round block's one-scan path), is used
470
+ verbatim instead of triggering a fresh :func:`round_outcome` scan — see
471
+ :func:`round_was_mem_terminated`."""
371
472
  if throttle_active or returncode != 0 or duration_s >= threshold_s:
372
473
  return False
373
- newest_before_ts: str | None = None
374
- newest_usage_ts: str | None = None
375
- usage_capable = False
376
- for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
377
- for ev in _iter_events(path):
378
- kind = ev.get("event")
379
- if kind == AGENT_USAGE_RECORDED:
380
- usage_capable = True
381
- ts = ev.get("ts")
382
- if not ts:
383
- continue
384
- if kind == ROUND_SUBSTRATE_BEFORE:
385
- newest_before_ts = ts
386
- elif kind == AGENT_USAGE_RECORDED:
387
- newest_usage_ts = ts
388
- if not usage_capable:
474
+ if outcome is None:
475
+ outcome = round_outcome(log_dir)
476
+ if not outcome.usage_capable:
389
477
  return False # this CLI/plugin stack has never emitted usage -- can't arm
390
- if newest_before_ts is None:
478
+ if outcome.newest_substrate_before_ts is None:
391
479
  return False
392
- if newest_usage_ts is None:
480
+ if outcome.newest_usage_ts is None:
393
481
  return True
394
- return parse_iso_ms(newest_usage_ts) < parse_iso_ms(newest_before_ts)
482
+ return parse_iso_ms(outcome.newest_usage_ts) < parse_iso_ms(outcome.newest_substrate_before_ts)
395
483
 
396
484
 
397
485
  def mem_loop_events_in_window(log_dir: Path, clock: Clock, window_s: int) -> int:
@@ -399,7 +487,7 @@ def mem_loop_events_in_window(log_dir: Path, clock: Clock, window_s: int) -> int
399
487
  ``window_s`` seconds of ``clock.epoch()`` — events-derived, no state file,
400
488
  same tail-reconstruction shape as :func:`round_was_mem_terminated` above.
401
489
 
402
- Feeds ``_serve_round.round_outcome_exit_code``'s cross-restart escalation
490
+ Feeds ``_serve_round.post_round_verdicts``'s cross-restart escalation
403
491
  (0.2.16 Task 5): ``MEM_LOOP_EXIT`` (71) alone resets on every serve
404
492
  process restart, so a host stuck in sustained pressure respawns into the
405
493
  identical loop forever (field-confirmed: NRestarts climbs, never
@@ -407,15 +495,16 @@ def mem_loop_events_in_window(log_dir: Path, clock: Clock, window_s: int) -> int
407
495
  an all-time total, or a restart-local counter — gives "ages out on its
408
496
  own after a sustained-healthy stretch" for free: an old mem_loop episode
409
497
  outside the window simply stops counting, no explicit reset needed."""
498
+ # Kept OUT of RoundOutcome/round_outcome (0.2.17 Task 1): give-up-only, and its
499
+ # window cutoff needs its OWN clock read, unlike anything folded into that struct.
410
500
  cutoff = clock.epoch() - window_s
411
501
  count = 0
412
- for path in sorted(log_dir.glob("events-*.jsonl"))[-2:]:
413
- for ev in _iter_events(path):
414
- if ev.get("event") != MEM_LOOP:
415
- continue
416
- ts = ev.get("ts")
417
- if ts and parse_iso_ms(ts).timestamp() >= cutoff:
418
- count += 1
502
+ for ev in _tail_events(log_dir):
503
+ if ev.get("event") != MEM_LOOP:
504
+ continue
505
+ ts = ev.get("ts")
506
+ if ts and parse_iso_ms(ts).timestamp() >= cutoff:
507
+ count += 1
419
508
  return count
420
509
 
421
510
 
@@ -657,9 +746,9 @@ def _interruptible_sleep(
657
746
  :func:`_apply_back_off` so a SIGTERM or a stop_file lands within one chunk instead
658
747
  of after the full sleep (e.g. the 8h back-off cap).
659
748
 
660
- ``should_stop`` matches :func:`_pause_poll`'s contract (serve_cmd.py): a zero-arg
661
- predicate the caller closes over its own stop_file check with, so this module never
662
- needs to know what "should stop" means beyond calling it.
749
+ ``should_stop`` matches :func:`_pause_poll`'s contract (cli/_serve_round.py): a
750
+ zero-arg predicate the caller closes over its own stop_file check with, so this
751
+ module never needs to know what "should stop" means beyond calling it.
663
752
 
664
753
  Counts down the *intended* nap per slice rather than measuring a wall/monotonic
665
754
  deadline: NTP-step immune (no clock read for the deadline) AND does not busy-spin
@@ -669,7 +758,7 @@ def _interruptible_sleep(
669
758
  ``deadline_epoch``, when given, additionally re-checks ``clock.epoch() >=
670
759
  deadline_epoch`` at each chunk boundary and returns False (completed, NOT
671
760
  interrupted) the moment it's reached — mirroring the skip path's self-correcting
672
- ``clock.epoch() >= wake_epoch`` (serve_cmd.py:251). Only :func:`_apply_back_off`
761
+ ``clock.epoch() >= wake_epoch`` (serve_cmd.py:238). Only :func:`_apply_back_off`
673
762
  passes this: its ``total_s`` is computed against the caller's OWN clock reading at
674
763
  call time, which on an RTC-less host booting hours behind is stale, inflating
675
764
  ``total_s`` well past the real wall-clock target; re-checking the target itself
@@ -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.16'
22
- __version_tuple__ = version_tuple = (0, 2, 16)
21
+ __version__ = version = '0.2.17'
22
+ __version_tuple__ = version_tuple = (0, 2, 17)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -319,9 +319,9 @@ def start(project: str | Path) -> ServiceStatus:
319
319
  _PID_SIGNAL_GRACE_S = 5
320
320
 
321
321
  # Grace after TERMing the round-lock holder before escalating to SIGKILL: mirrors
322
- # serve_cmd._ROUND_TERM_GRACE_S (the round's own SIGTERM handler needs
322
+ # cli._serve_round._ROUND_TERM_GRACE_S (the round's own SIGTERM handler needs
323
323
  # agent_runtime.REAP_GRACE_S + margin to reap its agent pgroup and exit).
324
- # Cross-checked against serve_cmd's value by
324
+ # Cross-checked against that module's value by
325
325
  # test_round_kill_grace_matches_serve so the two never drift.
326
326
  _ROUND_TERM_GRACE_S = 15
327
327
 
@@ -712,12 +712,29 @@ def _monitor_loop_iter(
712
712
  # work_dir with a non-preset log_dir would target the wrong dir, see no
713
713
  # pidfile, and no-op while serve keeps running. The Path resolves to the
714
714
  # real cfg.runtime.log_dir.
715
- monitor.on_alert(
715
+ verdict = monitor.on_alert(
716
716
  alert,
717
717
  project=work_dir,
718
718
  log_dir=cfg.runtime.log_dir,
719
719
  allowed_stop_names=cfg.monitor.auto_stop_on,
720
720
  )
721
+ if verdict == "draining":
722
+ # Nothing was recorded for this alert this poll (see on_alert's
723
+ # docstring) — force-clear its `seen` entry so it is NOT treated
724
+ # as "still firing, stay suppressed" below: the alert condition
725
+ # persisting is exactly the case that must re-fire on_alert next
726
+ # poll, unlike every other verdict (recorded outcome, or never
727
+ # eligible), which keeps the normal dedup.
728
+ #
729
+ # Trade-off: `seen` also gates the `yield alert` above (the
730
+ # dedup consumers of the monitor stream see), so re-arming it
731
+ # to retry on_alert ALSO re-yields this same alert to the
732
+ # stream consumer next poll. Accepted: a persisting alert
733
+ # re-appearing each poll while its stop drains just reads as a
734
+ # "still draining" signal to that consumer. A fuller design
735
+ # would split consumer-yield dedup from on_alert-retry state,
736
+ # but that split isn't warranted for this narrow case.
737
+ del seen[key]
721
738
  # Re-arm: an episode absent from this poll has cleared, so forget it — a
722
739
  # later recurrence is a NEW episode and must fire again (not stay suppressed
723
740
  # until bounded eviction). Only keys still firing this poll survive.