cli-agent-runner 0.2.14__tar.gz → 0.2.15__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 (336) hide show
  1. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.vulture-whitelist.py +2 -0
  2. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CHANGELOG.md +14 -0
  3. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/PKG-INFO +2 -2
  4. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/README.md +1 -1
  5. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_emit.py +37 -7
  6. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_serve_policy.py +30 -0
  7. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_throttle.py +8 -9
  8. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_version.py +2 -2
  9. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/api.py +4 -0
  10. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/serve_cmd.py +53 -38
  11. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/models.py +2 -0
  12. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/parsers.py +8 -0
  13. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/defenses.py +12 -0
  14. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/events.py +1 -0
  15. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/host_health.py +19 -17
  16. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/monitor.py +4 -0
  17. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/service_unit.py +2 -0
  18. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/architecture.md +4 -2
  19. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/commands.md +1 -1
  20. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/configuration.md +5 -1
  21. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/migrations/0.2.md +34 -3
  22. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/runbook.md +18 -6
  23. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/contract/test_public_api_surface.py +3 -1
  24. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_owned_paths.py +3 -3
  25. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_architecture.py +8 -1
  26. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_claims_match_ssot.py +11 -0
  27. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_kinds_ssot.py +5 -0
  28. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_module_boundaries.py +2 -2
  29. cli_agent_runner-0.2.15/tests/invariants/test_no_internal_refs.py +39 -0
  30. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_peek_schema_version.py +1 -1
  31. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_repo_constants_patched_in_tests.py +1 -1
  32. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_service.py +30 -0
  33. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_claude_error_detector.py +1 -1
  34. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config.py +90 -6
  35. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_defenses.py +2 -2
  36. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_gemini_plugin.py +1 -1
  37. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_host_health.py +34 -0
  38. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_crash_loop.py +35 -7
  39. cli_agent_runner-0.2.15/tests/unit/test_serve_policy.py +68 -0
  40. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_round_log.py +39 -0
  41. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_vcs_state.py +2 -2
  42. cli_agent_runner-0.2.14/tests/unit/test_serve_policy.py +0 -29
  43. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.codecov.yml +0 -0
  44. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  45. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  46. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  47. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  48. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/workflows/ci.yml +0 -0
  49. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.github/workflows/release.yml +0 -0
  50. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/.gitignore +0 -0
  51. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/BACKLOG.md +0 -0
  52. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CODE_OF_CONDUCT.md +0 -0
  53. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/CONTRIBUTING.md +0 -0
  54. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/LICENSE +0 -0
  55. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/README.zh.md +0 -0
  56. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/SECURITY.md +0 -0
  57. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/__init__.py +0 -0
  58. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_docgen.py +0 -0
  59. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_detectors.py +0 -0
  60. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_registry.py +0 -0
  61. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_monitor_state.py +0 -0
  62. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_redact.py +0 -0
  63. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_registry.py +0 -0
  64. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_resolve.py +0 -0
  65. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_round_support.py +0 -0
  66. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/_substrate.py +0 -0
  67. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/agent_runtime.py +0 -0
  68. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/api_types.py +0 -0
  69. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/__init__.py +0 -0
  70. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/_constants.py +0 -0
  71. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  72. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/codewhale.py +0 -0
  73. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  74. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/gemini.py +0 -0
  75. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/kimi.py +0 -0
  76. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/builtin_plugins/pi.py +0 -0
  77. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/__init__.py +0 -0
  78. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/__main__.py +0 -0
  79. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/common.py +0 -0
  80. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/events_cmd.py +0 -0
  81. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/init_cmd.py +0 -0
  82. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/install_cmd.py +0 -0
  83. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/migrate_cmd.py +0 -0
  84. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/monitor_cmd.py +0 -0
  85. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/peek_cmd.py +0 -0
  86. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/round_cmd.py +0 -0
  87. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/service_cmd.py +0 -0
  88. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/cli/upgrade_cmd.py +0 -0
  89. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/clock.py +0 -0
  90. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/__init__.py +0 -0
  91. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/errors.py +0 -0
  92. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/loader.py +0 -0
  93. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/config/validators.py +0 -0
  94. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/context_store.py +0 -0
  95. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/hooks.py +0 -0
  96. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/http_progress.py +0 -0
  97. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/lifecycle.py +0 -0
  98. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/metrics.py +0 -0
  99. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/migrations.py +0 -0
  100. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/phase_select.py +0 -0
  101. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/__init__.py +0 -0
  102. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/aider.toml +0 -0
  103. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/claude.toml +0 -0
  104. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/codewhale.toml +0 -0
  105. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/gemini.toml +0 -0
  106. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/kimi.toml +0 -0
  107. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/presets/pi.toml +0 -0
  108. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/prompt_loader.py +0 -0
  109. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/remote_relay.py +0 -0
  110. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/round_log.py +0 -0
  111. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/round_view.py +0 -0
  112. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/runner.py +0 -0
  113. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/scaffold.py +0 -0
  114. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/schedule.py +0 -0
  115. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/startup_check.py +0 -0
  116. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/agent_runner/vcs_state.py +0 -0
  117. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/build.sh +0 -0
  118. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/README.md +0 -0
  119. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/events.md +0 -0
  120. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/long-running-agents.md +0 -0
  121. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/plugins.md +0 -0
  122. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/quickstart.md +0 -0
  123. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/aider.md +0 -0
  124. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/codewhale.md +0 -0
  125. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/kimi.md +0 -0
  126. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/recipes/pi.md +0 -0
  127. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/docs/thesis.md +0 -0
  128. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/pyproject.toml +0 -0
  129. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/__init__.py +0 -0
  130. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/_clock.py +0 -0
  131. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/_test_helpers.py +0 -0
  132. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/conftest.py +0 -0
  133. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/contract/__init__.py +0 -0
  134. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/__init__.py +0 -0
  135. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/conftest.py +0 -0
  136. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  137. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_install_systemd.py +0 -0
  138. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  139. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  140. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  141. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  142. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  143. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/generate_vulture_whitelist.py +0 -0
  144. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/__init__.py +0 -0
  145. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_auto_stop_e2e.py +0 -0
  146. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_bounded_run.py +0 -0
  147. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_context_enricher_namespacing.py +0 -0
  148. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_dirty_handler_seam.py +0 -0
  149. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_fresh_eyes_signal.py +0 -0
  150. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_grace_kill_emission.py +0 -0
  151. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_install_dry_run.py +0 -0
  152. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_monitor_seeded.py +0 -0
  153. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_detector_loaded.py +0 -0
  154. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_plugin_real_flow.py +0 -0
  155. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  156. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  157. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_scaffold_presets.py +0 -0
  158. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_serve_loop.py +0 -0
  159. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  160. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_spawn_round_wedged.py +0 -0
  161. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_substrate_fingerprint.py +0 -0
  162. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/integration/test_transient_error_backoff.py +0 -0
  163. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/__init__.py +0 -0
  164. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/_docs.py +0 -0
  165. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/_event_scan.py +0 -0
  166. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_atomic_write_enforced.py +0 -0
  167. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_authored_facts_declared.py +0 -0
  168. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  169. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_catalogs.py +0 -0
  170. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_classification_ssot.py +0 -0
  171. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_config_error_consistency.py +0 -0
  172. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_config_value_set_ssot.py +0 -0
  173. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  174. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  175. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_cli_claims.py +0 -0
  176. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_operator_surface.py +0 -0
  177. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  178. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  179. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  180. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_docs_generated.py +0 -0
  181. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_docs_index_complete.py +0 -0
  182. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_entry_points_resolve.py +0 -0
  183. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_int_coercion.py +0 -0
  184. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_event_kind_registry.py +0 -0
  185. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_events_doc_contract.py +0 -0
  186. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  187. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_hook_contract_docs.py +0 -0
  188. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_layer_2_loop_size.py +0 -0
  189. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  190. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_module_sizes.py +0 -0
  191. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  192. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_ai_signatures.py +0 -0
  193. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  194. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_raw_time.py +0 -0
  195. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  196. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_phase_select_stateless.py +0 -0
  197. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_plugins_config_stable.py +0 -0
  198. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  199. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  200. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_num_single_source.py +0 -0
  201. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_round_result_stable.py +0 -0
  202. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  203. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  204. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  205. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_upstream_schema_canary.py +0 -0
  206. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  207. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/__init__.py +0 -0
  208. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/parser.py +0 -0
  209. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/test_parser.py +0 -0
  210. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/literate/test_quickstart.py +0 -0
  211. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/__init__.py +0 -0
  212. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime.py +0 -0
  213. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_grace.py +0 -0
  214. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  215. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_progress.py +0 -0
  216. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_reap.py +0 -0
  217. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  218. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_assemble_prompt.py +0 -0
  219. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_events_stream.py +0 -0
  220. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_install.py +0 -0
  221. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_observation.py +0 -0
  222. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_read_round_num.py +0 -0
  223. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_resolve_phase.py +0 -0
  224. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_api_types.py +0 -0
  225. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_auto_stop_gating.py +0 -0
  226. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_catalogs.py +0 -0
  227. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_claude_foreign_schema.py +0 -0
  228. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli.py +0 -0
  229. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_common.py +0 -0
  230. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_init_install.py +0 -0
  231. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_main_boundary.py +0 -0
  232. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_monitor_http.py +0 -0
  233. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  234. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_cli_upgrade.py +0 -0
  235. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_codewhale_code_precedence.py +0 -0
  236. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_codewhale_plugin.py +0 -0
  237. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_coerce_int_poison.py +0 -0
  238. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_fresh_eyes.py +0 -0
  239. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_max_rounds.py +0 -0
  240. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_nested_strictness.py +0 -0
  241. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_prompt_delivery.py +0 -0
  242. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_schedule.py +0 -0
  243. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_stop_file.py +0 -0
  244. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_strictness.py +0 -0
  245. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  246. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_config_transient_error_action.py +0 -0
  247. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_context_store.py +0 -0
  248. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_control_char_config.py +0 -0
  249. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_default_dirty_handler.py +0 -0
  250. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detect_hung_latch.py +0 -0
  251. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detector_isolation.py +0 -0
  252. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_detector_protocol.py +0 -0
  253. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_dirty_handlers.py +0 -0
  254. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_docgen.py +0 -0
  255. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_effective_throttle_view.py +0 -0
  256. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_config_migrated.py +0 -0
  257. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_schedule.py +0 -0
  258. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  259. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events.py +0 -0
  260. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd.py +0 -0
  261. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd_rollover.py +0 -0
  262. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_cmd_tail.py +0 -0
  263. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_events_jsonl_decode.py +0 -0
  264. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_example_dirty_handler.py +0 -0
  265. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_example_plugin.py +0 -0
  266. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_exit_cause.py +0 -0
  267. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_exit_classifier.py +0 -0
  268. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  269. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_git_timeout.py +0 -0
  270. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hook_failure_isolation.py +0 -0
  271. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hook_redact_order.py +0 -0
  272. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_hooks.py +0 -0
  273. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_http_progress.py +0 -0
  274. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_init_entry_points.py +0 -0
  275. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  276. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_kimi_plugin.py +0 -0
  277. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_lifecycle.py +0 -0
  278. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_metrics.py +0 -0
  279. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_migrate_cmd.py +0 -0
  280. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_migrations.py +0 -0
  281. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_assembly.py +0 -0
  282. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_cmd_severity.py +0 -0
  283. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_dedup_identity.py +0 -0
  284. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  285. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  286. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  287. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  288. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_detectors.py +0 -0
  289. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_event_tail.py +0 -0
  290. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_remote.py +0 -0
  291. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  292. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_monitor_schedule_pause.py +0 -0
  293. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_network_blip_signal_guard.py +0 -0
  294. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_open_round_log.py +0 -0
  295. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  296. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_outer_round_ceiling.py +0 -0
  297. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_argparse.py +0 -0
  298. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_project_isolation.py +0 -0
  299. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_schedule.py +0 -0
  300. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_peek_select.py +0 -0
  301. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_agent_launch.py +0 -0
  302. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_profiles.py +0 -0
  303. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_phase_select.py +0 -0
  304. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_pi_plugin.py +0 -0
  305. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_plugin_constants.py +0 -0
  306. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_presets.py +0 -0
  307. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_primary_prompt_file_home.py +0 -0
  308. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_prompt_loader.py +0 -0
  309. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_read_json_hardening.py +0 -0
  310. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_redact.py +0 -0
  311. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_resolve.py +0 -0
  312. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_cmd.py +0 -0
  313. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_cmd_sigterm.py +0 -0
  314. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_log_helpers.py +0 -0
  315. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_round_view.py +0 -0
  316. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner.py +0 -0
  317. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  318. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_runner_throttle.py +0 -0
  319. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_scaffold.py +0 -0
  320. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_schedule.py +0 -0
  321. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_cmd_bounded.py +0 -0
  322. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_config_broken.py +0 -0
  323. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_mem_gate.py +0 -0
  324. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_phase_schedule.py +0 -0
  325. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_schedule_gate.py +0 -0
  326. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_sentinel.py +0 -0
  327. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_serve_startup_hooks.py +0 -0
  328. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_service_mode.py +0 -0
  329. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_service_unit.py +0 -0
  330. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_startup_battery_exit.py +0 -0
  331. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_startup_check.py +0 -0
  332. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_substrate.py +0 -0
  333. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  334. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  335. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_throttle_plugin_classification.py +0 -0
  336. {cli_agent_runner-0.2.14 → cli_agent_runner-0.2.15}/tests/unit/test_upgrade_migrate.py +0 -0
