cli-agent-runner 0.3.3__tar.gz → 0.3.5__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 (415) hide show
  1. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.vulture-whitelist.py +2 -0
  2. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CHANGELOG.md +14 -0
  3. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/PKG-INFO +1 -1
  4. cli_agent_runner-0.3.5/agent_runner/_notify.py +245 -0
  5. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_manifest.py +20 -0
  6. cli_agent_runner-0.3.5/agent_runner/_procwait.py +165 -0
  7. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_serve_policy.py +13 -2
  8. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_throttle.py +49 -6
  9. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_version.py +2 -2
  10. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/agent_runtime.py +42 -19
  11. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/_serve_round.py +90 -45
  12. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/common.py +8 -2
  13. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/doctor_cmd.py +15 -0
  14. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/events_cmd.py +29 -24
  15. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/serve_cmd.py +95 -7
  16. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/models.py +26 -0
  17. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/parsers.py +24 -0
  18. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/events.py +2 -0
  19. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/runner.py +24 -0
  20. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/commands.md +1 -1
  21. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/configuration.md +15 -0
  22. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/memory-efficiency.md +95 -3
  23. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/plugins.md +2 -2
  24. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/pyproject.toml +2 -1
  25. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/conftest.py +24 -0
  26. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_auto_stop_e2e.py +9 -1
  27. cli_agent_runner-0.3.5/tests/integration/test_cooperative_grace_property.py +142 -0
  28. cli_agent_runner-0.3.5/tests/integration/test_reap_property.py +153 -0
  29. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_run_one_round_with_fake_agent.py +26 -0
  30. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_spawn_round_mem_floor.py +35 -10
  31. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_spawn_round_wedged.py +14 -0
  32. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_architecture.py +7 -0
  33. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_import_footprint.py +11 -0
  34. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_layer_2_loop_size.py +4 -1
  35. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_module_boundaries.py +1 -0
  36. cli_agent_runner-0.3.5/tests/invariants/test_no_asyncio_select.py +253 -0
  37. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_vacuous_invariants.py +2 -0
  38. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_peek_schema_version.py +1 -1
  39. cli_agent_runner-0.3.5/tests/invariants/test_procwait_shield.py +92 -0
  40. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_timeout_budget_invariant.py +32 -1
  41. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_grace.py +91 -50
  42. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_reap.py +53 -0
  43. cli_agent_runner-0.3.5/tests/unit/test_apply_reap_grace_env.py +92 -0
  44. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config.py +44 -0
  45. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_doctor_cmd.py +9 -0
  46. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd.py +32 -19
  47. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd_rollover.py +21 -2
  48. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_cmd_tail.py +31 -9
  49. cli_agent_runner-0.3.5/tests/unit/test_kill_pgroup_shielding.py +134 -0
  50. cli_agent_runner-0.3.5/tests/unit/test_notify.py +174 -0
  51. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_sandbox_block.py +26 -0
  52. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_manifest.py +44 -0
  53. cli_agent_runner-0.3.5/tests/unit/test_procwait.py +113 -0
  54. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner.py +35 -0
  55. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner_throttle.py +7 -2
  56. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_phase_schedule.py +8 -0
  57. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_cgroup_spine.py +24 -1
  58. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_terminate.py +19 -2
  59. cli_agent_runner-0.3.5/tests/unit/test_serve_round_wait_exit.py +81 -0
  60. cli_agent_runner-0.3.5/tests/unit/test_serve_wakeup.py +169 -0
  61. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_service_unit.py +4 -3
  62. cli_agent_runner-0.3.5/tests/unit/test_terminate_round_wait_exit.py +96 -0
  63. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.codecov.yml +0 -0
  64. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  65. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  66. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  67. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  68. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/dependabot.yml +0 -0
  69. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/workflows/ci.yml +0 -0
  70. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.github/workflows/release.yml +0 -0
  71. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/.gitignore +0 -0
  72. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/BACKLOG.md +0 -0
  73. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CODE_OF_CONDUCT.md +0 -0
  74. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/CONTRIBUTING.md +0 -0
  75. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/LICENSE +0 -0
  76. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/README.md +0 -0
  77. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/README.zh.md +0 -0
  78. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/SECURITY.md +0 -0
  79. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/__init__.py +0 -0
  80. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_docgen.py +0 -0
  81. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/__init__.py +0 -0
  82. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/agent.py +0 -0
  83. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/memory.py +0 -0
  84. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/plugins.py +0 -0
  85. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/rounds.py +0 -0
  86. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_emit/serve.py +0 -0
  87. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_install.py +0 -0
  88. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_lifecycle.py +0 -0
  89. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_detectors.py +0 -0
  90. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_registry.py +0 -0
  91. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_monitor_state.py +0 -0
  92. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_observe.py +0 -0
  93. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_checksum.py +0 -0
  94. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_sandbox.py +0 -0
  95. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_plugin_scan.py +0 -0
  96. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_redact.py +0 -0
  97. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_registry.py +0 -0
  98. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_resolve.py +0 -0
  99. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_round_outcome.py +0 -0
  100. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_round_support.py +0 -0
  101. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_sandbox_probe.py +0 -0
  102. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/_substrate.py +0 -0
  103. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/api.py +0 -0
  104. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/api_types.py +0 -0
  105. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/__init__.py +0 -0
  106. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/_constants.py +0 -0
  107. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  108. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/codewhale.py +0 -0
  109. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  110. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/gemini.py +0 -0
  111. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/kimi.py +0 -0
  112. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/builtin_plugins/pi.py +0 -0
  113. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/__init__.py +0 -0
  114. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/__main__.py +0 -0
  115. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/_serve_cgroup.py +0 -0
  116. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/init_cmd.py +0 -0
  117. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/install_cmd.py +0 -0
  118. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/migrate_cmd.py +0 -0
  119. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/monitor_cmd.py +0 -0
  120. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/peek_cmd.py +0 -0
  121. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/round_cmd.py +0 -0
  122. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/service_cmd.py +0 -0
  123. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/cli/upgrade_cmd.py +0 -0
  124. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/clock.py +0 -0
  125. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/__init__.py +0 -0
  126. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/errors.py +0 -0
  127. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/loader.py +0 -0
  128. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/config/validators.py +0 -0
  129. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/context_store.py +0 -0
  130. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/defenses.py +0 -0
  131. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/hooks.py +0 -0
  132. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/host_health.py +0 -0
  133. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/http_progress.py +0 -0
  134. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/lifecycle.py +0 -0
  135. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/metrics.py +0 -0
  136. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/migrations.py +0 -0
  137. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/monitor.py +0 -0
  138. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/phase_select.py +0 -0
  139. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/__init__.py +0 -0
  140. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/aider.toml +0 -0
  141. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/claude.toml +0 -0
  142. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/codewhale.toml +0 -0
  143. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/gemini.toml +0 -0
  144. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/kimi.toml +0 -0
  145. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/presets/pi.toml +0 -0
  146. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/prompt_loader.py +0 -0
  147. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/remote_relay.py +0 -0
  148. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/round_log.py +0 -0
  149. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/round_view.py +0 -0
  150. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/scaffold.py +0 -0
  151. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/schedule.py +0 -0
  152. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/service_unit.py +0 -0
  153. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/startup_check.py +0 -0
  154. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/agent_runner/vcs_state.py +0 -0
  155. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/build.sh +0 -0
  156. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/deploy/Dockerfile +0 -0
  157. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/deploy/docker-compose.yml +0 -0
  158. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/README.md +0 -0
  159. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/architecture.md +0 -0
  160. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/events.md +0 -0
  161. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/long-running-agents.md +0 -0
  162. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/migrations/0.2.md +0 -0
  163. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/migrations/0.3.md +0 -0
  164. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/quickstart.md +0 -0
  165. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/aider.md +0 -0
  166. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/codewhale.md +0 -0
  167. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/container-pi.md +0 -0
  168. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/deploy-all-in-one.md +0 -0
  169. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/kimi.md +0 -0
  170. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/recipes/pi.md +0 -0
  171. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/runbook.md +0 -0
  172. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/docs/thesis.md +0 -0
  173. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/__init__.py +0 -0
  174. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/_clock.py +0 -0
  175. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/_test_helpers.py +0 -0
  176. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/contract/__init__.py +0 -0
  177. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/contract/test_public_api_surface.py +0 -0
  178. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/__init__.py +0 -0
  179. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/conftest.py +0 -0
  180. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  181. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_install_systemd.py +0 -0
  182. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  183. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  184. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  185. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  186. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  187. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/generate_vulture_whitelist.py +0 -0
  188. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/__init__.py +0 -0
  189. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_bounded_run.py +0 -0
  190. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_context_enricher_namespacing.py +0 -0
  191. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_dirty_handler_seam.py +0 -0
  192. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_fresh_eyes_signal.py +0 -0
  193. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_grace_kill_emission.py +0 -0
  194. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_install_dry_run.py +0 -0
  195. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_monitor_seeded.py +0 -0
  196. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_detector_loaded.py +0 -0
  197. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_owned_paths.py +0 -0
  198. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_plugin_real_flow.py +0 -0
  199. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  200. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_scaffold_presets.py +0 -0
  201. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_serve_loop.py +0 -0
  202. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_substrate_fingerprint.py +0 -0
  203. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/integration/test_transient_error_backoff.py +0 -0
  204. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/__init__.py +0 -0
  205. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/_docs.py +0 -0
  206. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/_event_scan.py +0 -0
  207. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_atomic_write_enforced.py +0 -0
  208. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_authored_facts_declared.py +0 -0
  209. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  210. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_bdd_test_conventions.py +0 -0
  211. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_builtin_plugin_names_sync.py +0 -0
  212. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_catalogs.py +0 -0
  213. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_classification_ssot.py +0 -0
  214. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_config_error_consistency.py +0 -0
  215. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_config_value_set_ssot.py +0 -0
  216. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_container_stop_bound.py +0 -0
  217. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  218. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  219. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  220. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_cli_claims.py +0 -0
  221. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_operator_surface.py +0 -0
  222. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  223. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  224. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  225. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_docs_generated.py +0 -0
  226. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_docs_index_complete.py +0 -0
  227. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_entry_points_resolve.py +0 -0
  228. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_int_coercion.py +0 -0
  229. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_kind_registry.py +0 -0
  230. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_event_kinds_ssot.py +0 -0
  231. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_events_doc_contract.py +0 -0
  232. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  233. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_hook_contract_docs.py +0 -0
  234. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_kill_tests_not_skipped.py +0 -0
  235. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  236. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_module_sizes.py +0 -0
  237. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  238. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_ai_signatures.py +0 -0
  239. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_internal_refs.py +0 -0
  240. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_price_logic.py +0 -0
  241. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  242. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_raw_time.py +0 -0
  243. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_no_version_refs.py +0 -0
  244. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_phase_select_stateless.py +0 -0
  245. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_plugins_config_stable.py +0 -0
  246. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  247. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  248. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_alloc_growth.py +0 -0
  249. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  250. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_num_single_source.py +0 -0
  251. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_round_result_stable.py +0 -0
  252. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  253. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  254. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_upstream_schema_canary.py +0 -0
  255. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  256. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/linux/__init__.py +0 -0
  257. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/linux/test_plugin_sandbox_kill.py +0 -0
  258. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/__init__.py +0 -0
  259. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/parser.py +0 -0
  260. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/test_parser.py +0 -0
  261. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/literate/test_quickstart.py +0 -0
  262. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/__init__.py +0 -0
  263. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime.py +0 -0
  264. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_container.py +0 -0
  265. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  266. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_progress.py +0 -0
  267. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  268. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_assemble_prompt.py +0 -0
  269. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_events_stream.py +0 -0
  270. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_install.py +0 -0
  271. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_observation.py +0 -0
  272. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_read_round_num.py +0 -0
  273. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_resolve_phase.py +0 -0
  274. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_service.py +0 -0
  275. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_api_types.py +0 -0
  276. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_auto_stop_gating.py +0 -0
  277. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_builtin_provenance.py +0 -0
  278. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_catalogs.py +0 -0
  279. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cgroup_probe.py +0 -0
  280. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_claude_error_detector.py +0 -0
  281. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_claude_foreign_schema.py +0 -0
  282. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli.py +0 -0
  283. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_common.py +0 -0
  284. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_init_install.py +0 -0
  285. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_main_boundary.py +0 -0
  286. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_monitor_http.py +0 -0
  287. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  288. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_cli_upgrade.py +0 -0
  289. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_codewhale_code_precedence.py +0 -0
  290. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_codewhale_plugin.py +0 -0
  291. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_coerce_int_poison.py +0 -0
  292. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_collapse_spawn_decisions.py +0 -0
  293. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_fresh_eyes.py +0 -0
  294. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_max_rounds.py +0 -0
  295. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_nested_strictness.py +0 -0
  296. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_prompt_delivery.py +0 -0
  297. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_schedule.py +0 -0
  298. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_stop_file.py +0 -0
  299. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_strictness.py +0 -0
  300. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  301. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_config_transient_error_action.py +0 -0
  302. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_context_store.py +0 -0
  303. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_control_char_config.py +0 -0
  304. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_default_dirty_handler.py +0 -0
  305. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_defenses.py +0 -0
  306. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detect_hung_latch.py +0 -0
  307. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detector_isolation.py +0 -0
  308. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_detector_protocol.py +0 -0
  309. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_dirty_handlers.py +0 -0
  310. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_dispatch_dirty_owner_branch.py +0 -0
  311. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_docgen.py +0 -0
  312. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_doctor_sandbox.py +0 -0
  313. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_effective_throttle_view.py +0 -0
  314. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_config_migrated.py +0 -0
  315. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_schedule.py +0 -0
  316. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  317. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events.py +0 -0
  318. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_events_jsonl_decode.py +0 -0
  319. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_example_dirty_handler.py +0 -0
  320. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_example_plugin.py +0 -0
  321. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exec_prefix.py +0 -0
  322. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exit_cause.py +0 -0
  323. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_exit_classifier.py +0 -0
  324. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  325. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_gate_serve_boot.py +0 -0
  326. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_gemini_plugin.py +0 -0
  327. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_git_timeout.py +0 -0
  328. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hook_failure_isolation.py +0 -0
  329. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hook_redact_order.py +0 -0
  330. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_hooks.py +0 -0
  331. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_host_health.py +0 -0
  332. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_http_progress.py +0 -0
  333. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_http_progress_epoch.py +0 -0
  334. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_init_entry_points.py +0 -0
  335. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  336. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_kimi_plugin.py +0 -0
  337. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_lifecycle.py +0 -0
  338. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_load_fail_closed.py +0 -0
  339. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_loader_split.py +0 -0
  340. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_manifest_spawn_hooks.py +0 -0
  341. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics.py +0 -0
  342. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  343. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_metrics_swap.py +0 -0
  344. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_migrate_cmd.py +0 -0
  345. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_migrations.py +0 -0
  346. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_assembly.py +0 -0
  347. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_cmd_severity.py +0 -0
  348. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_dedup_identity.py +0 -0
  349. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  350. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  351. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  352. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  353. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_detectors.py +0 -0
  354. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_event_tail.py +0 -0
  355. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_loop.py +0 -0
  356. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_on_alert.py +0 -0
  357. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_remote.py +0 -0
  358. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  359. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_monitor_schedule_pause.py +0 -0
  360. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_network_blip_signal_guard.py +0 -0
  361. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_open_round_log.py +0 -0
  362. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  363. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_outer_round_ceiling.py +0 -0
  364. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_argparse.py +0 -0
  365. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_project_isolation.py +0 -0
  366. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_schedule.py +0 -0
  367. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_peek_select.py +0 -0
  368. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_agent_launch.py +0 -0
  369. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_profiles.py +0 -0
  370. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_phase_select.py +0 -0
  371. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_pi_plugin.py +0 -0
  372. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_checksum.py +0 -0
  373. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_constants.py +0 -0
  374. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_name_mismatch.py +0 -0
  375. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_sandbox_wire.py +0 -0
  376. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugin_scan_parity.py +0 -0
  377. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_plugins_config_fields.py +0 -0
  378. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_presets.py +0 -0
  379. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_primary_prompt_file_home.py +0 -0
  380. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_prompt_loader.py +0 -0
  381. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_read_json_hardening.py +0 -0
  382. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_redact.py +0 -0
  383. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_resolve.py +0 -0
  384. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_cmd.py +0 -0
  385. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_cmd_sigterm.py +0 -0
  386. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_log_helpers.py +0 -0
  387. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_outcome.py +0 -0
  388. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  389. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_round_view.py +0 -0
  390. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  391. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_sandbox_probe.py +0 -0
  392. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_scaffold.py +0 -0
  393. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_schedule.py +0 -0
  394. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_cmd_bounded.py +0 -0
  395. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_config_broken.py +0 -0
  396. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_crash_loop.py +0 -0
  397. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_mem_gate.py +0 -0
  398. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_policy.py +0 -0
  399. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round.py +0 -0
  400. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_log.py +0 -0
  401. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_round_oom_killed.py +0 -0
  402. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_schedule_gate.py +0 -0
  403. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_sentinel.py +0 -0
  404. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_serve_startup_hooks.py +0 -0
  405. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_service_mode.py +0 -0
  406. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_spawn_seam.py +0 -0
  407. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_startup_battery_exit.py +0 -0
  408. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_startup_check.py +0 -0
  409. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_substrate.py +0 -0
  410. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  411. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  412. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_epoch_bound.py +0 -0
  413. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_throttle_plugin_classification.py +0 -0
  414. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_upgrade_migrate.py +0 -0
  415. {cli_agent_runner-0.3.3 → cli_agent_runner-0.3.5}/tests/unit/test_vcs_state.py +0 -0
