cli-agent-runner 0.2.14__tar.gz → 0.2.16__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.14 → cli_agent_runner-0.2.16}/.vulture-whitelist.py +6 -0
  2. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CHANGELOG.md +33 -0
  3. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/PKG-INFO +2 -2
  4. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/README.md +1 -1
  5. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_emit.py +180 -10
  6. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_serve_policy.py +92 -0
  7. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_throttle.py +113 -9
  8. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_version.py +2 -2
  9. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/api.py +10 -0
  10. cli_agent_runner-0.2.16/agent_runner/cli/_serve_round.py +442 -0
  11. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/serve_cmd.py +79 -270
  12. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/models.py +17 -0
  13. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/parsers.py +25 -0
  14. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/validators.py +12 -0
  15. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/defenses.py +33 -0
  16. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/events.py +6 -0
  17. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/host_health.py +34 -27
  18. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/metrics.py +108 -0
  19. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/monitor.py +4 -0
  20. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/round_log.py +10 -0
  21. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/service_unit.py +9 -3
  22. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/architecture.md +16 -6
  23. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/commands.md +1 -1
  24. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/configuration.md +9 -1
  25. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/migrations/0.2.md +110 -3
  26. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/runbook.md +60 -16
  27. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/contract/test_public_api_surface.py +11 -1
  28. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_owned_paths.py +3 -3
  29. cli_agent_runner-0.2.16/tests/integration/test_spawn_round_mem_floor.py +441 -0
  30. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_architecture.py +112 -21
  31. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_claims_match_ssot.py +11 -0
  32. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_kinds_ssot.py +12 -1
  33. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_module_boundaries.py +3 -3
  34. cli_agent_runner-0.2.16/tests/invariants/test_no_internal_refs.py +39 -0
  35. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_peek_schema_version.py +1 -1
  36. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_repo_constants_patched_in_tests.py +1 -1
  37. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_service.py +30 -0
  38. cli_agent_runner-0.2.16/tests/unit/test_cgroup_probe.py +233 -0
  39. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_claude_error_detector.py +1 -1
  40. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config.py +165 -6
  41. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_defenses.py +2 -2
  42. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_gemini_plugin.py +1 -1
  43. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_host_health.py +47 -0
  44. cli_agent_runner-0.2.16/tests/unit/test_serve_crash_loop.py +640 -0
  45. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_mem_gate.py +25 -1
  46. cli_agent_runner-0.2.16/tests/unit/test_serve_policy.py +110 -0
  47. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_round_log.py +39 -0
  48. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_service_unit.py +27 -2
  49. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_vcs_state.py +2 -2
  50. cli_agent_runner-0.2.14/tests/integration/test_spawn_round_mem_floor.py +0 -224
  51. cli_agent_runner-0.2.14/tests/unit/test_serve_crash_loop.py +0 -198
  52. cli_agent_runner-0.2.14/tests/unit/test_serve_policy.py +0 -29
  53. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.codecov.yml +0 -0
  54. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  55. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  56. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  57. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  58. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/workflows/ci.yml +0 -0
  59. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.github/workflows/release.yml +0 -0
  60. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/.gitignore +0 -0
  61. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/BACKLOG.md +0 -0
  62. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CODE_OF_CONDUCT.md +0 -0
  63. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/CONTRIBUTING.md +0 -0
  64. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/LICENSE +0 -0
  65. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/README.zh.md +0 -0
  66. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/SECURITY.md +0 -0
  67. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/__init__.py +0 -0
  68. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_docgen.py +0 -0
  69. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_detectors.py +0 -0
  70. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_registry.py +0 -0
  71. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_monitor_state.py +0 -0
  72. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_redact.py +0 -0
  73. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_registry.py +0 -0
  74. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_resolve.py +0 -0
  75. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_round_support.py +0 -0
  76. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/_substrate.py +0 -0
  77. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/agent_runtime.py +0 -0
  78. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/api_types.py +0 -0
  79. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/__init__.py +0 -0
  80. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/_constants.py +0 -0
  81. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  82. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/codewhale.py +0 -0
  83. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  84. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/gemini.py +0 -0
  85. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/kimi.py +0 -0
  86. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/builtin_plugins/pi.py +0 -0
  87. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/__init__.py +0 -0
  88. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/__main__.py +0 -0
  89. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/common.py +0 -0
  90. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/events_cmd.py +0 -0
  91. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/init_cmd.py +0 -0
  92. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/install_cmd.py +0 -0
  93. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/migrate_cmd.py +0 -0
  94. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/monitor_cmd.py +0 -0
  95. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/peek_cmd.py +0 -0
  96. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/round_cmd.py +0 -0
  97. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/service_cmd.py +0 -0
  98. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/cli/upgrade_cmd.py +0 -0
  99. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/clock.py +0 -0
  100. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/__init__.py +0 -0
  101. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/errors.py +0 -0
  102. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/config/loader.py +0 -0
  103. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/context_store.py +0 -0
  104. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/hooks.py +0 -0
  105. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/http_progress.py +0 -0
  106. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/lifecycle.py +0 -0
  107. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/migrations.py +0 -0
  108. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/phase_select.py +0 -0
  109. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/__init__.py +0 -0
  110. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/aider.toml +0 -0
  111. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/claude.toml +0 -0
  112. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/codewhale.toml +0 -0
  113. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/gemini.toml +0 -0
  114. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/kimi.toml +0 -0
  115. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/presets/pi.toml +0 -0
  116. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/prompt_loader.py +0 -0
  117. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/remote_relay.py +0 -0
  118. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/round_view.py +0 -0
  119. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/runner.py +0 -0
  120. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/scaffold.py +0 -0
  121. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/schedule.py +0 -0
  122. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/startup_check.py +0 -0
  123. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/agent_runner/vcs_state.py +0 -0
  124. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/build.sh +0 -0
  125. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/README.md +0 -0
  126. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/events.md +0 -0
  127. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/long-running-agents.md +0 -0
  128. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/plugins.md +0 -0
  129. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/quickstart.md +0 -0
  130. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/aider.md +0 -0
  131. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/codewhale.md +0 -0
  132. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/kimi.md +0 -0
  133. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/recipes/pi.md +0 -0
  134. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/docs/thesis.md +0 -0
  135. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/pyproject.toml +0 -0
  136. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/__init__.py +0 -0
  137. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/_clock.py +0 -0
  138. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/_test_helpers.py +0 -0
  139. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/conftest.py +0 -0
  140. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/contract/__init__.py +0 -0
  141. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/__init__.py +0 -0
  142. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/conftest.py +0 -0
  143. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  144. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_install_systemd.py +0 -0
  145. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  146. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  147. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  148. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  149. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  150. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/generate_vulture_whitelist.py +0 -0
  151. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/__init__.py +0 -0
  152. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_auto_stop_e2e.py +0 -0
  153. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_bounded_run.py +0 -0
  154. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_context_enricher_namespacing.py +0 -0
  155. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_dirty_handler_seam.py +0 -0
  156. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_fresh_eyes_signal.py +0 -0
  157. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_grace_kill_emission.py +0 -0
  158. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_install_dry_run.py +0 -0
  159. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_monitor_seeded.py +0 -0
  160. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_detector_loaded.py +0 -0
  161. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_plugin_real_flow.py +0 -0
  162. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  163. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  164. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_scaffold_presets.py +0 -0
  165. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_serve_loop.py +0 -0
  166. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_spawn_round_wedged.py +0 -0
  167. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_substrate_fingerprint.py +0 -0
  168. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/integration/test_transient_error_backoff.py +0 -0
  169. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/__init__.py +0 -0
  170. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/_docs.py +0 -0
  171. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/_event_scan.py +0 -0
  172. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_atomic_write_enforced.py +0 -0
  173. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_authored_facts_declared.py +0 -0
  174. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  175. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_catalogs.py +0 -0
  176. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_classification_ssot.py +0 -0
  177. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_config_error_consistency.py +0 -0
  178. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_config_value_set_ssot.py +0 -0
  179. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  180. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  181. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_cli_claims.py +0 -0
  182. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_operator_surface.py +0 -0
  183. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  184. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  185. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  186. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_docs_generated.py +0 -0
  187. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_docs_index_complete.py +0 -0
  188. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_entry_points_resolve.py +0 -0
  189. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_int_coercion.py +0 -0
  190. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_event_kind_registry.py +0 -0
  191. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_events_doc_contract.py +0 -0
  192. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  193. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_hook_contract_docs.py +0 -0
  194. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_layer_2_loop_size.py +0 -0
  195. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  196. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_module_sizes.py +0 -0
  197. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  198. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_ai_signatures.py +0 -0
  199. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  200. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_raw_time.py +0 -0
  201. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  202. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_phase_select_stateless.py +0 -0
  203. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_plugins_config_stable.py +0 -0
  204. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  205. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  206. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_num_single_source.py +0 -0
  207. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_round_result_stable.py +0 -0
  208. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  209. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  210. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  211. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_upstream_schema_canary.py +0 -0
  212. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  213. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/__init__.py +0 -0
  214. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/parser.py +0 -0
  215. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/test_parser.py +0 -0
  216. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/literate/test_quickstart.py +0 -0
  217. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/__init__.py +0 -0
  218. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime.py +0 -0
  219. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_grace.py +0 -0
  220. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  221. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_progress.py +0 -0
  222. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_reap.py +0 -0
  223. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  224. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_assemble_prompt.py +0 -0
  225. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_events_stream.py +0 -0
  226. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_install.py +0 -0
  227. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_observation.py +0 -0
  228. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_read_round_num.py +0 -0
  229. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_resolve_phase.py +0 -0
  230. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_api_types.py +0 -0
  231. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_auto_stop_gating.py +0 -0
  232. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_catalogs.py +0 -0
  233. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_claude_foreign_schema.py +0 -0
  234. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli.py +0 -0
  235. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_common.py +0 -0
  236. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_init_install.py +0 -0
  237. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_main_boundary.py +0 -0
  238. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_monitor_http.py +0 -0
  239. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  240. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_cli_upgrade.py +0 -0
  241. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_codewhale_code_precedence.py +0 -0
  242. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_codewhale_plugin.py +0 -0
  243. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_coerce_int_poison.py +0 -0
  244. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_fresh_eyes.py +0 -0
  245. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_max_rounds.py +0 -0
  246. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_nested_strictness.py +0 -0
  247. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_prompt_delivery.py +0 -0
  248. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_schedule.py +0 -0
  249. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_stop_file.py +0 -0
  250. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_strictness.py +0 -0
  251. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  252. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_config_transient_error_action.py +0 -0
  253. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_context_store.py +0 -0
  254. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_control_char_config.py +0 -0
  255. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_default_dirty_handler.py +0 -0
  256. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detect_hung_latch.py +0 -0
  257. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detector_isolation.py +0 -0
  258. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_detector_protocol.py +0 -0
  259. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_dirty_handlers.py +0 -0
  260. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_docgen.py +0 -0
  261. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_effective_throttle_view.py +0 -0
  262. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_config_migrated.py +0 -0
  263. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_schedule.py +0 -0
  264. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  265. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events.py +0 -0
  266. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd.py +0 -0
  267. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd_rollover.py +0 -0
  268. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_cmd_tail.py +0 -0
  269. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_events_jsonl_decode.py +0 -0
  270. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_example_dirty_handler.py +0 -0
  271. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_example_plugin.py +0 -0
  272. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_exit_cause.py +0 -0
  273. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_exit_classifier.py +0 -0
  274. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  275. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_git_timeout.py +0 -0
  276. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hook_failure_isolation.py +0 -0
  277. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hook_redact_order.py +0 -0
  278. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_hooks.py +0 -0
  279. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_http_progress.py +0 -0
  280. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_init_entry_points.py +0 -0
  281. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  282. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_kimi_plugin.py +0 -0
  283. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_lifecycle.py +0 -0
  284. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_metrics.py +0 -0
  285. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_migrate_cmd.py +0 -0
  286. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_migrations.py +0 -0
  287. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_assembly.py +0 -0
  288. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_cmd_severity.py +0 -0
  289. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_dedup_identity.py +0 -0
  290. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  291. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  292. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  293. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  294. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_detectors.py +0 -0
  295. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_event_tail.py +0 -0
  296. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_remote.py +0 -0
  297. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  298. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_monitor_schedule_pause.py +0 -0
  299. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_network_blip_signal_guard.py +0 -0
  300. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_open_round_log.py +0 -0
  301. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  302. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_outer_round_ceiling.py +0 -0
  303. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_argparse.py +0 -0
  304. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_project_isolation.py +0 -0
  305. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_schedule.py +0 -0
  306. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_peek_select.py +0 -0
  307. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_agent_launch.py +0 -0
  308. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_profiles.py +0 -0
  309. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_phase_select.py +0 -0
  310. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_pi_plugin.py +0 -0
  311. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_plugin_constants.py +0 -0
  312. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_presets.py +0 -0
  313. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_primary_prompt_file_home.py +0 -0
  314. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_prompt_loader.py +0 -0
  315. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_read_json_hardening.py +0 -0
  316. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_redact.py +0 -0
  317. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_resolve.py +0 -0
  318. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_cmd.py +0 -0
  319. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_cmd_sigterm.py +0 -0
  320. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_log_helpers.py +0 -0
  321. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_round_view.py +0 -0
  322. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner.py +0 -0
  323. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  324. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_runner_throttle.py +0 -0
  325. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_scaffold.py +0 -0
  326. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_schedule.py +0 -0
  327. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_cmd_bounded.py +0 -0
  328. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_config_broken.py +0 -0
  329. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_phase_schedule.py +0 -0
  330. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_schedule_gate.py +0 -0
  331. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_sentinel.py +0 -0
  332. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_serve_startup_hooks.py +0 -0
  333. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_service_mode.py +0 -0
  334. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_startup_battery_exit.py +0 -0
  335. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_startup_check.py +0 -0
  336. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_substrate.py +0 -0
  337. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  338. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  339. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_throttle_plugin_classification.py +0 -0
  340. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.16}/tests/unit/test_upgrade_migrate.py +0 -0