@@ -148,6 +148,8 @@ 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
151
153
 
152
154
  # agent_runner.config.models.PhaseOverride
153
155
  round_timeout_s
@@ -5,6 +5,20 @@ 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.15] - 2026-09-04
9
+
10
+ ### Added
11
+ - `[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.
12
+ - `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).
13
+
14
+ ### Fixed
15
+ - 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.
16
+
17
+ ### Notes
18
+ - 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.
19
+
20
+ See `docs/migrations/0.2.md`.
21
+
8
22
  ## [0.2.14] - 2026-09-03
9
23
 
10
24
  ### 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.15
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
+ 14 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
+ 14 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
 
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "emit_config_migrated",
21
21
  "emit_fresh_eyes_round_triggered",
22
22
  "emit_max_rounds_reached",
23
+ "emit_mem_loop",
23
24
  "emit_rate_limit_stop",
24
25
  "emit_round_deferred",
25
26
  "emit_round_grace_extended",
@@ -64,20 +65,49 @@ def emit_config_broken(log_dir: Path, *, reason: str) -> None:
64
65
  emit(log_dir, CONFIG_BROKEN, reason=reason)
65
66
 
66
67
 
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.
68
+ def _emit_giveup(
69
+ log_dir: Path, kind: str, *, consecutive: int, exit_code: int, log_path: Path
70
+ ) -> None:
71
+ """Shared body for the give-up events (serve stopped/broke after consecutive
72
+ failures of one kind): capture a redacted tail of the round log as ``reason``
73
+ so the recurring failure can be inspected (or later classified into a
74
+ transient bucket), and emit ``kind`` with the standard give-up payload.
72
75
  """