@@ -158,6 +158,7 @@ overlaps
158
158
  plan
159
159
  sandbox
160
160
  cgroup
161
+ sigterm_grace
161
162
 
162
163
  # agent_runner.config.models.AgentConfig
163
164
  command
@@ -167,6 +168,7 @@ env
167
168
  prompt_delivery
168
169
  exec_prefix
169
170
  terminal_marker
171
+ sigterm_grace_s
170
172
 
171
173
  # agent_runner.config.models.Config
172
174
  agent
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.5] - 2026-09-15
9
+
10
+ ### Added
11
+ - `[agent] sigterm_grace_s` (default 10): the SIGTERM→SIGKILL grace a round gives its agent when the agent's preset declares itself cooperative (the built-in `gemini` preset does), so an agent that catches SIGTERM to flush or commit its work gets time to finish. A non-cooperative agent (e.g. `claude`) keeps the previous 5 s. Capped at 12 s, inside the supervisor's 15 s wait for the round; `peek --json` (schema `2.4`) and `doctor` report the resolved grace per agent.
12
+
13
+ ## [0.3.4] - 2026-09-15
14
+
15
+ ### Changed
16
+ - 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.
17
+ - 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).
18
+ - `agent-runner events --tail` shows a new matching event within a drain cycle of its being written, instead of on a ~1 s poll.
19
+
20
+ 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.
21
+
8
22
  ## [0.3.3] - 2026-09-14