@@ -148,6 +148,12 @@ supervisor_stale_threshold_s
148
148
  mem_avail_min_mb
149
149
  disk_warning_pct
150
150
  disk_critical_pct
151
+ swap_sout_noise_floor_mb
152
+ mem_free_low_mb
153
+ psi_full_avg10_critical
154
+ psi_some_avg10_warning
155
+ mem_critical_consecutive_samples
156
+ in_round_mem_terminate
151
157
 
152
158
  # agent_runner.config.models.PhaseOverride
153
159
  round_timeout_s
@@ -5,6 +5,39 @@ 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.16] - 2026-09-05
9
+
10
+ ### Added
11
+ - `[monitor.host_health]` gains PSI knobs — `psi_full_avg10_critical` (default 60.0) and `psi_some_avg10_warning` (default 5.0) — plus `mem_critical_consecutive_samples` (default 3) and an `in_round_mem_terminate` off switch for the mid-round memory floor.
12
+ - `serve` probes this process's cgroup v2 memory budget at startup (`host_cgroup_memory_limit`); when `memory.max` and `memory.swap.max` are both finite, the mid-round floor defers to kernel cgroup-OOM (`mem_pressure_deferred_to_cgroup`) instead of terminating the round itself.
13
+ - `mem_loop` now escalates across restarts: recurring 3 times inside a rolling 2h window stops the service for real (`mem_loop_persistent`, exit `70`) instead of respawning into the same loop forever.
14
+ - New exit-0 no-progress breaker: 5 consecutive fast, clean rounds with no recorded usage (a CLI that exits 0 without ever reaching the model) now stop `serve` (`stalled_no_progress`, exit `75`) instead of spinning invisibly.
15
+
16
+ ### Fixed
17
+ - PSI-full critical was a single ≥1% momentary sample — a hiccup, not a coma — on every PSI-capable host. It now requires a sustained ≥60% avg10 held for `mem_critical_consecutive_samples` (default 3, ~30s) consecutive checks.
18
+ - The mid-round swap-out check is now per-tick instead of cumulative since round-start, so a host that recovers mid-round is no longer permanently flagged for the rest of the round.
19
+ - The pre-round admission gate now defers only on `critical` pressure, not on any pressure (a mere `warning`, e.g. ordinary swap churn, no longer delays the next round).
20
+
21
+ ### Notes
22
+ - New events: `host_cgroup_memory_limit`, `mem_pressure_deferred_to_cgroup`, `mem_loop_persistent`, `stalled_no_progress`, `round_mem_critical_sample`; `round_mem_terminated` gained `consecutive`/`context` fields. Not breaking: no `agent-runner migrate` needed, `peek --json` schema is unchanged.
23
+ - Re-render the systemd unit (`agent-runner install`) after upgrading: the new unit adds exit `70` to `RestartPreventExitStatus` so `mem_loop_persistent` stops the service instead of restarting into the same loop.
24
+
25
+ See `docs/migrations/0.2.md`.
26
+
27
+ ## [0.2.15] - 2026-09-04
28
+
29
+ ### Added
30
+ - `[monitor.host_health]` gains two config floors — `swap_sout_noise_floor_mb` (default 32) and `mem_free_low_mb` (default 16) — making the 0.2.14 mem-pressure ladder's thresholds operator-tunable; defaults are byte-identical to the prior hardcoded behavior.
31
+ - `serve` now gives up after `MEM_LOOP_THRESHOLD` (5) consecutive mem-terminated rounds instead of retrying forever: emits a new `mem_loop` event and exits `71` — deliberately excluded from the systemd unit's `RestartPreventExitStatus` so the service restarts fresh (break-then-restart).
32
+
33
+ ### Fixed
34
+ - The mem-pressure warning no longer claims "active paging" it can't confirm; it now says only that swap-out crossed the configured noise floor.
35
+
36
+ ### Notes
37
+ - New event kind: `mem_loop`. Not breaking: no `agent-runner migrate` needed this release — nothing that used to load is rejected. `peek --json` schema is unchanged.
38
+
39
+ See `docs/migrations/0.2.md`.
40
+
8
41
  ## [0.2.14] - 2026-09-03