73
76
  from agent_runner._redact import redact_secrets
74
- from agent_runner.events import CRASH_LOOP, emit
77
+ from agent_runner.events import emit
75
78
 
76
79
  try:
77
80
  reason = redact_secrets(log_path.read_text(encoding="utf-8", errors="replace")[-2000:])
78
81
  except OSError:
79
82
  reason = ""
80
- emit(log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, reason=reason)
83
+ emit(log_dir, kind, consecutive=consecutive, exit_code=exit_code, reason=reason)
84
+
85
+
86
+ def emit_crash_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
87
+ """Emit crash_loop (serve stopped after consecutive unknown short crashes).
88
+
89
+ Captures the failure reason — a redacted tail of the round log — so a
90
+ recurring unknown crash can later be classified into a transient bucket.
91
+ """
92
+ from agent_runner.events import CRASH_LOOP
93
+
94
+ _emit_giveup(
95
+ log_dir, CRASH_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path
96
+ )
97
+
98
+
99
+ def emit_mem_loop(log_dir: Path, *, consecutive: int, exit_code: int, log_path: Path) -> None:
100
+ """Emit mem_loop (serve gave up after consecutive mid-round memory-pressure
101
+ terminations — the 0.2.15 coma-preventer's give-up cap). Distinct from
102
+ crash_loop: this is a break-then-restart, not a deliberate stop, so it is
103
+ deliberately absent from the unit's RestartPreventExitStatus.
104
+
105
+ Captures the failure reason — a redacted tail of the round log — same as
106
+ crash_loop, so an operator can see what the round was doing when the host
107
+ ran out of memory."""
108
+ from agent_runner.events import MEM_LOOP
109
+
110
+ _emit_giveup(log_dir, MEM_LOOP, consecutive=consecutive, exit_code=exit_code, log_path=log_path)
81
111
 
82
112
 
