cli-agent-runner 0.3.5__tar.gz → 0.3.7__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 (420) hide show
  1. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.vulture-whitelist.py +7 -0
  2. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/CHANGELOG.md +13 -0
  3. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/PKG-INFO +1 -1
  4. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/__init__.py +6 -0
  5. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/memory.py +64 -1
  6. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_lifecycle.py +16 -7
  7. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_notify.py +15 -6
  8. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_plugin_sandbox.py +93 -13
  9. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_procwait.py +38 -15
  10. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_version.py +2 -2
  11. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/agent_runtime.py +11 -5
  12. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/api.py +3 -0
  13. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/_serve_cgroup.py +103 -11
  14. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/_serve_round.py +149 -17
  15. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/common.py +5 -1
  16. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/doctor_cmd.py +21 -4
  17. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/serve_cmd.py +22 -7
  18. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/__init__.py +1 -0
  19. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/models.py +36 -9
  20. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/parsers.py +41 -0
  21. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/events.py +3 -0
  22. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/metrics.py +125 -6
  23. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/architecture.md +3 -0
  24. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/commands.md +5 -1
  25. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/configuration.md +4 -2
  26. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/memory-efficiency.md +61 -0
  27. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/plugins.md +2 -2
  28. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/runbook.md +4 -0
  29. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/contract/test_public_api_surface.py +3 -0
  30. cli_agent_runner-0.3.7/tests/integration/test_reap_property.py +314 -0
  31. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_spawn_round_mem_floor.py +521 -0
  32. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_architecture.py +17 -3
  33. cli_agent_runner-0.3.7/tests/invariants/test_brake_defaults_off.py +46 -0
  34. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_event_kinds_ssot.py +5 -0
  35. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_layer_2_loop_size.py +1 -1
  36. cli_agent_runner-0.3.7/tests/invariants/test_no_direct_inspect_import.py +47 -0
  37. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_peek_schema_version.py +3 -1
  38. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_timeout_budget_invariant.py +14 -3
  39. cli_agent_runner-0.3.7/tests/linux/test_memory_high_nudge_e2e.py +180 -0
  40. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_reap.py +27 -0
  41. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cgroup_probe.py +279 -3
  42. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config.py +20 -0
  43. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_nested_strictness.py +61 -0
  44. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_doctor_cmd.py +84 -0
  45. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_exec_prefix.py +3 -3
  46. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_notify.py +16 -0
  47. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_sandbox_wire.py +252 -1
  48. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_procwait.py +14 -0
  49. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_outcome.py +45 -0
  50. cli_agent_runner-0.3.7/tests/unit/test_serve_round.py +81 -0
  51. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_spawn_seam.py +61 -1
  52. cli_agent_runner-0.3.5/tests/integration/test_reap_property.py +0 -153
  53. cli_agent_runner-0.3.5/tests/unit/test_serve_round.py +0 -34
  54. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.codecov.yml +0 -0
  55. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  56. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  57. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  58. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  59. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/dependabot.yml +0 -0
  60. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/workflows/ci.yml +0 -0
  61. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.github/workflows/release.yml +0 -0
  62. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/.gitignore +0 -0
  63. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/BACKLOG.md +0 -0
  64. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/CODE_OF_CONDUCT.md +0 -0
  65. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/CONTRIBUTING.md +0 -0
  66. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/LICENSE +0 -0
  67. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/README.md +0 -0
  68. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/README.zh.md +0 -0
  69. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/SECURITY.md +0 -0
  70. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/__init__.py +0 -0
  71. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_docgen.py +0 -0
  72. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/agent.py +0 -0
  73. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/plugins.py +0 -0
  74. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/rounds.py +0 -0
  75. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_emit/serve.py +0 -0
  76. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_install.py +0 -0
  77. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_monitor_detectors.py +0 -0
  78. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_monitor_registry.py +0 -0
  79. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_monitor_state.py +0 -0
  80. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_observe.py +0 -0
  81. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_plugin_checksum.py +0 -0
  82. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_plugin_manifest.py +0 -0
  83. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_plugin_scan.py +0 -0
  84. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_redact.py +0 -0
  85. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_registry.py +0 -0
  86. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_resolve.py +0 -0
  87. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_round_outcome.py +0 -0
  88. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_round_support.py +0 -0
  89. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_sandbox_probe.py +0 -0
  90. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_serve_policy.py +0 -0
  91. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_substrate.py +0 -0
  92. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/_throttle.py +0 -0
  93. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/api_types.py +0 -0
  94. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/__init__.py +0 -0
  95. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/_constants.py +0 -0
  96. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  97. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/codewhale.py +0 -0
  98. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  99. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/gemini.py +0 -0
  100. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/kimi.py +0 -0
  101. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/builtin_plugins/pi.py +0 -0
  102. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/__init__.py +0 -0
  103. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/__main__.py +0 -0
  104. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/events_cmd.py +0 -0
  105. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/init_cmd.py +0 -0
  106. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/install_cmd.py +0 -0
  107. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/migrate_cmd.py +0 -0
  108. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/monitor_cmd.py +0 -0
  109. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/peek_cmd.py +0 -0
  110. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/round_cmd.py +0 -0
  111. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/service_cmd.py +0 -0
  112. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/cli/upgrade_cmd.py +0 -0
  113. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/clock.py +0 -0
  114. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/errors.py +0 -0
  115. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/loader.py +0 -0
  116. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/config/validators.py +0 -0
  117. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/context_store.py +0 -0
  118. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/defenses.py +0 -0
  119. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/hooks.py +0 -0
  120. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/host_health.py +0 -0
  121. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/http_progress.py +0 -0
  122. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/lifecycle.py +0 -0
  123. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/migrations.py +0 -0
  124. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/monitor.py +0 -0
  125. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/phase_select.py +0 -0
  126. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/__init__.py +0 -0
  127. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/aider.toml +0 -0
  128. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/claude.toml +0 -0
  129. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/codewhale.toml +0 -0
  130. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/gemini.toml +0 -0
  131. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/kimi.toml +0 -0
  132. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/presets/pi.toml +0 -0
  133. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/prompt_loader.py +0 -0
  134. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/remote_relay.py +0 -0
  135. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/round_log.py +0 -0
  136. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/round_view.py +0 -0
  137. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/runner.py +0 -0
  138. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/scaffold.py +0 -0
  139. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/schedule.py +0 -0
  140. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/service_unit.py +0 -0
  141. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/startup_check.py +0 -0
  142. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/agent_runner/vcs_state.py +0 -0
  143. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/build.sh +0 -0
  144. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/deploy/Dockerfile +0 -0
  145. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/deploy/docker-compose.yml +0 -0
  146. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/README.md +0 -0
  147. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/events.md +0 -0
  148. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/long-running-agents.md +0 -0
  149. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/migrations/0.2.md +0 -0
  150. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/migrations/0.3.md +0 -0
  151. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/quickstart.md +0 -0
  152. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/aider.md +0 -0
  153. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/codewhale.md +0 -0
  154. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/container-pi.md +0 -0
  155. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/deploy-all-in-one.md +0 -0
  156. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/kimi.md +0 -0
  157. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/recipes/pi.md +0 -0
  158. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/docs/thesis.md +0 -0
  159. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/pyproject.toml +0 -0
  160. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/__init__.py +0 -0
  161. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/_clock.py +0 -0
  162. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/_test_helpers.py +0 -0
  163. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/conftest.py +0 -0
  164. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/contract/__init__.py +0 -0
  165. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/__init__.py +0 -0
  166. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/conftest.py +0 -0
  167. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  168. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/test_e2e_install_systemd.py +0 -0
  169. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  170. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  171. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  172. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  173. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  174. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/generate_vulture_whitelist.py +0 -0
  175. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/__init__.py +0 -0
  176. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_auto_stop_e2e.py +0 -0
  177. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_bounded_run.py +0 -0
  178. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_context_enricher_namespacing.py +0 -0
  179. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_cooperative_grace_property.py +0 -0
  180. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_dirty_handler_seam.py +0 -0
  181. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_fresh_eyes_signal.py +0 -0
  182. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_grace_kill_emission.py +0 -0
  183. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_install_dry_run.py +0 -0
  184. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_monitor_seeded.py +0 -0
  185. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_plugin_detector_loaded.py +0 -0
  186. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_plugin_owned_paths.py +0 -0
  187. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_plugin_real_flow.py +0 -0
  188. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  189. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  190. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_scaffold_presets.py +0 -0
  191. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_serve_loop.py +0 -0
  192. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_spawn_round_wedged.py +0 -0
  193. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_substrate_fingerprint.py +0 -0
  194. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/integration/test_transient_error_backoff.py +0 -0
  195. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/__init__.py +0 -0
  196. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/_docs.py +0 -0
  197. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/_event_scan.py +0 -0
  198. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_atomic_write_enforced.py +0 -0
  199. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_authored_facts_declared.py +0 -0
  200. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  201. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_bdd_test_conventions.py +0 -0
  202. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
  203. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_catalogs.py +0 -0
  204. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_classification_ssot.py +0 -0
  205. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_config_error_consistency.py +0 -0
  206. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_config_value_set_ssot.py +0 -0
  207. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_container_stop_bound.py +0 -0
  208. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  209. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  210. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  211. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_cli_claims.py +0 -0
  212. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_operator_surface.py +0 -0
  213. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  214. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  215. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  216. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_docs_generated.py +0 -0
  217. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_docs_index_complete.py +0 -0
  218. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_entry_points_resolve.py +0 -0
  219. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_event_int_coercion.py +0 -0
  220. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_event_kind_registry.py +0 -0
  221. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_events_doc_contract.py +0 -0
  222. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  223. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_hook_contract_docs.py +0 -0
  224. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_import_footprint.py +0 -0
  225. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
  226. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  227. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_module_boundaries.py +0 -0
  228. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_module_sizes.py +0 -0
  229. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  230. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_ai_signatures.py +0 -0
  231. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_asyncio_select.py +0 -0
  232. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_internal_refs.py +0 -0
  233. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_price_logic.py +0 -0
  234. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  235. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_raw_time.py +0 -0
  236. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  237. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_no_version_refs.py +0 -0
  238. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_phase_select_stateless.py +0 -0
  239. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_plugins_config_stable.py +0 -0
  240. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_procwait_shield.py +0 -0
  241. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  242. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  243. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_round_alloc_growth.py +0 -0
  244. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  245. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_round_num_single_source.py +0 -0
  246. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_round_result_stable.py +0 -0
  247. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  248. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  249. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_upstream_schema_canary.py +0 -0
  250. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  251. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/linux/__init__.py +0 -0
  252. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/linux/test_plugin_sandbox_kill.py +0 -0
  253. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/literate/__init__.py +0 -0
  254. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/literate/parser.py +0 -0
  255. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/literate/test_parser.py +0 -0
  256. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/literate/test_quickstart.py +0 -0
  257. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/__init__.py +0 -0
  258. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime.py +0 -0
  259. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_container.py +0 -0
  260. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_grace.py +0 -0
  261. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  262. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_progress.py +0 -0
  263. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  264. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_assemble_prompt.py +0 -0
  265. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_events_stream.py +0 -0
  266. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_install.py +0 -0
  267. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_observation.py +0 -0
  268. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_read_round_num.py +0 -0
  269. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_resolve_phase.py +0 -0
  270. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_service.py +0 -0
  271. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_api_types.py +0 -0
  272. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_apply_reap_grace_env.py +0 -0
  273. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_auto_stop_gating.py +0 -0
  274. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_builtin_provenance.py +0 -0
  275. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_catalogs.py +0 -0
  276. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_claude_error_detector.py +0 -0
  277. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_claude_foreign_schema.py +0 -0
  278. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli.py +0 -0
  279. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_common.py +0 -0
  280. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_init_install.py +0 -0
  281. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_main_boundary.py +0 -0
  282. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_monitor_http.py +0 -0
  283. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  284. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_cli_upgrade.py +0 -0
  285. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_codewhale_code_precedence.py +0 -0
  286. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_codewhale_plugin.py +0 -0
  287. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_coerce_int_poison.py +0 -0
  288. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_collapse_spawn_decisions.py +0 -0
  289. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_fresh_eyes.py +0 -0
  290. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_max_rounds.py +0 -0
  291. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_prompt_delivery.py +0 -0
  292. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_schedule.py +0 -0
  293. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_stop_file.py +0 -0
  294. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_strictness.py +0 -0
  295. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  296. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_config_transient_error_action.py +0 -0
  297. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_context_store.py +0 -0
  298. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_control_char_config.py +0 -0
  299. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_default_dirty_handler.py +0 -0
  300. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_defenses.py +0 -0
  301. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_detect_hung_latch.py +0 -0
  302. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_detector_isolation.py +0 -0
  303. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_detector_protocol.py +0 -0
  304. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_dirty_handlers.py +0 -0
  305. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
  306. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_docgen.py +0 -0
  307. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_doctor_sandbox.py +0 -0
  308. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_effective_throttle_view.py +0 -0
  309. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_emit_config_migrated.py +0 -0
  310. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_emit_schedule.py +0 -0
  311. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  312. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_events.py +0 -0
  313. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_events_cmd.py +0 -0
  314. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_events_cmd_rollover.py +0 -0
  315. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_events_cmd_tail.py +0 -0
  316. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_events_jsonl_decode.py +0 -0
  317. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_example_dirty_handler.py +0 -0
  318. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_example_plugin.py +0 -0
  319. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_exit_cause.py +0 -0
  320. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_exit_classifier.py +0 -0
  321. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  322. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_gate_serve_boot.py +0 -0
  323. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_gemini_plugin.py +0 -0
  324. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_git_timeout.py +0 -0
  325. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_hook_failure_isolation.py +0 -0
  326. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_hook_redact_order.py +0 -0
  327. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_hooks.py +0 -0
  328. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_host_health.py +0 -0
  329. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_http_progress.py +0 -0
  330. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_http_progress_epoch.py +0 -0
  331. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_init_entry_points.py +0 -0
  332. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  333. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_kill_pgroup_shielding.py +0 -0
  334. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_kimi_plugin.py +0 -0
  335. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_lifecycle.py +0 -0
  336. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_load_fail_closed.py +0 -0
  337. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_loader_split.py +0 -0
  338. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_manifest_spawn_hooks.py +0 -0
  339. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_metrics.py +0 -0
  340. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  341. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_metrics_swap.py +0 -0
  342. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_migrate_cmd.py +0 -0
  343. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_migrations.py +0 -0
  344. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_assembly.py +0 -0
  345. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_cmd_severity.py +0 -0
  346. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_dedup_identity.py +0 -0
  347. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  348. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  349. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  350. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  351. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_detectors.py +0 -0
  352. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_event_tail.py +0 -0
  353. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_loop.py +0 -0
  354. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_on_alert.py +0 -0
  355. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_remote.py +0 -0
  356. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  357. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_monitor_schedule_pause.py +0 -0
  358. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_network_blip_signal_guard.py +0 -0
  359. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_open_round_log.py +0 -0
  360. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  361. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_outer_round_ceiling.py +0 -0
  362. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_peek_argparse.py +0 -0
  363. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_peek_project_isolation.py +0 -0
  364. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_peek_sandbox_block.py +0 -0
  365. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_peek_schedule.py +0 -0
  366. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_peek_select.py +0 -0
  367. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_phase_agent_launch.py +0 -0
  368. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_phase_profiles.py +0 -0
  369. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_phase_select.py +0 -0
  370. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_pi_plugin.py +0 -0
  371. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_checksum.py +0 -0
  372. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_constants.py +0 -0
  373. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_manifest.py +0 -0
  374. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_name_mismatch.py +0 -0
  375. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugin_scan_parity.py +0 -0
  376. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_plugins_config_fields.py +0 -0
  377. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_presets.py +0 -0
  378. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_primary_prompt_file_home.py +0 -0
  379. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_prompt_loader.py +0 -0
  380. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_read_json_hardening.py +0 -0
  381. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_redact.py +0 -0
  382. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_resolve.py +0 -0
  383. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_cmd.py +0 -0
  384. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_cmd_sigterm.py +0 -0
  385. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_log_helpers.py +0 -0
  386. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  387. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_round_view.py +0 -0
  388. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_runner.py +0 -0
  389. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  390. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_runner_throttle.py +0 -0
  391. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_sandbox_probe.py +0 -0
  392. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_scaffold.py +0 -0
  393. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_schedule.py +0 -0
  394. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_cmd_bounded.py +0 -0
  395. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_config_broken.py +0 -0
  396. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_crash_loop.py +0 -0
  397. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_mem_gate.py +0 -0
  398. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_phase_schedule.py +0 -0
  399. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_policy.py +0 -0
  400. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  401. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_round_log.py +0 -0
  402. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_round_oom_killed.py +0 -0
  403. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_round_terminate.py +0 -0
  404. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_round_wait_exit.py +0 -0
  405. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_schedule_gate.py +0 -0
  406. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_sentinel.py +0 -0
  407. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_startup_hooks.py +0 -0
  408. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_serve_wakeup.py +0 -0
  409. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_service_mode.py +0 -0
  410. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_service_unit.py +0 -0
  411. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_startup_battery_exit.py +0 -0
  412. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_startup_check.py +0 -0
  413. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_substrate.py +0 -0
  414. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  415. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_terminate_round_wait_exit.py +0 -0
  416. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  417. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_throttle_epoch_bound.py +0 -0
  418. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_throttle_plugin_classification.py +0 -0
  419. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_upgrade_migrate.py +0 -0
  420. {cli_agent_runner-0.3.5 → cli_agent_runner-0.3.7}/tests/unit/test_vcs_state.py +0 -0