9
42
 
10
43
  ### Breaking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.14
3
+ Version: 0.2.16
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
@@ -94,7 +94,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
94
94
 
95
95
  ## Defenses (built in)
96
96
 
97
- 13 named defenses, structured as data — see `agent-runner peek --select defenses`.
97
+ 15 named defenses, structured as data — see `agent-runner peek --select defenses`.
98
98
  Each carries the historical incident it codifies and the invariant test that
99
99
  guards it. Highlights:
100
100
 
@@ -58,7 +58,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
58
58
 
59
59
  ## Defenses (built in)
60
60
 
61
- 13 named defenses, structured as data — see `agent-runner peek --select defenses`.
61
+ 15 named defenses, structured as data — see `agent-runner peek --select defenses`.
62
62
  Each carries the historical incident it codifies and the invariant test that
63
63
  guards it. Highlights:
64
64
 
@@ -19,12 +19,17 @@ __all__ = [
19
19
  "emit_anomaly_repetitive_tool",
20
20
  "emit_config_migrated",
21
21
  "emit_fresh_eyes_round_triggered",
22
+ "emit_host_cgroup_memory_limit",
22
23
  "emit_max_rounds_reached",
24
+ "emit_mem_loop",
25
+ "emit_mem_loop_persistent",
26
+ "emit_mem_pressure_deferred_to_cgroup",
23
27
  "emit_rate_limit_stop",
24
28
  "emit_round_deferred",
25
29
  "emit_round_grace_extended",
26
30
  "emit_round_grace_kill",
27
31
  "emit_round_logs_prune_deferred",
32
+ "emit_round_mem_critical_sample",
28
33
  "emit_round_mem_terminated",
29
34
  "emit_round_progress",
30
35
  "emit_round_resumed",
@@ -35,6 +40,7 @@ __all__ = [
35
40
  "emit_schedule_phase_skipped",
36
41
  "emit_schedule_resumed",
37
42
  "emit_stale_index_lock_cleared",
43
+ "emit_stalled_no_progress",
38
44
  "emit_stop_file_detected",
39
45
  "emit_transient_error_backoff_capped",
40
46
  "emit_transient_error_detected",
@@ -64,20 +70,103 @@ def emit_config_broken(log_dir: Path, *, reason: str) -> None:
64
70
  emit(log_dir, CONFIG_BROKEN, reason=reason)
65
71
 
66
72
 
67
- def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
68
- """Emit crash_loop (serve stopped after consecutive unknown short crashes).
69
-
70
- Captures the failure reason a redacted tail of the round log — so a
71
- recurring unknown crash can later be classified into a transient bucket.
73
+ def _emit_giveup(
74
+ log_dir: Path, kind: str, *, consecutive: int, exit_code: int, log_path: Path
75
+ ) -> None:
76
+ """Shared body for the give-up events (serve stopped/broke after consecutive
77
+ failures of one kind): capture a redacted tail of the round log as ``reason``
78
+ so the recurring failure can be inspected (or later classified into a
79
+ transient bucket), and emit ``kind`` with the standard give-up payload.
72
80
  """
73
81
  from agent_runner._redact import redact_secrets
74
- from agent_runner.events import CRASH_LOOP, emit
82
+ from agent_runner.events import emit
75
83
 
76
84
  try:
77
85
  reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
78
86
  except OSError:
79
87
  reason = ""
80
- emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
88
+ emit(log_dir, kind, consecutive=consecutive, exit_code=exit_code, reason=reason)
89
+
90
+
91
+ def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
92
+ """Emit crash_loop (serve stopped after consecutive unknown short crashes).
93
+
94
+ Captures the failure reason — a redacted tail of the round log — so a
95
+ recurring unknown crash can later be classified into a transient bucket.
96
+ """
97
+ from agent_runner.events import CRASH_LOOP
98
+
99
+ _emit_giveup(
100
+ log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path
101
+ )
102
+
103
+
104
+ def emit_mem_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
105
+ """Emit mem_loop (serve gave up after consecutive mid-round memory-pressure
106
+ terminations — the 0.2.15 coma-preventer's give-up cap). Distinct from
107
+ crash_loop: this is a break-then-restart, not a deliberate stop, so it is
108
+ deliberately absent from the unit's RestartPreventExitStatus.
109
+
110
+ Captures the failure reason — a redacted tail of the round log — same as
111
+ crash_loop, so an operator can see what the round was doing when the host
112
+ ran out of memory."""
113
+ from agent_runner.events import MEM_LOOP
114
+
115
+ _emit_giveup(log_dir, MEM_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path)
116
+
117
+
118
+ def emit_mem_loop_persistent(
119
+ log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path
120
+ ) -> None:
121
+ """Emit mem_loop_persistent (serve STOPS for real — 0.2.16 Task 5 cross-restart
122
+ convergence: mem_loop itself kept recurring across restarts within the
123
+ escalation window, so systemd is told to stop rather than respawn into the
124
+ identical loop forever). Distinct from mem_loop: this IS a deliberate stop,
125
+ like crash_loop/config_broken, so it belongs in the unit's
126
+ RestartPreventExitStatus.
127
+
128
+ ``consecutive`` counts mem_loop EPISODES within the persistence window
129
+ (up to ``_serve_policy._MEM_LOOP_PERSIST_THRESHOLD``) — NOT mid-round
130
+ terminations within one episode, which is mem_loop's own ``consecutive``
131
+ (up to ``MEM_LOOP_THRESHOLD``). Same redacted-log-tail reason capture as
132
+ crash_loop/mem_loop."""
133
+ from agent_runner.events import MEM_LOOP_PERSISTENT
134
+
135
+ _emit_giveup(
136
+ log_dir,
137
+ MEM_LOOP_PERSISTENT,
138
+ consecutive=consecutive,
139
+ exit_code=exit_code,
140
+ log_path=log_path,
141
+ )
142
+
143
+
144
+ def emit_stalled_no_progress(
145
+ log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path
146
+ ) -> None:
147
+ """Emit stalled_no_progress (serve gave up after consecutive clean-but-
148
+ no-progress rounds -- 0.2.16 Task 6). A round that exits 0 fast with no
149
+ ``agent_usage_recorded`` never reached the model (pi, and CLIs like it,
150
+ exit 0 on a provider failure) -- ``_round_ok = exit_code == 0``
151
+ (api_types.py) reads that as clean, so without this breaker it is a fast,
152
+ invisible, unclassified-failure spin, no different in kind from an
153
+ unknown short crash except for the exit code it hides behind.
154
+
155
+ Deliberately reuses ``CRASH_LOOP_EXIT`` (75), not a new exit code: this is
156
+ the SAME give-up verdict as crash_loop ("an unknown failure kept
157
+ recurring, stop for real") reached via a different signal (no usage
158
+ instead of a non-zero exit) -- not a new failure class, so it needs no new
159
+ entry in the systemd unit's ``RestartPreventExitStatus``. Same redacted-
160
+ log-tail reason capture as crash_loop/mem_loop."""
161
+ from agent_runner.events import STALLED_NO_PROGRESS
162
+
163
+ _emit_giveup(
164
+ log_dir,
165
+ STALLED_NO_PROGRESS,
166
+ consecutive=consecutive,
167
+ exit_code=exit_code,
168
+ log_path=log_path,
169
+ )
81
170
 
82
171
 
83
172
  def emit_config_migrated(
@@ -172,15 +261,96 @@ def emit_round_resumed(log_dir: Path, *, deferred_for_s: int) -> None:
172
261
 
173
262
 
174
263
  def emit_round_mem_terminated(
175
- log_dir: Path, *, pid: int, severity: str, signal: str, message: str
264
+ log_dir: Path,
265
+ *,
266
+ pid: int,
267
+ severity: str,
268
+ signal: str,
269
+ message: str,
270
+ consecutive: int,
271
+ context: dict,
176
272
  ) -> None:
177
273
  """Emit when _spawn_round's mid-round hard floor terminated a ballooning
178
274
  round on critical host_health pressure -- the actual coma-preventer (a
179
275
  pre-round-only gate can't stop a single round mid-flight). Distinct from
180
- round_supervisor_wedged (a wall-clock ceiling breach, unrelated cause)."""
276
+ round_supervisor_wedged (a wall-clock ceiling breach, unrelated cause).
277
+
278
+ 0.2.16: ``consecutive`` (the critical_streak that crossed the threshold)
279
+ and ``context`` (Pressure.context -- the actual psi/mem numbers, e.g.
280
+ psi_full_avg10) make the kill legible from the event stream alone, so an
281
+ operator can retune host_health thresholds without SSH."""
181
282
  from agent_runner.events import ROUND_MEM_TERMINATED, emit
182
283
 
183
- emit(log_dir, ROUND_MEM_TERMINATED, pid=pid, severity=severity, signal=signal, message=message)
284
+ emit(
285
+ log_dir,
286
+ ROUND_MEM_TERMINATED,
287
+ pid=pid,
288
+ severity=severity,
289
+ signal=signal,
290
+ message=message,
291
+ consecutive=consecutive,
292
+ context=context,
293
+ )
294
+
295
+
296
+ def emit_round_mem_critical_sample(
297
+ log_dir: Path, *, round_num: int, consecutive: int, context: dict
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
305
+ round_mem_terminated. Fires only during critical pressure -- never during
306
+ warning/healthy -- so it adds no normal-operation noise, including while
307
+ ``defer_to_cgroup`` steps back from terminating (the streak/context is
308
+ still useful calibration there; this event stays scoped to the
309
+ sample-level signal, distinct from mem_pressure_deferred_to_cgroup's
310
+ once-per-episode terminate-vs-defer notice)."""
311
+ from agent_runner.events import ROUND_MEM_CRITICAL_SAMPLE, emit
312
+
313
+ emit(
314
+ log_dir,
315
+ ROUND_MEM_CRITICAL_SAMPLE,
316
+ round_num=round_num,
317
+ consecutive=consecutive,
318
+ context=context,
319
+ )
320
+
321
+
322
+ def emit_host_cgroup_memory_limit(
323
+ log_dir: Path, *, memory_max: int | None, memory_swap_max: int | None, cgroup_path: str | None
324
+ ) -> None:
325
+ """Emit once at serve startup: this process's cgroup v2 memory budget
326
+ (``metrics.cgroup_memory_limits``). ``None`` fields mean unlimited (or
327
+ cgroup v2 unavailable). Serve uses this once to decide whether the
328
+ mid-round hard floor can defer to kernel cgroup-OOM -- see
329
+ ``emit_mem_pressure_deferred_to_cgroup`` below."""
330
+ from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
331
+
332
+ emit(
333
+ log_dir,
334
+ HOST_CGROUP_MEMORY_LIMIT,
335
+ memory_max=memory_max,
336
+ memory_swap_max=memory_swap_max,
337
+ cgroup_path=cgroup_path,
338
+ )
339
+
340
+
341
+ def emit_mem_pressure_deferred_to_cgroup(
342
+ log_dir: Path, *, pid: int, signal: str, message: str
343
+ ) -> None:
344
+ """Emit when _spawn_round's mid-round hard floor hits sustained critical
345
+ pressure but this cgroup's (mem+swap) budget is bounded end to end
346
+ (both memory.max and memory.swap.max finite) -- kernel cgroup-OOM will
347
+ contain the agent and keep the host responsive on its own, so the
348
+ cruder host-wide round-kill steps back instead of firing
349
+ round_mem_terminated. This OVERRIDES in_round_mem_terminate=True: a
350
+ bounded cgroup makes the host floor strictly worse, not just redundant."""
351
+ from agent_runner.events import MEM_PRESSURE_DEFERRED_TO_CGROUP, emit
352
+
353
+ emit(log_dir, MEM_PRESSURE_DEFERRED_TO_CGROUP, pid=pid, signal=signal, message=message)
184
354
 
185
355
 
186
356
  def emit_round_substrate_before(
@@ -43,6 +43,57 @@ CRASH_LOOP_THRESHOLD = 5
43
43
  CRASH_LOOP_SHORT_EXIT_S = 60 # mirrors monitor.SHORT_EXIT_THRESHOLD_S
44
44
  CRASH_LOOP_MAX_DELAY_S = 1800 # cap the escalating restart delay (30 min)
45
45
 
46
+ # Exit code for the mem-loop give-up cap (0.2.15 coma-preventer, Task 4): serve
47
+ # gives up after MEM_LOOP_THRESHOLD consecutive mid-round memory-terminated
48
+ # rounds rather than retrying forever. Distinct from CRASH_LOOP_EXIT (75) on
49
+ # purpose: this is a "break-then-restart", NOT a deliberate stop — the host
50
+ # memory condition may have cleared by the time systemd respawns a fresh serve
51
+ # process, so unlike config_broken/crash_loop this code is deliberately absent
52
+ # from the unit's RestartPreventExitStatus. 71 = EX_OSERR (sysexits).
53
+ MEM_LOOP_EXIT = 71
54
+ # Consecutive mem-terminated rounds before serve gives up (break-then-restart).
55
+ MEM_LOOP_THRESHOLD = 5
56
+
57
+ # Exit code for mem_loop's cross-restart escalation (0.2.16 Task 5 — field
58
+ # confirmed: NRestarts climbs, never converges). MEM_LOOP_EXIT (71) alone
59
+ # resets on every process restart, so a host stuck in sustained pressure
60
+ # respawns into the identical break-then-restart loop forever. Once mem_loop
61
+ # itself keeps recurring across restarts (see _MEM_LOOP_PERSIST_THRESHOLD),
62
+ # this is a DELIBERATE stop instead — "gave up for real, needs a human" — so,
63
+ # unlike 71, it IS listed in the unit's RestartPreventExitStatus. 70 =
64
+ # EX_SOFTWARE (sysexits); verified free (grep -rnE "= 70\b|code=70\b").
65
+ MEM_LOOP_PERSISTENT_EXIT = 70
66
+
67
+ # Escalation window/threshold for the cross-restart convergence above: prior
68
+ # mem_loop events are counted only within the last _MEM_LOOP_PERSIST_WINDOW_S
69
+ # seconds (2h) of "now" — old episodes age out on their own, so a host that
70
+ # stays healthy for a sustained window earns a fresh restartable 71 again
71
+ # instead of ratcheting toward the persistent stop forever (no state file:
72
+ # purely events-tail derived, restart-safe by construction). Once this run's
73
+ # mem_loop would be the _MEM_LOOP_PERSIST_THRESHOLD-th (3rd) occurrence inside
74
+ # the window (~2 prior + this one, ⇒ ~15 mem-terminated kills total), serve
75
+ # escalates to MEM_LOOP_PERSISTENT_EXIT instead of the usual 71.
76
+ _MEM_LOOP_PERSIST_WINDOW_S = 7200
77
+ _MEM_LOOP_PERSIST_THRESHOLD = 3
78
+
79
+
80
+ # Exit-0 no-progress breaker (0.2.16 Task 6). Some CLIs (pi -- see
81
+ # builtin_plugins/pi.py's "pi exits 0 on provider failure") exit 0 on a
82
+ # provider failure that never reaches the model: `_round_ok = exit_code == 0`
83
+ # (api_types.py) reads that as a clean round, so without this breaker an
84
+ # invalid credential (or an exhausted-retries outage) spins as a fast,
85
+ # invisible "success" loop with no breaker, no back-off, and no alert -- on a
86
+ # constrained host, that tight loop is itself a memory-pressure generator.
87
+ # "No progress" = exit 0, a SHORT round (< _NO_PROGRESS_SHORT_S), and no
88
+ # agent_usage_recorded for it (see _throttle.round_had_no_progress for the
89
+ # events-derived scoping). Deliberately reuses CRASH_LOOP_THRESHOLD /
90
+ # CRASH_LOOP_EXIT rather than minting new ones: this is the SAME give-up
91
+ # verdict as the crash-loop breaker ("an unknown failure kept recurring, stop
92
+ # for real"), reached via a different signal (no usage instead of a non-zero
93
+ # exit) -- not a new failure class needing its own systemd
94
+ # RestartPreventExitStatus entry.
95
+ _NO_PROGRESS_SHORT_S = 30
96
+
46
97
 
47
98
  class EnvironmentalError(Exception):
48
99
  """Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
@@ -106,6 +157,47 @@ def post_round_decision(
106
157
  return ("continue", delay, 0)
107
158
 
108
159
 
160
+ def _mem_loop_decision(
161
+ *, mem_terminated: bool, consecutive: int
162
+ ) -> tuple[Literal["mem_loop", "continue"], int]:
163
+ """Mem-loop give-up cap (0.2.15 Task 4) — a NEW, SEPARATE, private counter
164
+ from ``post_round_decision``'s crash-loop breaker, not a widening of its
165
+ 3-tuple contract. A mem-terminated round already sets ``throttle_active``
166
+ for ``post_round_decision`` (never counted as a crash), so without this
167
+ cap a sustained memory-pressure host would retry forever instead of
168
+ eventually giving up. Returns ``("mem_loop", consecutive)`` once
169
+ ``consecutive`` reaches ``MEM_LOOP_THRESHOLD``; a non-mem-terminated round
170
+ 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)
177
+
178
+
179
+ def _no_progress_decision(
180
+ *, no_progress: bool, consecutive: int
181
+ ) -> tuple[Literal["stalled_no_progress", "continue"], int]:
182
+ """Exit-0 no-progress give-up cap (0.2.16 Task 6) -- a NEW, SEPARATE,
183
+ private counter from ``post_round_decision``'s crash-loop breaker (that one
184
+ keys on ``returncode != 0``; here returncode IS 0, so a round that never
185
+ reached the model would otherwise sail through with no breaker at all).
186
+ Same shape as :func:`_mem_loop_decision`: returns ``("stalled_no_progress",
187
+ consecutive)`` once ``consecutive`` reaches ``CRASH_LOOP_THRESHOLD`` --
188
+ reusing that threshold (and, at the call site, ``CRASH_LOOP_EXIT``) rather
189
+ than minting new ones, since this is the identical give-up verdict reached
190
+ via a different signal. A round WITH progress (usage recorded, non-zero
191
+ exit, or slow) resets the counter to 0, same reset shape as
192
+ 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)
199
+
200
+
109
201
  def classify_round_exit(exc: BaseException) -> int:
110
202
  """The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
111
203
  identity, decides serve's response. Replaces the old exception *whitelist*
@@ -22,6 +22,7 @@ from agent_runner.api_types import TransientErrorState
22
22
  from agent_runner.clock import SYSTEM_CLOCK, Clock
23
23
  from agent_runner.events import (
24
24
  AGENT_USAGE_RECORDED,
25
+ MEM_LOOP,
25
26
  ROUND_MEM_TERMINATED,
26
27
  ROUND_SUBSTRATE_BEFORE,
27
28
  TRANSIENT_ERROR_DETECTED,
@@ -37,7 +38,9 @@ __all__ = [
37
38
  "_interruptible_sleep",
38
39
  "compute_adjusted_reset_at",
39
40
  "effective_throttle_view",
41
+ "mem_loop_events_in_window",
40
42
  "pending_recovered",
43
+ "round_had_no_progress",
41
44
  "round_was_mem_terminated",
42
45
  ]
43
46
 
@@ -277,21 +280,20 @@ def _active_throttles(
277
280
  def round_was_mem_terminated(log_dir: Path) -> bool:
278
281
  """True iff the round that JUST ran was killed by ``_spawn_round``'s
279
282
  mid-round memory-pressure hard floor (``round_mem_terminated``) rather
280
- than a genuine crash — so ``serve_cmd._ran_agent_throttled`` can excuse it
281
- from the crash-loop breaker exactly like an active throttle (flat
282
- back-off, ``consecutive`` reset to 0), the same treatment
283
- ``ENV_BATTERY_EXIT`` already gets. A mem-terminated round can be killed
284
- within the first ~10s (the mid-round check's own cadence) — far under the
285
- crash-loop breaker's 60s "short crash" window — so, unlike a wall-clock-
286
- ceiling wedge (whose long duration alone dodges the breaker), this needs
287
- an explicit signal.
283
+ than a genuine crash — so ``serve_cmd.cmd`` can excuse it from the
284
+ crash-loop breaker exactly like an active throttle (flat back-off,
285
+ ``consecutive`` reset to 0), the same treatment ``ENV_BATTERY_EXIT``
286
+ already gets. A mem-terminated round can be killed within the first ~10s
287
+ (the mid-round check's own cadence) — far under the crash-loop breaker's
288
+ 60s "short crash" window — so, unlike a wall-clock-ceiling wedge (whose
289
+ long duration alone dodges the breaker), this needs an explicit signal.
288
290
 
289
291
  Scoped to "this round" (not some earlier one) by comparing the newest
290
292
  ``round_mem_terminated`` event's timestamp against the newest
291
293
  ``round_substrate_before``'s: ``round_substrate_before(N)`` always
292
294
  precedes round N's own attempt, and no later round's
293
295
  ``round_substrate_before(N+1)`` has been emitted yet at the point
294
- ``_ran_agent_throttled`` runs (right after ``_spawn_round`` returns) —
296
+ ``cmd()`` runs this check (right after ``_spawn_round`` returns) —
295
297
  ``round_substrate_after``, also emitted before this check runs, cannot
296
298
  serve as that boundary, since it always comes AFTER any
297
299
  ``round_mem_terminated`` within the very round it is scoping. ``>=``, not
@@ -315,6 +317,108 @@ def round_was_mem_terminated(log_dir: Path) -> bool:
315
317
  return parse_iso_ms(newest_terminated_ts) >= parse_iso_ms(newest_before_ts)
316
318
 
317
319
 
320
+ def round_had_no_progress(
321
+ log_dir: Path,
322
+ *,
323
+ returncode: int,
324
+ duration_s: float,
325
+ threshold_s: float,
326
+ throttle_active: bool = False,
327
+ ) -> bool:
328
+ """True iff the round that JUST ran exited 0, finished fast, but never
329
+ reached the model -- pi (and CLIs like it: see builtin_plugins/pi.py's
330
+ "pi exits 0 on provider failure") can exit 0 on an auth failure or an
331
+ exhausted-retries outage that ``_round_ok = exit_code == 0`` (api_types.py)
332
+ reads as clean, so this needs its own events-derived signal (feeding
333
+ ``_serve_policy._no_progress_decision``'s "clean-but-no-progress" streak)
334
+ exactly parallel to :func:`round_was_mem_terminated`'s mem-terminated one.
335
+
336
+ TWO gates guard the verdict against over-firing on healthy deployments
337
+ (0.2.16 fix-wave CRITICAL #1):
338
+
339
+ 1. ``throttle_active`` (the SAME value ``cmd()`` already computed for
340
+ ``post_round_decision`` -- caller threads it through, never
341
+ recomputed here): a provider outage that exhausts retries and exits 0
342
+ (429/503) is ``transient_error_detected`` and excused from the
343
+ crash-loop breaker; without this gate it would be double-counted here
344
+ as "no progress" and stop the loop instead of riding the back-off.
345
+ 2. Usage-capability: armed ONLY when at least one ``agent_usage_recorded``
346
+ event exists anywhere in the scanned tail. Some CLIs (kimi -- see
347
+ builtin_plugins/kimi.py; aider; any custom ``[agent] command`` with no
348
+ usage-emitting plugin) never emit usage BY DESIGN -- for those, "no
349
+ usage this round" is indistinguishable from "normal for this CLI", so
350
+ arming would stop a perfectly healthy deployment after 5 fast clean
351
+ rounds. If the CLI's plugin stack ever emits usage (pi does on a good
352
+ round), a round with none is genuine no-progress and still trips --
353
+ this makes the breaker CLI-adaptive with no config descriptor.
354
+
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
358
+ loop -- a wedged/hung round already has its own signal
359
+ (``round_supervisor_wedged``), so this floor is specifically the fast spin.
360
+
361
+ "Never reached the model" = no ``agent_usage_recorded`` event stamped at
362
+ or after this round's ``round_substrate_before`` -- the CLI plugins only
363
+ emit usage when the round actually consumed tokens (e.g.
364
+ ``builtin_plugins/pi.py``'s ``_aggregate_usage`` returns ``None`` on
365
+ all-zero usage), so an auth failure or exhausted retries leaves no such
366
+ event. Same round-scoping shape as :func:`round_was_mem_terminated`
367
+ (newest-event-timestamp comparison against the newest
368
+ ``round_substrate_before``), with the comparison inverted: no progress
369
+ means the newest usage event is EITHER absent OR older than this round's
370
+ own start."""
371
+ if throttle_active or returncode != 0 or duration_s >= threshold_s:
372
+ 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:
389
+ return False # this CLI/plugin stack has never emitted usage -- can't arm
390
+ if newest_before_ts is None:
391
+ return False
392
+ if newest_usage_ts is None:
393
+ return True
394
+ return parse_iso_ms(newest_usage_ts) < parse_iso_ms(newest_before_ts)
395
+
396
+
397
+ def mem_loop_events_in_window(log_dir: Path, clock: Clock, window_s: int) -> int:
398
+ """Count ``mem_loop`` events in the events tail stamped within the last
399
+ ``window_s`` seconds of ``clock.epoch()`` — events-derived, no state file,
400
+ same tail-reconstruction shape as :func:`round_was_mem_terminated` above.
401
+
402
+ Feeds ``_serve_round.round_outcome_exit_code``'s cross-restart escalation
403
+ (0.2.16 Task 5): ``MEM_LOOP_EXIT`` (71) alone resets on every serve
404
+ process restart, so a host stuck in sustained pressure respawns into the
405
+ identical loop forever (field-confirmed: NRestarts climbs, never
406
+ converges). Counting PRIOR occurrences in a bounded window — rather than
407
+ an all-time total, or a restart-local counter — gives "ages out on its
408
+ own after a sustained-healthy stretch" for free: an old mem_loop episode
409
+ outside the window simply stops counting, no explicit reset needed."""
410
+ cutoff = clock.epoch() - window_s
411
+ 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
419
+ return count
420
+
421
+
318
422
  def effective_throttle_view(
319
423
  log_dir: Path, *, clock: Clock = SYSTEM_CLOCK
320
424
  ) -> tuple[TransientErrorState | None, dict[str, TransientErrorState]]:
@@ -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.14'
22
- __version_tuple__ = version_tuple = (0, 2, 14)
21
+ __version__ = version = '0.2.16'
22
+ __version_tuple__ = version_tuple = (0, 2, 16)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -28,6 +28,8 @@ from agent_runner._serve_policy import ( # noqa: F401 — public re-export
28
28
  CRASH_LOOP_SHORT_EXIT_S,
29
29
  CRASH_LOOP_THRESHOLD,
30
30
  ENV_BATTERY_EXIT,
31
+ MEM_LOOP_EXIT,
32
+ MEM_LOOP_PERSISTENT_EXIT,
31
33
  PERMANENT_CONFIG_EXIT,
32
34
  post_round_decision,
33
35
  )
@@ -629,6 +631,8 @@ def _poll_once(
629
631
  mem_avail_min_mb=cfg.monitor.host_health.mem_avail_min_mb,
630
632
  disk_warning_pct=cfg.monitor.host_health.disk_warning_pct,
631
633
  disk_critical_pct=cfg.monitor.host_health.disk_critical_pct,
634
+ swap_sout_noise_floor_mb=cfg.monitor.host_health.swap_sout_noise_floor_mb,
635
+ mem_free_low_mb=cfg.monitor.host_health.mem_free_low_mb,
632
636
  log_dir=cfg.runtime.log_dir,
633
637
  )
634
638
  if not monitor._PLUGIN_DETECTORS:
@@ -783,12 +787,17 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
783
787
  emit_config_migrated,
784
788
  emit_crash_loop,
785
789
  emit_fresh_eyes_round_triggered,
790
+ emit_host_cgroup_memory_limit,
786
791
  emit_max_rounds_reached,
792
+ emit_mem_loop,
793
+ emit_mem_loop_persistent,
794
+ emit_mem_pressure_deferred_to_cgroup,
787
795
  emit_rate_limit_stop,
788
796
  emit_round_deferred,
789
797
  emit_round_grace_extended,
790
798
  emit_round_grace_kill,
791
799
  emit_round_logs_prune_deferred,
800
+ emit_round_mem_critical_sample,
792
801
  emit_round_mem_terminated,
793
802
  emit_round_progress,
794
803
  emit_round_resumed,
@@ -799,6 +808,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
799
808
  emit_schedule_phase_skipped,
800
809
  emit_schedule_resumed,
801
810
  emit_stale_index_lock_cleared,
811
+ emit_stalled_no_progress,
802
812
  emit_stop_file_detected,
803
813
  emit_transient_error_backoff_capped,
804
814
  emit_transient_error_detected,