cli-agent-runner 0.3.3__tar.gz → 0.3.4__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 (413) hide show
  1. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/CHANGELOG.md +9 -0
  2. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/PKG-INFO +1 -1
  3. cli_agent_runner-0.3.4/agent_runner/_notify.py +245 -0
  4. cli_agent_runner-0.3.4/agent_runner/_procwait.py +165 -0
  5. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_throttle.py +49 -6
  6. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_version.py +2 -2
  7. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/agent_runtime.py +26 -15
  8. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/_serve_round.py +84 -41
  9. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/events_cmd.py +29 -24
  10. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/serve_cmd.py +46 -7
  11. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/events.py +2 -0
  12. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/memory-efficiency.md +72 -3
  13. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/pyproject.toml +2 -1
  14. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/conftest.py +24 -0
  15. cli_agent_runner-0.3.4/tests/integration/test_reap_property.py +153 -0
  16. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_spawn_round_mem_floor.py +35 -10
  17. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_architecture.py +6 -0
  18. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_import_footprint.py +11 -0
  19. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_layer_2_loop_size.py +4 -1
  20. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_module_boundaries.py +1 -0
  21. cli_agent_runner-0.3.4/tests/invariants/test_no_asyncio_select.py +253 -0
  22. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_vacuous_invariants.py +2 -0
  23. cli_agent_runner-0.3.4/tests/invariants/test_procwait_shield.py +92 -0
  24. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_grace.py +9 -50
  25. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd.py +32 -19
  26. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd_rollover.py +21 -2
  27. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_cmd_tail.py +31 -9
  28. cli_agent_runner-0.3.4/tests/unit/test_kill_pgroup_shielding.py +110 -0
  29. cli_agent_runner-0.3.4/tests/unit/test_notify.py +174 -0
  30. cli_agent_runner-0.3.4/tests/unit/test_procwait.py +113 -0
  31. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner_throttle.py +7 -2
  32. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_phase_schedule.py +8 -0
  33. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_cgroup_spine.py +24 -1
  34. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_terminate.py +19 -2
  35. cli_agent_runner-0.3.4/tests/unit/test_serve_round_wait_exit.py +81 -0
  36. cli_agent_runner-0.3.4/tests/unit/test_serve_wakeup.py +169 -0
  37. cli_agent_runner-0.3.4/tests/unit/test_terminate_round_wait_exit.py +96 -0
  38. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.codecov.yml +0 -0
  39. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  40. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  41. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  42. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  43. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/dependabot.yml +0 -0
  44. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/workflows/ci.yml +0 -0
  45. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.github/workflows/release.yml +0 -0
  46. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.gitignore +0 -0
  47. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/.vulture-whitelist.py +0 -0
  48. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/BACKLOG.md +0 -0
  49. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/CODE_OF_CONDUCT.md +0 -0
  50. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/CONTRIBUTING.md +0 -0
  51. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/LICENSE +0 -0
  52. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/README.md +0 -0
  53. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/README.zh.md +0 -0
  54. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/SECURITY.md +0 -0
  55. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/__init__.py +0 -0
  56. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_docgen.py +0 -0
  57. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/__init__.py +0 -0
  58. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/agent.py +0 -0
  59. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/memory.py +0 -0
  60. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/plugins.py +0 -0
  61. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/rounds.py +0 -0
  62. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_emit/serve.py +0 -0
  63. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_install.py +0 -0
  64. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_lifecycle.py +0 -0
  65. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_detectors.py +0 -0
  66. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_registry.py +0 -0
  67. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_monitor_state.py +0 -0
  68. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_observe.py +0 -0
  69. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_checksum.py +0 -0
  70. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_manifest.py +0 -0
  71. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_sandbox.py +0 -0
  72. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_plugin_scan.py +0 -0
  73. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_redact.py +0 -0
  74. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_registry.py +0 -0
  75. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_resolve.py +0 -0
  76. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_round_outcome.py +0 -0
  77. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_round_support.py +0 -0
  78. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_sandbox_probe.py +0 -0
  79. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_serve_policy.py +0 -0
  80. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/_substrate.py +0 -0
  81. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/api.py +0 -0
  82. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/api_types.py +0 -0
  83. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/__init__.py +0 -0
  84. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/_constants.py +0 -0
  85. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  86. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/codewhale.py +0 -0
  87. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  88. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/gemini.py +0 -0
  89. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/kimi.py +0 -0
  90. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/builtin_plugins/pi.py +0 -0
  91. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/__init__.py +0 -0
  92. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/__main__.py +0 -0
  93. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/_serve_cgroup.py +0 -0
  94. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/common.py +0 -0
  95. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/doctor_cmd.py +0 -0
  96. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/init_cmd.py +0 -0
  97. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/install_cmd.py +0 -0
  98. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/migrate_cmd.py +0 -0
  99. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/monitor_cmd.py +0 -0
  100. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/peek_cmd.py +0 -0
  101. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/round_cmd.py +0 -0
  102. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/service_cmd.py +0 -0
  103. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/cli/upgrade_cmd.py +0 -0
  104. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/clock.py +0 -0
  105. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/__init__.py +0 -0
  106. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/errors.py +0 -0
  107. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/loader.py +0 -0
  108. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/models.py +0 -0
  109. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/parsers.py +0 -0
  110. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/config/validators.py +0 -0
  111. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/context_store.py +0 -0
  112. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/defenses.py +0 -0
  113. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/hooks.py +0 -0
  114. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/host_health.py +0 -0
  115. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/http_progress.py +0 -0
  116. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/lifecycle.py +0 -0
  117. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/metrics.py +0 -0
  118. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/migrations.py +0 -0
  119. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/monitor.py +0 -0
  120. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/phase_select.py +0 -0
  121. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/__init__.py +0 -0
  122. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/aider.toml +0 -0
  123. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/claude.toml +0 -0
  124. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/codewhale.toml +0 -0
  125. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/gemini.toml +0 -0
  126. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/kimi.toml +0 -0
  127. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/presets/pi.toml +0 -0
  128. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/prompt_loader.py +0 -0
  129. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/remote_relay.py +0 -0
  130. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/round_log.py +0 -0
  131. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/round_view.py +0 -0
  132. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/runner.py +0 -0
  133. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/scaffold.py +0 -0
  134. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/schedule.py +0 -0
  135. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/service_unit.py +0 -0
  136. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/startup_check.py +0 -0
  137. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/agent_runner/vcs_state.py +0 -0
  138. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/build.sh +0 -0
  139. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/deploy/Dockerfile +0 -0
  140. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/deploy/docker-compose.yml +0 -0
  141. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/README.md +0 -0
  142. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/architecture.md +0 -0
  143. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/commands.md +0 -0
  144. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/configuration.md +0 -0
  145. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/events.md +0 -0
  146. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/long-running-agents.md +0 -0
  147. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/migrations/0.2.md +0 -0
  148. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/migrations/0.3.md +0 -0
  149. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/plugins.md +0 -0
  150. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/quickstart.md +0 -0
  151. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/aider.md +0 -0
  152. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/codewhale.md +0 -0
  153. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/container-pi.md +0 -0
  154. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/deploy-all-in-one.md +0 -0
  155. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/kimi.md +0 -0
  156. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/recipes/pi.md +0 -0
  157. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/runbook.md +0 -0
  158. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/docs/thesis.md +0 -0
  159. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/__init__.py +0 -0
  160. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/_clock.py +0 -0
  161. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/_test_helpers.py +0 -0
  162. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/contract/__init__.py +0 -0
  163. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/contract/test_public_api_surface.py +0 -0
  164. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/__init__.py +0 -0
  165. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/conftest.py +0 -0
  166. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  167. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_install_systemd.py +0 -0
  168. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  169. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  170. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  171. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  172. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  173. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/generate_vulture_whitelist.py +0 -0
  174. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/__init__.py +0 -0
  175. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_auto_stop_e2e.py +0 -0
  176. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_bounded_run.py +0 -0
  177. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_context_enricher_namespacing.py +0 -0
  178. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_dirty_handler_seam.py +0 -0
  179. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_fresh_eyes_signal.py +0 -0
  180. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_grace_kill_emission.py +0 -0
  181. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_install_dry_run.py +0 -0
  182. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_monitor_seeded.py +0 -0
  183. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_detector_loaded.py +0 -0
  184. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_owned_paths.py +0 -0
  185. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_plugin_real_flow.py +0 -0
  186. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  187. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  188. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_scaffold_presets.py +0 -0
  189. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_serve_loop.py +0 -0
  190. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_spawn_round_wedged.py +0 -0
  191. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_substrate_fingerprint.py +0 -0
  192. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/integration/test_transient_error_backoff.py +0 -0
  193. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/__init__.py +0 -0
  194. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/_docs.py +0 -0
  195. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/_event_scan.py +0 -0
  196. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_atomic_write_enforced.py +0 -0
  197. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_authored_facts_declared.py +0 -0
  198. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  199. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_bdd_test_conventions.py +0 -0
  200. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
  201. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_catalogs.py +0 -0
  202. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_classification_ssot.py +0 -0
  203. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_config_error_consistency.py +0 -0
  204. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_config_value_set_ssot.py +0 -0
  205. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_container_stop_bound.py +0 -0
  206. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  207. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  208. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  209. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_cli_claims.py +0 -0
  210. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_operator_surface.py +0 -0
  211. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  212. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  213. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  214. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_docs_generated.py +0 -0
  215. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_docs_index_complete.py +0 -0
  216. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_entry_points_resolve.py +0 -0
  217. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_int_coercion.py +0 -0
  218. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_kind_registry.py +0 -0
  219. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_event_kinds_ssot.py +0 -0
  220. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_events_doc_contract.py +0 -0
  221. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  222. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_hook_contract_docs.py +0 -0
  223. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
  224. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  225. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_module_sizes.py +0 -0
  226. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  227. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_ai_signatures.py +0 -0
  228. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_internal_refs.py +0 -0
  229. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_price_logic.py +0 -0
  230. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  231. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_raw_time.py +0 -0
  232. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_no_version_refs.py +0 -0
  233. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_peek_schema_version.py +0 -0
  234. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_phase_select_stateless.py +0 -0
  235. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_plugins_config_stable.py +0 -0
  236. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  237. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  238. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_alloc_growth.py +0 -0
  239. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  240. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_num_single_source.py +0 -0
  241. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_round_result_stable.py +0 -0
  242. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  243. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  244. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  245. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_upstream_schema_canary.py +0 -0
  246. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  247. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/linux/__init__.py +0 -0
  248. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/linux/test_plugin_sandbox_kill.py +0 -0
  249. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/__init__.py +0 -0
  250. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/parser.py +0 -0
  251. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/test_parser.py +0 -0
  252. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/literate/test_quickstart.py +0 -0
  253. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/__init__.py +0 -0
  254. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime.py +0 -0
  255. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_container.py +0 -0
  256. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  257. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_progress.py +0 -0
  258. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_reap.py +0 -0
  259. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  260. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_assemble_prompt.py +0 -0
  261. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_events_stream.py +0 -0
  262. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_install.py +0 -0
  263. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_observation.py +0 -0
  264. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_read_round_num.py +0 -0
  265. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_resolve_phase.py +0 -0
  266. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_service.py +0 -0
  267. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_api_types.py +0 -0
  268. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_auto_stop_gating.py +0 -0
  269. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_builtin_provenance.py +0 -0
  270. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_catalogs.py +0 -0
  271. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cgroup_probe.py +0 -0
  272. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_claude_error_detector.py +0 -0
  273. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_claude_foreign_schema.py +0 -0
  274. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli.py +0 -0
  275. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_common.py +0 -0
  276. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_init_install.py +0 -0
  277. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_main_boundary.py +0 -0
  278. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_monitor_http.py +0 -0
  279. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  280. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_cli_upgrade.py +0 -0
  281. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_codewhale_code_precedence.py +0 -0
  282. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_codewhale_plugin.py +0 -0
  283. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_coerce_int_poison.py +0 -0
  284. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_collapse_spawn_decisions.py +0 -0
  285. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config.py +0 -0
  286. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_fresh_eyes.py +0 -0
  287. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_max_rounds.py +0 -0
  288. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_nested_strictness.py +0 -0
  289. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_prompt_delivery.py +0 -0
  290. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_schedule.py +0 -0
  291. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_stop_file.py +0 -0
  292. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_strictness.py +0 -0
  293. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  294. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_config_transient_error_action.py +0 -0
  295. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_context_store.py +0 -0
  296. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_control_char_config.py +0 -0
  297. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_default_dirty_handler.py +0 -0
  298. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_defenses.py +0 -0
  299. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detect_hung_latch.py +0 -0
  300. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detector_isolation.py +0 -0
  301. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_detector_protocol.py +0 -0
  302. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_dirty_handlers.py +0 -0
  303. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
  304. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_docgen.py +0 -0
  305. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_doctor_cmd.py +0 -0
  306. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_doctor_sandbox.py +0 -0
  307. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_effective_throttle_view.py +0 -0
  308. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_config_migrated.py +0 -0
  309. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_schedule.py +0 -0
  310. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  311. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events.py +0 -0
  312. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_events_jsonl_decode.py +0 -0
  313. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_example_dirty_handler.py +0 -0
  314. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_example_plugin.py +0 -0
  315. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exec_prefix.py +0 -0
  316. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exit_cause.py +0 -0
  317. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_exit_classifier.py +0 -0
  318. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  319. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_gate_serve_boot.py +0 -0
  320. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_gemini_plugin.py +0 -0
  321. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_git_timeout.py +0 -0
  322. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hook_failure_isolation.py +0 -0
  323. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hook_redact_order.py +0 -0
  324. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_hooks.py +0 -0
  325. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_host_health.py +0 -0
  326. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_http_progress.py +0 -0
  327. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_http_progress_epoch.py +0 -0
  328. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_init_entry_points.py +0 -0
  329. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  330. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_kimi_plugin.py +0 -0
  331. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_lifecycle.py +0 -0
  332. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_load_fail_closed.py +0 -0
  333. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_loader_split.py +0 -0
  334. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_manifest_spawn_hooks.py +0 -0
  335. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics.py +0 -0
  336. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  337. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_metrics_swap.py +0 -0
  338. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_migrate_cmd.py +0 -0
  339. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_migrations.py +0 -0
  340. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_assembly.py +0 -0
  341. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_cmd_severity.py +0 -0
  342. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_dedup_identity.py +0 -0
  343. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  344. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  345. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  346. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  347. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_detectors.py +0 -0
  348. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_event_tail.py +0 -0
  349. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_loop.py +0 -0
  350. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_on_alert.py +0 -0
  351. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_remote.py +0 -0
  352. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  353. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_monitor_schedule_pause.py +0 -0
  354. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_network_blip_signal_guard.py +0 -0
  355. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_open_round_log.py +0 -0
  356. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  357. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_outer_round_ceiling.py +0 -0
  358. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_argparse.py +0 -0
  359. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_project_isolation.py +0 -0
  360. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_sandbox_block.py +0 -0
  361. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_schedule.py +0 -0
  362. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_peek_select.py +0 -0
  363. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_agent_launch.py +0 -0
  364. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_profiles.py +0 -0
  365. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_phase_select.py +0 -0
  366. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_pi_plugin.py +0 -0
  367. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_checksum.py +0 -0
  368. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_constants.py +0 -0
  369. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_manifest.py +0 -0
  370. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_name_mismatch.py +0 -0
  371. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_sandbox_wire.py +0 -0
  372. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugin_scan_parity.py +0 -0
  373. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_plugins_config_fields.py +0 -0
  374. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_presets.py +0 -0
  375. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_primary_prompt_file_home.py +0 -0
  376. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_prompt_loader.py +0 -0
  377. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_read_json_hardening.py +0 -0
  378. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_redact.py +0 -0
  379. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_resolve.py +0 -0
  380. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_cmd.py +0 -0
  381. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_cmd_sigterm.py +0 -0
  382. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_log_helpers.py +0 -0
  383. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_outcome.py +0 -0
  384. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  385. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_round_view.py +0 -0
  386. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner.py +0 -0
  387. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  388. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_sandbox_probe.py +0 -0
  389. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_scaffold.py +0 -0
  390. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_schedule.py +0 -0
  391. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_cmd_bounded.py +0 -0
  392. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_config_broken.py +0 -0
  393. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_crash_loop.py +0 -0
  394. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_mem_gate.py +0 -0
  395. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_policy.py +0 -0
  396. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round.py +0 -0
  397. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_log.py +0 -0
  398. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_round_oom_killed.py +0 -0
  399. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_schedule_gate.py +0 -0
  400. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_sentinel.py +0 -0
  401. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_serve_startup_hooks.py +0 -0
  402. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_service_mode.py +0 -0
  403. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_service_unit.py +0 -0
  404. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_spawn_seam.py +0 -0
  405. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_startup_battery_exit.py +0 -0
  406. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_startup_check.py +0 -0
  407. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_substrate.py +0 -0
  408. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  409. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  410. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_epoch_bound.py +0 -0
  411. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_throttle_plugin_classification.py +0 -0
  412. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/tests/unit/test_upgrade_migrate.py +0 -0
  413. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.4}/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.3.4] - 2026-09-15