83
113
  def emit_config_migrated(
@@ -43,6 +43,17 @@ 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
+
46
57
 
47
58
  class EnvironmentalError(Exception):
48
59
  """Marks a round-child failure as ENVIRONMENTAL (recoverable) rather than a
@@ -106,6 +117,25 @@ def post_round_decision(
106
117
  return ("continue", delay, 0)
107
118
 
108
119
 
120
+ def _mem_loop_decision(
121
+ *, mem_terminated: bool, consecutive: int
122
+ ) -> tuple[Literal["mem_loop", "continue"], int]:
123
+ """Mem-loop give-up cap (0.2.15 Task 4) — a NEW, SEPARATE, private counter
124
+ from ``post_round_decision``'s crash-loop breaker, not a widening of its
125
+ 3-tuple contract. A mem-terminated round already sets ``throttle_active``
126
+ for ``post_round_decision`` (never counted as a crash), so without this
127
+ cap a sustained memory-pressure host would retry forever instead of
128
+ eventually giving up. Returns ``("mem_loop", consecutive)`` once
129
+ ``consecutive`` reaches ``MEM_LOOP_THRESHOLD``; a non-mem-terminated round
130
+ resets the counter to 0, same reset shape as the crash-loop breaker."""
131
+ if not mem_terminated:
132
+ return ("continue", 0)
133
+ consecutive += 1
134
+ if consecutive >= MEM_LOOP_THRESHOLD:
135
+ return ("mem_loop", consecutive)
136
+ return ("continue", consecutive)
137
+
138
+
109
139
  def classify_round_exit(exc: BaseException) -> int:
110
140
  """The round-child exit-code CLASSIFIER (Group A) — permanence, not exception
111
141
  identity, decides serve's response. Replaces the old exception *whitelist*
@@ -277,21 +277,20 @@ def _active_throttles(
277
277
  def round_was_mem_terminated(log_dir: Path) -> bool:
278
278
  """True iff the round that JUST ran was killed by ``_spawn_round``'s
279
279
  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.
280
+ than a genuine crash — so ``serve_cmd.cmd`` can excuse it from the
281
+ crash-loop breaker exactly like an active throttle (flat back-off,
282
+ ``consecutive`` reset to 0), the same treatment ``ENV_BATTERY_EXIT``
283
+ already gets. A mem-terminated round can be killed within the first ~10s
284
+ (the mid-round check's own cadence) — far under the crash-loop breaker's
285
+ 60s "short crash" window — so, unlike a wall-clock-ceiling wedge (whose
286
+ long duration alone dodges the breaker), this needs an explicit signal.
288
287
 
289
288
  Scoped to "this round" (not some earlier one) by comparing the newest
290
289
  ``round_mem_terminated`` event's timestamp against the newest
291
290
  ``round_substrate_before``'s: ``round_substrate_before(N)`` always
292
291
  precedes round N's own attempt, and no later round's
293
292
  ``round_substrate_before(N+1)`` has been emitted yet at the point
294
- ``_ran_agent_throttled`` runs (right after ``_spawn_round`` returns) —
293
+ ``cmd()`` runs this check (right after ``_spawn_round`` returns) —
295
294
  ``round_substrate_after``, also emitted before this check runs, cannot
296
295
  serve as that boundary, since it always comes AFTER any
297
296
  ``round_mem_terminated`` within the very round it is scoping. ``>=``, not
@@ -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.15'
22
+ __version_tuple__ = version_tuple = (0, 2, 15)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -28,6 +28,7 @@ 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,
31
32
  PERMANENT_CONFIG_EXIT,
32
33
  post_round_decision,
33
34
  )
@@ -629,6 +630,8 @@ def _poll_once(
629
630
  mem_avail_min_mb=cfg.monitor.host_health.mem_avail_min_mb,
630
631
  disk_warning_pct=cfg.monitor.host_health.disk_warning_pct,
631
632
  disk_critical_pct=cfg.monitor.host_health.disk_critical_pct,
633
+ swap_sout_noise_floor_mb=cfg.monitor.host_health.swap_sout_noise_floor_mb,
634
+ mem_free_low_mb=cfg.monitor.host_health.mem_free_low_mb,
632
635
  log_dir=cfg.runtime.log_dir,
633
636
  )
634
637
  if not monitor._PLUGIN_DETECTORS:
@@ -784,6 +787,7 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
784
787
  emit_crash_loop,
785
788
  emit_fresh_eyes_round_triggered,
786
789
  emit_max_rounds_reached,
790
+ emit_mem_loop,
787
791
  emit_rate_limit_stop,
788
792
  emit_round_deferred,
789
793
  emit_round_grace_extended,
@@ -22,7 +22,9 @@ from typing import Literal
22
22
  from agent_runner import host_health, metrics, phase_select, schedule
23
23
  from agent_runner._serve_policy import (
24
24
  CRASH_LOOP_EXIT,
25
+ MEM_LOOP_EXIT,
25
26
  PERMANENT_CONFIG_EXIT,
27
+ _mem_loop_decision,
26
28
  post_round_decision,
27
29
  )
28
30
  from agent_runner._substrate import compute_git_head, compute_paths_hash
@@ -40,6 +42,7 @@ from agent_runner.api import (
40
42
  emit_crash_loop,
41
43
  emit_fresh_eyes_round_triggered,
42
44
  emit_max_rounds_reached,
45
+ emit_mem_loop,
43
46
  emit_rate_limit_stop,
44
47
  emit_round_deferred,
45
48
  emit_round_logs_prune_deferred,
@@ -432,22 +435,18 @@ def _throttle_skip_context(cfg, log_dir) -> tuple[frozenset[str], int | None]:
432
435
 
433
436
 
434
437
  def _ran_agent_throttled(cfg, phase_arg, log_dir) -> bool:
435
- """Was the round that JUST ran throttled, or killed by the mid-round
436
- memory-pressure hard floor? so the crash-loop breaker excuses either a
437
- fast throttle-induced exit or a mem-terminated round (round_mem_terminated
438
- a coma-preventer kill, not a crash) rather than counting it as a crash.
439
- Checked first: a mem-terminated round can happen within ~10s, far under
440
- the crash-loop breaker's 60s short-crash window, so unlike a wall-clock
441
- wedge kill, whose long duration alone dodges the breaker — it needs this
442
- explicit exemption.
438
+ """Was the round that JUST ran throttled? so the crash-loop breaker
439
+ excuses a fast throttle-induced exit rather than counting it as a crash.
440
+ A mem-terminated round (round_mem_terminated a coma-preventer kill, not
441
+ a crash) is excused the same way, but that check is computed ONCE in
442
+ cmd() (``round_was_mem_terminated``) and OR'd into ``throttle_active``
443
+ there, not repeated here.
443
444
 
444
445
  When serve chose the phase (``phase_arg`` set) we check that exact agent. When the
445
446
  round self-rotated (``phase_arg`` None: no ``[phases]``, or ``--ignore-schedule``),
446
447
  serve does NOT know which agent ran, so fall back to "any agent throttled" — the
447
448
  pre-0.2.11 agent-agnostic check. Erring toward excusing keeps a real throttle from
448
449
  being misread as a crash (a false ``crash_loop`` permanent stop)."""
449
- if round_was_mem_terminated(log_dir):
450
- return True
451
450
  active = _active_throttles(log_dir)
452
451
  if phase_arg is None:
453
452
  return bool(active)
@@ -587,6 +586,14 @@ def _apply_round_num_env(round_env: dict, round_num: int) -> None:
587
586
  round_env["AGENT_RUNNER_ROUND_NUM"] = str(round_num)
588
587
 
589
588
 
589
+ def _capture_substrate(work_dir, cfg, log_dir, round_num, *, when):
590
+ """Snapshot git-head + paths-hash and emit the round-substrate event for `when`."""
591
+ git_head = compute_git_head(work_dir)
592
+ paths_hash = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
593
+ emit = emit_round_substrate_before if when == "before" else emit_round_substrate_after
594
+ emit(log_dir, round_num=round_num, git_head=git_head, paths_hash=paths_hash)
595
+
596
+
590
597
  # Grace after TERMing a wedged round before killpg: the round's own SIGTERM handler
591
598
  # reaps its agent pgroup (agent_runtime.REAP_GRACE_S) then exits, so allow that plus
592
599
  # margin. Kept local to honor serve_cmd's import allowlist; test_spawn_round_wedged
@@ -645,9 +652,10 @@ def _spawn_round(
645
652
  (``prev`` passed to ``host_health.memory_pressure``), so the delta grows
646
653
  across the whole round rather than resetting each ~10s interval. A slow
647
654
  SD/USB-backed swap device pages only a few MB per interval — below the
648
- 32 MiB floor — but crosses it over a long round; a per-interval delta would
649
- stay inert on exactly that host. PSI-full stays an independent immediate
650
- critical path (it reads the current sample alone, no baseline needed)."""
655
+ configured swap-out floor (32 MiB by default) — but crosses it over a long
656
+ round; a per-interval delta would stay inert on exactly that host. PSI-full
657
+ stays an independent immediate critical path (it reads the current sample
658
+ alone, no baseline needed)."""
651
659
  log_dir = round_log_path.parent
652
660
  with round_log_path.open("w") as f:
653
661
  proc = subprocess.Popen(
@@ -665,10 +673,11 @@ def _spawn_round(
665
673
  # Baseline pinned at the first mid-round sample (round-start), held
666
674
  # fixed so the swap-out delta the floor tests is CUMULATIVE SINCE
667
675
  # ROUND-START, not per-interval: a slow SD/USB swap device trickles
668
- # only a few MB per ~10s tick (below the 32 MiB floor) yet crosses
669
- # it over a long round, so a per-interval comparison stayed inert on
670
- # exactly the field host this floor exists for. PSI-full stays an
671
- # independent immediate path (it reads the current sample alone).
676
+ # only a few MB per ~10s tick (below the configured swap-out floor,
677
+ # 32 MiB by default) yet crosses it over a long round, so a
678
+ # per-interval comparison stayed inert on exactly the field host
679
+ # this floor exists for. PSI-full stays an independent immediate
680
+ # path (it reads the current sample alone).
672
681
  round_start_sample: dict | None = None
673
682
  while True:
674
683
  try:
@@ -776,6 +785,7 @@ def cmd(args) -> int:
776
785
  work_dir = cfg.runtime.work_dir
777
786
  rounds_completed = 0
778
787
  consecutive_crashes = 0 # b12: consecutive UNKNOWN short crashes (crash-loop breaker)
788
+ consecutive_mem_terminations = 0 # 0.2.15: consecutive mem-terminated rounds (mem-loop cap)
779
789
  # Give-up stops (config_broken/crash_loop) return a distinct non-zero code the
780
790
  # systemd unit lists in RestartPreventExitStatus so they stay stopped; every
781
791
  # other stop (sentinel/stop_file/max_rounds/SIGTERM/once) is a clean exit 0.
@@ -823,16 +833,7 @@ def cmd(args) -> int:
823
833
  continue
824
834
  if stop["requested"]:
825
835
  break # SIGTERM landed during selection/back-off — don't spawn a round
826
- git_head_before = compute_git_head(work_dir)
827
- paths_hash_before = compute_paths_hash(
828
- work_dir, cfg.runtime.substrate_fingerprint_paths
829
- )
830
- emit_round_substrate_before(
831
- log_dir,
832
- round_num=round_num,
833
- git_head=git_head_before,
834
- paths_hash=paths_hash_before,
835
- )
836
+ _capture_substrate(work_dir, cfg, log_dir, round_num, when="before")
836
837
  _apply_fresh_eyes(cfg, log_dir, round_num, round_env)
837
838
  _apply_round_num_env(round_env, round_num)
838
839
  round_log_path = log_dir / f"round-{round_num}.log"
@@ -856,27 +857,28 @@ def cmd(args) -> int:
856
857
  )
857
858
  round_duration_s = SYSTEM_CLOCK.monotonic() - round_started
858
859
  atomic_relink(log_dir / ROUND_CURRENT_LINK, round_log_path)
859
- git_head_after = compute_git_head(work_dir)
860
- paths_hash_after = compute_paths_hash(work_dir, cfg.runtime.substrate_fingerprint_paths)
861
- emit_round_substrate_after(
862
- log_dir,
863
- round_num=round_num,
864
- git_head=git_head_after,
865
- paths_hash=paths_hash_after,
866
- )
860
+ _capture_substrate(work_dir, cfg, log_dir, round_num, when="after")
867
861
  rounds_completed += 1
868
862
  # Restart policy (config_broken / crash_loop / continue) lives in the
869
863
  # tested api.post_round_decision helper so this loop stays thin. Those
870
864
  # strings are that enum, not events.py kinds — do not normalize them.
865
+ # mem_terminated computed ONCE here and OR'd into throttle_active so
866
+ # crash_loop never double-counts a mem-terminated round; _mem_loop_decision
867
+ # (a separate, private cap) tracks it on its own counter below.
868
+ mem_terminated = round_was_mem_terminated(log_dir)
871
869
  action, delay, consecutive_crashes = post_round_decision(
872
870
  returncode=r_returncode,
873
871
  duration_s=round_duration_s,
874
- # Crash-loop breaker excuses a fast exit only when the agent that just
875
- # ran is throttled (agent-agnostic when the round self-rotated the phase).
876
- throttle_active=_ran_agent_throttled(cfg, phase_arg, log_dir),
872
+ # Crash-loop breaker excuses a fast exit when the agent that just ran is
873
+ # throttled (agent-agnostic when the round self-rotated the phase) or
874
+ # mem-terminated.
875
+ throttle_active=mem_terminated or _ran_agent_throttled(cfg, phase_arg, log_dir),
877
876
  consecutive=consecutive_crashes,
878
877
  restart_delay_s=cfg.runtime.restart_delay_s,
879
878
  )
879
+ mem_action, consecutive_mem_terminations = _mem_loop_decision(
880
+ mem_terminated=mem_terminated, consecutive=consecutive_mem_terminations
881
+ )
880
882
  if action == "config_broken":
881
883
  # classify_round_exit maps ANY ConfigError to this exit code (Group
882
884
  # A) — not only a startup-battery check failure (e.g. _phase_for's
@@ -888,6 +890,19 @@ def cmd(args) -> int:
888
890
  )
889
891
  exit_code = PERMANENT_CONFIG_EXIT
890
892
  break
893
+ if mem_action == "mem_loop":
894
+ # Checked before crash_loop: a mem-terminated round always has
895
+ # throttle_active=True above, so post_round_decision never returns
896
+ # crash_loop for it anyway — this ordering just makes the precedence
897
+ # explicit (config_broken > mem_loop > crash_loop).
898
+ emit_mem_loop(
899
+ log_dir,
900
+ consecutive=consecutive_mem_terminations,
901
+ exit_code=r_returncode,
902
+ log_path=round_log_path,
903
+ )
904
+ exit_code = MEM_LOOP_EXIT
905
+ break
891
906
  if action == "crash_loop":
892
907
  emit_crash_loop(
893
908
  log_dir,
@@ -160,6 +160,8 @@ class MonitorHostHealthConfig:
160
160
  mem_avail_min_mb: int = 200 # mem_pressure fires when mem_available_mb < this
161
161
  disk_warning_pct: float = 90.0 # disk_warning fires when disk_used_pct >= this
162
162
  disk_critical_pct: float = 95.0 # disk_critical fires when disk_used_pct >= this
163
+ swap_sout_noise_floor_mb: int = 32 # tier-2 swap-out noise floor (MiB); compare uses *1024*1024
164
+ mem_free_low_mb: int = 16 # tier-3 MemFree floor (MB)
163
165
 
164
166
 
165
167
  @dataclass(frozen=True)
@@ -462,6 +462,14 @@ def _parse_monitor(monitor_d: dict) -> MonitorConfig:
462
462
  hh_d.get("disk_critical_pct", 95.0),
463
463
  field="monitor.host_health.disk_critical_pct",
464
464
  ),
465
+ swap_sout_noise_floor_mb=_require_positive_int(
466
+ hh_d.get("swap_sout_noise_floor_mb", 32),
467
+ field="monitor.host_health.swap_sout_noise_floor_mb",
468
+ ),
469
+ mem_free_low_mb=_require_positive_int(
470
+ hh_d.get("mem_free_low_mb", 16),
471
+ field="monitor.host_health.mem_free_low_mb",
472
+ ),
465
473
  )
466
474
  monitor = MonitorConfig(
467
475
  auth_fail_patterns=_validate_regex_list(
@@ -103,6 +103,18 @@ def catalog(cfg: Config) -> list[Defense]:
103
103
  guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
104
104
  current_state="active",
105
105
  ),
106
+ Defense(
107
+ name="mem_loop_breaker",
108
+ value="stop after 5 consecutive mid-round memory-terminated rounds (exit 71)",
109
+ codifies=(
110
+ "0.2.15 — a host stuck under sustained memory pressure could mem-terminate "
111
+ "every round forever with no give-up; break-then-restart cap added (exit 71 "
112
+ "stays outside RestartPreventExitStatus, so systemd restarts serve, which may "
113
+ "find the pressure has cleared)"
114
+ ),
115
+ guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
116
+ current_state="active",
117
+ ),
106
118
  Defense(
107
119
  name="bulk_round_log_prune_guard",
108
120
  value="a prune deleting more files than it keeps deletes nothing",
@@ -46,6 +46,7 @@ DIRTY_DETECTED = "dirty_detected"
46
46
  FRESH_EYES_ROUND_TRIGGERED = "fresh_eyes_round_triggered"
47
47
  HOOK_FAILED = "hook_failed"
48
48
  MAX_ROUNDS_REACHED = "max_rounds_reached"
49
+ MEM_LOOP = "mem_loop"
49
50
  MONITOR_ALERT_EMITTED = "monitor_alert_emitted"
50
51
  MONITOR_AUTO_STOP_FAILED = "monitor_auto_stop_failed"
51
52
  MONITOR_AUTO_STOP_TRIGGERED = "monitor_auto_stop_triggered"
@@ -11,8 +11,9 @@ defined signal ladder with graceful degrade:
11
11
  1. **PSI** (``psi_some_avg10``/``psi_full_avg10``) if readable — the kernel's
12
12
  own "reclaim is slowing progress" measure, immune to cache inflation.
13
13
  2. else **swap-out rate** — the delta of ``swap_sout`` (cumulative bytes) over
14
- the ``prev_sample`` the caller passes; more than a noise floor (tens of MB)
15
- means active paging, not benign startup/idle churn. Escalates to
14
+ the ``prev_sample`` the caller passes; more than the configured noise floor
15
+ (tens of MB by default) is flagged as pressure — above the noise floor, not
16
+ benign startup/idle churn. Escalates to
16
17
  **critical** when ``mem_free_mb`` is ALSO critically low (the same absolute
17
18
  floor tier 3 uses below) WHILE swap-out is positive — the unambiguous
18
19
  "actively dying" state. This is gated on MemFree, not on the swap-out
@@ -46,21 +47,22 @@ from typing import Any
46
47
  _PSI_SOME_AVG10_WARNING = 5.0
47
48
  _PSI_FULL_AVG10_CRITICAL = 1.0
48
49
 
49
- # Tier 2 -- swap-out delta (bytes) between two samples. Below this is noise:
50
- # tens of MB is ordinary startup/idle churn between successive samples (round
51
- # boundaries, or the mid-round loop's own ~10s ticks) and is NOT, on its own,
52
- # a real paging signal -- a single page (4096B, the old floor) is far too
53
- # sensitive and made the pre-round gate defer/resume on a few idle KB of
54
- # swap movement every round.
55
- _SWAP_SOUT_DELTA_NOISE_FLOOR_BYTES = 32 * 1024 * 1024 # 32 MiB
50
+ # Tier 2 -- swap-out delta (bytes) between two samples. Below
51
+ # cfg.swap_sout_noise_floor_mb is noise: tens of MB is ordinary startup/idle
52
+ # churn between successive samples (round boundaries, or the mid-round loop's
53
+ # own ~10s ticks) and is NOT, on its own, a real paging signal -- a single
54
+ # page (4096B, the old floor) is far too sensitive and made the pre-round gate
55
+ # defer/resume on a few idle KB of swap movement every round. Config-tunable
56
+ # (``MonitorHostHealthConfig.swap_sout_noise_floor_mb``, default 32 MiB) so a
57
+ # tiny host can lower it and a large host can raise it.
56
58
 
57
59
  # Tier 3 -- combined-low. MemFree alone is always low on a cache-heavy host
58
60
  # (the kernel prefers to keep it near-zero and use spare RAM for cache), so
59
61
  # it must never gate alone -- only together with a low MemAvailable (tier 3)
60
62
  # or an actual measured, above-floor swap-out delta (tier 2's critical
61
63
  # escalation, below). Also doubles as the critical-escalation MemFree floor:
62
- # "critically low" is the same absolute bar either way.
63
- _MEM_FREE_LOW_MB = 16
64
+ # "critically low" is the same absolute bar either way. Config-tunable
65
+ # (``MonitorHostHealthConfig.mem_free_low_mb``, default 16 MiB).
64
66
 
65
67
 
66
68
  @dataclass(frozen=True)
@@ -122,20 +124,20 @@ def memory_pressure(
122
124
  return None # PSI is readable and says healthy -- trust it, no fall-through.
123
125
 
124
126
  delta = _swap_sout_delta(sample, prev_sample)
125
- if delta is not None and delta > _SWAP_SOUT_DELTA_NOISE_FLOOR_BYTES:
127
+ if delta is not None and delta > cfg.swap_sout_noise_floor_mb * 1024 * 1024:
126
128
  mem_free = sample.get("mem_free_mb")
127
- if mem_free is not None and mem_free < _MEM_FREE_LOW_MB:
129
+ if mem_free is not None and mem_free < cfg.mem_free_low_mb:
128
130
  return Pressure(
129
131
  "critical",
130
132
  "swap_out_rate",
131
133
  f"swap sout +{delta}B since last sample while mem_free_mb {mem_free} "
132
- f"< {_MEM_FREE_LOW_MB} (actively dying, independent of swap-device speed)",
134
+ f"< {cfg.mem_free_low_mb} (actively dying, independent of swap-device speed)",
133
135
  {"swap_sout_delta": delta, "mem_free_mb": mem_free},
134
136
  )
135
137
  return Pressure(
136
138
  "warning",
137
139
  "swap_out_rate",
138
- f"swap sout +{delta}B since last sample (active paging)",
140
+ f"swap sout +{delta}B since last sample (above the noise floor)",
139
141
  {"swap_sout_delta": delta},
140
142
  )
141
143
  # Swap tier gave no evidence either way (no data, or below the noise
@@ -146,11 +148,11 @@ def memory_pressure(
146
148
  mem_free = sample.get("mem_free_mb")
147
149
  mem_avail = sample.get("mem_available_mb")
148
150
  if mem_free is not None and mem_avail is not None:
149
- if mem_free < _MEM_FREE_LOW_MB and mem_avail < cfg.mem_avail_min_mb:
151
+ if mem_free < cfg.mem_free_low_mb and mem_avail < cfg.mem_avail_min_mb:
150
152
  return Pressure(
151
153
  "warning",
152
154
  "combined_low",
153
- f"mem_free_mb {mem_free} < {_MEM_FREE_LOW_MB} and "
155
+ f"mem_free_mb {mem_free} < {cfg.mem_free_low_mb} and "
154
156
  f"mem_available_mb {mem_avail} < {cfg.mem_avail_min_mb}",
155
157
  {"mem_free_mb": mem_free, "mem_available_mb": mem_avail},
156
158
  )
@@ -91,6 +91,8 @@ def run_all_detectors(
91
91
  mem_avail_min_mb: int = 200,
92
92
  disk_warning_pct: float = 90.0,
93
93
  disk_critical_pct: float = 95.0,
94
+ swap_sout_noise_floor_mb: int = 32,
95
+ mem_free_low_mb: int = 16,
94
96
  log_dir: Path | None = None,
95
97
  ) -> list[Alert]:
96
98
  """Run all 13 detectors; returns alerts (empty = healthy).
@@ -112,6 +114,8 @@ def run_all_detectors(
112
114
  mem_avail_min_mb=mem_avail_min_mb,
113
115
  disk_warning_pct=disk_warning_pct,
114
116
  disk_critical_pct=disk_critical_pct,
117
+ swap_sout_noise_floor_mb=swap_sout_noise_floor_mb,
118
+ mem_free_low_mb=mem_free_low_mb,
115
119
  )
116
120
  detectors: list[tuple[str, Callable[[], Alert | None]]] = [
117
121
  ("timeout_rate", lambda: detect_timeout_rate(events)),
@@ -103,6 +103,8 @@ def render_serve_unit(
103
103
  f"Restart=on-failure\n"
104
104
  # 78 = api.PERMANENT_CONFIG_EXIT, 75 = api.CRASH_LOOP_EXIT (literal here to
105
105
  # avoid an api→service_unit→api import cycle; pinned by test_service_unit).
106
+ # NOTE: mem_loop (71) is deliberately absent here — it must restart
107
+ # (break-then-restart, not a deliberate stop; see api.MEM_LOOP_EXIT).
106
108
  f"RestartPreventExitStatus=78 75\n"
107
109
  f"RestartSec=3\n"
108
110
  f"KillMode=mixed\n"
@@ -38,7 +38,7 @@ common to all three. Operator learns one mental model, three lenses.
38
38
 
39
39
  ## Defenses-as-data
40
40
 
41
- `agent_runner.defenses.catalog(cfg)` returns 13 structured `Defense` entries.
41
+ `agent_runner.defenses.catalog(cfg)` returns 14 structured `Defense` entries.
42
42
  Each entry carries:
43
43
 
44
44
  - `name` — stable identifier
@@ -66,6 +66,7 @@ surfacing everywhere.
66
66
  | `critical_envs_injection` | Env injection via [agent.env] block — preset-supplied per CLI (e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates) | `tests/unit/test_agent_runtime.py` |
67
67
  | `startup_smoke_check` | R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; now halts serve (config_broken) instead of respawning a broken config | `tests/unit/test_serve_config_broken.py` |
68
68
  | `crash_loop_breaker` | Run 6 — crashing agent respawned ~100 empty rounds at a fixed 2x delay | `tests/unit/test_serve_crash_loop.py` |
69
+ | `mem_loop_breaker` | 0.2.15 — a host stuck under sustained memory pressure could mem-terminate every round forever with no give-up; break-then-restart cap added (exit 71 stays outside RestartPreventExitStatus, so systemd restarts serve, which may find the pressure has cleared) | `tests/unit/test_serve_crash_loop.py` |
69
70
  | `bulk_round_log_prune_guard` | 0.2.4 — rounds/ pruning shipped against backlogs it never built; one deployment's first post-upgrade round would have deleted 12,193 of 12,293 transcripts silently | `tests/unit/test_round_log_helpers.py` |
70
71
  | `flock_concurrency` | Architectural — prevent concurrent supervisors corrupting state | `tests/unit/test_runner.py` |
71
72
  | `atomic_state_writes` | Data integrity — crashes never leave half-written state files | `tests/invariants/test_atomic_write_enforced.py` |
@@ -212,6 +213,7 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
212
213
  - `fresh_eyes_round_triggered`
213
214
  - `hook_failed`
214
215
  - `max_rounds_reached`
216
+ - `mem_loop`
215
217
  - `monitor_alert_emitted`
216
218
  - `monitor_auto_stop_failed`
217
219
  - `monitor_auto_stop_triggered`
@@ -257,4 +259,4 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
257
259
 
258
260
  三层架构:Round(一轮 agent)/ Loop(serve 薄壳)/ Witness(monitor)。
259
261
  三视角对称:peek(快照)/ watch(快照循环)/ monitor(异常检测),共用下钻参数。
260
- 防御以结构化目录形式存在(13 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
262
+ 防御以结构化目录形式存在(14 条),每条防御自描述「防的是哪条历史教训、被哪个 invariant test 守、当前状态」。
@@ -283,7 +283,7 @@ The transforms `migrate` applies (generated from the registry):
283
283
  - unknown [phases.<name>.prompt] key(s) rejected in 0.2.13; delete them (allowed: ['files'])
284
284
  - [agent] prompt_arg_template has no {prompt} placeholder; the prompt is never delivered to the agent — add {prompt} to one of the argv tokens
285
285
  - a [phases.<name>.agent] prompt_arg_template has no {prompt} placeholder; add one, or set that phase's prompt.files = [] if it truly sends no prompt
286
- - unknown [monitor.host_health] key(s) rejected in 0.2.14; delete them (allowed: ['disk_critical_pct', 'disk_warning_pct', 'mem_avail_min_mb'])
286
+ - unknown [monitor.host_health] key(s) rejected in 0.2.14; delete them (allowed: ['disk_critical_pct', 'disk_warning_pct', 'mem_avail_min_mb', 'mem_free_low_mb', 'swap_sout_noise_floor_mb'])
287
287
  <!-- /gen:migrate-transforms -->
288
288
 
289
289
  ## 中文摘要