9
23
 
10
24
  ### 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.5
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
@@ -62,6 +62,26 @@ def cooperative_manifest_names() -> list[str]:
62
62
  return [m.name for m in _LOADED_MANIFESTS if m.sigterm_cooperative]
63
63
 
64
64
 
65
+ def is_cooperative_agent(agent_binary: str | None) -> bool:
66
+ """Whether the agent's preset declares itself SIGTERM-cooperative.
67
+ `None in [...]` is safely False, so no None-guard is needed."""
68
+ return agent_binary in cooperative_manifest_names()
69
+
70
+
71
+ def resolve_sigterm_grace_s(agent_binary: str | None, sigterm_grace_s: int) -> int:
72
+ """The SIGTERM->SIGKILL grace this round's agent actually gets:
73
+ `sigterm_grace_s` (config's [agent] sigterm_grace_s, boot-capped at
74
+ _ROUND_TERM_GRACE_S) when `agent_binary` names a manifest declaring
75
+ sigterm_cooperative=True, else agent_runtime.REAP_GRACE_S (5s)
76
+ unchanged. One function, two callers: cli.serve_cmd._apply_reap_grace_env
77
+ (what the agent actually gets) and doctor/peek (what an operator sees)."""
78
+ from agent_runner.agent_runtime import REAP_GRACE_S
79
+
80
+ if is_cooperative_agent(agent_binary):
81
+ return sigterm_grace_s
82
+ return REAP_GRACE_S
83
+
84
+
65
85
  def register_manifest(
66
86
  manifest: PluginManifest,
67
87
  *,
@@ -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))
@@ -312,13 +312,24 @@ def timeout_budget(round_budget_s: int) -> tuple[int, int]:
312
312
  round subprocess (``api.outer_round_ceiling_s``): ``round_budget_s``
