cli-agent-runner 0.2.23__tar.gz → 0.2.24__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 (373) hide show
  1. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/CHANGELOG.md +9 -0
  2. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/PKG-INFO +1 -1
  3. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/memory.py +7 -3
  4. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_version.py +2 -2
  5. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_cgroup.py +41 -11
  6. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/doctor_cmd.py +8 -1
  7. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/metrics.py +18 -2
  8. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/memory-efficiency.md +43 -0
  9. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/runbook.md +1 -1
  10. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_serve_loop.py +5 -2
  11. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_grace.py +18 -12
  12. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cgroup_probe.py +191 -7
  13. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_doctor_cmd.py +57 -0
  14. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.codecov.yml +0 -0
  15. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/dependabot.yml +0 -0
  20. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/workflows/ci.yml +0 -0
  21. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.github/workflows/release.yml +0 -0
  22. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.gitignore +0 -0
  23. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/.vulture-whitelist.py +0 -0
  24. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/BACKLOG.md +0 -0
  25. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/CODE_OF_CONDUCT.md +0 -0
  26. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/CONTRIBUTING.md +0 -0
  27. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/LICENSE +0 -0
  28. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/README.md +0 -0
  29. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/README.zh.md +0 -0
  30. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/SECURITY.md +0 -0
  31. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/__init__.py +0 -0
  32. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_docgen.py +0 -0
  33. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/__init__.py +0 -0
  34. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/agent.py +0 -0
  35. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/rounds.py +0 -0
  36. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_emit/serve.py +0 -0
  37. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_detectors.py +0 -0
  38. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_registry.py +0 -0
  39. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_monitor_state.py +0 -0
  40. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_plugin_scan.py +0 -0
  41. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_redact.py +0 -0
  42. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_registry.py +0 -0
  43. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_resolve.py +0 -0
  44. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_round_outcome.py +0 -0
  45. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_round_support.py +0 -0
  46. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_serve_policy.py +0 -0
  47. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_substrate.py +0 -0
  48. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/_throttle.py +0 -0
  49. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/agent_runtime.py +0 -0
  50. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/api.py +0 -0
  51. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/api_types.py +0 -0
  52. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/__init__.py +0 -0
  53. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/_constants.py +0 -0
  54. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  55. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/codewhale.py +0 -0
  56. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  57. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/gemini.py +0 -0
  58. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/kimi.py +0 -0
  59. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/builtin_plugins/pi.py +0 -0
  60. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/__init__.py +0 -0
  61. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/__main__.py +0 -0
  62. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/_serve_round.py +0 -0
  63. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/common.py +0 -0
  64. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/events_cmd.py +0 -0
  65. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/init_cmd.py +0 -0
  66. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/install_cmd.py +0 -0
  67. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/migrate_cmd.py +0 -0
  68. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/monitor_cmd.py +0 -0
  69. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/peek_cmd.py +0 -0
  70. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/round_cmd.py +0 -0
  71. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/serve_cmd.py +0 -0
  72. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/service_cmd.py +0 -0
  73. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/cli/upgrade_cmd.py +0 -0
  74. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/clock.py +0 -0
  75. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/__init__.py +0 -0
  76. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/errors.py +0 -0
  77. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/loader.py +0 -0
  78. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/models.py +0 -0
  79. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/parsers.py +0 -0
  80. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/config/validators.py +0 -0
  81. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/context_store.py +0 -0
  82. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/defenses.py +0 -0
  83. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/events.py +0 -0
  84. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/hooks.py +0 -0
  85. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/host_health.py +0 -0
  86. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/http_progress.py +0 -0
  87. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/lifecycle.py +0 -0
  88. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/migrations.py +0 -0
  89. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/monitor.py +0 -0
  90. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/phase_select.py +0 -0
  91. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/__init__.py +0 -0
  92. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/aider.toml +0 -0
  93. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/claude.toml +0 -0
  94. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/codewhale.toml +0 -0
  95. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/gemini.toml +0 -0
  96. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/kimi.toml +0 -0
  97. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/presets/pi.toml +0 -0
  98. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/prompt_loader.py +0 -0
  99. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/remote_relay.py +0 -0
  100. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/round_log.py +0 -0
  101. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/round_view.py +0 -0
  102. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/runner.py +0 -0
  103. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/scaffold.py +0 -0
  104. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/schedule.py +0 -0
  105. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/service_unit.py +0 -0
  106. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/startup_check.py +0 -0
  107. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/agent_runner/vcs_state.py +0 -0
  108. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/build.sh +0 -0
  109. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/deploy/Dockerfile +0 -0
  110. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/deploy/docker-compose.yml +0 -0
  111. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/README.md +0 -0
  112. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/architecture.md +0 -0
  113. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/commands.md +0 -0
  114. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/configuration.md +0 -0
  115. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/events.md +0 -0
  116. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/long-running-agents.md +0 -0
  117. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/migrations/0.2.md +0 -0
  118. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/plugins.md +0 -0
  119. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/quickstart.md +0 -0
  120. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/aider.md +0 -0
  121. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/codewhale.md +0 -0
  122. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/container-pi.md +0 -0
  123. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/deploy-all-in-one.md +0 -0
  124. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/kimi.md +0 -0
  125. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/recipes/pi.md +0 -0
  126. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/docs/thesis.md +0 -0
  127. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/pyproject.toml +0 -0
  128. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/__init__.py +0 -0
  129. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/_clock.py +0 -0
  130. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/_test_helpers.py +0 -0
  131. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/conftest.py +0 -0
  132. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/contract/__init__.py +0 -0
  133. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/contract/test_public_api_surface.py +0 -0
  134. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/__init__.py +0 -0
  135. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/conftest.py +0 -0
  136. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  137. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_install_systemd.py +0 -0
  138. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  139. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  140. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  141. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  142. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  143. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/generate_vulture_whitelist.py +0 -0
  144. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/__init__.py +0 -0
  145. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_auto_stop_e2e.py +0 -0
  146. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_bounded_run.py +0 -0
  147. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_context_enricher_namespacing.py +0 -0
  148. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_dirty_handler_seam.py +0 -0
  149. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_fresh_eyes_signal.py +0 -0
  150. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_grace_kill_emission.py +0 -0
  151. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_install_dry_run.py +0 -0
  152. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_monitor_seeded.py +0 -0
  153. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_detector_loaded.py +0 -0
  154. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_owned_paths.py +0 -0
  155. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_plugin_real_flow.py +0 -0
  156. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  157. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  158. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_scaffold_presets.py +0 -0
  159. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  160. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_spawn_round_wedged.py +0 -0
  161. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_substrate_fingerprint.py +0 -0
  162. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/integration/test_transient_error_backoff.py +0 -0
  163. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/__init__.py +0 -0
  164. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/_docs.py +0 -0
  165. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/_event_scan.py +0 -0
  166. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_architecture.py +0 -0
  167. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_atomic_write_enforced.py +0 -0
  168. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_authored_facts_declared.py +0 -0
  169. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  170. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_bdd_test_conventions.py +0 -0
  171. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_catalogs.py +0 -0
  172. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_classification_ssot.py +0 -0
  173. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_config_error_consistency.py +0 -0
  174. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_config_value_set_ssot.py +0 -0
  175. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_container_stop_bound.py +0 -0
  176. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  177. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  178. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  179. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_cli_claims.py +0 -0
  180. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_operator_surface.py +0 -0
  181. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  182. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  183. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  184. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_generated.py +0 -0
  185. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_docs_index_complete.py +0 -0
  186. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_entry_points_resolve.py +0 -0
  187. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_int_coercion.py +0 -0
  188. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kind_registry.py +0 -0
  189. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_event_kinds_ssot.py +0 -0
  190. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_events_doc_contract.py +0 -0
  191. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  192. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_hook_contract_docs.py +0 -0
  193. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_import_footprint.py +0 -0
  194. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_layer_2_loop_size.py +0 -0
  195. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  196. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_module_boundaries.py +0 -0
  197. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_module_sizes.py +0 -0
  198. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  199. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_ai_signatures.py +0 -0
  200. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_internal_refs.py +0 -0
  201. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_price_logic.py +0 -0
  202. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  203. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_raw_time.py +0 -0
  204. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  205. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_no_version_refs.py +0 -0
  206. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_peek_schema_version.py +0 -0
  207. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_phase_select_stateless.py +0 -0
  208. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_plugins_config_stable.py +0 -0
  209. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  210. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  211. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_alloc_growth.py +0 -0
  212. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  213. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_num_single_source.py +0 -0
  214. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_round_result_stable.py +0 -0
  215. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  216. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  217. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  218. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_upstream_schema_canary.py +0 -0
  219. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  220. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/__init__.py +0 -0
  221. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/parser.py +0 -0
  222. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/test_parser.py +0 -0
  223. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/literate/test_quickstart.py +0 -0
  224. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/__init__.py +0 -0
  225. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime.py +0 -0
  226. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_container.py +0 -0
  227. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  228. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_progress.py +0 -0
  229. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_reap.py +0 -0
  230. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  231. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_assemble_prompt.py +0 -0
  232. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_events_stream.py +0 -0
  233. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_install.py +0 -0
  234. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_observation.py +0 -0
  235. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_read_round_num.py +0 -0
  236. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_resolve_phase.py +0 -0
  237. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_service.py +0 -0
  238. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_api_types.py +0 -0
  239. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_auto_stop_gating.py +0 -0
  240. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_catalogs.py +0 -0
  241. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_claude_error_detector.py +0 -0
  242. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_claude_foreign_schema.py +0 -0
  243. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli.py +0 -0
  244. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_common.py +0 -0
  245. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_init_install.py +0 -0
  246. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_main_boundary.py +0 -0
  247. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_monitor_http.py +0 -0
  248. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  249. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_cli_upgrade.py +0 -0
  250. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_code_precedence.py +0 -0
  251. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_codewhale_plugin.py +0 -0
  252. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_coerce_int_poison.py +0 -0
  253. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config.py +0 -0
  254. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_fresh_eyes.py +0 -0
  255. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_max_rounds.py +0 -0
  256. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_nested_strictness.py +0 -0
  257. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_prompt_delivery.py +0 -0
  258. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_schedule.py +0 -0
  259. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_stop_file.py +0 -0
  260. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_strictness.py +0 -0
  261. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  262. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_config_transient_error_action.py +0 -0
  263. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_context_store.py +0 -0
  264. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_control_char_config.py +0 -0
  265. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_default_dirty_handler.py +0 -0
  266. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_defenses.py +0 -0
  267. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detect_hung_latch.py +0 -0
  268. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detector_isolation.py +0 -0
  269. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_detector_protocol.py +0 -0
  270. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_dirty_handlers.py +0 -0
  271. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_docgen.py +0 -0
  272. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_effective_throttle_view.py +0 -0
  273. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_config_migrated.py +0 -0
  274. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule.py +0 -0
  275. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  276. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events.py +0 -0
  277. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd.py +0 -0
  278. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_rollover.py +0 -0
  279. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_cmd_tail.py +0 -0
  280. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_events_jsonl_decode.py +0 -0
  281. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_example_dirty_handler.py +0 -0
  282. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_example_plugin.py +0 -0
  283. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exec_prefix.py +0 -0
  284. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exit_cause.py +0 -0
  285. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_exit_classifier.py +0 -0
  286. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  287. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_gemini_plugin.py +0 -0
  288. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_git_timeout.py +0 -0
  289. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hook_failure_isolation.py +0 -0
  290. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hook_redact_order.py +0 -0
  291. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_hooks.py +0 -0
  292. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_host_health.py +0 -0
  293. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress.py +0 -0
  294. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_http_progress_epoch.py +0 -0
  295. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_init_entry_points.py +0 -0
  296. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  297. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_kimi_plugin.py +0 -0
  298. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_lifecycle.py +0 -0
  299. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics.py +0 -0
  300. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  301. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_metrics_swap.py +0 -0
  302. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_migrate_cmd.py +0 -0
  303. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_migrations.py +0 -0
  304. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_assembly.py +0 -0
  305. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_cmd_severity.py +0 -0
  306. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_identity.py +0 -0
  307. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  308. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  309. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  310. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  311. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_detectors.py +0 -0
  312. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_event_tail.py +0 -0
  313. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_loop.py +0 -0
  314. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_on_alert.py +0 -0
  315. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_remote.py +0 -0
  316. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  317. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_monitor_schedule_pause.py +0 -0
  318. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_network_blip_signal_guard.py +0 -0
  319. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_open_round_log.py +0 -0
  320. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  321. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_outer_round_ceiling.py +0 -0
  322. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_argparse.py +0 -0
  323. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_project_isolation.py +0 -0
  324. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_schedule.py +0 -0
  325. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_peek_select.py +0 -0
  326. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_agent_launch.py +0 -0
  327. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_profiles.py +0 -0
  328. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_phase_select.py +0 -0
  329. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_pi_plugin.py +0 -0
  330. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_constants.py +0 -0
  331. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_plugin_scan_parity.py +0 -0
  332. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_presets.py +0 -0
  333. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_primary_prompt_file_home.py +0 -0
  334. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_prompt_loader.py +0 -0
  335. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_read_json_hardening.py +0 -0
  336. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_redact.py +0 -0
  337. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_resolve.py +0 -0
  338. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd.py +0 -0
  339. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_cmd_sigterm.py +0 -0
  340. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_log_helpers.py +0 -0
  341. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome.py +0 -0
  342. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  343. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_round_view.py +0 -0
  344. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner.py +0 -0
  345. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  346. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_runner_throttle.py +0 -0
  347. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_scaffold.py +0 -0
  348. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_schedule.py +0 -0
  349. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_cmd_bounded.py +0 -0
  350. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_config_broken.py +0 -0
  351. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_crash_loop.py +0 -0
  352. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_mem_gate.py +0 -0
  353. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_phase_schedule.py +0 -0
  354. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_policy.py +0 -0
  355. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round.py +0 -0
  356. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  357. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_log.py +0 -0
  358. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_oom_killed.py +0 -0
  359. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_round_terminate.py +0 -0
  360. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_schedule_gate.py +0 -0
  361. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_sentinel.py +0 -0
  362. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_serve_startup_hooks.py +0 -0
  363. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_service_mode.py +0 -0
  364. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_service_unit.py +0 -0
  365. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_startup_battery_exit.py +0 -0
  366. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_startup_check.py +0 -0
  367. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_substrate.py +0 -0
  368. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  369. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  370. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_epoch_bound.py +0 -0
  371. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_throttle_plugin_classification.py +0 -0
  372. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_upgrade_migrate.py +0 -0
  373. {cli_agent_runner-0.2.23 → cli_agent_runner-0.2.24}/tests/unit/test_vcs_state.py +0 -0