9
+
10
+ ### Changed
11
+ - While a round runs, the supervisor now blocks on the round process's exit instead of polling about once a second — it uses no CPU while waiting and notices a round's exit immediately.
12
+ - A stop (`SIGTERM` / `agent-runner serve stop`) during a schedule, memory, or phase pause, or during the inter-round restart delay, now takes effect near-instantly instead of after the current pause chunk (up to 30 s).
13
+ - `agent-runner events --tail` shows a new matching event within a drain cycle of its being written, instead of on a ~1 s poll.
14
+
15
+ No change to the rules for when a round is deferred, terminated, or reaped. The mid-round memory check and the round budget now take effect on schedule instead of up to ~1 s late, so within that window the sustained-pressure floor or the budget cutoff can act on a round the old ~1 s poll would have seen finish first.
16
+
8
17
  ## [0.3.3] - 2026-09-14
9
18
 
10
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.3.3
3
+ Version: 0.3.4
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
@@ -0,0 +1,245 @@
1
+ """A broker-less FIFO doorbell -- wake a waiting consumer within milliseconds
2
+ of a durable event write, instead of a busy-poll loop checking every second.
3
+
4
+ Per-listener FIFO under ``log_dir/.notify/``: one named pipe per live
5
+ ``Listener``, path-addressed so the producer (``events.emit``) needs no
6
+ registry, no socket, and no persistent connection -- it just fans a single
7
+ byte out to every ``*.fifo`` file it finds. Zero cost when nobody is
8
+ listening (the directory is empty, ``ring`` is a fast no-op scan).
9
+ Self-healing on a stale entry left behind by a crashed listener: opening a
10
+ FIFO with no reader raises ``ENXIO``, which ``ring`` treats as "nobody's
11
+ there" and cleans up.
12
+
13
+ Stdlib only (``os``, ``select``, ``pathlib``) -- no watchfiles/inotify,
14
+ no socketpair/AF_UNIX. A FIFO is opened ``O_RDWR`` (the self-pipe trick):
15
+ holding the write side open ourselves means the read side never sees the
16
+ "all writers closed" EOF a plain read-only open would eventually hit.
17
+
18
+ Fail-open throughout: any ``OSError`` from ``mkfifo``/``open`` degrades a
19
+ would-be ``Listener`` to a ``NullListener`` that just sleeps out its
20
+ timeout, so a filesystem without FIFO support (or without write
21
+ permission under ``log_dir``) never breaks the caller -- it only falls
22
+ back to the busy-poll cadence this module was built to avoid.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import errno
28
+ import os
29
+ import select
30
+ from pathlib import Path
31
+
32
+ from agent_runner.clock import SYSTEM_CLOCK, Clock
33
+
34
+ _NOTIFY_DIRNAME = ".notify"
35
+
36
+
37
+ def ring(log_dir: Path) -> None:
38
+ """Best-effort wake for every live ``Listener`` under ``log_dir/.notify/``.
39
+
40
+ For each ``*.fifo``: open ``O_WRONLY | O_NONBLOCK``, write one byte,
41
+ close. Every ``OSError`` is swallowed -- ``EAGAIN`` (the listener's
42
+ kernel pipe buffer is full) is harmless, the wake already landed;
43
+ ``ENXIO`` (no reader -- a stale FIFO left by a listener that crashed
44
+ without unlinking) means the entry is dead, so it is unlinked. Never
45
+ raises into the caller's durable-write path.
46
+ """
47
+ notify_dir = log_dir / _NOTIFY_DIRNAME
48
+ try:
49
+ with os.scandir(notify_dir) as it:
50
+ paths = [e.path for e in it if e.name.endswith(".fifo")]
51
+ except OSError:
52
+ return
53
+
54
+ for path in paths:
55
+ try:
56
+ fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
57
+ except OSError as e:
58
+ if e.errno == errno.ENXIO:
59
+ try:
60
+ os.unlink(path)
61
+ except OSError:
62
+ pass # a permission-denied .notify/ can't be cleaned up -- never mind
63
+ continue
64
+ try:
65
+ os.write(fd, b"\x00")
66
+ except OSError:
67
+ pass # EAGAIN: listener's kernel buffer is full -- the wake already landed
68
+ finally:
69
+ try:
70
+ os.close(fd)
71
+ except OSError:
72
+ pass # EINTR or similar -- fail-open, never let a close raise into emit()
73
+
74
+
75
+ def drain(fd: int) -> None:
76
+ """Read a level-triggered wakeup fd until ``EAGAIN`` so a delivered byte
77
+ (a ``ring()`` or a ``signal.set_wakeup_fd`` signal) doesn't leave the next
78
+ ``select()`` on it instantly ready -- an undrained fd busy-spins its
79
+ caller at 100% CPU forever after the first wake, since a level-triggered
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.
86
+ """
87
+ while True:
88
+ try:
89
+ if not os.read(fd, 4096):
90
+ break
91
+ except OSError:
92
+ break
93
+
94
+
95
+ class Listener:
96
+ """Context manager owning one FIFO under ``log_dir/.notify/``.
97
+
98
+ ``__enter__`` creates ``log_dir/.notify/<pid>-<id(self)>.fifo`` and
99
+ opens it ``O_RDWR | O_NONBLOCK`` -- holding the write side ourselves
100
+ is what keeps the read side from ever seeing a persistent EOF. It
101
+ retries up to 3 times if a concurrent ``ring()`` wins the race between
102
+ ``mkfifo`` and ``open`` and unlinks the readerless FIFO out from under
103
+ us (see ``__enter__``'s own comment).
104
+ ``__exit__`` unlinks the FIFO unconditionally (including when an
105
+ exception, e.g. ``KeyboardInterrupt``, propagates through the
106
+ ``with`` block).
107
+ """
108
+
109
+ def __init__(self, log_dir: Path) -> None:
110
+ self._notify_dir = log_dir / _NOTIFY_DIRNAME
111
+ self._path = self._notify_dir / f"{os.getpid()}-{id(self)}.fifo"
112
+ self._fd: int | None = None
113
+
114
+ def __enter__(self) -> Listener:
115
+ # Idempotent: open_listener() pre-enters a Listener to detect OSError
116
+ # and degrade to a NullListener, then hands the already-entered
117
+ # instance to the caller's own `with` statement, which enters it a
118
+ # second time -- that second entry must be a no-op, not a re-mkfifo.
119
+ if self._fd is not None:
120
+ return self
121
+ self._notify_dir.mkdir(parents=True, exist_ok=True)
122
+ # Bounded retry: mkfifo and open are two separate syscalls, so there is a
123
+ # window between them where the FIFO exists but has no reader yet. A
124
+ # concurrent ring() from another process sharing this log_dir can open
125
+ # our fresh, readerless FIFO O_WRONLY|O_NONBLOCK, get ENXIO, and (by
126
+ # design -- see ring()'s docstring) unlink it as "stale". Our own
127
+ # os.open then loses the file underneath us with FileNotFoundError.
128
+ # Retrying re-creates the FIFO from scratch; three tries makes losing
129
+ # the race every single time astronomically unlikely.
130
+ last_exc: OSError | None = None
131
+ for _ in range(3):
132
+ try:
133
+ os.mkfifo(self._path)
134
+ except FileExistsError:
135
+ pass # a prior retry (or the same path) already made it -- try to open it
136
+ try:
137
+ self._fd = os.open(self._path, os.O_RDWR | os.O_NONBLOCK)
138
+ return self
139
+ except FileNotFoundError as e:
140
+ last_exc = e # a concurrent ring() unlinked our readerless FIFO -- retry
141
+ raise last_exc if last_exc is not None else OSError("Listener.__enter__ exhausted retries")
142
+
143
+ def __exit__(
144
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: object
145
+ ) -> None:
146
+ if self._fd is not None:
147
+ try:
148
+ os.close(self._fd)
149
+ except OSError:
150
+ pass
151
+ self._fd = None
152
+ try:
153
+ self._path.unlink(missing_ok=True)
154
+ except OSError:
155
+ pass # e.g. a permission-denied .notify/ -- fail-open, never crash teardown
156
+
157
+ @property
158
+ def fd(self) -> int:
159
+ """The FIFO's read/write fd -- selectable, and the value handed to
160
+ ``signal.set_wakeup_fd``."""
161
+ assert self._fd is not None, "Listener.fd read outside its `with` block"
162
+ return self._fd
163
+
164
+ def wait(self, timeout_s: float, *, clock: Clock = SYSTEM_CLOCK) -> bool:
165
+ """Block until a ring wakes this listener or ``timeout_s`` elapses.
166
+
167
+ ``select.select`` against a ``clock.monotonic()`` deadline so a
168
+ real timeout still elapses under real time even though the wake
169
+ itself is an fd event no fake clock can simulate. On wake, drains
170
+ the fd (reads until ``EAGAIN``) so a burst of rings before the
171
+ next ``wait`` call coalesces into a single wake instead of one
172
+ per byte.
173
+
174
+ Returns ``True`` iff woken before the timeout -- a ring, a signal
175
+ delivered via ``signal.set_wakeup_fd`` on this same fd, or a
176
+ spurious wake all look identical here; callers never distinguish
177
+ them because the wake is purely advisory -- either way the caller
178
+ re-reads its own state after ``wait`` returns.
179
+ """
180
+ deadline = clock.monotonic() + timeout_s
181
+ remaining = max(0.0, deadline - clock.monotonic())
182
+ ready, _, _ = select.select([self.fd], [], [], remaining)
183
+ if not ready:
184
+ return False
185
+
186
+ drain(self.fd)
187
+ return True
188
+
189
+
190
+ class NullListener:
191
+ """Fallback when ``mkfifo``/``open`` fails (no filesystem permission,
192
+ FIFOs unsupported). ``fd`` is ``None`` -- there is nothing to select
193
+ on. A no-op context manager so ``with open_listener(...) as listener``
194
+ works identically whether or not the FIFO could be created.
195
+ """
196
+
197
+ @property
198
+ def fd(self) -> None:
199
+ return None
200
+
201
+ def __enter__(self) -> NullListener:
202
+ return self
203
+
204
+ def __exit__(
205
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: object
206
+ ) -> None:
207
+ return None
208
+
209
+ def wait(self, timeout_s: float, *, clock: Clock = SYSTEM_CLOCK) -> bool:
210
+ """Degrades to a plain timed sleep -- preserves every ``FakeClock``
211
+ test's semantics unchanged: waiting still advances virtual time by
212
+ exactly ``timeout_s``, same as a bare ``clock.sleep(timeout_s)``."""
213
+ clock.sleep(timeout_s)
214
+ return False
215
+
216
+
217
+ # Stateless (fd=None, wait==clock.sleep, no-op ctx mgr), so one module-level
218
+ # instance is a safe default arg -- the stable-singleton default (mirrors
219
+ # SYSTEM_CLOCK) every non-serve caller of a listener-aware function keeps by
220
+ # not passing one, staying byte-identical to its pre-doorbell behavior.
221
+ NULL_LISTENER = NullListener()
222
+
223
+
224
+ def open_listener(log_dir: Path) -> Listener | NullListener:
225
+ """Try to construct a live ``Listener``; degrade to a ``NullListener``
226
+ on any ``OSError`` from ``mkfifo``/``open`` (e.g. a read-only
227
+ filesystem or a platform without FIFO support) so the consumer never
228
+ has to branch on which one it got.
229
+
230
+ Pre-enters the ``Listener`` here (rather than leaving that to the
231
+ caller's own ``with``) so the failure can be caught and swapped for a
232
+ ``NullListener``; ``Listener.__enter__`` is idempotent, so the
233
+ caller's subsequent ``with open_listener(log_dir) as listener:`` is
234
+ safe to call it a second time.
235
+ """
236
+ listener = Listener(log_dir)
237
+ try:
238
+ listener.__enter__()
239
+ except OSError:
240
+ try:
241
+ listener._path.unlink(missing_ok=True)
242
+ except OSError:
243
+ pass # cleanup best-effort -- must not crash the caller's degrade-to-Null path
244
+ return NullListener()
245
+ return listener
@@ -0,0 +1,165 @@
1
+ """Process-exit waiting on a readable fd, not a busy-poll loop.
2
+
3
+ ``exit_fd`` opens a throwaway fd that becomes readable once a subprocess has
4
+ exited (``os.pidfd_open`` on Linux, a kqueue ``NOTE_EXIT`` registration on
5
+ macOS/BSD, ``None`` on any other platform or any setup failure). ``wait_exit``
6
+ blocks in one ``select.select`` on that fd until the process exits or a real
7
+ deadline passes.
8
+
9
+ Neither function ever reaps the process on this fast path -- a pidfd or a
10
+ kqueue registration only observes the kernel's process table, it never
11
+ consumes the zombie slot. There is exactly one path from "notices exit" to
12
+ "reaps": the caller, in the same thread, right after ``wait_exit`` returns
13
+ ``"exited"``. No background watcher thread ever touches the process table,
14
+ which is what keeps this immune to CPython's pidfd/child-watcher race
15
+ (https://github.com/python/cpython/issues/127049): a background reaper can
16
+ consume a zombie's pid slot between the moment some other code decides to
17
+ signal that pid and the moment it actually does, letting a freshly spawned,
18
+ unrelated process reuse the pid and get signalled by mistake. With no
19
+ background reaper at all, that window doesn't exist.
20
+
21
+ The one exception is the poll fallback used when ``exit_fd`` returns
22
+ ``None`` (no pidfd/kqueue support, or a kernel/pid-race failure on setup):
23
+ it calls ``proc.poll()`` on a short cadence, exactly like the busy-poll loop
24
+ this module replaces. That does reap on exit, but synchronously, inside the
25
+ one call, in the caller's own thread -- there is still no separate
26
+ notice-then-reap window and no background thread, so the safety argument
27
+ above still holds; only the fast path is structurally two separate steps
28
+ (notice, then a later caller-owned reap).
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ import select
35
+ import subprocess # noqa: TID251 -- exit_fd/wait_exit observe Popen.pid; see pyproject.toml
36
+ import sys
37
+ from typing import Literal
38
+
39
+ from agent_runner.clock import SYSTEM_CLOCK, Clock
40
+
41
+ # Fallback poll cadence when exit_fd() is unsupported -- matches the
42
+ # real-second value of the busy-poll tick this module's callers used to run
43
+ # themselves before switching to wait_exit (e.g. _spawn_round's old
44
+ # proc.wait(timeout=1)). Kept as its own constant, not imported by/from a
45
+ # caller, so this module has no dependency on any of them.
46
+ _POLL_TICK_S = 1.0
47
+
48
+
49
+ def exit_fd(proc: subprocess.Popen) -> int | None:
50
+ """A fd that becomes readable once ``proc``'s pid has exited (zombie, not
51
+ yet reaped) -- this never itself waits on or reaps it.
52
+
53
+ Linux: ``os.pidfd_open(proc.pid, 0)``. macOS/BSD: a kqueue registered
54
+ with ``EVFILT_PROC``/``NOTE_EXIT`` on ``proc.pid``, whose underlying
55
+ kernel object is kept alive by handing back a ``os.dup``'d descriptor
56
+ rather than the kqueue object's own fileno -- the ``select.kqueue``
57
+ Python object is closed before this function returns (its ``__del__``
58
+ would otherwise race a caller's pending ``select`` and close the fd out
59
+ from under it), while the duplicated fd keeps the same underlying kqueue
60
+ alive until the caller closes it. Any other platform, or any ``OSError``
61
+ from either path (kernel too old, permission denied, or the pid already
62
+ reaped out from under us): ``None`` -- always fail-open, never fatal.
63
+
64
+ The caller owns the returned fd for exactly the lifetime of the one
65
+ ``wait_exit`` call that opened it; see that function's docstring for the
66
+ close-after-use discipline this enables.
67
+ """
68
+ if sys.platform.startswith("linux") and hasattr(os, "pidfd_open"):
69
+ try:
70
+ return os.pidfd_open(proc.pid, 0)
71
+ except OSError:
72
+ return None
73
+
74
+ if hasattr(select, "kqueue"):
75
+ try:
76
+ kq = select.kqueue()
77
+ try:
78
+ event = select.kevent(
79
+ proc.pid,
80
+ filter=select.KQ_FILTER_PROC,
81
+ flags=select.KQ_EV_ADD,
82
+ fflags=select.KQ_NOTE_EXIT,
83
+ )
84
+ kq.control([event], 0, 0)
85
+ return os.dup(kq.fileno())
86
+ finally:
87
+ kq.close()
88
+ except OSError:
89
+ return None
90
+
91
+ return None
92
+
93
+
94
+ def wait_exit(
95
+ proc: subprocess.Popen,
96
+ *,
97
+ deadline: float,
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).
105
+
106
+ Opens and closes its own ``exit_fd(proc)`` registration internally, once
107
+ per call, in a single ``select.select`` -- callers invoke this at most
108
+ every few seconds (a round's mem-check interval) or once per grace
109
+ window, so the per-call setup cost is negligible and no fd is held open
110
+ across calls.
111
+
112
+ 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"``.
118
+
119
+ Fallback (``exit_fd(proc)`` is ``None``): polls ``proc.poll()`` at a
120
+ 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.
123
+
124
+ Short-circuits to ``"exited"`` when ``proc.returncode`` is already set:
125
+ this owner has already reaped it, so its pid is freed (and may be reused).
126
+ Re-opening ``exit_fd(proc.pid)`` on a freed pid could bind pidfd/kqueue to
127
+ an UNRELATED process and ride out the whole deadline as a false
128
+ ``"timeout"``. The most common way this happens is ``Popen.terminate()``,
129
+ whose ``send_signal`` calls ``proc.poll()`` first and reaps a leader that
130
+ exited in the microseconds before the signal.
131
+ """
132
+ if proc.returncode is not None:
133
+ return "exited" # already reaped by this owner -- never re-open a pid we no longer hold
134
+
135
+ fd = exit_fd(proc)
136
+
137
+ if fd is None:
138
+ return _wait_exit_by_polling(proc, deadline=deadline, clock=clock)
139
+
140
+ try:
141
+ remaining = max(0.0, deadline - clock.monotonic())
142
+ ready, _, _ = select.select([fd], [], [], remaining)
143
+ return "exited" if ready else "timeout"
144
+ finally:
145
+ os.close(fd)
146
+
147
+
148
+ def _wait_exit_by_polling(
149
+ proc: subprocess.Popen,
150
+ *,
151
+ deadline: float,
152
+ clock: Clock,
153
+ ) -> Literal["exited", "timeout"]:
154
+ # Not clock.wait_until: keep this poll loop in _procwait so
155
+ # test_procwait_shield.py's _procwait-only scan guards its KeyboardInterrupt
156
+ # propagation (the grace-kill shield depends on it).
157
+ while True:
158
+ if proc.poll() is not None:
159
+ return "exited"
160
+
161
+ remaining = deadline - clock.monotonic()
162
+ if remaining <= 0:
163
+ return "timeout"
164
+
165
+ clock.sleep(min(_POLL_TICK_S, remaining))
@@ -18,6 +18,7 @@ from datetime import UTC, datetime
18
18
  from pathlib import Path
19
19
  from typing import Any
20
20
 
21
+ from agent_runner._notify import NULL_LISTENER, Listener, NullListener
21
22
  from agent_runner.api_types import TransientErrorState
22
23
  from agent_runner.clock import SYSTEM_CLOCK, Clock
23
24
  from agent_runner.events import (
@@ -594,6 +595,7 @@ def _interruptible_sleep(
594
595
  chunk_s: int = 30,
595
596
  should_stop: Callable[[], bool] | None = None,
596
597
  deadline_epoch: float | None = None,
598
+ listener: Listener | NullListener = NULL_LISTENER,
597
599
  ) -> bool:
598
600
  """Sleep ``total_s`` in ``<= chunk_s`` slices, re-checking ``stop`` (and, if given,
599
601
  ``should_stop()``) at each boundary; return True iff ``stop["requested"]`` OR
@@ -601,14 +603,45 @@ def _interruptible_sleep(
601
603
  :func:`_apply_back_off` so a SIGTERM or a stop_file lands within one chunk instead
602
604
  of after the full sleep (e.g. the 8h back-off cap).
603
605
 
606
+ ``listener`` (default :data:`agent_runner._notify.NULL_LISTENER`) is the single
607
+ wakeup mechanism for the per-chunk nap below: a real ``Listener`` wakes early on a
608
+ SIGTERM/SIGINT (via ``signal.set_wakeup_fd``) or a cross-process ``ring()``,
609
+ dropping the wait to milliseconds instead of riding out the full ``chunk_s``; the
610
+ default ``NullListener`` degrades ``listener.wait(nap, clock=clock)`` to a plain
611
+ ``clock.sleep(nap)`` on the SAME clock, so every existing caller (none of which
612
+ pass ``listener``) stays byte-identical.
613
+
604
614
  ``should_stop`` matches :func:`_pause_poll`'s contract (cli/_serve_round.py): a
605
615
  zero-arg predicate the caller closes over its own stop_file check with, so this
606
616
  module never needs to know what "should stop" means beyond calling it.
607
617
 
608
- Counts down the *intended* nap per slice rather than measuring a wall/monotonic
609
- deadline: NTP-step immune (no clock read for the deadline) AND does not busy-spin
610
- when ``clock.sleep`` is a no-op (a test patching ``time.sleep`` runs it instantly
611
- instead of looping until real time advances).
618
+ A live ``Listener`` can return from ``wait`` early on an advisory event byte that
619
+ is not a stop (e.g. a doorbell already rung by a concurrent ``events.emit``
620
+ before this call even started, or another emit landing mid-sleep) crediting
621
+ the full nap for a wait that actually returned in microseconds would collapse
622
+ the whole sleep to ~0 on the first stray byte, defeating callers like the serve
623
+ restart/crash back-off delay that depend on this function to ride out its full
624
+ duration. So a live listener is credited its *actual* ``clock.monotonic()``
625
+ elapsed per slice, not the intended nap.
626
+
627
+ ``NullListener.wait``, by contrast, can never return early — there is no fd to
628
+ wake on, it always just calls ``clock.sleep(nap)`` and returns — so for it the
629
+ intended nap IS the correct credit, and this function keeps using it directly
630
+ (``remaining -= nap``) rather than measuring elapsed around it. This is
631
+ deliberate, not an optimization: a monotonic-elapsed measurement would only be
632
+ byte-identical to the old intended-nap count if the injected clock's
633
+ ``monotonic()`` genuinely tracked its own ``sleep()`` — true for ``FakeClock``,
634
+ but NOT for a test that monkeypatches the stdlib ``time.sleep`` (what
635
+ ``RealClock.sleep`` calls) to skip real waiting without touching
636
+ ``time.monotonic()`` (a real, used-elsewhere-in-this-suite pattern, e.g.
637
+ ``test_serve_crash_loop.py``): that combination would leave elapsed pinned
638
+ near 0 forever and busy-spin the loop for the full ``chunk_s``/``total_s``
639
+ instead of terminating. Branching on the listener type sidesteps that clock
640
+ fragility entirely and keeps every existing NULL_LISTENER caller provably
641
+ byte-identical to the pre-fix accounting. Still NTP-step immune either way:
642
+ ``clock.monotonic()`` is never stepped by an NTP correction (only
643
+ ``clock.epoch()`` is, which the ``deadline_epoch`` branch below handles
644
+ separately).
612
645
 
613
646
  ``deadline_epoch``, when given, additionally re-checks ``clock.epoch() >=
614
647
  deadline_epoch`` at each chunk boundary and returns False (completed, NOT
@@ -627,8 +660,18 @@ def _interruptible_sleep(
627
660
  if deadline_epoch is not None and clock.epoch() >= deadline_epoch:
628
661
  return False
629
662
  nap = min(float(chunk_s), remaining)
630
- clock.sleep(nap)
631
- remaining -= nap
663
+ if isinstance(listener, NullListener):
664
+ # Never returns early (no fd to wake on) -- the intended nap is exactly
665
+ # what it consumed, so crediting it directly is provably byte-identical
666
+ # to the pre-fix accounting for every existing (non-serve) caller.
667
+ listener.wait(nap, clock=clock)
668
+ remaining -= nap
669
+ else:
670
+ # May return early on a spurious/stale ring -- credit only what the
671
+ # wait actually consumed, or a stray event byte collapses the sleep.
672
+ started = clock.monotonic()
673
+ listener.wait(nap, clock=clock)
674
+ remaining -= max(0.0, clock.monotonic() - started)
632
675
  return False
633
676
 
634
677
 
@@ -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.3'
22
- __version_tuple__ = version_tuple = (0, 3, 3)
21
+ __version__ = version = '0.3.4'
22
+ __version_tuple__ = version_tuple = (0, 3, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -19,9 +19,11 @@ import threading
19
19
  from collections.abc import Callable
20
20
  from dataclasses import dataclass
21
21
  from pathlib import Path
22
+ from typing import Literal
22
23
 
23
24
  import psutil
24
25
 
26
+ from agent_runner._procwait import wait_exit
25
27
  from agent_runner.api_types import _round_ok
26
28
  from agent_runner.clock import SYSTEM_CLOCK, Clock
27
29
 
@@ -144,14 +146,30 @@ def _snapshot_stray_descendants(proc: subprocess.Popen) -> list[dict]:
144
146
  return stray
145
147
 
146
148
 
149
+ def _wait_exit_shielded(
150
+ proc: subprocess.Popen, *, deadline: float, clock: Clock
151
+ ) -> Literal["exited", "timeout"]:
152
+ """``wait_exit`` retried against the SAME absolute ``deadline`` on a
153
+ re-entrant ``KeyboardInterrupt`` (round_cmd's process-wide SIGTERM handler
154
+ raises one through the blocking wait) -- so an impatient double-kill can
155
+ never inflate the grace window or skip the SIGKILL escalation. The deadline
156
+ is absolute, so a retry never extends it."""
157
+ while True:
158
+ try:
159
+ return wait_exit(proc, deadline=deadline, clock=clock)
160
+ except KeyboardInterrupt:
161
+ continue
162
+
163
+
147
164
  def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
148
165
  """SIGTERM the pgroup, grace, then SIGKILL — the reap primitive shared by
149
166
  the round-timeout path and ``run``'s BaseException handler (which fires on
150
167
  a SIGTERM landing while the round is unwinding from a first one).
151
168
  ``round_cmd``'s SIGTERM handler stays installed for the whole process life
152
169
  (it converts every SIGTERM into a fresh ``KeyboardInterrupt``, not just the
153
- first), so a re-entrant SIGTERM during the grace sleep below raises HERE —
154
- shielded (caught and retried) so the SIGKILL escalation always runs. An
170
+ first), so a re-entrant SIGTERM during the grace wait below raises HERE —
171
+ shielded (caught and retried against the SAME absolute deadline, so it
172
+ cannot inflate the grace window) so the SIGKILL escalation always runs. An
155
173
  operator's impatient double-kill must not leave the agent outliving the
156
174
  grace period unreaped.
157
175
 
@@ -169,24 +187,17 @@ def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
169
187
  deadline = (
170
188
  clock.monotonic() + REAP_GRACE_S
171
189
  ) # monotonic: an NTP step must not stretch/skip the reap
172
- while clock.monotonic() < deadline and proc.poll() is None:
173
- try:
174
- clock.sleep(0.1)
175
- except KeyboardInterrupt:
176
- pass # shielded: keep waiting out the grace window, never skip SIGKILL
190
+ _wait_exit_shielded(proc, deadline=deadline, clock=clock)
191
+ # "exited" or "timeout": either way, fall through to the SIGKILL last-resort
177
192
  try:
178
193
  os.killpg(pgid, signal.SIGKILL)
179
194
  except OSError:
180
195
  pass
181
196
  _kill_stray_descendants(stray)
182
- while True:
183
- try:
184
- proc.wait(timeout=10)
185
- except subprocess.TimeoutExpired:
186
- pass # reaped via the SIGKILL above regardless; nothing more to wait for
187
- except KeyboardInterrupt:
188
- continue # shielded: still must reap before returning
189
- return
197
+ final_deadline = clock.monotonic() + 10
198
+ if _wait_exit_shielded(proc, deadline=final_deadline, clock=clock) == "exited":
199
+ proc.wait() # reap the zombie the fast path left; idempotent if already reaped
200
+ return
190
201
 
191
202
 
192
203
  def _live_children(