@@ -195,6 +195,7 @@ supervisor_stale_threshold_s
195
195
  disk
196
196
  memory
197
197
  pressure
198
+ brake
198
199
 
199
200
  # agent_runner.config.models.PhaseOverride
200
201
  round_budget_s
@@ -255,6 +256,11 @@ pause_windows
255
256
  stash_idempotency_s
256
257
  dirty_action
257
258
 
259
+ # agent_runner.config.models._HostHealthBrakeConfig
260
+ memory_high
261
+ memory_high_step_pct
262
+ warning_consecutive_samples
263
+
258
264
  # agent_runner.config.models._HostHealthDiskConfig
259
265
  warning_pct
260
266
  critical_pct
@@ -270,6 +276,7 @@ some_avg10_warning
270
276
  critical_consecutive_samples
271
277
  in_round_terminate
272
278
  cgroup_growth_rate_warning_mb_per_min
279
+ in_round_nudge
273
280
 
274
281
  # agent_runner.context_store.OrphanState
275
282
  round_num
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.7] - 2026-09-15
9
+
10
+ ### Added
11
+ - Opt-in cgroup v2 `memory.high` soft-brake (`[monitor.host_health.brake] memory_high`, default off): under sustained warning-level memory pressure the supervisor reversibly writes `memory.high` on its OWN cgroup leaf to throttle a growing round before the host swaps, then restores it on recovery or at round end. `memory_high_step_pct` defaults to `0` = cap-at-current (throttle further growth with no synchronous reclaim burst — safe for SD-backed hosts); `step_pct >= 1` is opt-in aggressive reclaim that dumps ~step% of the leaf to swap at engage. Fail-open; arms only when the leaf is delegated.
12
+ - Opt-in early cooperative SIGTERM (`[monitor.host_health.pressure] in_round_nudge`, default off): fires the hard floor's SIGTERM two samples earlier so a cooperative agent gets its wrap-up grace; recorded as `round_mem_terminated` with a new `tier` field so the mem-loop give-up still converges.
13
+ - New events `memory_high_engaged` / `memory_high_released` / `memory_high_write_failed`; `doctor` and `peek --json` (schema `2.5`) report the brake as `off | armed | inert(<reason>)`.
14
+
15
+ ## [0.3.6] - 2026-09-15
16
+
17
+ ### Changed
18
+ - `agent-runner kill` now reaps a stuck round's detached descendants (a child that `setsid()`'d off its process group) the same way `serve stop` already does, so a forced stop never leaves an orphan behind.
19
+ - A stop (`SIGTERM` / `serve stop`) during a sandboxed third-party spawn hook, or during a plugin-requested `defer`, now takes effect immediately instead of after up to 30 s — and the confined hook's child process is always reaped, never left running. A background process the hook itself forks is not reaped; its abandoned output pipe is dropped after a bounded 2 s drain wait (on a normal hook exit, surfaced as `hook_failed`) rather than stalling the stop.
20
+
8
21
  ## [0.3.5] - 2026-09-15