313
313
  plus reap grace + git-commit ceiling + hook allowance, so it only trips
314
314
  when the round supervisor itself is wedged, never during its own
315
- bounded post-round cleanup.
315
+ bounded post-round cleanup. The reap margin must cover the widest grace
316
+ ANY round can be configured with -- a cooperative agent's
317
+ ``sigterm_grace_s`` is boot-capped at ``_ROUND_TERM_GRACE_S``
318
+ (``config/models.py``'s mirror of this module's single source), not
319
+ the non-cooperative default ``_REAP_GRACE_S``, so the margin is
320
+ ``max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)`` -- else a long cooperative
321
+ grace could trip the outer wall-clock ceiling mid-grace.
316
322
  - ``timeout_stop_sec`` — systemd's ``TimeoutStopSec``
317
323
  (``service_unit.py``): must clear ``outer_ceiling_s`` by enough for a
318
324
  SIGTERM to reach and drain the round (``_ROUND_TERM_GRACE_S``) plus a
319
325
  stop-request overhead pad, so `systemctl stop` never SIGKILLs a round
320
326
  that is draining normally.
321
327
  """
322
- outer_ceiling_s = round_budget_s + _REAP_GRACE_S + _GIT_COMMIT_TIMEOUT_S + _HOOK_ALLOWANCE_S
328
+ outer_ceiling_s = (
329
+ round_budget_s
330
+ + max(_REAP_GRACE_S, _ROUND_TERM_GRACE_S)
331
+ + _GIT_COMMIT_TIMEOUT_S
332
+ + _HOOK_ALLOWANCE_S
333
+ )
323
334
  timeout_stop_sec = outer_ceiling_s + _ROUND_TERM_GRACE_S + _STOP_GRACE_MARGIN_S
324
335
  return timeout_stop_sec, outer_ceiling_s
@@ -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.5'
22
+ __version_tuple__ = version_tuple = (0, 3, 5)
23
23
 
24
24
  __commit_id__ = commit_id = None