@@ -5,6 +5,15 @@ 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.24] - 2026-09-12
9
+
10
+ ### Added
11
+ - `host_cgroup_memory_limit` now advises adding `MemoryHigh` when a cgroup has `memory.max` set without `memory.high` (graceful pre-OOM throttling) — only for a bound on the supervisor's own cgroup, with the PSI-floor caveat when the mid-round floor isn't already deferring.
12
+
13
+ ### Changed
14
+ - `doctor`'s phase-plan preview now shows a paused phase's resume time.
15
+ - Trimmed the grace-kill test wall-time (~80s faster suite; behavior-identical).
16
+
8
17
  ## [0.2.23] - 2026-09-12
9
18
 
10
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.23
3
+ Version: 0.2.24
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
@@ -113,7 +113,7 @@ def emit_host_cgroup_memory_limit(
113
113
  mid-round hard floor can defer to kernel cgroup-OOM -- see
114
114
  ``emit_mem_pressure_deferred_to_cgroup`` below.
115
115
 
116
- This also carries an optional startup swap-cap advisory
116
+ This also carries an optional startup advisory
117
117
  (``swap_total_bytes`` / ``swap_cap_pct`` / ``memory_high`` / ``advisory``)
118
118
  as FIELDS on this SAME event -- never a separate event kind, and never an
119
119
  auto-change to the operator's cgroup or unit. ``swap_total_bytes`` is the
@@ -123,8 +123,12 @@ def emit_host_cgroup_memory_limit(
123
123
  bytes (``metrics.cgroup_memory_high``) -- ``None`` means unset (the
124
124
  cgroup read the literal ``"max"``, or no finite value at all), never the
125
125
  raw ``"max"`` token, so a caller can't mistake "unset" for a real
126
- ceiling; ``advisory`` is the human-readable warning text when the swap
127
- cap looks implausibly tight, else ``None``."""
126
+ ceiling; ``advisory`` is the human-readable warning text, else ``None`` --
127
+ it covers two independent conditions, joined with ``"; "`` when both
128
+ fire: a ``memory.swap.max`` that looks implausibly tight against host
129
+ swap, and a ``memory.max`` set on this process's OWN cgroup without a
130
+ ``memory.high``, which recommends adding one (below ``memory.max``) for
131
+ graceful pre-OOM throttling instead of a hard kill."""
128
132
  from agent_runner.events import HOST_CGROUP_MEMORY_LIMIT, emit
129
133
 
130
134
  emit(
@@ -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.23'
22
- __version_tuple__ = version_tuple = (0, 2, 23)
21
+ __version__ = version = '0.2.24'
22
+ __version_tuple__ = version_tuple = (0, 2, 24)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -225,9 +225,18 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
225
225
  memory.swap.max is bounded but far below the host's own available swap
226
226
  (``_SWAP_CAP_ADVISORY_PCT``): the operator capped the cgroup's swap well
227
227
  under what the host has, so the mid-round floor may terminate a round
228
- the kernel would have contained on a wider cap. One line is also printed
229
- to stderr in that case. The event also carries ``memory_high`` -- the
230
- bounding ancestor's ``memory.high`` soft-throttle threshold
228
+ the kernel would have contained on a wider cap. A second, independent
229
+ advisory recommends ``memory.high`` when ``memory.max`` is set but
230
+ ``memory.high`` isn't -- ONLY when the bounding ancestor is the
231
+ operator's OWN leaf (``bounding_cgroup_path == cgroup_path``): since
232
+ ``memory.max`` is the MIN across every ancestor, "set" can also mean an
233
+ inherited parent slice or a container root, where "add MemoryHigh" is
234
+ noise the operator can't act on from their own unit. That hint also
235
+ appends a PSI-floor caveat, but only when the floor ISN'T already
236
+ deferring to kernel cgroup-OOM (a deferring host has no floor for the
237
+ throttle's PSI-full rise to trip). Both advisories join with ``"; "``
238
+ and print as one stderr line. The event also carries ``memory_high`` --
239
+ the bounding ancestor's ``memory.high`` soft-throttle threshold
231
240
  (``metrics.cgroup_memory_high``), ``None`` when unset -- for the same
232
241
  reason: an operator asking "is MemoryHigh even set" is exactly this
233
242
  release's field ask. Advisory only -- this never changes the operator's
@@ -247,13 +256,39 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
247
256
  swap_cap_pct = (
248
257
  round(100.0 * swap_max / swap_total, 1) if swap_max is not None and swap_total > 0 else None
249
258
  )
250
- advisory: str | None = None
259
+ advisories: list[str] = []
251
260
  if swap_cap_pct is not None and swap_cap_pct < _SWAP_CAP_ADVISORY_PCT:
252
- advisory = (
261
+ advisories.append(
253
262
  "cgroup memory.swap.max is far below host swap; the mid-round floor may "
254
263
  "terminate rounds the kernel would have contained -- consider bounding "
255
264
  "both memory.max and memory.swap.max, or set in_round_mem_terminate=false"
256
265
  )
266
+ defer = (
267
+ limits["memory_max"] is not None
268
+ and swap_max is not None
269
+ and limits["memory_max"] < metrics.mem_total_bytes()
270
+ and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
271
+ )
272
+ own_scope = (
273
+ limits["memory_max"] is not None
274
+ and memory_high is None
275
+ and limits["bounding_cgroup_path"] == limits["cgroup_path"]
276
+ )
277
+ if own_scope:
278
+ hint = (
279
+ "memory.max is set on this cgroup without memory.high -- add "
280
+ "memory.high (below memory.max) for graceful throttling before an "
281
+ "OOM kill"
282
+ )
283
+ if not defer:
284
+ hint += (
285
+ "; bound memory.swap.max (at or below host swap) too, or set "
286
+ "in_round_mem_terminate=false, so the throttle's PSI-full rise "
287
+ "doesn't trip the mid-round floor"
288
+ )
289
+ advisories.append(hint)
290
+ advisory = "; ".join(advisories) or None
291
+ if advisory is not None:
257
292
  print(f"agent-runner: {advisory}", file=sys.stderr)
258
293
  emit_host_cgroup_memory_limit(
259
294
  log_dir,
@@ -265,9 +300,4 @@ def _probe_and_emit_cgroup_defer(log_dir: Path) -> bool:
265
300
  memory_high=memory_high,
266
301
  advisory=advisory,
267
302
  )
268
- return (
269
- limits["memory_max"] is not None
270
- and swap_max is not None
271
- and limits["memory_max"] < metrics.mem_total_bytes()
272
- and swap_max <= swap_total # 4th guard: a >> host-swap cap can't bind -> stay armed
273
- )
303
+ return defer
@@ -33,6 +33,8 @@ def _plan(cfg, rounds: int) -> list[dict]:
33
33
  "phase": sel.phase,
34
34
  "paused": sel.paused,
35
35
  "resume_phase": sel.resume_phase,
36
+ "resume_at": sel.resume_at.isoformat() if sel.resume_at else None,
37
+ "resume_timezone": sel.resume_timezone,
36
38
  "skipped": sel.skipped,
37
39
  "active_window": sel.active_window,
38
40
  }
@@ -72,6 +74,11 @@ def _format(report: DoctorReport) -> str:
72
74
  lines.append(f" {o.phase_a} ({o.window_a}) vs {o.phase_b} ({o.window_b})")
73
75
  lines.append("phase plan:")
74
76
  for step in report.plan:
75
- where = "paused" if step["paused"] else (step["phase"] or "base")
77
+ if step["paused"]:
78
+ where = "paused"
79
+ if step["resume_at"] is not None:
80
+ where += f" -> resumes {step['resume_phase']} at {step['resume_at']}"
81
+ else:
82
+ where = step["phase"] or "base"
76
83
  lines.append(f" round {step['round']}: {where}")
77
84
  return "\n".join(lines)
@@ -297,6 +297,12 @@ def cgroup_memory_limits(
297
297
  host, or any host missing that file, has no reliable fixed-path budget
298
298
  file to read, so this returns all-``None`` rather than guessing.
299
299
 
300
+ ``cgroup_path`` is always the caller's OWN leaf; ``bounding_cgroup_path``
301
+ is the ancestor (possibly the leaf itself) that actually owns the
302
+ winning ``memory.max`` (see :func:`_bounding_ancestor_path`) -- callers
303
+ that need to tell "my own limit" from "an inherited parent/container
304
+ bound" compare the two.
305
+
300
306
  ``root`` and ``proc_self_cgroup`` default to the real paths and are
301
307
  injectable so tests can point at a fake ``/sys/fs/cgroup`` tree;
302
308
  ``self_cgroup`` lets a caller supply the resolved cgroup path directly,
@@ -307,12 +313,22 @@ def cgroup_memory_limits(
307
313
  """
308
314
  resolved = _resolve_cgroup(root, proc_self_cgroup, self_cgroup)
309
315
  if resolved is None:
310
- return {"memory_max": None, "memory_swap_max": None, "cgroup_path": None}
316
+ return {
317
+ "memory_max": None,
318
+ "memory_swap_max": None,
319
+ "cgroup_path": None,
320
+ "bounding_cgroup_path": None,
321
+ }
311
322
  cgroup_path, ancestors = resolved
323
+ # One ancestor walk for memory.max, shared for both its value and its
324
+ # owning path (the "SAME winning ancestor" _min_ancestor_candidate's
325
+ # docstring promises) -- memory.swap.max is a distinct file, its own walk.
326
+ max_candidate = _min_ancestor_candidate(root, ancestors, "memory.max")
312
327
  return {
313
- "memory_max": _min_ancestor_limit(root, ancestors, "memory.max"),
328
+ "memory_max": max_candidate[0] if max_candidate else None,
314
329
  "memory_swap_max": _min_ancestor_limit(root, ancestors, "memory.swap.max"),
315
330
  "cgroup_path": cgroup_path,
331
+ "bounding_cgroup_path": max_candidate[1] if max_candidate else None,
316
332
  }
317
333
 
318
334
 
@@ -175,6 +175,49 @@ reads/round) weren't re-measured this release since neither's code path
175
175
  moved — re-running the full harness would report the same number as
176
176
  0.2.19 for a code path this release didn't touch, not new signal.
177
177
 
178
+ ## 0.2.23 → 0.2.24
179
+
180
+ Measured the same way as the table above (see Methodology), same machine,
181
+ comparing **v0.2.23**'s base — `60c666a` (one commit past the `v0.2.23` tag:
182
+ a ruff-format-only fix, so equivalent to the shipped release for this
183
+ purpose) — against **0.2.24**, the tip of this branch at measurement time.
184
+
185
+ ### Import/startup RSS
186
+
187
+ | | v0.2.23 | 0.2.24 | delta |
188
+ |---|---|---|---|
189
+ | RSS (avg of 5 runs) | 24.0 MB | 24.1 MB | +0.03 MB (flat, within noise) |
190
+ | RSS range | 23.9–24.1 MB | 24.0–24.3 MB | |
191
+ | `sys.modules` count | 229 | 229 | +0 |
192
+
193
+ ### Module count, source LOC, largest module
194
+
195
+ Tracked source only (`git ls-tree`, same convention as the row above).
196
+
197
+ | | v0.2.23 | 0.2.24 | delta |
198
+ |---|---|---|---|
199
+ | `.py` files | 75 | 75 | +0 |
200
+ | total LOC | 18,276 | 18,325 | +49 |
201
+ | largest module | `api.py`, 990 | `api.py`, 990 | +0 |
202
+
203
+ ### Why it's flat
204
+
205
+ This release's only startup-path-adjacent change is the second
206
+ `host_cgroup_memory_limit` advisory (recommending `memory.high` when
207
+ `memory.max` is set on the supervisor's own cgroup): it lives inside the
208
+ probe function already called from the already-loaded `cli/_serve_cgroup`
209
+ module, adds no new import, and only walks its extra branch when a real
210
+ cgroup bound is present at startup. The `doctor` phase-plan resume-time
211
+ addition and the grace-kill test wall-time trim touch neither the startup
212
+ import graph nor any per-round allocation path.
213
+ `tests/invariants/test_import_footprint.py` and
214
+ `tests/invariants/test_round_alloc_growth.py` both stay green unmodified,
215
+ confirming no new eager import and no new per-round leak.
216
+
217
+ **Suite wall-time**: trimming the grace-kill negative test's sleep cuts
218
+ roughly 80s off the test suite's wall-clock time — a test-only change, not
219
+ reflected in any of the runtime numbers above.
220
+
178
221
  ## Methodology
179
222
 
180
223
  Machine: macOS 26.6.2, arm64, 16 KB pages. Python 3.11.3 (CPython, pyenv),
@@ -748,7 +748,7 @@ host-safety needs are already in-core. Detector rows require a running
748
748
  | Runaway round | `[runtime] round_timeout_s` (default 1800) <!-- authored: default round_timeout_s; SSOT agent_runner/config/models.py --> | Wall-clock kill, emits `round_timeout_kill`. For a CLI with no self-timeout (pi has no turn cap, runtime timeout, or token budget) this is the **only** brake. Per-phase override: `[phases.<name>] round_timeout_s` |
749
749
  | Cap the agent process itself | `[agent.env]` | Passed verbatim into the round subprocess env and takes precedence over the inherited `os.environ`, so e.g. `NODE_OPTIONS = "--max-old-space-size=384"` reaches a Node-based CLI |
750
750
  | Host memory pressure | `mem_pressure` detector + serve-loop admission gate | `host_health`'s signal ladder (PSI → swap-out rate → combined MemFree+MemAvailable low → unavailable) reports `warning` or `critical` — a bare `mem_available_mb` gate is unreliable on cache-poor small-memory hosts. PSI's critical bar is `[monitor.host_health] psi_full_avg10_critical` (default 60.0, i.e. sustained ~60% avg10, not a single 1% blip); `psi_some_avg10_warning` (default 5.0) gates the warning tier. `mem_pressure_gate_inert` fires once if a cache-poor-valid signal shows real pressure while the configured `mem_avail_min_mb` (default 200) gate stays green; `mem_signal_unavailable` fires once when no tier is readable. The detector itself is notify-only (`auto_action="none"`), but `serve` separately **defers the next round** while `host_health` reports **`critical`** pressure only (`round_deferred`/`round_resumed` — a mere `warning` no longer delays a round) and, mid-round, **terminates a ballooning round** once pressure reads `critical` for `mem_critical_consecutive_samples` (default 3) consecutive ~10s ticks in a row (`round_mem_terminated`, `round_mem_critical_sample` per tick, capped at `2 × mem_critical_consecutive_samples` consecutive ticks — 1..6 at the default — so a sustained-critical deferred/off run doesn't write one event per tick for the whole round; any non-critical tick resets both the streak and the cap). `in_round_mem_terminate = false` keeps sampling and counting that streak without ever terminating a round. If this process's cgroup has both `memory.max` and `memory.swap.max` set (`host_cgroup_memory_limit` at startup), the mid-round floor defers to kernel cgroup-OOM instead (`mem_pressure_deferred_to_cgroup`) — the kernel will already contain the agent within that budget. Whole-host figures, not the agent's own share <!-- authored: default mem_avail_min_mb / psi_full_avg10_critical / psi_some_avg10_warning / mem_critical_consecutive_samples; SSOT agent_runner/config/models.py --> |
751
- | Cgroup swap cap set far below what the host actually has | `host_cgroup_memory_limit` startup event (advisory fields) + one stderr line | Computed once at serve startup, alongside the existing `memory_max`/`memory_swap_max`/`cgroup_path` fields on this SAME event (no new event kind): `swap_total_bytes` (the host's total swap), `swap_cap_pct` (`memory_swap_max` as a percentage of it, `null` when either side is unknown), `memory_high` (the cgroup's `memory.high` soft-throttle threshold in bytes, `null` when unset — the cgroup read the literal `"max"`, or the file/cgroup v2 itself is unavailable), and `advisory` (a warning string, else `null`). When a finite `memory.swap.max` sits under 25% of host swap, `advisory` is set and one `agent-runner: ...` line prints to stderr — this is the blind spot where the mid-round floor can terminate a round the kernel's own cgroup-OOM would have contained on a wider cap. Advisory only: nothing here ever edits the operator's cgroup or systemd unit. The auto-defer decision above also requires `memory_swap_max` to be at most `swap_total_bytes` — a cap far ABOVE host swap can't bind before host-wide swap exhaustion either, so it can't disarm the floor |
751
+ | Cgroup memory bound missing graceful throttling, or a swap cap set far below what the host actually has | `host_cgroup_memory_limit` startup event (advisory fields) + one stderr line | Computed once at serve startup, alongside the existing `memory_max`/`memory_swap_max`/`cgroup_path` fields on this SAME event (no new event kind): `swap_total_bytes` (the host's total swap), `swap_cap_pct` (`memory_swap_max` as a percentage of it, `null` when either side is unknown), `memory_high` (the cgroup's `memory.high` soft-throttle threshold in bytes, `null` when unset — the cgroup read the literal `"max"`, or the file/cgroup v2 itself is unavailable), and `advisory` (one or two warning strings joined with `"; "`, else `null`). One advisory fires when a finite `memory.swap.max` sits under 25% of host swap — this is the blind spot where the mid-round floor can terminate a round the kernel's own cgroup-OOM would have contained on a wider cap. A second, independent advisory fires when `memory.max` is set on the supervisor's OWN cgroup (not an inherited parent slice or container root, where the operator couldn't act on the hint from their own unit anyway) without `memory.high`: it recommends adding `memory.high` below `memory.max` for graceful pre-OOM throttling, plus — only when the mid-round floor ISN'T already deferring to kernel cgroup-OOM — a caveat to also bound `memory.swap.max` or set `in_round_mem_terminate=false`, so the throttle's own PSI-full rise doesn't trip the floor mid-round. Either advisory prints its own `agent-runner: ...` line to stderr. Advisory only: nothing here ever edits the operator's cgroup or systemd unit. The auto-defer decision above also requires `memory_swap_max` to be at most `swap_total_bytes` — a cap far ABOVE host swap can't bind before host-wide swap exhaustion either, so it can't disarm the floor |
752
752
  | See cgroup memory pressure BUILDING (not just the terminate/defer verdict) | `round_cgroup_memory` event, once per round | Emitted once per round when this process's cgroup has a finite `memory.max` (see `host_cgroup_memory_limit` above): the round's peak `memory.current`/`memory.swap.current` (the max seen across the existing ~10s mid-round ticks, not the cumulative-since-startup `memory.peak`) and the `memory.events` counter DELTAS over the round (`events_high_delta`/`events_max_delta`/`events_oom_delta`/`events_oom_kill_delta`) — pressure an operator can watch build toward an OOM without SSH-ing in. Deltas, not absolutes: a nonzero `events_oom_kill_delta` means the kernel OOM-killed something inside this round specifically. `bounding_cgroup_path` names the ancestor these figures are read at (deliberately distinct from `host_cgroup_memory_limit`'s `cgroup_path`, which is this process's own leaf cgroup — the two can differ). No-op (nothing emitted) on a host with no finite cgroup bound, or if that bounding cgroup is no longer readable at round end |
753
753
  | A round dies to the kernel's own cgroup-OOM, not just a silent 137 | `round_oom_killed` event, folded from the same `round_cgroup_memory` scan | Emitted when `memory.events.oom_kill` rose over the round (the same delta `round_cgroup_memory` already computed — no second read). Pointer-only: `log_path`, `log_bytes`, and `oom_kill_delta`, never the round-log/transcript content itself. `partial_log` reports whether the supervisor appended its own truncation trailer to the round log (a residue marker, not content) so a later reader can tell the tail is supervisor-added. The round still exits 137 and counts toward the crash streak exactly as today — this event only makes the kill legible, it never changes the give-up decision |
754
754
  | Disk filling up | `disk_warning` at ≥90%, `disk_critical` at ≥95% | Sampled on `log_dir`'s partition. `disk_critical` auto-stops the service by default; `disk_warning` only alerts. Thresholds under `[monitor.host_health]` <!-- authored: disk_critical ships in the default auto_stop_on set; SSOT agent_runner/config/models.py --> |
@@ -17,7 +17,8 @@ def _write_toml(
17
17
  prompt = tmp_git_repo / "p.md"
18
18
  prompt.write_text("Body content for serve loop test. " * 50)
19
19
  log_dir = tmp_git_repo / "logs"
20
- toml.write_text(f"""
20
+ toml.write_text(
21
+ f"""
21
22
  [agent]
22
23
  command = ["{fake_agent}"]
23
24
  prompt_arg_template = ["{{prompt}}"]
@@ -28,7 +29,9 @@ round_timeout_s = {round_timeout}
28
29
  restart_delay_s = 1
29
30
  [prompt]
30
31
  file = "{prompt}"
31
- """ + phases_block)
32
+ """
33
+ + phases_block
34
+ )
32
35
  (tmp_git_repo / ".gitignore").write_text("logs/\n")
33
36
  subprocess.run(["git", "add", "."], cwd=tmp_git_repo, check=True)
34
37
  subprocess.run(
@@ -165,15 +165,18 @@ def test_run_should_wait_for_ceiling_when_marker_never_matches(tmp_path, monkeyp
165
165
  starts a countdown, so the round rides out to the wall-clock ceiling exactly
166
166
  like a round that emitted nothing at all.
167
167
 
168
- Childless (exec) + a generous 45s wall + the 0.1s patched scan interval, all
168
+ Childless (exec) + a 5s wall + the 0.1s patched scan interval, all
169
169
  mirroring test_run_should_start_grace_countdown_when_configured_marker_appears:
170
170
  if the match were ever broken (e.g. treating any marker as a hit), this round
171
- would be reaped for grace at ~1s -- well before the 45s wall -- so the
171
+ would be reaped for grace at ~1s -- well before the 5s wall -- so the
172
172
  `killed_for_grace is False` below actually exercises the match, it doesn't
173
173
  just win a timing race against a 2s wall (see the T3 review finding this test
174
- used to be vacuous under). The exec'd sleep must OUTLAST the wall (60s > 45s)
175
- -- otherwise the sleep would just exit on its own before timeout_s ever
176
- fires, making `timed_out is True` false-fail regardless of the marker logic."""
174
+ used to be vacuous under). The exec'd sleep is kept at a long 60s (must
175
+ OUTLAST the wall) -- a short sleep would let the child exit on its own
176
+ before timeout_s ever fires (a risk under `-n auto` contention, see
177
+ test_run_should_kill_for_grace_when_agent_has_no_live_children_after_result's
178
+ docstring above), making `timed_out is True` false-fail regardless of the
179
+ marker logic; only the wall (timeout_s) shrinks."""
177
180
  monkeypatch.setattr(agent_runtime, "_RESULT_SCAN_INTERVAL_S", 0.1)
178
181
  script = _write_fake_script(
179
182
  tmp_path,
@@ -186,7 +189,7 @@ def test_run_should_wait_for_ceiling_when_marker_never_matches(tmp_path, monkeyp
186
189
  command=[str(script)],
187
190
  prompt_arg_template=[],
188
191
  prompt="x",
189
- timeout_s=45,
192
+ timeout_s=5,
190
193
  log_path=log_path,
191
194
  env_extra={},
192
195
  max_grace_after_result_s=1,
@@ -203,16 +206,19 @@ def test_run_should_disable_marker_grace_when_terminal_marker_empty(tmp_path, mo
203
206
  never runs (guarded by `and marker_bytes`) -- no empty-substring-matches-
204
207
  everything trap. The round rides out to the wall-clock ceiling.
205
208
 
206
- Childless (exec) + a generous 45s wall + the 0.1s patched scan interval, all
209
+ Childless (exec) + a 5s wall + the 0.1s patched scan interval, all
207
210
  mirroring test_run_should_start_grace_countdown_when_configured_marker_appears:
208
211
  if the `and marker_bytes` guard were ever dropped, an empty marker_bytes is
209
212
  a substring of everything, so this round would be reaped for grace at ~1s --
210
- well before the 45s wall -- so `killed_for_grace is False` below actually
213
+ well before the 5s wall -- so `killed_for_grace is False` below actually
211
214
  exercises the guard, it doesn't just win a timing race against a 2s wall (see
212
215
  the T3 review finding this test used to be vacuous under). The exec'd sleep
213
- must OUTLAST the wall (60s > 45s) -- otherwise the sleep would just exit on
214
- its own before timeout_s ever fires, making `timed_out is True` false-fail
215
- regardless of the marker logic."""
216
+ is kept at a long 60s (must OUTLAST the wall) -- a short sleep would let the
217
+ child exit on its own before timeout_s ever fires (a risk under `-n auto`
218
+ contention, see
219
+ test_run_should_kill_for_grace_when_agent_has_no_live_children_after_result's
220
+ docstring above), making `timed_out is True` false-fail regardless of the
221
+ marker logic; only the wall (timeout_s) shrinks."""
216
222
  monkeypatch.setattr(agent_runtime, "_RESULT_SCAN_INTERVAL_S", 0.1)
217
223
  script = _write_fake_script(
218
224
  tmp_path,
@@ -225,7 +231,7 @@ def test_run_should_disable_marker_grace_when_terminal_marker_empty(tmp_path, mo
225
231
  command=[str(script)],
226
232
  prompt_arg_template=[],
227
233
  prompt="x",
228
- timeout_s=45,
234
+ timeout_s=5,
229
235
  log_path=log_path,
230
236
  env_extra={},
231
237
  max_grace_after_result_s=1,
@@ -97,6 +97,7 @@ def test_cgroup_memory_limits_should_return_both_limits_when_both_finite(
97
97
  "memory_max": 335544320,
98
98
  "memory_swap_max": 167772160,
99
99
  "cgroup_path": _LEAF,
100
+ "bounding_cgroup_path": _LEAF,
100
101
  }
101
102
 
102
103
 
@@ -141,6 +142,33 @@ def test_cgroup_memory_limits_should_pick_lower_of_leaf_and_ancestor_when_both_f
141
142
  assert lim["memory_max"] == 335544320
142
143
 
143
144
 
145
+ def test_cgroup_memory_limits_should_report_bounding_path_as_the_min_max_owner_when_resolved(
146
+ fake_cgroup: _FakeCgroup,
147
+ ) -> None:
148
+ """The own leaf sets memory.max itself, so it's also the bounding
149
+ ancestor -- ``bounding_cgroup_path`` equals ``cgroup_path``."""
150
+ fake_cgroup(memory_max="335544320", memory_swap_max="167772160")
151
+
152
+ lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
153
+
154
+ assert lim["bounding_cgroup_path"] == lim["cgroup_path"]
155
+
156
+
157
+ def test_cgroup_memory_limits_should_report_bounding_path_as_the_ancestor_when_leaf_unlimited(
158
+ fake_cgroup: _FakeCgroup,
159
+ ) -> None:
160
+ """The leaf itself is unlimited; a parent slice owns the real budget --
161
+ ``bounding_cgroup_path`` names that ancestor, NOT the (looser) leaf, so a
162
+ caller can tell an inherited bound from an own-scope one."""
163
+ fake_cgroup(memory_max="max", memory_swap_max="max")
164
+ fake_cgroup.set_limit("/system.slice", memory_max="335544320", memory_swap_max="167772160")
165
+
166
+ lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
167
+
168
+ assert lim["bounding_cgroup_path"] == "/system.slice"
169
+ assert lim["bounding_cgroup_path"] != lim["cgroup_path"]
170
+
171
+
144
172
  def test_cgroup_memory_limits_should_return_all_none_when_cgroup_v1_or_missing(
145
173
  fake_cgroup: _FakeCgroup,
146
174
  ) -> None:
@@ -151,7 +179,12 @@ def test_cgroup_memory_limits_should_return_all_none_when_cgroup_v1_or_missing(
151
179
 
152
180
  lim = metrics.cgroup_memory_limits(root=fake_cgroup.root, self_cgroup=fake_cgroup.self_cgroup)
153
181
 
154
- assert lim == {"memory_max": None, "memory_swap_max": None, "cgroup_path": None}
182
+ assert lim == {
183
+ "memory_max": None,
184
+ "memory_swap_max": None,
185
+ "cgroup_path": None,
186
+ "bounding_cgroup_path": None,
187
+ }
155
188
 
156
189
 
157
190
  def test_cgroup_memory_limits_should_parse_self_cgroup_from_proc_file(tmp_path: Path) -> None:
@@ -176,6 +209,7 @@ def test_cgroup_memory_limits_should_parse_self_cgroup_from_proc_file(tmp_path:
176
209
  "memory_max": 335544320,
177
210
  "memory_swap_max": 167772160,
178
211
  "cgroup_path": "/system.slice/example.service",
212
+ "bounding_cgroup_path": "/system.slice/example.service",
179
213
  }
180
214
 
181
215
 
@@ -190,7 +224,12 @@ def test_cgroup_memory_limits_should_return_all_none_when_proc_self_cgroup_missi
190
224
 
191
225
  lim = metrics.cgroup_memory_limits(root=root, proc_self_cgroup=tmp_path / "does-not-exist")
192
226
 
193
- assert lim == {"memory_max": None, "memory_swap_max": None, "cgroup_path": None}
227
+ assert lim == {
228
+ "memory_max": None,
229
+ "memory_swap_max": None,
230
+ "cgroup_path": None,
231
+ "bounding_cgroup_path": None,
232
+ }
194
233
 
195
234
 
196
235
  # --- 0.2.18 T1c fix round 1: cgroup_memory_high, a REAL memory.high read ---
@@ -277,15 +316,22 @@ def _patch_probe(
277
316
  monkeypatch,
278
317
  *,
279
318
  memory_max: int | None,
280
- memory_swap_max: int,
319
+ memory_swap_max: int | None,
281
320
  mem_total: int,
282
321
  swap_total: int,
283
322
  memory_high: int | None = None,
323
+ cgroup_path: str | None = "/x",
324
+ bounding_cgroup_path: str | None = "/x",
284
325
  ):
285
326
  monkeypatch.setattr(
286
327
  metrics,
287
328
  "cgroup_memory_limits",
288
- lambda: {"memory_max": memory_max, "memory_swap_max": memory_swap_max, "cgroup_path": "/x"},
329
+ lambda: {
330
+ "memory_max": memory_max,
331
+ "memory_swap_max": memory_swap_max,
332
+ "cgroup_path": cgroup_path,
333
+ "bounding_cgroup_path": bounding_cgroup_path,
334
+ },
289
335
  )
290
336
  monkeypatch.setattr(metrics, "mem_total_bytes", lambda: mem_total)
291
337
  monkeypatch.setattr(metrics, "swap_total_bytes", lambda: swap_total)
@@ -369,7 +415,9 @@ def test_probe_and_emit_cgroup_defer_should_gate_on_limit_plausibility(
369
415
  pytest.param(
370
416
  200_000_000, 12.5, True, "swap.max is far below host swap", id="far_below_host"
371
417
  ),
372
- pytest.param(1_280_000_000, 80.0, False, None, id="within_host"),
418
+ pytest.param(
419
+ 1_280_000_000, 80.0, True, "memory.high", id="within_host_swap_but_memory_high_unset"
420
+ ),
373
421
  ],
374
422
  )
375
423
  def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
@@ -383,8 +431,10 @@ def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
383
431
  ) -> None:
384
432
  """swap.max below the 25% advisory floor (here 12.5% of host swap) rides
385
433
  as fields on the EXISTING host_cgroup_memory_limit event -- not a new
386
- kind -- plus one stderr line; a plausible cap (80%) gets neither. Never
387
- changes the cgroup/unit either way."""
434
+ kind -- plus one stderr line; a plausible cap (80%) gets no swap-cap
435
+ advisory, but BOTH cases still leave memory.high unset on the own leaf,
436
+ so both also carry the memory.high hint (joined onto the swap-cap one
437
+ for the far-below-host case). Never changes the cgroup/unit either way."""
388
438
  from agent_runner.cli._serve_cgroup import _probe_and_emit_cgroup_defer
389
439
 
390
440
  _patch_probe(
@@ -409,6 +459,7 @@ def test_probe_and_emit_cgroup_defer_should_gate_advisory_on_swap_cap_pct(
409
459
  assert ev["swap_cap_pct"] == expected_swap_cap_pct
410
460
  assert ev["memory_high"] is None
411
461
  assert (ev["advisory"] is not None) is expect_advisory
462
+ assert "memory.high" in ev["advisory"]
412
463
 
413
464
  captured = capsys.readouterr()
414
465
  if expected_stderr_substring is None:
@@ -446,3 +497,136 @@ def test_probe_and_emit_cgroup_defer_should_emit_real_memory_high_when_set(
446
497
  for line in f.read_text().splitlines()
447
498
  ]
448
499
  assert ev["memory_high"] == 192_000_000
500
+
501
+
502
+ # --- 0.2.24 T1: the memory.high advisory, own-scope-gated + defer-aware ---
503
+ #
504
+ # memory.max set (anywhere on the ancestor chain) without memory.high means
505
+ # the operator has a hard kill ceiling but no soft pre-OOM throttle -- worth
506
+ # flagging. But cgroup_memory_limits reports memory.max as the MIN across
507
+ # ALL ancestors, so "set" can also mean an inherited parent slice or a
508
+ # container root, where "add MemoryHigh" is noise the operator can't act on
509
+ # from their own unit. The hint fires ONLY when the bounding ancestor IS the
510
+ # operator's own leaf (bounding_cgroup_path == cgroup_path), and only
511
+ # appends the PSI-floor swap caveat when the mid-round floor isn't already
512
+ # deferring to kernel cgroup-OOM. Advisory only -- never writes a cgroup or
513
+ # unit file.
514
+
515
+
516
+ def _run_probe(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, **kwargs) -> None:
517
+ from agent_runner.cli._serve_cgroup import _probe_and_emit_cgroup_defer
518
+
519
+ _patch_probe(monkeypatch, **kwargs)
520
+ log_dir = tmp_path / "logs"
521
+ log_dir.mkdir()
522
+ _probe_and_emit_cgroup_defer(log_dir)
523
+
524
+
525
+ def _only_event(tmp_path: Path) -> dict:
526
+ log_dir = tmp_path / "logs"
527
+ [ev] = [
528
+ json.loads(line)
529
+ for f in sorted(log_dir.glob("events-*.jsonl"))
530
+ for line in f.read_text().splitlines()
531
+ ]
532
+ return ev
533
+
534
+
535
+ def test_advisory_should_not_hint_memory_high_when_bound_is_an_inherited_ancestor(
536
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
537
+ ) -> None:
538
+ # memory.max finite but owned by a PARENT slice, not the operator's leaf -> no hint
539
+
540
+ _run_probe(
541
+ monkeypatch,
542
+ tmp_path,
543
+ memory_max=256_000_000,
544
+ memory_swap_max=1_280_000_000,
545
+ mem_total=462_000_000,
546
+ swap_total=1_600_000_000,
547
+ memory_high=None,
548
+ bounding_cgroup_path="/system.slice",
549
+ )
550
+
551
+ ev = _only_event(tmp_path)
552
+ assert ev["advisory"] is None
553
+
554
+
555
+ def test_advisory_should_omit_memory_high_hint_when_high_already_set(
556
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
557
+ ) -> None:
558
+ # high already set -> no hint (and no swap-cap advisory here either)
559
+
560
+ _run_probe(
561
+ monkeypatch,
562
+ tmp_path,
563
+ memory_max=256_000_000,
564
+ memory_swap_max=1_280_000_000,
565
+ mem_total=462_000_000,
566
+ swap_total=1_600_000_000,
567
+ memory_high=224_000_000,
568
+ )
569
+
570
+ ev = _only_event(tmp_path)
571
+ assert ev["advisory"] is None
572
+
573
+
574
+ def test_advisory_memory_high_hint_should_omit_swap_caveat_when_already_deferring(
575
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
576
+ ) -> None:
577
+ # both-finite + plausible -> floor defers -> hint fires WITHOUT the "bound swap" caveat
578
+
579
+ _run_probe(
580
+ monkeypatch,
581
+ tmp_path,
582
+ memory_max=256_000_000,
583
+ memory_swap_max=1_280_000_000,
584
+ mem_total=462_000_000,
585
+ swap_total=1_600_000_000,
586
+ memory_high=None,
587
+ )
588
+
589
+ ev = _only_event(tmp_path)
590
+ assert "memory.high" in ev["advisory"]
591
+ assert "swap" not in ev["advisory"].lower()
592
+
593
+
594
+ def test_advisory_memory_high_hint_should_include_swap_caveat_when_not_deferring(
595
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
596
+ ) -> None:
597
+ # memory.max bounded but memory.swap.max UNBOUNDED -> floor stays armed (no defer) -> caveat
598
+
599
+ _run_probe(
600
+ monkeypatch,
601
+ tmp_path,
602
+ memory_max=256_000_000,
603
+ memory_swap_max=None,
604
+ mem_total=462_000_000,
605
+ swap_total=1_600_000_000,
606
+ memory_high=None,
607
+ )
608
+
609
+ ev = _only_event(tmp_path)
610
+ assert "memory.high" in ev["advisory"]
611
+ assert "swap" in ev["advisory"].lower()
612
+
613
+
614
+ def test_advisory_should_be_none_when_cgroup_v2_unavailable(
615
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
616
+ ) -> None:
617
+ # no cgroup v2 -> memory_max/cgroup_path/bounding_cgroup_path all None -> own_scope stays inert
618
+
619
+ _run_probe(
620
+ monkeypatch,
621
+ tmp_path,
622
+ memory_max=None,
623
+ memory_swap_max=None,
624
+ mem_total=462_000_000,
625
+ swap_total=1_600_000_000,
626
+ memory_high=None,
627
+ cgroup_path=None,
628
+ bounding_cgroup_path=None,
629
+ )
630
+
631
+ ev = _only_event(tmp_path)
632
+ assert ev["advisory"] is None