9
22
 
10
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.3.5
3
+ Version: 0.3.7
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
@@ -32,6 +32,9 @@ from agent_runner._emit.memory import ( # noqa: F401 — public re-export
32
32
  emit_cgroup_growth_rate_warning,
33
33
  emit_host_cgroup_memory_limit,
34
34
  emit_mem_pressure_deferred_to_cgroup,
35
+ emit_memory_high_engaged,
36
+ emit_memory_high_released,
37
+ emit_memory_high_write_failed,
35
38
  emit_round_cgroup_memory,
36
39
  emit_round_deferred,
37
40
  emit_round_mem_critical_sample,
@@ -88,6 +91,9 @@ __all__ = [
88
91
  "emit_mem_loop",
89
92
  "emit_mem_loop_persistent",
90
93
  "emit_mem_pressure_deferred_to_cgroup",
94
+ "emit_memory_high_engaged",
95
+ "emit_memory_high_released",
96
+ "emit_memory_high_write_failed",
91
97
  "emit_plugin_builtin_name_squat",
92
98
  "emit_plugin_checksum_mismatch",
93
99
  "emit_plugin_sandbox_degraded",
@@ -36,6 +36,7 @@ def emit_round_mem_terminated(
36
36
  message: str,
37
37
  consecutive: int,
38
38
  context: dict,
39
+ tier: str = "terminate",
39
40
  ) -> None:
40
41
  """Emit when _spawn_round's mid-round hard floor terminated a ballooning
41
42
  round on critical host_health pressure -- the actual coma-preventer (a
@@ -45,7 +46,15 @@ def emit_round_mem_terminated(
45
46
  ``consecutive`` (the critical_streak that crossed the threshold)
46
47
  and ``context`` (Pressure.context -- the actual psi/mem numbers, e.g.
47
48
  psi_full_avg10) make the kill legible from the event stream alone, so an
48
- operator can retune host_health thresholds without SSH."""
49
+ operator can retune host_health thresholds without SSH.
50
+
51
+ ``tier`` (``"terminate"`` | ``"nudge"``) distinguishes the hard floor's
52
+ ``_terminate_round`` (TERM -> grace -> killpg) from the early cooperative
53
+ nudge (a bare ``proc.terminate()`` two samples earlier, no wait, no
54
+ killpg) -- both fire this SAME event kind so ``round_was_mem_terminated``
55
+ and the ``mem_loop`` give-up breaker count a nudged round exactly like a
56
+ hard-terminated one; the default keeps every existing caller byte-
57
+ compatible."""
49
58
  from agent_runner.events import ROUND_MEM_TERMINATED, emit
50
59
 
51
60
  emit(
@@ -57,6 +66,7 @@ def emit_round_mem_terminated(
57
66
  message=message,
58
67
  consecutive=consecutive,
59
68
  context=context,
69
+ tier=tier,
60
70
  )
61
71
 
62
72
 
@@ -246,6 +256,59 @@ def emit_mem_pressure_deferred_to_cgroup(
246
256
  emit(log_dir, MEM_PRESSURE_DEFERRED_TO_CGROUP, pid=pid, signal=signal, message=message)
247
257
 
248
258
 
259
+ def emit_memory_high_engaged(
260
+ log_dir: Path,
261
+ *,
262
+ round_num: int,
263
+ previous: str,
264
+ written: int,
265
+ memory_current: int,
266
+ write_ms: int,
267
+ signal: str,
268
+ context: dict,
269
+ rate_mb_per_min: float | None,
270
+ ) -> None:
271
+ """Emit when the reversible cgroup memory.high soft-brake engaged on serve's
272
+ own leaf under sustained warning pressure. ``previous``/``written`` bracket
273
+ the reversible write; ``signal``/``context`` are the triggering Pressure;
274
+ ``rate_mb_per_min`` rides along as growth-rate context for Pi calibration."""
275
+ from agent_runner.events import MEMORY_HIGH_ENGAGED, emit
276
+
277
+ emit(
278
+ log_dir,
279
+ MEMORY_HIGH_ENGAGED,
280
+ round_num=round_num,
281
+ previous=previous,
282
+ written=written,
283
+ memory_current=memory_current,
284
+ write_ms=write_ms,
285
+ signal=signal,
286
+ context=context,
287
+ rate_mb_per_min=rate_mb_per_min,
288
+ )
289
+
290
+
291
+ def emit_memory_high_released(log_dir: Path, *, round_num: int, reason: str) -> None:
292
+ """Emit when the soft-brake was restored to its stashed value. ``reason`` is
293
+ ``recovered`` (N healthy ticks) or ``round_end`` (the finally restore, which
294
+ covers every exit path out of the round incl. wedged/exception)."""
295
+ from agent_runner.events import MEMORY_HIGH_RELEASED, emit
296
+
297
+ emit(log_dir, MEMORY_HIGH_RELEASED, round_num=round_num, reason=reason)
298
+
299
+
300
+ def emit_memory_high_write_failed(log_dir: Path, *, round_num: int, errno: int | None) -> None:
301
+ """Emit once when a memory.high write (engage or restore) hit an OSError --
302
+ fail-open: the brake disarms for the round and serve continues unthrottled,
303
+ exactly as when the brake is configured off. ``errno`` is the real OSError
304
+ code from the engage path; the restore path (which only sees a bool from
305
+ :func:`agent_runner.metrics.restore_leaf_memory_high`) passes ``None`` --
306
+ never ``0``, which conventionally reads as POSIX success."""
307
+ from agent_runner.events import MEMORY_HIGH_WRITE_FAILED, emit
308
+
309
+ emit(log_dir, MEMORY_HIGH_WRITE_FAILED, round_num=round_num, errno=errno)
310
+
311
+
249
312
  def emit_cgroup_growth_rate_warning(
250
313
  log_dir: Path,
251
314
  *,
@@ -181,17 +181,26 @@ def _terminate_round_pid(pid: int) -> None:
181
181
  """TERM-first -> grace -> SIGKILL a bare round-child pid.
182
182
 
183
183
  Mirrors ``serve_cmd._terminate_round``'s shape, but this runs from a
184
- SEPARATE CLI process (``kill()``) that only has the pid not serve's own
184
+ SEPARATE CLI process (``kill()``) that only has the pid -- not serve's own
185
185
  ``Popen`` handle. Never ``killpg``: the round is ``start_new_session=True``,
186
- so its own pgid holds only itself, and even a killpg on it cannot reach the
187
- agent (itself a separate session again, ``agent_runtime.py``) only the
188
- round's own SIGTERM -> KeyboardInterrupt handler (``round_cmd.py``) walks
189
- that link and reaps the agent pgroup via ``_kill_pgroup``. A plain SIGTERM
190
- here fires exactly that handler; SIGKILL is only the last-resort escalation
191
- for a round that doesn't even get to run its handler."""
186
+ so a plain SIGTERM here fires the round's own SIGTERM -> KeyboardInterrupt
187
+ handler (``round_cmd.py``), which walks the link to the agent pgroup and
188
+ reaps it via ``_kill_pgroup`` (itself snapshotting and reaping any stray).
189
+ SIGKILL is only the last-resort escalation for a round that never even runs
190
+ its handler -- and on THAT path the leader's own cooperative reap never
191
+ happened, so we snapshot the leader's descendants up front (while the leader
192
+ subtree is still resolvable) via a bare-pid shim and reap the strays here:
193
+ a descendant that ``setsid()``'d off the leader's pgroup would otherwise
194
+ survive the leader's death, orphaned. Symmetric with ``serve stop``."""
195
+ import types
196
+
197
+ from agent_runner import agent_runtime
198
+
199
+ stray = agent_runtime._snapshot_stray_descendants(types.SimpleNamespace(pid=pid))
192
200
  send_signal_to_pid(pid, signal.SIGTERM)
193
201
  if _await_pid_exit(pid, _ROUND_TERM_GRACE_S):
194
202
  send_signal_to_pid(pid, signal.SIGKILL)
203
+ agent_runtime._kill_stray_descendants(stray)
195
204
 
196
205
 
197
206
  def stop(project: str | Path) -> ServiceStatus:
@@ -78,11 +78,14 @@ def drain(fd: int) -> None:
78
78
  ``select()`` on it instantly ready -- an undrained fd busy-spins its
79
79
  caller at 100% CPU forever after the first wake, since a level-triggered
80
80
  fd stays readable until read. Advisory: the bytes carry no payload and are
81
- discarded. Used only by :meth:`Listener.wait` (its own timeout branch) --
82
- the pause/sleep waits (``_interruptible_sleep``/``_pause_poll``) are the
83
- sole consumers of a live ``Listener``. Nothing watches this fd mid-round
84
- (``_spawn_round`` only ever waits on the round leader's own exit fd), so
85
- there is no other caller that must drain it itself.
81
+ discarded. Two callers: :meth:`Listener.wait` self-drains its own fd on the
82
+ wake branch (the pause/sleep waits ``_interruptible_sleep``/``_pause_poll``
83
+ are the consumers of a live ``Listener``); and the sandboxed-hook trampoline
84
+ (``_plugin_sandbox._run_child_process``, P3) drains the CALLER-OWNED wake fd
85
+ it was handed on the ``"woken"`` branch of its exit wait, before re-checking
86
+ ``should_stop()`` -- that fd is the serve listener's, watched by
87
+ ``wait_exit`` for the duration of the hook, so the trampoline must drain it
88
+ itself rather than leave the next wait instantly ready.
86
89
  """
87
90
  while True:
88
91
  try:
@@ -179,7 +182,13 @@ class Listener:
179
182
  """
180
183
  deadline = clock.monotonic() + timeout_s
181
184
  remaining = max(0.0, deadline - clock.monotonic())
182
- ready, _, _ = select.select([self.fd], [], [], remaining)
185
+ try:
186
+ ready, _, _ = select.select([self.fd], [], [], remaining)
187
+ except ValueError:
188
+ # fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
189
+ # degrade to a plain timed sleep instead of crashing.
190
+ clock.sleep(remaining)
191
+ return False
183
192
  if not ready:
184
193
  return False
185
194
 
@@ -17,12 +17,15 @@ from __future__ import annotations
17
17
  import json
18
18
  import subprocess
19
19
  import sys
20
+ from collections.abc import Callable
20
21
  from pathlib import Path
21
22
  from typing import Literal
22
23
 
23
- from agent_runner import hooks
24
+ from agent_runner import _notify, hooks
25
+ from agent_runner._procwait import wait_exit
24
26
  from agent_runner._registry import resolve_entry_target
25
27
  from agent_runner.api_types import DirtyOutcome, SpawnDecision
28
+ from agent_runner.clock import SYSTEM_CLOCK
26
29
 
27
30
  _TRAMPOLINE_TIMEOUT_S = 30.0 # wall-clock; not config-tunable this release
28
31
  _MAX_WIRE_BYTES = 64 * 1024
@@ -30,6 +33,17 @@ _MAX_STDERR_BYTES = 8 * 1024 # hard read cap; a runaway child can't OOM the sup
30
33
  _REF_CAP = 256
31
34
  _STDERR_CAP = 512
32
35
 
36
+ # Total wall budget for draining a confined child's pipes AFTER it is reaped. A
37
+ # process the hook itself fork()'d inherits the child's stdout/stderr and can hold
38
+ # them open past the child's own death, so the drain join is BOUNDED, not
39
+ # unbounded -- otherwise a single such pipe-holder stalls the stop forever. The
40
+ # pair of joins shares ONE absolute deadline (not _DRAIN_JOIN_S each), and
41
+ # _DRAIN_JOIN_S + 3 <= _lifecycle._PID_SIGNAL_GRACE_S leaves >=3 s of the
42
+ # stop-confirm window for the rest of the unwind (pinned by
43
+ # tests/invariants/test_timeout_budget_invariant.py; a literal mirror -- this
44
+ # module must never import _lifecycle in production).
45
+ _DRAIN_JOIN_S = 2
46
+
33
47
  # The child's environment is built default-DENY from this allowlist -- NOT copied
34
48
  # from the parent's, which holds the real agent secrets (ANTHROPIC_*, and every
35
49
  # *_API_KEY/*_TOKEN/*_SECRET agent_runtime injects). A confined plugin is Python
@@ -126,6 +140,13 @@ def _ctx_from_wire(data: dict) -> hooks.HookContext:
126
140
  # ---------------------------------------------------------------------------
127
141
 
128
142
 
143
+ class SpawnHookInterrupted(Exception): # noqa: N818 -- a control-flow signal, not an error
144
+ """Raised by ``_run_child_process`` when ``should_stop()`` fires on a wake
145
+ during a sandboxed spawn hook -- distinct from a plugin failure so the seam
146
+ returns ``None`` WITHOUT emitting ``hook_failed`` (a stop is not the plugin's
147
+ fault). Never raised on the dirty round path, which passes no ``wake_fd``."""
148
+
149
+
129
150
  def run_hook_sandboxed(
130
151
  hook_kind: Literal["spawn_hook", "dirty_handler"],
131
152
  module_path: str,
@@ -137,6 +158,8 @@ def run_hook_sandboxed(
137
158
  dirty_files: list[str] | None = None,
138
159
  view: hooks.SpawnView | None = None,
139
160
  timeout_s: float = _TRAMPOLINE_TIMEOUT_S,
161
+ wake_fd: int | None = None,
162
+ should_stop: Callable[[], bool] | None = None,
140
163
  ) -> SpawnDecision | DirtyOutcome | None:
141
164
  """Launch the confinement child for one third-party hook call and return its
142
165
  validated outcome. ``module_path``/``attr_path`` locate the plugin the child
@@ -172,6 +195,8 @@ def run_hook_sandboxed(
172
195
  ],
173
196
  json.dumps(payload).encode("utf-8"),
174
197
  timeout_s,
198
+ wake_fd=wake_fd,
199
+ should_stop=should_stop,
175
200
  )
176
201
  if returncode < 0:
177
202
  from agent_runner.api import emit_plugin_sandbox_kill
@@ -224,14 +249,50 @@ def _drain_capped(stream, cap: int, out: list[bytes]) -> None:
224
249
  out.append(bytes(data))
225
250
 
226
251
 
252
+ def _join_drain_threads(threads: tuple, budget_s: float) -> bool:
253
+ """Join the drain threads under ONE absolute deadline -- the whole set is
254
+ bounded by ``budget_s``, NOT ``budget_s`` each -- so a process the hook
255
+ ``fork()``'d that still holds the reaped child's output pipe open cannot stall
256
+ the join past ``budget_s``. Returns True iff every thread finished draining
257
+ within the budget (the normal-exit path raises on False; the ``BaseException``
258
+ cleanup path ignores it, since it is already unwinding)."""
259
+ join_deadline = SYSTEM_CLOCK.monotonic() + budget_s
260
+ for t in threads:
261
+ t.join(max(0.0, join_deadline - SYSTEM_CLOCK.monotonic()))
262
+ return not any(t.is_alive() for t in threads)
263
+
264
+
227
265
  def _run_child_process(
228
- argv: list[str], stdin_bytes: bytes, timeout_s: float
266
+ argv: list[str],
267
+ stdin_bytes: bytes,
268
+ timeout_s: float,
269
+ *,
270
+ wake_fd: int | None = None,
271
+ should_stop: Callable[[], bool] | None = None,
229
272
  ) -> tuple[int, bytes, bytes]:
230
273
  """Spawn the confined child with the minimal env and read BOUNDED stdout/stderr
231
274
  concurrently (a thread per stream), so a plugin printing without limit within
232
275
  the wall-timeout cannot exhaust supervisor memory. Concurrent draining also
233
- avoids a full-pipe deadlock. On timeout the child is killed and
234
- ``TimeoutExpired`` propagates (the caller isolates it as ``hook_failed``)."""
276
+ avoids a full-pipe deadlock.
277
+
278
+ The exit wait is the fd-driven ``_procwait.wait_exit`` (not a sleep-poll):
279
+ ``"exited"`` reaps and returns; ``"timeout"`` kills and raises
280
+ ``TimeoutExpired`` (the caller isolates it as ``hook_failed``); ``"woken"``
281
+ (only when ``wake_fd`` is set) drains the advisory byte and, if
282
+ ``should_stop()`` is now true, kills the child and raises
283
+ ``SpawnHookInterrupted`` -- otherwise a stray ring re-enters the wait on the
284
+ SAME ABSOLUTE deadline so it never shortens the hook's budget. The whole wait
285
+ is wrapped so the confined child NEVER outlives its parent: any BaseException
286
+ (a round ``KeyboardInterrupt``, a timeout, an interrupt) kills and reaps the
287
+ child before propagating.
288
+
289
+ A process the hook itself ``fork()``s is NOT reaped (that child has no process
290
+ group of its own -- see the Option-B follow-up), and it can inherit and hold
291
+ the child's stdout/stderr open past the child's own death; so the drain joins
292
+ are BOUNDED (``_DRAIN_JOIN_S``, one absolute deadline across the pair) at BOTH
293
+ the normal-exit tail and the ``BaseException`` cleanup -- such a pipe-holder can
294
+ never stall the parent. On the normal path a timed-out drain surfaces as a
295
+ ``RuntimeError`` the seam isolates as ``hook_failed``."""
235
296
  import threading
236
297
 
237
298
  proc = subprocess.Popen(
@@ -243,9 +304,17 @@ def _run_child_process(
243
304
  )
244
305
  out_box: list[bytes] = []
245
306
  err_box: list[bytes] = []
246
- # +1 so an over-cap stdout stays over the limit and _parse_dirty_stdout rejects it.
247
- t_out = threading.Thread(target=_drain_capped, args=(proc.stdout, _MAX_WIRE_BYTES + 1, out_box))
248
- t_err = threading.Thread(target=_drain_capped, args=(proc.stderr, _MAX_STDERR_BYTES, err_box))
307
+ # daemon=True is load-bearing, not cosmetic: nothing in agent_runner/ calls
308
+ # os._exit, so a non-daemon drain thread still blocked on a fork()'d
309
+ # pipe-holder's read() would be joined by threading._shutdown() at interpreter
310
+ # exit and re-hang the stop the bounded join below just rescued. Matches the
311
+ # repo's other pump threads (remote_relay._pump, agent_runtime._write_stdin).
312
+ t_out = threading.Thread(
313
+ target=_drain_capped, args=(proc.stdout, _MAX_WIRE_BYTES + 1, out_box), daemon=True
314
+ )
315
+ t_err = threading.Thread(
316
+ target=_drain_capped, args=(proc.stderr, _MAX_STDERR_BYTES, err_box), daemon=True
317
+ )
249
318
  t_out.start()
250
319
  t_err.start()
251
320
  try:
@@ -255,15 +324,26 @@ def _run_child_process(
255
324
  pass
256
325
  finally:
257
326
  proc.stdin.close()
258
- proc.wait(timeout=timeout_s)
259
- except subprocess.TimeoutExpired:
327
+ deadline = SYSTEM_CLOCK.monotonic() + timeout_s
328
+ while True:
329
+ outcome = wait_exit(proc, deadline=deadline, wake_fd=wake_fd)
330
+ if outcome == "exited":
331
+ proc.wait()
332
+ break
333
+ if outcome == "timeout":
334
+ raise subprocess.TimeoutExpired(proc.args, timeout_s)
335
+ _notify.drain(wake_fd)
336
+ if should_stop is not None and should_stop():
337
+ raise SpawnHookInterrupted(f"spawn hook interrupted for {argv[0]!r}")
338
+ except BaseException:
260
339
  proc.kill()
261
340
  proc.wait()
262
- t_out.join()
263
- t_err.join()
341
+ _join_drain_threads((t_out, t_err), _DRAIN_JOIN_S)
264
342
  raise
265
- t_out.join()
266
- t_err.join()
343
+ if not _join_drain_threads((t_out, t_err), _DRAIN_JOIN_S):
344
+ raise RuntimeError(
345
+ "drain timed out: a process the hook forked still holds its output pipe (not reaped)"
346
+ )
267
347
  return proc.returncode, out_box[0], err_box[0]
268
348
 
269
349
 
@@ -96,12 +96,13 @@ def wait_exit(
96
96
  *,
97
97
  deadline: float,
98
98
  clock: Clock = SYSTEM_CLOCK,
99
- ) -> Literal["exited", "timeout"]:
100
- """Block until ``proc`` exits or ``deadline`` (a ``clock.monotonic()``
101
- timestamp) is reached, whichever is soonest. Never calls
102
- ``os.waitpid``/``proc.wait``/``proc.poll`` on the fast path -- a caller
103
- that gets back ``"exited"`` must reap ``proc`` itself right after this
104
- returns (see the module docstring).
99
+ wake_fd: int | None = None,
100
+ ) -> Literal["exited", "timeout", "woken"]:
101
+ """Block until ``proc`` exits, ``deadline`` (a ``clock.monotonic()``
102
+ timestamp) is reached, or ``wake_fd`` becomes readable -- whichever is
103
+ soonest. Never calls ``os.waitpid``/``proc.wait``/``proc.poll`` on the
104
+ fast path -- a caller that gets back ``"exited"`` must reap ``proc``
105
+ itself right after this returns (see the module docstring).
105
106
 
106
107
  Opens and closes its own ``exit_fd(proc)`` registration internally, once
107
108
  per call, in a single ``select.select`` -- callers invoke this at most
@@ -110,16 +111,27 @@ def wait_exit(
110
111
  across calls.
111
112
 
112
113
  Returns ``"exited"`` (the caller should immediately reap, e.g.
113
- ``proc.wait()``) or ``"timeout"`` (deadline reached, proc still running,
114
- caller re-evaluates its own logic). When ``proc`` has already exited by
115
- the time ``deadline`` is reached, the exit fd is ready in the SAME
116
- ``select`` call that also times out -- the exit always wins that tie, so
117
- an already-dead proc is never mistakenly reported as ``"timeout"``.
114
+ ``proc.wait()``), ``"timeout"`` (deadline reached, proc still running,
115
+ caller re-evaluates its own logic), or ``"woken"`` (only possible when
116
+ ``wake_fd`` is given: it became readable before either of the other two).
117
+ When ``proc`` has already exited by the time ``deadline`` is reached, the
118
+ exit fd is ready in the SAME ``select`` call that also times out -- the
119
+ exit always wins that tie, so an already-dead proc is never mistakenly
120
+ reported as ``"timeout"``. Likewise, when both the exit fd and
121
+ ``wake_fd`` are ready in the same ``select`` call, the exit fd wins --
122
+ an already-dead proc is never mistakenly reported as ``"woken"``.
123
+ ``wake_fd`` is caller-owned: this function neither opens, closes, nor
124
+ drains it (the caller drains it after a ``"woken"`` return); ``_procwait``
125
+ never imports ``_notify``.
118
126
 
119
127
  Fallback (``exit_fd(proc)`` is ``None``): polls ``proc.poll()`` at a
120
128
  short, fixed cadence via ``clock.sleep``, with the same two return
121
- meanings, so a test driving this path with a fake clock still makes
122
- progress without blocking on real wall time.
129
+ meanings (``wake_fd`` is IGNORED on this path -- a documented latency
130
+ degrade, not a "woken" report), so a test driving this path with a fake
131
+ clock still makes progress without blocking on real wall time. The same
132
+ poll fallback is also used when the fast path's ``select.select`` raises
133
+ ``ValueError`` (a fd >= ``FD_SETSIZE``, 1024, is unselectable) instead of
134
+ crashing.
123
135
 
124
136
  Short-circuits to ``"exited"`` when ``proc.returncode`` is already set:
125
137
  this owner has already reaped it, so its pid is freed (and may be reused).
@@ -139,8 +151,19 @@ def wait_exit(
139
151
 
140
152
  try:
141
153
  remaining = max(0.0, deadline - clock.monotonic())
142
- ready, _, _ = select.select([fd], [], [], remaining)
143
- return "exited" if ready else "timeout"
154
+ rlist = [fd, wake_fd] if wake_fd is not None else [fd]
155
+ try:
156
+ ready, _, _ = select.select(rlist, [], [], remaining)
157
+ except ValueError:
158
+ # fd >= FD_SETSIZE (1024) makes select.select raise ValueError --
159
+ # degrade to the poll fallback (its documented latency) instead
160
+ # of crashing.
161
+ return _wait_exit_by_polling(proc, deadline=deadline, clock=clock)
162
+ if fd in ready:
163
+ return "exited"
164
+ if ready:
165
+ return "woken"
166
+ return "timeout"
144
167
  finally:
145
168
  os.close(fd)
146
169
 
@@ -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.3.5'
22
- __version_tuple__ = version_tuple = (0, 3, 5)
21
+ __version__ = version = '0.3.7'
22
+ __version_tuple__ = version_tuple = (0, 3, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -141,7 +141,7 @@ def _snapshot_stray_descendants(proc: subprocess.Popen) -> list[dict]:
141
141
  while the leader (and hence its subtree) is still alive -- both reap sites
142
142
  (``_kill_pgroup`` and ``cli/_serve_round._terminate_round``) open with it, so
143
143
  the capture-before-signal ordering lives in ONE place, not copy-pasted."""
144
- stray, _ignored = _live_children(proc)
144
+ stray, _ignored = _live_children(proc, max_n=None)
145
145
  _capture_descendant_pgids(stray)
146
146
  return stray
147
147
 
@@ -206,7 +206,7 @@ def _live_children(
206
206
  proc: subprocess.Popen,
207
207
  *,
208
208
  ignore_patterns: list[re.Pattern[str]] | None = None,
209
- max_n: int = 5,
209
+ max_n: int | None = 5,
210
210
  ) -> tuple[list[dict], list[dict]]:
211
211
  """Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
212
212
 
@@ -217,6 +217,12 @@ def _live_children(
217
217
  events-*.jsonl. Ignore-pattern MATCHING runs against the full cmdline
218
218
  (detection unchanged); only what we STORE is minimized.
219
219
 
220
+ ``max_n`` (default 5) bounds only the event-persisted lists this returns
221
+ to a caller that writes them to events-*.jsonl. The reap snapshot
222
+ (``_snapshot_stray_descendants``) passes ``max_n=None`` — a real subtree
223
+ can exceed 5 live descendants, and undercounting it here would leave the
224
+ excess unreaped, not merely under-logged.
225
+
220
226
  The stored name is read from the kernel-reported process name (comm —
221
227
  e.g. /proc/<pid>/comm on Linux), not from ``Path(argv[0]).name``: comm is
222
228
  derived from the exec()'d binary, not from argv[0], and is itself
@@ -251,11 +257,11 @@ def _live_children(
251
257
  matched = p.pattern
252
258
  break
253
259
  if matched is not None:
254
- if len(ignored) < max_n:
260
+ if max_n is None or len(ignored) < max_n:
255
261
  ignored.append({"name": name, "pid": pid, "matched": matched})
256
- elif len(live) < max_n:
262
+ elif max_n is None or len(live) < max_n:
257
263
  live.append({"name": name, "pid": pid})
258
- if len(live) >= max_n and len(ignored) >= max_n:
264
+ if max_n is not None and len(live) >= max_n and len(ignored) >= max_n:
259
265
  break
260
266
  return live, ignored
261
267
 
@@ -41,6 +41,9 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
41
41
  emit_mem_loop,
42
42
  emit_mem_loop_persistent,
43
43
  emit_mem_pressure_deferred_to_cgroup,
44
+ emit_memory_high_engaged,
45
+ emit_memory_high_released,
46
+ emit_memory_high_write_failed,
44
47
  emit_plugin_builtin_name_squat,
45
48
  emit_plugin_checksum_mismatch,
46
49
  emit_plugin_sandbox_degraded,